fix(m1): _random_window fallback could return end < start
Root cause (found during 50k sample generation at seed 6975, surfacing as 'ValueError: Number of samples, -187, must be non-negative' from linspace in _inject_drift): _random_window's best-effort fallback after 50 failed non-overlap attempts drew start and end from TWO INDEPENDENT random integers, so end could be < start. The primary loop was consistent (start, start+length). Fix: reuse the same start in the fallback so end >= start always holds (overlap is still allowed in best-effort, just no inversion). - tests/test_synthesis.py: regression sweeping 300 seeds × 4 type combos, asserting every segment's end >= start. 146 tests passing.
This commit is contained in:
@@ -103,8 +103,10 @@ def _random_window(rng: np.random.Generator, T: int, length: int, occupied=None)
|
|||||||
cand = (start, start + length)
|
cand = (start, start + length)
|
||||||
if all(cand[1] <= s or cand[0] >= e for (s, e) in occupied):
|
if all(cand[1] <= s or cand[0] >= e for (s, e) in occupied):
|
||||||
return cand
|
return cand
|
||||||
# could not find a non-overlapping window; return best effort
|
# could not find a non-overlapping window; return best effort (overlap
|
||||||
return int(rng.integers(0, T - length + 1)), int(rng.integers(0, T - length + 1)) + length
|
# allowed) but keep start/end consistent so end >= start always holds.
|
||||||
|
start = int(rng.integers(0, T - length + 1))
|
||||||
|
return start, start + length
|
||||||
|
|
||||||
|
|
||||||
def _inject_spike(rng, series, T, C, occupied):
|
def _inject_spike(rng, series, T, C, occupied):
|
||||||
|
|||||||
@@ -127,3 +127,22 @@ def test_add_missing_reproducible():
|
|||||||
a = add_missing(s, rate=0.1, seed=3)
|
a = add_missing(s, rate=0.1, seed=3)
|
||||||
b = add_missing(s, rate=0.1, seed=3)
|
b = add_missing(s, rate=0.1, seed=3)
|
||||||
assert np.array_equal(a, b, equal_nan=True)
|
assert np.array_equal(a, b, equal_nan=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── regression: _random_window end >= start (found during 50k generation) ─
|
||||||
|
def test_inject_anomaly_end_ge_start_across_many_seeds():
|
||||||
|
"""Regression: _random_window fallback used to draw end independently,
|
||||||
|
producing end < start (ValueError in linspace). Sweep many seeds + types."""
|
||||||
|
import numpy as np
|
||||||
|
s0 = generate_series(T=512, C=5, seed=0)
|
||||||
|
for seed in range(300):
|
||||||
|
for types in (
|
||||||
|
["drift"], ["level_shift"], ["variance_change"],
|
||||||
|
["drift", "level_shift", "variance_change", "spike"],
|
||||||
|
):
|
||||||
|
s = s0.copy()
|
||||||
|
out, labels, segments = inject_anomaly(s, types=types, seed=seed)
|
||||||
|
for seg in segments:
|
||||||
|
assert seg["end"] >= seg["start"], (seed, types, seg)
|
||||||
|
assert out.shape == s.shape
|
||||||
|
assert labels.shape == (512,)
|
||||||
|
|||||||
Reference in New Issue
Block a user