anomaly-detection / tests /test_stream.py
zeetay's picture
initial deploy
b0add2b verified
Raw
History Blame Contribute Delete
4.78 kB
"""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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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)
)
@pytest.mark.asyncio
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)
@pytest.mark.asyncio
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}"