Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gutenTAG/anomalies/types/trend.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def generate(self, anomaly_protocol: AnomalyProtocol) -> AnomalyProtocol:
return anomaly_protocol

length = anomaly_protocol.end - anomaly_protocol.start
transition_length = int(length * 0.2)
plateau_length = int(length * 0.8)
transition_length = max(1, int(length * 0.2))
plateau_length = length - transition_length
start_transition = norm.pdf(np.linspace(-3, 0, transition_length), scale=1.05)
amplitude_bell = np.concatenate(
[start_transition / start_transition.max(), np.ones(plateau_length)]
Expand Down
54 changes: 54 additions & 0 deletions tests/test_generator/test_trend_anomaly_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import unittest
from typing import Any, Dict

from gutenTAG import GutenTAG


class TestTrendAnomalyLength(unittest.TestCase):
"""Regression tests for the trend-anomaly amplitude-envelope broadcast bug.

The envelope used to be built as ``int(length * 0.2) + int(length * 0.8)``
samples, which is one short of ``length`` whenever ``length`` is not a
multiple of 5, raising a NumPy broadcasting ``ValueError``. These tests
exercise window lengths covering every residue mod 5 (plus very small
lengths) to make sure the trend anomaly generates without error.
"""

seed = 42

def _build_config(self, length: int) -> Dict[str, Any]:
return {
"timeseries": [
{
"name": f"test-trend-{length}",
"length": 500,
"base-oscillations": [{"kind": "sine"}],
"anomalies": [
{
"position": "middle",
"length": length,
"channel": 0,
"kinds": [
{"kind": "trend", "oscillation": {"kind": "sine"}}
],
}
],
}
]
}

def test_trend_anomaly_length(self) -> None:
# Lengths cover every residue mod 5; non-multiples of 5 (e.g. 93, 94,
# 96, 97) used to crash, very small lengths (2, 3) guard the secondary
# transition_length == 0 edge case.
for length in [2, 3, 5, 11, 90, 93, 94, 95, 96, 97, 100]:
with self.subTest(length=length):
gutentag = GutenTAG(seed=self.seed)
gutentag.load_config_dict(self._build_config(length))
ts = gutentag.generate(return_timeseries=True)
self.assertIsNotNone(ts)
self.assertEqual(len(ts), 1) # type: ignore


if __name__ == "__main__":
unittest.main()