use lox_core::units::{Decibel, Frequency};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Modulation {
Bpsk,
Qpsk,
Psk8,
Qam16,
Qam32,
Qam64,
Qam128,
Qam256,
}
impl Modulation {
pub fn bits_per_symbol(self) -> u8 {
match self {
Modulation::Bpsk => 1,
Modulation::Qpsk => 2,
Modulation::Psk8 => 3,
Modulation::Qam16 => 4,
Modulation::Qam32 => 5,
Modulation::Qam64 => 6,
Modulation::Qam128 => 7,
Modulation::Qam256 => 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LinkDirection {
Uplink,
Downlink,
Crosslink,
}
impl std::fmt::Display for LinkDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LinkDirection::Uplink => write!(f, "uplink"),
LinkDirection::Downlink => write!(f, "downlink"),
LinkDirection::Crosslink => write!(f, "crosslink"),
}
}
}
impl std::str::FromStr for LinkDirection {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"uplink" => Ok(LinkDirection::Uplink),
"downlink" => Ok(LinkDirection::Downlink),
"crosslink" => Ok(LinkDirection::Crosslink),
_ => Err(format!(
"unknown link direction: '{s}', expected 'uplink', 'downlink', or 'crosslink'"
)),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Channel {
pub link_type: LinkDirection,
pub symbol_rate: Frequency,
pub required_eb_n0: Decibel,
pub margin: Decibel,
pub modulation: Modulation,
pub roll_off: f64,
pub fec: f64,
pub chip_rate: Option<Frequency>,
}
impl Channel {
pub fn data_rate(&self) -> Frequency {
self.modulation.bits_per_symbol() as f64 * self.symbol_rate
}
pub fn information_rate(&self) -> Frequency {
self.fec * self.data_rate()
}
pub fn bandwidth(&self) -> Frequency {
let rate = self.chip_rate.unwrap_or(self.symbol_rate);
(1.0 + self.roll_off) * rate
}
pub fn es_n0(&self, c_n0: Decibel) -> Decibel {
c_n0 - Decibel::from_linear(self.symbol_rate.to_hertz())
}
pub fn eb_n0(&self, c_n0: Decibel) -> Decibel {
let es_n0 = self.es_n0(c_n0);
let bps_fec = self.modulation.bits_per_symbol() as f64 * self.fec;
es_n0 - Decibel::from_linear(bps_fec)
}
pub fn c_n(&self, c_n0: Decibel) -> Decibel {
c_n0 - Decibel::from_linear(self.bandwidth().to_hertz())
}
pub fn link_margin(&self, eb_n0: Decibel) -> Decibel {
eb_n0 - self.required_eb_n0 - self.margin
}
pub fn spreading_factor(&self) -> Option<f64> {
self.chip_rate
.map(|cr| cr.to_hertz() / self.symbol_rate.to_hertz())
}
pub fn processing_gain(&self) -> Option<Decibel> {
self.spreading_factor().map(Decibel::from_linear)
}
pub fn es_n0_spread(&self, c_n0: Decibel) -> Option<Decibel> {
self.chip_rate
.map(|cr| c_n0 - Decibel::from_linear(cr.to_hertz()))
}
}
#[cfg(test)]
mod tests {
use lox_core::units::{DecibelUnits, FrequencyUnits};
use lox_test_utils::assert_approx_eq;
use super::*;
fn narrowband_channel() -> Channel {
Channel {
link_type: LinkDirection::Downlink,
symbol_rate: 5.0.mhz(),
required_eb_n0: 10.0.db(),
margin: 3.0.db(),
modulation: Modulation::Qpsk,
roll_off: 0.35,
fec: 0.5,
chip_rate: None,
}
}
#[test]
fn test_bits_per_symbol() {
assert_eq!(Modulation::Bpsk.bits_per_symbol(), 1);
assert_eq!(Modulation::Qpsk.bits_per_symbol(), 2);
assert_eq!(Modulation::Psk8.bits_per_symbol(), 3);
assert_eq!(Modulation::Qam16.bits_per_symbol(), 4);
assert_eq!(Modulation::Qam32.bits_per_symbol(), 5);
assert_eq!(Modulation::Qam64.bits_per_symbol(), 6);
assert_eq!(Modulation::Qam128.bits_per_symbol(), 7);
assert_eq!(Modulation::Qam256.bits_per_symbol(), 8);
}
#[test]
fn test_data_rate() {
let ch = narrowband_channel();
assert_approx_eq!(ch.data_rate().to_hertz(), 10e6, rtol <= 1e-10);
}
#[test]
fn test_information_rate() {
let ch = narrowband_channel();
assert_approx_eq!(ch.information_rate().to_hertz(), 5e6, rtol <= 1e-10);
}
#[test]
fn test_bandwidth_narrowband() {
let ch = narrowband_channel();
assert_approx_eq!(ch.bandwidth().to_hertz(), 6.75e6, rtol <= 1e-10);
}
#[test]
fn test_bandwidth_bpsk() {
let ch = Channel {
link_type: LinkDirection::Downlink,
symbol_rate: 1.0.mhz(),
required_eb_n0: 10.0.db(),
margin: 3.0.db(),
modulation: Modulation::Bpsk,
roll_off: 0.5,
fec: 0.5,
chip_rate: None,
};
assert_approx_eq!(ch.bandwidth().to_hertz(), 1.5e6, rtol <= 1e-10);
}
#[test]
fn test_es_n0() {
let ch = narrowband_channel();
let es_n0 = ch.es_n0(80.0.db());
assert_approx_eq!(es_n0.as_f64(), 13.0103, atol <= 0.001);
}
#[test]
fn test_eb_n0() {
let ch = narrowband_channel();
let eb_n0 = ch.eb_n0(80.0.db());
assert_approx_eq!(eb_n0.as_f64(), 13.0103, atol <= 0.001);
}
#[test]
fn test_eb_n0_with_higher_code_rate() {
let ch = Channel {
fec: 5.0 / 6.0,
..narrowband_channel()
};
let eb_n0 = ch.eb_n0(80.0.db());
let expected = 13.0103 - 10.0 * (2.0 * 5.0 / 6.0_f64).log10();
assert_approx_eq!(eb_n0.as_f64(), expected, atol <= 0.001);
}
#[test]
fn test_c_n() {
let ch = narrowband_channel();
let c_n = ch.c_n(80.0.db());
let expected = 80.0 - 10.0 * 6.75e6_f64.log10();
assert_approx_eq!(c_n.as_f64(), expected, atol <= 0.001);
}
#[test]
fn test_link_margin() {
let ch = narrowband_channel();
let margin = ch.link_margin(15.0.db());
assert_approx_eq!(margin.as_f64(), 2.0, atol <= 1e-10);
}
#[test]
fn test_dsss_spreading_factor() {
let ch = Channel {
chip_rate: Some(4.0.mhz()),
symbol_rate: 0.01.mhz(),
modulation: Modulation::Bpsk,
..narrowband_channel()
};
assert_approx_eq!(ch.spreading_factor().unwrap(), 400.0, rtol <= 1e-10);
}
#[test]
fn test_dsss_processing_gain() {
let ch = Channel {
chip_rate: Some(4.0.mhz()),
symbol_rate: 0.01.mhz(),
modulation: Modulation::Bpsk,
..narrowband_channel()
};
assert_approx_eq!(
ch.processing_gain().unwrap().as_f64(),
26.0206,
atol <= 0.001
);
}
#[test]
fn test_dsss_bandwidth() {
let ch = Channel {
chip_rate: Some(4.0.mhz()),
symbol_rate: 0.01.mhz(),
modulation: Modulation::Bpsk,
..narrowband_channel()
};
assert_approx_eq!(ch.bandwidth().to_hertz(), 5.4e6, rtol <= 1e-10);
}
#[test]
fn test_dsss_es_n0_spread_vs_despread() {
let ch = Channel {
chip_rate: Some(4.0.mhz()),
symbol_rate: 0.01.mhz(),
modulation: Modulation::Bpsk,
..narrowband_channel()
};
let c_n0 = 60.0.db();
let es_n0_spread = ch.es_n0_spread(c_n0).unwrap();
let es_n0_despread = ch.es_n0(c_n0);
let diff = es_n0_despread - es_n0_spread;
assert_approx_eq!(
diff.as_f64(),
ch.processing_gain().unwrap().as_f64(),
atol <= 1e-10
);
}
#[test]
fn test_narrowband_no_dsss() {
let ch = narrowband_channel();
assert!(ch.spreading_factor().is_none());
assert!(ch.processing_gain().is_none());
assert!(ch.es_n0_spread(80.0.db()).is_none());
}
#[test]
fn test_link_direction_display() {
assert_eq!(LinkDirection::Uplink.to_string(), "uplink");
assert_eq!(LinkDirection::Downlink.to_string(), "downlink");
assert_eq!(LinkDirection::Crosslink.to_string(), "crosslink");
}
#[test]
fn test_link_direction_from_str() {
assert_eq!(
"uplink".parse::<LinkDirection>().unwrap(),
LinkDirection::Uplink
);
assert_eq!(
"downlink".parse::<LinkDirection>().unwrap(),
LinkDirection::Downlink
);
assert_eq!(
"crosslink".parse::<LinkDirection>().unwrap(),
LinkDirection::Crosslink
);
assert!("invalid".parse::<LinkDirection>().is_err());
}
}