from __future__ import annotations import os import pandas as pd from ..ingest import TOPIC_USBL from ..models import Anomaly from .base import Rule class UsblSnrLowRule(Rule): """Fire when SNR stays below threshold for `consec` consecutive samples.""" name = "usbl_snr_low" topic = TOPIC_USBL severity = "warn" def __init__( self, subject: str = "AUV000", min_snr_db: float | None = None, consec: int = 3, ) -> None: super().__init__(subject) if min_snr_db is None: min_snr_db = float(os.environ.get("USBL_SNR_LOW", 5.0)) self.min_snr_db = min_snr_db self.consec = consec def detect(self, df: pd.DataFrame) -> list[Anomaly]: if df.empty or len(df) < self.consec: return [] low = df["snr_db"] < self.min_snr_db run = low.astype(int).groupby((~low).cumsum()).cumsum() anomalies: list[Anomaly] = [] fired_run = -1 runs_id = (~low).cumsum() for idx in df.index: if run.iloc[idx] >= self.consec and runs_id.iloc[idx] != fired_run: fired_run = int(runs_id.iloc[idx]) anomalies.append( self._make( ts=float(df.at[idx, "ts"]), value=float(df.at[idx, "snr_db"]), context={ "min_snr_db": self.min_snr_db, "consec": self.consec, }, ) ) return anomalies