"""Learn pressure thresholds from pre-event normal history, never burst truth.""" from __future__ import annotations import math from datetime import datetime, timedelta from statistics import median def learn_pressure_thresholds( rows: list[dict], device_ids: list[str], cutoff: datetime, *, history_days: int = 15, interval_seconds: int = 900, relative_floor_percent: float = 3.0, noise_multiplier: float = 3.0, absolute_floor_m: float = 0.5, ) -> dict: """Estimate noise after removing each daily slot's median operating pattern.""" if (cutoff.tzinfo is None or history_days < 1 or interval_seconds <= 0 or 86400 % interval_seconds or not device_ids or len(set(device_ids)) != len(device_ids)): raise ValueError("an aware cutoff and whole-day sampling grid are required") if not all(math.isfinite(value) and value > 0 for value in (relative_floor_percent, noise_multiplier, absolute_floor_m)): raise ValueError("threshold floors and noise multiplier must be positive") count = history_days * 86400 // interval_seconds start = cutoff - timedelta(days=history_days) expected = {start + timedelta(seconds=interval_seconds * index) for index in range(count)} data = {device: {} for device in device_ids} for row in rows: device = row["device_id"] if device not in data: continue at = row["time"] if at.tzinfo is None or at >= cutoff: raise ValueError("adaptive fitting must not consume event or future observations") if at < start: continue value = row["monitored_value"] if at not in expected or at in data[device] or value is None or not math.isfinite(float(value)) or float(value) <= 0: raise ValueError("adaptive fitting requires distinct positive finite pressure samples on the grid") data[device][at] = float(value) insufficient = {device: len(values) for device, values in data.items() if set(values) != expected} if insufficient: return {"status": "history_not_ready", "required_samples_per_device": count, "available_samples": insufficient, "knowledge_cutoff": cutoff.isoformat(), "devices": {}} fitted = {} for device, values in data.items(): slots = {} for at, value in values.items(): slot = int((at - start).total_seconds()) // interval_seconds % (86400 // interval_seconds) slots.setdefault(slot, []).append(value) references = {slot: median(group) for slot, group in slots.items()} residuals = [] for at, value in values.items(): slot = int((at - start).total_seconds()) // interval_seconds % (86400 // interval_seconds) residuals.append(100 * (value / references[slot] - 1)) center = median(residuals) robust_sigma = 1.4826 * median(abs(value - center) for value in residuals) fitted[device] = {"relative_drop_percent": max(relative_floor_percent, noise_multiplier * robust_sigma), "absolute_drop_m": absolute_floor_m, "robust_noise_percent": robust_sigma, "reference_pressure_m_by_slot": {str(slot): value for slot, value in references.items()}} return {"status": "ready", "required_samples_per_device": count, "knowledge_cutoff": cutoff.isoformat(), "devices": fitted, "reference_grid_start": start.isoformat(), "interval_seconds": interval_seconds, "method": "daily-slot median removal; 1.4826*MAD; thresholds frozen before event"} def evaluate_pressure_thresholds(rows: list[dict], fitted: dict, *, consecutive_samples: int = 2) -> dict: """Experimental adaptive evaluator; this is not the production replay worker.""" if fitted.get("status") != "ready" or consecutive_samples < 1: raise ValueError("a fitted rule and positive consecutive sample count are required") start = datetime.fromisoformat(fitted["reference_grid_start"]) cutoff = datetime.fromisoformat(fitted["knowledge_cutoff"]) step = fitted["interval_seconds"] states = {} signals = [] for row in sorted(rows, key=lambda item: (item["time"], item["device_id"])): device = row["device_id"] if device not in fitted["devices"]: continue at, value = row["time"], row["monitored_value"] if at.tzinfo is None or at < cutoff or (at - start).total_seconds() % step: raise ValueError("evaluation samples must follow the fit cutoff on the sampling grid") previous, hits, signaled = states.get(device, (None, 0, False)) if previous is not None and at <= previous: raise ValueError("evaluation samples must be distinct") if previous is None or at - previous != timedelta(seconds=step): hits = 0 rule = fitted["devices"][device] slot = int((at - start).total_seconds()) // step % (86400 // step) expected = rule["reference_pressure_m_by_slot"][str(slot)] valid = value is not None and math.isfinite(float(value)) and float(value) > 0 drop = expected - float(value) if valid else 0 hit = valid and drop >= rule["absolute_drop_m"] and 100 * drop / expected >= rule["relative_drop_percent"] hits = hits + 1 if hit else 0 if hits >= consecutive_samples and not signaled: signals.append({"device_id": device, "first_signal_at": at.isoformat(), "reference_pressure_m": expected, "absolute_drop_m": drop, "relative_drop_percent": 100 * drop / expected}) signaled = True states[device] = (at, hits, signaled) return {"raised": bool(signals), "signals": signals, "scope": "experimental adaptive pressure rule only; not a worker or Agent result"}