use super::{stats, Baseline, MonitorEvent, MonitorReport, Series};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Cn0Settings {
pub baseline_s: f64,
pub stride: usize,
pub shift_db: f64,
pub h: f64,
pub sigma_floor_db: f64,
}
impl Default for Cn0Settings {
fn default() -> Self {
Cn0Settings {
baseline_s: 2.0,
stride: 0,
shift_db: 3.0,
h: 5.0,
sigma_floor_db: 0.1,
}
}
}
#[derive(Clone, Debug)]
struct Cusum {
kind: &'static str,
g: f64,
left_zero_at: f64,
open: Option<MonitorEvent>,
events: Vec<MonitorEvent>,
}
impl Cusum {
fn new(kind: &'static str) -> Self {
Cusum {
kind,
g: 0.0,
left_zero_at: 0.0,
open: None,
events: Vec::new(),
}
}
fn push(&mut self, channel: &str, t: f64, z: f64, k: f64, h: f64, prev_t: f64) {
let was_zero = self.g == 0.0;
self.g = (self.g + z - k).max(0.0);
if was_zero && self.g > 0.0 {
self.left_zero_at = prev_t;
}
if let Some(ev) = self.open.as_mut() {
ev.peak = ev.peak.max(self.g);
if self.g == 0.0 {
let mut ev = self.open.take().expect("open");
ev.t_end_s = Some(t);
self.events.push(ev);
}
} else if self.g > h {
self.open = Some(MonitorEvent {
kind: self.kind.to_string(),
channel: Some(channel.to_string()),
t_start_s: self.left_zero_at,
t_alarm_s: t,
t_end_s: None,
peak: self.g,
threshold: h,
});
}
}
fn events(&self) -> Vec<MonitorEvent> {
let mut v = self.events.clone();
v.extend(self.open.clone());
v
}
}
pub struct Cn0Monitor {
label: String,
settings: Cn0Settings,
t0: Option<f64>,
baseline: Baseline,
reference: Option<(f64, f64)>,
n_seen: u64,
prev_t: f64,
series: Series,
s_drop: Series,
s_rise: Series,
drop: Cusum,
rise: Cusum,
}
impl Cn0Monitor {
pub fn new(label: &str, settings: Cn0Settings) -> Self {
Cn0Monitor {
label: label.to_string(),
settings,
t0: None,
baseline: Baseline::default(),
reference: None,
n_seen: 0,
prev_t: 0.0,
series: Series::new("cn0_dbhz", "dB-Hz", Some(label)),
s_drop: Series::new("cn0_cusum_drop", "1", Some(label)),
s_rise: Series::new("cn0_cusum_rise", "1", Some(label)),
drop: Cusum::new("cn0_drop"),
rise: Cusum::new("cn0_rise"),
}
}
pub fn reference(&self) -> Option<(f64, f64)> {
self.reference
}
pub fn push(&mut self, t_s: f64, cn0_dbhz: f64) {
if !cn0_dbhz.is_finite() {
return;
}
self.series.push(t_s, cn0_dbhz);
let t0 = *self.t0.get_or_insert(t_s);
let take = self.n_seen % self.settings.stride.max(1) as u64 == 0;
self.n_seen += 1;
if !take {
return;
}
match self.reference {
None => {
self.baseline.push(cn0_dbhz);
if t_s - t0 >= self.settings.baseline_s && self.baseline.count() >= 2 {
self.reference = Some((
self.baseline.mean(),
self.baseline.std().max(self.settings.sigma_floor_db),
));
}
}
Some((mu, sd)) => {
let z = (cn0_dbhz - mu) / sd;
let k = self.settings.shift_db / (2.0 * sd);
let h = self.settings.h;
self.drop.push(&self.label, t_s, -z, k, h, self.prev_t);
self.rise.push(&self.label, t_s, z, k, h, self.prev_t);
self.s_drop.push(t_s, self.drop.g);
self.s_rise.push(t_s, self.rise.g);
}
}
self.prev_t = t_s;
}
pub fn events(&self) -> Vec<MonitorEvent> {
let mut v = self.drop.events();
v.extend(self.rise.events());
v
}
pub fn report(&self) -> MonitorReport {
let mut notes = Vec::new();
let l = &self.label;
match self.reference {
Some((mu, sd)) => {
let k = self.settings.shift_db / (2.0 * sd);
notes.push((format!("cn0.{l}.baseline_dbhz"), format!("{mu:.3}")));
notes.push((format!("cn0.{l}.sigma_db"), format!("{sd:.3}")));
notes.push((
format!("cn0.{l}.arl0_samples_independent"),
format!("{:.0}", stats::cusum_arl0(k, self.settings.h)),
));
notes.push((
format!("cn0.{l}.arl1_samples_independent"),
format!(
"{:.1}",
stats::cusum_arl(k, self.settings.h, self.settings.shift_db / sd)
),
));
}
None => notes.push((format!("cn0.{l}.baseline"), "not reached".into())),
}
let mut r = MonitorReport {
series: vec![
self.series.clone(),
self.s_drop.clone(),
self.s_rise.clone(),
],
events: self.events(),
spectra: Vec::new(),
notes,
};
r.events.sort_by(|a, b| a.t_start_s.total_cmp(&b.t_start_s));
r
}
}