use crate::blocks::schema::ParamType;
use crate::blocks::{Frame, Params};
use comnum_core::{Error, Result, ensure_shape};
use comnum_math::{C64, energy};
use comnum_metrics as metrics;
use comnum_sim::scenario::Record;
use serde::Deserialize;
use serde_json::Value as Json;
use std::str::FromStr;
macro_rules! metrics {
($($variant:ident = $name:literal $(, $coded:ident)?;)*) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Metric {
$(
#[doc = concat!("`", $name, "`.")]
$variant,
)*
}
pub const METRICS: [&str; [$($name),*].len()] = [$($name),*];
impl Metric {
pub const ALL: [Metric; METRICS.len()] = [$(Metric::$variant),*];
pub fn name(self) -> &'static str {
match self {
$(Metric::$variant => $name,)*
}
}
pub fn is_coded(self) -> bool {
match self {
$(Metric::$variant => metrics!(@coded $($coded)?),)*
}
}
}
};
(@coded coded) => { true };
(@coded) => { false };
}
metrics! {
Ber = "ber";
Ser = "ser";
SnrDb = "snr_db";
SnrEffDb = "snr_eff_db";
Evm = "evm";
Mse = "mse";
Gmi = "gmi";
Ngmi = "ngmi";
Mi = "mi";
Entropy = "entropy";
Air = "air";
AirBmd = "air_bmd";
AirOpt = "air_opt";
AirBmdOpt = "air_bmd_opt";
PosteriorCalibration = "posterior_calibration";
CodedBer = "coded_ber", coded;
Bler = "bler", coded;
}
impl FromStr for Metric {
type Err = Error;
fn from_str(s: &str) -> Result<Metric> {
Metric::ALL
.into_iter()
.find(|m| m.name().eq_ignore_ascii_case(s))
.ok_or_else(|| crate::unknown("metric", s, &METRICS))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Scale {
Power,
Ls,
None,
}
impl ParamType for Scale {
fn schema() -> Json {
String::schema()
}
}
crate::block_params! {
#[derive(Clone, Debug)]
pub(crate) struct MetricsWindow {
skip_start: usize [0] "symbols", "symbols left out at the start of every polarisation (the edges of the filters)";
skip_end: usize [0] "symbols", "symbols left out at the end of every polarisation";
scale: Scale ["power"] one_of ["power", "ls", "none"],
"scaling before the decisions: power (blind), ls (least-squares complex gain, genie-aided) or none";
}
}
crate::block_params! {
struct MetricsSpec {
report: Vec<String> [["ber", "ser", "snr_db"] => "[ber, ser, snr_db]"] one_of (METRICS), "the metrics reported";
window: MetricsWindow [flatten], "the window and the scaling";
}
}
#[derive(Clone, Debug)]
pub struct Metrics {
report: Vec<Metric>,
window: MetricsWindow,
}
#[derive(Default)]
struct Shared {
info: Option<metrics::InformationRates>,
posterior: Option<metrics::PosteriorRates>,
}
impl Metrics {
pub fn from_json(v: &Json) -> Result<Metrics> {
let q = MetricsSpec::parse("metrics", v.clone())?;
let report = q.report.iter().map(|m| m.parse()).collect::<Result<_>>()?;
Ok(Metrics { report, window: q.window })
}
pub(crate) fn with_window(report: Vec<Metric>, window: MetricsWindow) -> Metrics {
Metrics { report, window }
}
pub fn names(&self) -> Vec<&'static str> {
self.report.iter().map(|m| m.name()).collect()
}
pub fn record_frame(&self, s: &Frame, rec: &mut Record) -> Result<()> {
self.record(s, rec)?;
for (k, v) in &s.values {
rec.value(k, *v);
}
rec.merge(&s.probes);
Ok(())
}
pub fn record(&self, s: &Frame, rec: &mut Record) -> Result<()> {
if self.report.is_empty() {
return Ok(());
}
let segments = if s.layout.polarisations { s.n_streams() } else { 1 };
let pooled = s.pooled();
let s: &Frame = &pooled;
self.record_coded(s, rec)?;
if self.report.iter().all(|m| m.is_coded()) {
return Ok(());
}
s.require_symbols("metrics")?;
let idx = self.window(s, segments)?;
let x: Vec<C64> = idx.iter().map(|&i| s.symbols[i]).collect();
let raw: Vec<C64> = idx.iter().map(|&i| s.samples[i]).collect();
let tx: Vec<usize> = idx.iter().filter_map(|&i| s.indices.get(i).copied()).collect();
let g = self.gain(&raw, &x)?;
let z: Vec<C64> = raw.iter().map(|v| v * g).collect();
let mut shared = Shared::default();
for &m in &self.report {
let name = m.name();
match m {
Metric::Ber => {
let c = s.constellation("metrics: ber")?;
let b = c.bits_per_symbol();
let bits: Vec<u8> = idx.iter().flat_map(|&i| s.bits[i * b..(i + 1) * b].iter().copied()).collect();
rec.errors(name, metrics::bit_errors(&c.demap_bits(&z), &bits)? as u64, bits.len() as u64);
}
Metric::Ser => {
let c = s.constellation("metrics: ser")?;
rec.errors(name, metrics::symbol_errors(&c.decide(&z), &tx)? as u64, tx.len() as u64);
}
Metric::SnrDb => rec.value(name, metrics::snr_db(&z, &x)?),
Metric::SnrEffDb => rec.value(name, metrics::effective_snr_db(&raw, &x)?),
Metric::Evm => rec.value(name, metrics::evm_data_aided(&z, &x)?),
Metric::Mse => rec.value(name, metrics::mse(&z, &x)?),
Metric::Gmi | Metric::Ngmi | Metric::Mi | Metric::Entropy => {
let r = match shared.info {
Some(r) => r,
None => *shared.info.insert(metrics::information_rates(
&raw,
&tx,
s.constellation("metrics: information rates")?,
s.pmf.as_deref(),
)?),
};
let v = match m {
Metric::Gmi => r.gmi,
Metric::Ngmi => r.ngmi,
Metric::Entropy => r.entropy,
_ => r.mi,
};
rec.value(name, v);
}
Metric::Air | Metric::AirBmd | Metric::AirOpt | Metric::AirBmdOpt | Metric::PosteriorCalibration => {
let r = match shared.posterior {
Some(r) => r,
None => *shared.posterior.insert(Self::posterior(s, name, &raw, &tx)?),
};
let v = match m {
Metric::Air => r.air,
Metric::AirBmd => r.air_bmd,
Metric::AirOpt => r.air_opt,
Metric::AirBmdOpt => r.air_bmd_opt,
_ => r.calibration,
};
rec.value(name, v);
}
Metric::CodedBer | Metric::Bler => {}
}
}
Ok(())
}
fn record_coded(&self, s: &Frame, rec: &mut Record) -> Result<()> {
for m in self.report.iter().filter(|m| m.is_coded()) {
let c = &s.coding;
if c.payload.is_empty() || c.decoded.len() != c.payload.len() {
return Err(Error::Config(format!(
"metric {:?} needs a coded link: fec_encode first and fec_decode before the metrics",
m.name()
)));
}
if *m == Metric::CodedBer {
let e = metrics::bit_errors(&c.payload, &c.decoded)?;
rec.errors(m.name(), e as u64, c.payload.len() as u64);
} else {
let k = c.payload.len() / c.n_blocks.max(1);
let e = metrics::block_errors(&c.decoded, &c.payload, k)?;
rec.errors(m.name(), e as u64, c.n_blocks.max(1) as u64);
}
}
Ok(())
}
fn window(&self, s: &Frame, segments: usize) -> Result<Vec<usize>> {
let n = s.n_symbols();
let per = n / segments;
let (start, end) = (self.window.skip_start, self.window.skip_end);
let idx: Vec<usize> = (0..segments)
.flat_map(|p| {
let hi = per.saturating_sub(end);
(start.min(hi)..hi).map(move |i| p * per + i)
})
.filter(|&i| !s.excluded.get(i).copied().unwrap_or(false))
.collect();
ensure_shape(!idx.is_empty(), || format!("the metrics window of the {n} symbols is empty"))?;
Ok(idx)
}
fn gain(&self, z: &[C64], x: &[C64]) -> Result<C64> {
Ok(match self.window.scale {
Scale::None => C64::new(1.0, 0.0),
Scale::Power => {
let (pz, px) = (energy(z), energy(x));
C64::new(if pz > 0.0 { (px / pz).sqrt() } else { 1.0 }, 0.0)
}
Scale::Ls => metrics::ls_gain(z, x)?,
})
}
fn posterior(s: &Frame, name: &str, raw: &[C64], tx: &[usize]) -> Result<metrics::PosteriorRates> {
let s2 = s.receiver.noise_var.ok_or_else(|| {
Error::Config(format!(
"metric {name:?} needs a block that declares the receiver's posterior \
(a learned receiver, or noise_estimate after a classic receiver)"
))
})?;
metrics::posterior_rates(raw, tx, s.constellation("metrics: air")?, s2, s.pmf.as_deref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_names_are_read_without_case() {
let m = Metrics::from_json(&serde_json::json!({"report": ["BER", "Snr_dB"], "scale": "LS"})).unwrap();
assert_eq!(m.names(), ["ber", "snr_db"]);
assert_eq!(m.window.scale, Scale::Ls);
let e = Metrics::from_json(&serde_json::json!({"report": ["fer"]})).unwrap_err().to_string();
assert!(e.contains("report must be one of") && e.contains("coded_ber"), "{e}");
assert_eq!(Metrics::from_json(&Json::Null).unwrap().names(), ["ber", "ser", "snr_db"]);
assert_eq!(Metric::ALL.map(Metric::name), METRICS);
}
#[test]
fn gaussian_symbols_have_an_snr_and_no_symbol_errors() {
let spec = crate::Spec::from_yaml(
"name: g\nlink: [{type: source, constellation: gaussian, n_symbols: 256}, {type: awgn, snr_db: 10}]\n\
metrics: {report: [snr_db]}\nstop: {max_trials: 2}",
)
.unwrap();
let r = crate::run(&spec, &crate::RunOptions::default()).unwrap();
assert!((r[0].metric("snr_db").unwrap().value() - 10.0).abs() < 1.0);
}
}