Source code for scitex_seizure_metrics.policy

"""AlarmPolicy — explicit container for the alarm-generation knobs that
drive forecasting metrics. Required argument to all alarm-aware functions
in scitex_seizure_metrics; never silently defaulted, so every reported number can be
traced to the policy that produced it.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal


[docs] @dataclass(frozen=True) class AlarmPolicy: """Knobs that govern how a continuous prediction stream is converted into discrete alarms, and how those alarms are matched to seizures. Every alarm-based metric in scitex_seizure_metrics requires an explicit AlarmPolicy so reports are reproducible across papers. Args: sph_seconds: Seizure Prediction Horizon (lead time required between an alarm and the earliest valid seizure). Andrade et al. 2024. sop_seconds: Seizure Occurrence Period (validity window after SPH within which the seizure must occur). cadence_seconds: Time step between successive predictions in the continuous stream. 60 s ≈ once-per-minute, 360 s ≈ once-per- 6-min. Defines the maximum theoretical alarm rate before refractory. refractory_seconds: Minimum gap between consecutive alarms. After an alarm fires, the next earliest alarm is suppressed until this much time has passed. Common choices: SOP, 30 min, 1 h. alarm_threshold: Probability threshold above which a window's prediction triggers an alarm-candidate. merge_consecutive: If True, runs of contiguous above-threshold windows count as one alarm (fired at the first window). fp_denominator: Whether FP/hr is normalised by total recording time or by interictal-only time (with seizure ± SOP windows removed). The Mormann tradition is "interictal". """ sph_seconds: float sop_seconds: float cadence_seconds: float refractory_seconds: float alarm_threshold: float = 0.5 merge_consecutive: bool = True fp_denominator: Literal["interictal", "total"] = "interictal" def __post_init__(self): if self.sph_seconds < 0: raise ValueError("sph_seconds must be >= 0") if self.sop_seconds <= 0: raise ValueError("sop_seconds must be > 0") if self.cadence_seconds <= 0: raise ValueError("cadence_seconds must be > 0") if self.refractory_seconds < 0: raise ValueError("refractory_seconds must be >= 0") if not (0 <= self.alarm_threshold <= 1): raise ValueError("alarm_threshold must be in [0, 1]")
[docs] def describe(self) -> dict: """Compact dict suitable for serialising into a metric report.""" return { "sph_s": self.sph_seconds, "sop_s": self.sop_seconds, "cadence_s": self.cadence_seconds, "refractory_s": self.refractory_seconds, "alarm_threshold": self.alarm_threshold, "merge_consecutive": self.merge_consecutive, "fp_denominator": self.fp_denominator, }