Spaces:
Sleeping
Sleeping
| """Phase 1: Stream simulator tests.""" | |
| import asyncio | |
| import statistics | |
| import pytest | |
| from src.stream.generator import generate_stream | |
| from src.stream.models import Observation | |
| # Baseline from generator | |
| BASELINE_TEMP = 25.0 | |
| BASELINE_PRESSURE = 101.3 | |
| BASELINE_VIBRATION = 0.5 | |
| NOISE_SIGMA = 0.5 | |
| async def test_generator_is_async(): | |
| """The generator is async: confirmed by consuming it with async for.""" | |
| count = 0 | |
| async for obs in generate_stream( | |
| phase_a_length=10, | |
| phase_b_length=5, | |
| phase_c_length=10, | |
| seed=42, | |
| ): | |
| assert isinstance(obs, Observation) | |
| assert obs.timestamp >= 0 | |
| assert obs.label in ("normal", "point_anomaly", "contextual_anomaly") | |
| assert obs.phase in ("A", "B", "C") | |
| count += 1 | |
| assert count == 25 | |
| async def test_phase_a_normality(stream_params_phase_a_only): | |
| """Phase A observations are statistically normal: means and variances match baselines.""" | |
| obs_list: list[Observation] = [] | |
| async for obs in generate_stream(**stream_params_phase_a_only): | |
| obs_list.append(obs) | |
| assert len(obs_list) == 200 | |
| assert all(o.label == "normal" for o in obs_list) | |
| temps = [o.temperature for o in obs_list] | |
| pressures = [o.pressure for o in obs_list] | |
| vibrations = [o.vibration for o in obs_list] | |
| # Means within tolerance of baseline (allow for sampling variance) | |
| assert abs(statistics.mean(temps) - BASELINE_TEMP) < 0.15 | |
| assert abs(statistics.mean(pressures) - BASELINE_PRESSURE) < 0.15 | |
| assert abs(statistics.mean(vibrations) - BASELINE_VIBRATION) < 0.15 | |
| # Variances in expected ballpark (noise sigma^2 ~ 0.25, sample var can vary) | |
| var_t = statistics.variance(temps) | |
| var_p = statistics.variance(pressures) | |
| var_v = statistics.variance(vibrations) | |
| assert 0.1 < var_t < 1.0 | |
| assert 0.1 < var_p < 1.0 | |
| assert 0.1 < var_v < 1.0 | |
| async def test_phase_b_drift(stream_params_short): | |
| """Phase B observations show a measurable distribution shift by end of phase.""" | |
| params = dict(stream_params_short) | |
| phase_a_len = params["phase_a_length"] | |
| phase_b_len = params["phase_b_length"] | |
| drift_mag = params["drift_magnitude"] | |
| obs_list: list[Observation] = [] | |
| async for obs in generate_stream(**params): | |
| obs_list.append(obs) | |
| phase_a_temps = [o.temperature for o in obs_list[:phase_a_len]] | |
| phase_b_end_temps = [o.temperature for o in obs_list[phase_a_len + phase_b_len - 50 : phase_a_len + phase_b_len]] | |
| mean_a = statistics.mean(phase_a_temps) | |
| mean_b_end = statistics.mean(phase_b_end_temps) | |
| # Allow for sampling variance: require a measurable positive shift | |
| assert mean_b_end - mean_a >= drift_mag * 0.25, ( | |
| "End of Phase B mean should shift; got mean_a=%s mean_b_end=%s" % (mean_a, mean_b_end) | |
| ) | |
| async def test_phase_c_anomaly_count(stream_params_short): | |
| """Phase C contains the configured number of injected anomalies at the configured rate.""" | |
| params = dict(stream_params_short) | |
| phase_c_len = params["phase_c_length"] | |
| anomaly_rate = params["anomaly_rate"] | |
| phase_a_len = params["phase_a_length"] | |
| phase_b_len = params["phase_b_length"] | |
| obs_list: list[Observation] = [] | |
| async for obs in generate_stream(**params): | |
| obs_list.append(obs) | |
| phase_c = obs_list[phase_a_len + phase_b_len :] | |
| assert len(phase_c) == phase_c_len | |
| anomalies = [o for o in phase_c if o.label != "normal"] | |
| expected_min = int(phase_c_len * anomaly_rate * 0.5) | |
| expected_max = int(phase_c_len * anomaly_rate * 2) + 10 | |
| assert expected_min <= len(anomalies) <= expected_max, ( | |
| f"Expected roughly {phase_c_len * anomaly_rate:.0f} anomalies, got {len(anomalies)}" | |
| ) | |
| point_count = sum(1 for o in phase_c if o.label == "point_anomaly") | |
| contextual_count = sum(1 for o in phase_c if o.label == "contextual_anomaly") | |
| assert point_count + contextual_count == len(anomalies) | |
| async def test_phase_labels_match_boundaries(stream_params_short): | |
| """Phase field on each Observation matches the correct stream phase (A/B/C).""" | |
| params = dict(stream_params_short) | |
| PA = params["phase_a_length"] | |
| PB = params["phase_b_length"] | |
| obs_list: list[Observation] = [] | |
| async for obs in generate_stream(**params): | |
| obs_list.append(obs) | |
| for i, obs in enumerate(obs_list): | |
| if i < PA: | |
| assert obs.phase == "A", f"obs {i} should be Phase A, got {obs.phase}" | |
| elif i < PA + PB: | |
| assert obs.phase == "B", f"obs {i} should be Phase B, got {obs.phase}" | |
| else: | |
| assert obs.phase == "C", f"obs {i} should be Phase C, got {obs.phase}" | |