use crate::ber;
use crate::coding::CodedModulation;
use crate::energy;
use crate::modulation::Modulation;
use crate::path_loss::PathLoss;
use crate::phy::PhyRate;
use crate::receiver::Receiver;
use crate::transmitter::Transmitter;
#[doc(alias = "link margin")]
pub struct LinkBudget {
pub name: &'static str,
pub bandwidth: f64,
pub transmitter: Transmitter,
pub receiver: Receiver,
pub path_loss: PathLoss,
#[doc(alias = "rain fade")]
pub frequency_dependent_loss: Option<f64>,
}
impl LinkBudget {
#[doc(alias = "FSPL")]
#[must_use]
pub fn path_loss(&self) -> f64 {
let path_loss_in_db: f64 = self.path_loss.calculate();
let mut total_path_loss_in_db: f64 = path_loss_in_db;
if let Some(frequency_dependent_loss) = self.frequency_dependent_loss {
total_path_loss_in_db += frequency_dependent_loss;
}
total_path_loss_in_db
}
#[must_use]
pub fn pin_at_receiver(&self) -> f64 {
let path_loss_in_db = self.path_loss();
self.transmitter.output_power + self.transmitter.gain - path_loss_in_db + self.receiver.gain
}
#[doc(alias = "SNR")]
#[must_use]
pub fn snr(&self) -> f64 {
self.receiver.calculate_snr(self.pin_at_receiver())
}
#[doc(alias = "SNR")]
#[must_use]
pub fn snr_linear(&self) -> f64 {
10.0_f64.powf(self.snr() / 10.0)
}
#[must_use]
pub fn phy_rate(&self) -> PhyRate {
PhyRate {
bandwidth: self.bandwidth,
snr: self.snr_linear(),
}
}
#[doc(alias = "C/N")]
#[must_use]
pub fn c_over_no(&self) -> f64 {
energy::snr_to_c_over_no(self.snr(), self.receiver.bandwidth)
}
#[doc(alias = "Eb/N0")]
#[must_use]
pub fn eb_no_db(&self, modulation: &Modulation) -> f64 {
let eta = modulation.bits_per_symbol();
self.snr() - 10.0 * eta.log10()
}
#[doc(alias = "Eb/N0")]
#[must_use]
pub fn eb_no_coded_db(&self, coded_mod: &CodedModulation) -> f64 {
let eta = coded_mod.spectral_efficiency();
self.snr() - 10.0 * eta.log10()
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber(&self, modulation: &Modulation) -> f64 {
let eb_no_db = self.eb_no_db(modulation);
ber::ber_from_db(eb_no_db, modulation)
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber_coded(&self, coded_mod: &CodedModulation) -> f64 {
let eb_no_db = self.eb_no_coded_db(coded_mod);
coded_mod.ber_from_db(eb_no_db)
}
#[doc(alias = "link margin")]
#[must_use]
pub fn link_margin_db(&self, modulation: &Modulation, target_ber: f64) -> Option<f64> {
let actual = self.eb_no_db(modulation);
let required = ber::required_eb_no_db(target_ber, modulation)?;
let margin = actual - required;
tracing::debug!(
actual_eb_no_db = actual,
required_eb_no_db = required,
margin_db = margin,
target_ber,
"Link margin"
);
Some(margin)
}
#[doc(alias = "link margin")]
#[must_use]
pub fn link_margin_coded_db(
&self,
coded_mod: &CodedModulation,
target_ber: f64,
) -> Option<f64> {
let actual = self.eb_no_coded_db(coded_mod);
coded_mod.link_margin_db(actual, target_ber)
}
#[must_use]
pub fn throughput_bps(&self, coded_mod: &CodedModulation) -> f64 {
coded_mod.throughput_bps(self.bandwidth)
}
}
#[cfg(test)]
mod budget_tests {
use super::*;
use crate::coding::dvbs2_qpsk_r34;
fn sample_budget() -> LinkBudget {
LinkBudget {
name: "Test Ka-band LEO",
bandwidth: 36e6,
transmitter: Transmitter {
output_power: 10.0,
gain: 35.0,
bandwidth: 36e6,
},
receiver: Receiver {
gain: 40.0,
temperature: 290.0,
noise_figure: 2.0,
bandwidth: 36e6,
},
path_loss: PathLoss {
frequency: 20e9,
distance: 550e3,
},
frequency_dependent_loss: Some(3.0),
}
}
#[test]
fn c_over_no_positive() {
let b = sample_budget();
let c_no = b.c_over_no();
let expected = b.snr() + 10.0 * 36e6_f64.log10();
assert!((c_no - expected).abs() < 0.01);
}
#[test]
fn eb_no_less_than_snr_for_qpsk() {
let b = sample_budget();
let eb_no = b.eb_no_db(&Modulation::Qpsk);
let expected = b.snr() - 10.0 * 2.0_f64.log10();
assert!((eb_no - expected).abs() < 0.01);
}
#[test]
fn ber_returns_valid_value() {
let b = sample_budget();
let ber_val = b.ber(&Modulation::Qpsk);
assert!((0.0..=0.5).contains(&ber_val));
}
#[test]
fn link_margin_qpsk() {
let b = sample_budget();
let margin = b.link_margin_db(&Modulation::Qpsk, 1e-5);
assert!(margin.is_some());
let m = margin.unwrap();
assert!(m > -50.0 && m < 100.0);
}
#[test]
fn coded_margin_better_than_uncoded() {
let b = sample_budget();
let uncoded_margin = b.link_margin_db(&Modulation::Qpsk, 1e-5).unwrap();
let coded = dvbs2_qpsk_r34();
let coded_margin = b.link_margin_coded_db(&coded, 1e-5).unwrap();
assert!(
coded_margin > uncoded_margin,
"Coded margin ({:.1} dB) should exceed uncoded ({:.1} dB)",
coded_margin,
uncoded_margin
);
}
#[test]
fn throughput_matches_spectral_efficiency() {
let b = sample_budget();
let cm = dvbs2_qpsk_r34();
let tp = b.throughput_bps(&cm);
assert!((tp - 54e6).abs() < 1.0);
}
#[test]
fn coded_ber_lower_than_uncoded() {
let b = sample_budget();
let ber_uncoded = b.ber(&Modulation::Qpsk);
let coded = dvbs2_qpsk_r34();
let ber_coded = b.ber_coded(&coded);
assert!(
ber_coded < ber_uncoded,
"Coded BER ({:.2e}) should be lower than uncoded ({:.2e})",
ber_coded,
ber_uncoded
);
}
}