use std::f64::consts::PI;
use crate::modulation::Modulation;
#[must_use]
pub fn erfc(x: f64) -> f64 {
if x < 0.0 {
return 2.0 - erfc(-x);
}
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254829592
+ t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
poly * (-x * x).exp()
}
#[must_use]
pub fn q_function(x: f64) -> f64 {
0.5 * erfc(x / std::f64::consts::SQRT_2)
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber_bpsk(eb_no_linear: f64) -> f64 {
q_function((2.0 * eb_no_linear).sqrt())
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber_mpsk(eb_no_linear: f64, m: u32) -> f64 {
if m == 2 {
return ber_bpsk(eb_no_linear);
}
let k = (m as f64).log2();
let sin_term = (PI / m as f64).sin();
(2.0 / k) * q_function((2.0 * k * eb_no_linear).sqrt() * sin_term)
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber_mqam(eb_no_linear: f64, m: u32) -> f64 {
if m == 4 {
return ber_bpsk(eb_no_linear); }
let k = (m as f64).log2();
let sqrt_m = (m as f64).sqrt();
let coeff = (4.0 / k) * (1.0 - 1.0 / sqrt_m);
let arg = (3.0 * k * eb_no_linear / (m as f64 - 1.0)).sqrt();
coeff * q_function(arg)
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber(eb_no_linear: f64, modulation: &Modulation) -> f64 {
match modulation {
Modulation::Bpsk => ber_bpsk(eb_no_linear),
Modulation::Qpsk => ber_bpsk(eb_no_linear),
Modulation::Mpsk(m) => ber_mpsk(eb_no_linear, *m),
Modulation::Mqam(m) => ber_mqam(eb_no_linear, *m),
Modulation::Msk => ber_bpsk(eb_no_linear), }
}
#[doc(alias = "BER")]
#[doc(alias = "Eb/N0")]
#[must_use]
pub fn ber_from_db(eb_no_db: f64, modulation: &Modulation) -> f64 {
let eb_no_linear = 10.0_f64.powf(eb_no_db / 10.0);
ber(eb_no_linear, modulation)
}
#[doc(alias = "link margin")]
#[must_use]
pub fn link_margin_db(
actual_eb_no_db: f64,
target_ber: f64,
modulation: &Modulation,
) -> Option<f64> {
let required = required_eb_no_db(target_ber, modulation)?;
Some(actual_eb_no_db - required)
}
#[doc(alias = "Eb/N0")]
#[must_use]
pub fn required_eb_no_db(target_ber: f64, modulation: &Modulation) -> Option<f64> {
let mut lo = -5.0_f64;
let mut hi = 50.0_f64;
for _ in 0..100 {
let mid = (lo + hi) / 2.0;
let ber_mid = ber_from_db(mid, modulation);
if (ber_mid - target_ber).abs() / target_ber < 1e-6 {
return Some(mid);
}
if ber_mid > target_ber {
lo = mid; } else {
hi = mid; }
}
Some((lo + hi) / 2.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modulation::Modulation;
#[test]
fn erfc_zero() {
assert!((erfc(0.0) - 1.0).abs() < 1e-6);
}
#[test]
fn erfc_large() {
assert!(erfc(5.0) < 1e-10);
}
#[test]
fn erfc_negative() {
assert!((erfc(-0.0) - 1.0).abs() < 1e-6);
let val = erfc(-1.0);
assert!((val - (2.0 - erfc(1.0))).abs() < 1e-6);
}
#[test]
fn bpsk_ber_at_known_points() {
let ber_0db = ber_bpsk(1.0);
assert!((ber_0db - 0.0786).abs() < 0.005);
let ber_10db = ber_bpsk(10.0);
assert!(ber_10db < 1e-4);
assert!(ber_10db > 1e-7);
}
#[test]
fn qpsk_same_as_bpsk() {
let eb_no = 5.0;
let ber_bpsk_val = ber_bpsk(eb_no);
let ber_qpsk_val = ber(eb_no, &Modulation::Qpsk);
assert!((ber_bpsk_val - ber_qpsk_val).abs() < 1e-15);
}
#[test]
fn higher_order_qam_needs_more_eb_no() {
let eb_no = 10.0; let ber_16 = ber_mqam(eb_no, 16);
let ber_64 = ber_mqam(eb_no, 64);
assert!(
ber_64 > ber_16,
"64-QAM should have higher BER than 16-QAM at same Eb/No"
);
}
#[test]
fn required_eb_no_bpsk_1e_minus_5() {
let eb_no = required_eb_no_db(1e-5, &Modulation::Bpsk).unwrap();
assert!((eb_no - 9.6).abs() < 0.2, "Expected ~9.6 dB, got {}", eb_no);
}
#[test]
fn required_eb_no_16qam_1e_minus_6() {
let eb_no = required_eb_no_db(1e-6, &Modulation::Mqam(16)).unwrap();
assert!(
eb_no > 13.0 && eb_no < 16.0,
"Expected ~14.4 dB, got {}",
eb_no
);
}
#[test]
fn ber_from_db_wrapper() {
let ber_val = ber_from_db(10.0, &Modulation::Bpsk);
let ber_direct = ber_bpsk(10.0_f64.powf(10.0 / 10.0));
assert!((ber_val - ber_direct).abs() < 1e-15);
}
#[test]
fn msk_same_as_bpsk() {
let eb_no = 7.0;
let ber_msk = ber(eb_no, &Modulation::Msk);
let ber_bpsk_val = ber_bpsk(eb_no);
assert!((ber_msk - ber_bpsk_val).abs() < 1e-15);
}
#[test]
fn ber_8psk() {
let eb_no = 10.0_f64.powf(10.0 / 10.0);
let ber_qpsk = ber(eb_no, &Modulation::Qpsk);
let ber_8psk = ber(eb_no, &Modulation::Mpsk(8));
assert!(
ber_8psk > ber_qpsk,
"8-PSK should have higher BER than QPSK"
);
}
}