Spaces:
Sleeping
Sleeping
| """Async pipeline: generator -> scorer -> broadcaster, with structured logging.""" | |
| import logging | |
| import time | |
| from collections.abc import AsyncIterator, Awaitable, Callable | |
| from typing import Any | |
| from src.detector.anomaly import AnomalyDetector | |
| from src.detector.drift import DriftDetector | |
| from src.detector.metrics import RunningMetrics | |
| from src.stream.generator import ( | |
| BASELINE_PRESSURE, | |
| BASELINE_TEMP, | |
| BASELINE_VIBRATION, | |
| NOISE_SIGMA, | |
| ) | |
| from src.stream.models import Observation | |
| logger = logging.getLogger(__name__) | |
| def _drift_signal(obs: Observation) -> float: | |
| """Average normalized deviation of all three sensors from Phase A baselines.""" | |
| return ( | |
| (obs.temperature - BASELINE_TEMP) / NOISE_SIGMA | |
| + (obs.pressure - BASELINE_PRESSURE) / NOISE_SIGMA | |
| + (obs.vibration - BASELINE_VIBRATION) / NOISE_SIGMA | |
| ) / 3.0 | |
| def _broadcast_message( | |
| obs: Observation, | |
| anomaly_score: float, | |
| alert: bool, | |
| drift_event: bool, | |
| running_precision: float, | |
| running_recall: float, | |
| running_f1: float, | |
| latency_ms: float, | |
| observation_index: int, | |
| total_anomalies_detected: int, | |
| total_drift_events: int, | |
| ) -> dict[str, Any]: | |
| return { | |
| "timestamp": obs.timestamp, | |
| "temperature": obs.temperature, | |
| "pressure": obs.pressure, | |
| "vibration": obs.vibration, | |
| "label": obs.label, | |
| "phase": obs.phase, | |
| "anomaly_score": anomaly_score, | |
| "alert": alert, | |
| "drift_event": drift_event, | |
| "running_precision": running_precision, | |
| "running_recall": running_recall, | |
| "running_f1": running_f1, | |
| "latency_ms": latency_ms, | |
| "observation_index": observation_index, | |
| "total_anomalies_detected": total_anomalies_detected, | |
| "total_drift_events": total_drift_events, | |
| } | |
| async def run_pipeline( | |
| stream: AsyncIterator[Observation], | |
| broadcaster: Callable[[dict[str, Any]], Awaitable[None]], | |
| *, | |
| anomaly_threshold: float = 0.5, | |
| n_trees: int = 10, | |
| height: int = 6, | |
| window_size: int = 100, | |
| drift_delta: float = 0.002, | |
| drift_grace_period: int = 30, | |
| state: dict[str, Any] | None = None, | |
| ) -> None: | |
| """Run the detection pipeline: score each observation, detect drift, broadcast messages.""" | |
| detector = AnomalyDetector( | |
| threshold=anomaly_threshold, | |
| n_trees=n_trees, | |
| height=height, | |
| window_size=window_size, | |
| ) | |
| drift_detector = DriftDetector(delta=drift_delta, grace_period=drift_grace_period) | |
| metrics = RunningMetrics() | |
| observation_index = 0 | |
| total_anomalies = 0 | |
| total_drifts = 0 | |
| logger.info("pipeline_start", extra={"threshold": anomaly_threshold, "n_trees": n_trees}) | |
| _baseline_frozen = False | |
| async for obs in stream: | |
| t0 = time.perf_counter() | |
| if obs.phase == "A": | |
| detector.learn_scaler(obs) | |
| elif not _baseline_frozen: | |
| detector.freeze_baseline() | |
| _baseline_frozen = True | |
| score = detector.score(obs) | |
| drift_event_flag = drift_detector.update(_drift_signal(obs)) | |
| if drift_event_flag: | |
| total_drifts += 1 | |
| logger.info( | |
| "drift_detected", | |
| extra={ | |
| "observation_index": observation_index, | |
| "phase": obs.phase, | |
| "drift_signal": round(_drift_signal(obs), 4), | |
| }, | |
| ) | |
| alert = score > anomaly_threshold | |
| if alert: | |
| total_anomalies += 1 | |
| metrics.update(ground_truth=(obs.label != "normal"), predicted=alert) | |
| latency_ms = (time.perf_counter() - t0) * 1000 | |
| if state is not None: | |
| state["total_observations"] = observation_index + 1 | |
| state["total_anomalies_detected"] = total_anomalies | |
| state["total_drift_events"] = total_drifts | |
| state["current_precision"] = metrics.precision | |
| state["current_recall"] = metrics.recall | |
| state["current_f1"] = metrics.f1 | |
| msg = _broadcast_message( | |
| obs=obs, | |
| anomaly_score=score, | |
| alert=alert, | |
| drift_event=drift_event_flag, | |
| running_precision=metrics.precision, | |
| running_recall=metrics.recall, | |
| running_f1=metrics.f1, | |
| latency_ms=round(latency_ms, 3), | |
| observation_index=observation_index, | |
| total_anomalies_detected=total_anomalies, | |
| total_drift_events=total_drifts, | |
| ) | |
| await broadcaster(msg) | |
| observation_index += 1 | |
| logger.info( | |
| "pipeline_end", | |
| extra={ | |
| "total_observations": observation_index, | |
| "total_anomalies": total_anomalies, | |
| "total_drifts": total_drifts, | |
| "final_precision": round(metrics.precision, 4), | |
| "final_recall": round(metrics.recall, 4), | |
| "final_f1": round(metrics.f1, 4), | |
| }, | |
| ) | |