pub mod cn0;
pub mod lock;
pub mod power;
pub mod run;
pub mod spectral;
pub mod sqm;
pub mod stats;
use serde::{Deserialize, Serialize};
pub(crate) fn norm_sqr(s: crate::iq::Cf64) -> f64 {
s.re * s.re + s.im * s.im
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Series {
pub name: String,
pub unit: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
pub t_s: Vec<f64>,
pub value: Vec<f64>,
}
impl Series {
pub fn new(name: &str, unit: &str, channel: Option<&str>) -> Self {
Series {
name: name.to_string(),
unit: unit.to_string(),
channel: channel.map(str::to_string),
t_s: Vec::new(),
value: Vec::new(),
}
}
pub fn push(&mut self, t_s: f64, v: f64) {
self.t_s.push(t_s);
self.value.push(v);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MonitorEvent {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
pub t_start_s: f64,
pub t_alarm_s: f64,
pub t_end_s: Option<f64>,
pub peak: f64,
pub threshold: f64,
}
impl MonitorEvent {
pub fn duration_s(&self) -> Option<f64> {
self.t_end_s.map(|e| e - self.t_start_s)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Side {
Above,
Below,
}
#[derive(Clone, Debug)]
pub struct SpanDetector {
kind: String,
channel: Option<String>,
side: Side,
threshold: f64,
min_count: usize,
run: Option<(f64, usize, f64)>,
open: Option<MonitorEvent>,
events: Vec<MonitorEvent>,
}
impl SpanDetector {
pub fn new(
kind: &str,
channel: Option<&str>,
side: Side,
threshold: f64,
min_count: usize,
) -> Self {
SpanDetector {
kind: kind.to_string(),
channel: channel.map(str::to_string),
side,
threshold,
min_count: min_count.max(1),
run: None,
open: None,
events: Vec::new(),
}
}
fn past(&self, v: f64) -> bool {
match self.side {
Side::Above => v > self.threshold,
Side::Below => v < self.threshold,
}
}
fn more_extreme(&self, a: f64, b: f64) -> f64 {
match self.side {
Side::Above => a.max(b),
Side::Below => a.min(b),
}
}
pub fn push(&mut self, t_s: f64, v: f64) {
if !v.is_finite() {
return;
}
if self.past(v) {
if let Some(ev) = self.open.as_mut() {
ev.peak = match self.side {
Side::Above => ev.peak.max(v),
Side::Below => ev.peak.min(v),
};
return;
}
let (t0, n, peak) = match self.run {
Some((t0, n, p)) => (t0, n + 1, self.more_extreme(p, v)),
None => (t_s, 1, v),
};
if n >= self.min_count {
self.open = Some(MonitorEvent {
kind: self.kind.clone(),
channel: self.channel.clone(),
t_start_s: t0,
t_alarm_s: t_s,
t_end_s: None,
peak,
threshold: self.threshold,
});
self.run = None;
} else {
self.run = Some((t0, n, peak));
}
} else {
self.run = None;
if let Some(mut ev) = self.open.take() {
ev.t_end_s = Some(t_s);
self.events.push(ev);
}
}
}
pub fn events(&self) -> Vec<MonitorEvent> {
let mut v = self.events.clone();
v.extend(self.open.clone());
v
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Baseline {
n: u64,
mean: f64,
m2: f64,
}
impl Baseline {
pub fn push(&mut self, v: f64) {
if !v.is_finite() {
return;
}
self.n += 1;
let d = v - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (v - self.mean);
}
pub fn count(&self) -> u64 {
self.n
}
pub fn mean(&self) -> f64 {
self.mean
}
pub fn std(&self) -> f64 {
if self.n < 2 {
0.0
} else {
(self.m2 / (self.n - 1) as f64).sqrt()
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct MonitorReport {
pub series: Vec<Series>,
pub events: Vec<MonitorEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spectra: Vec<Spectrum>,
pub notes: Vec<(String, String)>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Spectrum {
pub name: String,
pub freq_hz: Vec<f64>,
pub value_db: Vec<f64>,
}
impl MonitorReport {
pub fn extend(&mut self, other: MonitorReport) {
self.series.extend(other.series);
self.events.extend(other.events);
self.spectra.extend(other.spectra);
self.notes.extend(other.notes);
self.events
.sort_by(|a, b| a.t_start_s.total_cmp(&b.t_start_s));
}
pub fn series_named(&self, name: &str, channel: Option<&str>) -> Option<&Series> {
self.series
.iter()
.find(|s| s.name == name && (channel.is_none() || s.channel.as_deref() == channel))
}
pub fn events_of(&self, kind: &str) -> Vec<&MonitorEvent> {
self.events.iter().filter(|e| e.kind == kind).collect()
}
pub fn to_json(&self) -> Result<String, crate::iq::IqError> {
serde_json::to_string_pretty(self).map_err(|e| crate::iq::IqError::Format(e.to_string()))
}
pub fn series_csv(&self) -> String {
let mut out = String::from("series,channel,unit,t_s,value\n");
for s in &self.series {
let ch = s.channel.as_deref().unwrap_or("");
for (t, v) in s.t_s.iter().zip(&s.value) {
out.push_str(&format!("{},{ch},{},{t},{v}\n", s.name, s.unit));
}
}
out
}
pub fn events_csv(&self) -> String {
let mut out = String::from("kind,channel,t_start_s,t_alarm_s,t_end_s,peak,threshold\n");
for e in &self.events {
out.push_str(&format!(
"{},{},{},{},{},{},{}\n",
e.kind,
e.channel.as_deref().unwrap_or(""),
e.t_start_s,
e.t_alarm_s,
e.t_end_s.map(|v| v.to_string()).unwrap_or_default(),
e.peak,
e.threshold
));
}
out
}
}
pub struct EpochMonitors {
pub cn0: cn0::Cn0Monitor,
pub sqm: sqm::SqmMonitor,
pub lock: lock::LockMonitor,
}
impl EpochMonitors {
pub fn new(label: &str, spacing_chips: f64, settings: &EpochMonitorSettings) -> Self {
EpochMonitors {
cn0: cn0::Cn0Monitor::new(label, settings.cn0),
sqm: sqm::SqmMonitor::new(label, spacing_chips, settings.sqm),
lock: lock::LockMonitor::new(label, settings.lock),
}
}
pub fn push_epoch(&mut self, e: &crate::iq::track::EpochOutput) {
let t = e.code_epoch_s;
if let Some(c) = e.cn0_nwpr_dbhz {
self.cn0.push(t, c);
}
self.sqm.push(
t,
e.t_coh_s,
e.early,
e.prompt,
e.late,
e.cn0_nwpr_dbhz,
&[],
);
self.lock.push(t, e.prompt, e.pli);
}
pub fn report(&self) -> MonitorReport {
let mut r = self.cn0.report();
r.extend(self.sqm.report());
r.extend(self.lock.report());
r
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct EpochMonitorSettings {
#[serde(default)]
pub cn0: cn0::Cn0Settings,
#[serde(default)]
pub sqm: sqm::SqmSettings,
#[serde(default)]
pub lock: lock::LockSettings,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_detector_confirms_and_closes() {
let mut d = SpanDetector::new("x", None, Side::Above, 1.0, 3);
for (k, v) in [0.0, 2.0, 2.0, 0.5, 2.0, 3.0, 2.5, 4.0, 0.0, 2.0]
.iter()
.enumerate()
{
d.push(k as f64, *v);
}
let ev = d.events();
assert_eq!(ev.len(), 1);
assert_eq!(ev[0].t_start_s, 4.0);
assert_eq!(ev[0].t_alarm_s, 6.0);
assert_eq!(ev[0].t_end_s, Some(8.0));
assert_eq!(ev[0].peak, 4.0);
let mut b = SpanDetector::new("y", Some("G01"), Side::Below, 0.0, 1);
b.push(0.0, -1.0);
b.push(1.0, -3.0);
let ev = b.events();
assert_eq!(ev[0].t_end_s, None);
assert_eq!(ev[0].peak, -3.0);
}
#[test]
fn baseline_is_welford() {
let mut b = Baseline::default();
for v in [1.0, 2.0, 3.0, 4.0, f64::NAN] {
b.push(v);
}
assert_eq!(b.count(), 4);
assert!((b.mean() - 2.5).abs() < 1e-15);
assert!((b.std() - (5.0f64 / 3.0).sqrt()).abs() < 1e-15);
}
}