use crate::blocks::{BuildContext, parse_constellation};
use comnum_channel::optical::gn::{FormatMoments, GnModel, WdmSpectrum};
use comnum_core::{Error, Result, ensure};
use comnum_mapping::constellation::{Constellation, ConstellationKind};
use comnum_mapping::theory;
use comnum_math::{db2lin, lin2db};
use comnum_metrics as metrics;
use serde::{Deserialize, Deserializer};
use serde_json::Value as Json;
#[derive(Deserialize, Debug)]
#[serde(tag = "function", rename_all = "snake_case")]
pub(super) enum Theory {
Ber(ErrorRate),
Ser(ErrorRate),
Mi(Information),
Gmi(Information),
Ebn0ForBer(Inverse),
Ebn0ForSer(Inverse),
SnrForBer(Inverse),
SnrForSer(Inverse),
GnSnrDb(Gn),
BerRayleigh(Rayleigh),
Dvbs2Esn0Db(Dvbs2),
}
fn parsed<'de, D: Deserializer<'de>, T: std::str::FromStr<Err = Error>>(d: D) -> std::result::Result<T, D::Error> {
String::deserialize(d)?.parse().map_err(serde::de::Error::custom)
}
fn d_qam() -> ConstellationKind {
ConstellationKind::Qam
}
fn d_m() -> usize {
4
}
fn d_points() -> usize {
comnum_mapping::theory::DEFAULT_QUADRATURE_POINTS
}
fn d_one() -> usize {
1
}
fn d_true() -> bool {
true
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct ErrorRate {
#[serde(default = "d_qam", deserialize_with = "parsed")]
kind: ConstellationKind,
#[serde(default = "d_m")]
m: usize,
#[serde(default)]
ebn0_db: Option<f64>,
#[serde(default)]
snr_db: Option<f64>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct Information {
#[serde(default = "d_qam", deserialize_with = "parsed")]
kind: ConstellationKind,
#[serde(default = "d_m")]
m: usize,
#[serde(default)]
ebn0_db: Option<f64>,
#[serde(default)]
snr_db: Option<f64>,
#[serde(default = "d_points")]
points: usize,
#[serde(default)]
mb_lambda: Option<f64>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct Inverse {
#[serde(default = "d_qam", deserialize_with = "parsed")]
kind: ConstellationKind,
#[serde(default = "d_m")]
m: usize,
target: f64,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct Gn {
#[serde(default)]
fiber: Option<Json>,
n_channels: usize,
spacing_hz: f64,
symbol_rate: f64,
#[serde(default)]
rolloff: f64,
power_dbm: f64,
#[serde(default = "d_true")]
coherent: bool,
#[serde(default)]
format: Option<Json>,
#[serde(default)]
mb_lambda: Option<f64>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct Rayleigh {
#[serde(default = "d_qam", deserialize_with = "parsed")]
kind: ConstellationKind,
#[serde(default = "d_m")]
m: usize,
#[serde(default)]
ebn0_db: Option<f64>,
#[serde(default)]
snr_db: Option<f64>,
#[serde(default)]
offset_db: f64,
#[serde(default = "d_one")]
branches: usize,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(super) struct Dvbs2 {
modulation: String,
rate: String,
#[serde(default)]
offset_db: f64,
}
fn ebn0_of(ebn0_db: Option<f64>, snr_db: Option<f64>, m: usize, name: &str) -> Result<f64> {
ebn0_db
.or(snr_db.map(|s| s - lin2db((m as f64).log2())))
.ok_or_else(|| Error::Config(format!("expectation {name:?}: the theory needs ebn0_db or snr_db")))
}
impl Theory {
pub(super) fn value(&self, name: &str) -> Result<f64> {
let bits = |m: usize| lin2db((m as f64).log2());
Ok(match self {
Theory::Ber(t) => metrics::theory_ber(t.kind, t.m, ebn0_of(t.ebn0_db, t.snr_db, t.m, name)?)?,
Theory::Ser(t) => metrics::theory_ser(t.kind, t.m, ebn0_of(t.ebn0_db, t.snr_db, t.m, name)?)?,
Theory::Mi(t) | Theory::Gmi(t) => {
let px = match t.mb_lambda {
Some(l) => Some(comnum_mapping::source::maxwell_boltzmann(
&Constellation::new(t.kind, t.m)?.normalized(),
l,
)),
None => None,
};
let snr = t.snr_db.or(t.ebn0_db.map(|e| e + bits(t.m))).ok_or_else(|| {
Error::Config(format!("expectation {name:?}: the theory needs snr_db or ebn0_db"))
})?;
let f = if matches!(self, Theory::Mi(_)) { metrics::theory_mi } else { metrics::theory_gmi };
f(t.kind, t.m, snr, px.as_deref(), t.points)?
}
Theory::Ebn0ForBer(t) => theory::ebn0_for_ber(t.kind, t.m, t.target)?,
Theory::Ebn0ForSer(t) => theory::ebn0_for_ser(t.kind, t.m, t.target)?,
Theory::SnrForBer(t) => theory::ebn0_for_ber(t.kind, t.m, t.target)? + bits(t.m),
Theory::SnrForSer(t) => theory::ebn0_for_ser(t.kind, t.m, t.target)? + bits(t.m),
Theory::GnSnrDb(t) => t.snr_db()?,
Theory::BerRayleigh(t) => {
ensure(t.m <= 4 && matches!(t.kind, ConstellationKind::Qam | ConstellationKind::Psk), || {
format!("expectation {name:?}: ber_rayleigh is for BPSK and QPSK")
})?;
theory::ber_rayleigh_mrc(db2lin(ebn0_of(t.ebn0_db, t.snr_db, t.m, name)? + t.offset_db), t.branches)?
}
Theory::Dvbs2Esn0Db(t) => {
comnum_systems::dvbs2::Modcod::from_name(&t.modulation, &t.rate)?.threshold_db() + t.offset_db
}
})
}
}
impl Gn {
fn snr_db(&self) -> Result<f64> {
let ctx = BuildContext::default();
let fiber = ctx.fiber(self.fiber.as_ref())?;
let freqs = comnum_dsp::signal::wdm_grid(self.n_channels, self.spacing_hz);
let p = comnum_math::dbm2w(self.power_dbm);
let egn = match &self.format {
None => None,
Some(Json::String(s)) if s.eq_ignore_ascii_case("gaussian") => Some(FormatMoments::GAUSSIAN),
Some(v) => {
let c = parse_constellation(v)?.normalized();
let px = self.mb_lambda.map(|l| comnum_mapping::source::maxwell_boltzmann(&c, l));
Some(FormatMoments::of(c.points(), px.as_deref())?)
}
};
let model =
GnModel { coherent: self.coherent, raman: ctx.raman(self.fiber.as_ref())?, egn, ..GnModel::new(fiber) };
let snr = model.snr(&WdmSpectrum::uniform(freqs, p, self.symbol_rate, self.rolloff), self.n_channels / 2)?;
Ok(lin2db(snr))
}
}