use crate::ber;
use crate::modulation::Modulation;
pub const CODING_GAIN_CONV_R12_K7: f64 = 5.0;
pub const CODING_GAIN_CONV_R34_K7: f64 = 3.5;
pub const CODING_GAIN_TURBO_R12: f64 = 7.5;
pub const CODING_GAIN_TURBO_R34: f64 = 5.5;
pub const CODING_GAIN_LDPC_R12: f64 = 8.0;
pub const CODING_GAIN_LDPC_R23: f64 = 7.0;
pub const CODING_GAIN_LDPC_R34: f64 = 6.5;
pub const CODING_GAIN_LDPC_R56: f64 = 5.5;
pub const CODING_GAIN_LDPC_R910: f64 = 5.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FecCode {
Uncoded,
Convolutional {
rate: f64,
},
Turbo {
rate: f64,
},
Ldpc {
rate: f64,
},
Custom {
rate: f64,
coding_gain_db: f64,
},
}
impl FecCode {
#[must_use]
pub fn rate(&self) -> f64 {
match self {
FecCode::Uncoded => 1.0,
FecCode::Convolutional { rate } => *rate,
FecCode::Turbo { rate } => *rate,
FecCode::Ldpc { rate } => *rate,
FecCode::Custom { rate, .. } => *rate,
}
}
#[must_use]
pub fn coding_gain_db(&self) -> f64 {
match self {
FecCode::Uncoded => 0.0,
FecCode::Convolutional { rate } => {
lerp_gain(
*rate,
0.5,
CODING_GAIN_CONV_R12_K7,
0.75,
CODING_GAIN_CONV_R34_K7,
)
}
FecCode::Turbo { rate } => lerp_gain(
*rate,
0.5,
CODING_GAIN_TURBO_R12,
0.75,
CODING_GAIN_TURBO_R34,
),
FecCode::Ldpc { rate } => {
if *rate <= 0.5 {
CODING_GAIN_LDPC_R12
} else if *rate <= 2.0 / 3.0 {
lerp_gain(
*rate,
0.5,
CODING_GAIN_LDPC_R12,
2.0 / 3.0,
CODING_GAIN_LDPC_R23,
)
} else if *rate <= 0.75 {
lerp_gain(
*rate,
2.0 / 3.0,
CODING_GAIN_LDPC_R23,
0.75,
CODING_GAIN_LDPC_R34,
)
} else if *rate <= 5.0 / 6.0 {
lerp_gain(
*rate,
0.75,
CODING_GAIN_LDPC_R34,
5.0 / 6.0,
CODING_GAIN_LDPC_R56,
)
} else {
lerp_gain(
*rate,
5.0 / 6.0,
CODING_GAIN_LDPC_R56,
0.9,
CODING_GAIN_LDPC_R910,
)
}
}
FecCode::Custom { coding_gain_db, .. } => *coding_gain_db,
}
}
}
fn lerp_gain(rate: f64, r1: f64, g1: f64, r2: f64, g2: f64) -> f64 {
if (r2 - r1).abs() < 1e-10 {
return g1;
}
let t = ((rate - r1) / (r2 - r1)).clamp(0.0, 1.0);
g1 + t * (g2 - g1)
}
impl std::fmt::Display for FecCode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FecCode::Uncoded => write!(f, "Uncoded"),
FecCode::Convolutional { rate } => write!(f, "Convolutional (R={})", rate),
FecCode::Turbo { rate } => write!(f, "Turbo (R={})", rate),
FecCode::Ldpc { rate } => write!(f, "LDPC (R={})", rate),
FecCode::Custom {
rate,
coding_gain_db,
} => {
write!(f, "Custom (R={}, gain={} dB)", rate, coding_gain_db)
}
}
}
}
#[derive(Debug, Clone)]
pub struct CodedModulation {
pub modulation: Modulation,
pub fec: FecCode,
}
impl CodedModulation {
#[must_use]
pub fn new(modulation: Modulation, fec: FecCode) -> Self {
Self { modulation, fec }
}
#[must_use]
pub fn code_rate(&self) -> f64 {
self.fec.rate()
}
#[must_use]
pub fn coding_gain_db(&self) -> f64 {
self.fec.coding_gain_db()
}
#[must_use]
pub fn spectral_efficiency(&self) -> f64 {
self.modulation.spectral_efficiency(self.fec.rate())
}
#[must_use]
pub fn throughput_bps(&self, bandwidth_hz: f64) -> f64 {
bandwidth_hz * self.spectral_efficiency()
}
#[doc(alias = "Eb/N0")]
#[must_use]
pub fn required_eb_no_db(&self, target_ber: f64) -> Option<f64> {
let uncoded = ber::required_eb_no_db(target_ber, &self.modulation)?;
Some(uncoded - self.fec.coding_gain_db())
}
#[doc(alias = "BER")]
#[must_use]
pub fn ber_from_db(&self, eb_no_db: f64) -> f64 {
let effective_eb_no_db = eb_no_db + self.fec.coding_gain_db();
ber::ber_from_db(effective_eb_no_db, &self.modulation)
}
#[doc(alias = "link margin")]
#[must_use]
pub fn link_margin_db(&self, actual_eb_no_db: f64, target_ber: f64) -> Option<f64> {
let required = self.required_eb_no_db(target_ber)?;
Some(actual_eb_no_db - required)
}
#[must_use]
pub fn symbol_rate(&self, info_bit_rate_bps: f64) -> f64 {
self.modulation
.symbol_rate(info_bit_rate_bps, self.fec.rate())
}
}
impl std::fmt::Display for CodedModulation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} + {}", self.modulation, self.fec)
}
}
#[must_use]
pub fn dvbs2_qpsk_r12() -> CodedModulation {
CodedModulation::new(Modulation::Qpsk, FecCode::Ldpc { rate: 0.5 })
}
#[must_use]
pub fn dvbs2_qpsk_r34() -> CodedModulation {
CodedModulation::new(Modulation::Qpsk, FecCode::Ldpc { rate: 0.75 })
}
#[must_use]
pub fn dvbs2_8psk_r23() -> CodedModulation {
CodedModulation::new(Modulation::Mpsk(8), FecCode::Ldpc { rate: 2.0 / 3.0 })
}
#[must_use]
pub fn dvbs2_16apsk_r34() -> CodedModulation {
CodedModulation::new(Modulation::Mqam(16), FecCode::Ldpc { rate: 0.75 })
}
#[must_use]
pub fn dvbs2_32apsk_r56() -> CodedModulation {
CodedModulation::new(Modulation::Mqam(32), FecCode::Ldpc { rate: 5.0 / 6.0 })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uncoded_zero_gain() {
let fec = FecCode::Uncoded;
assert_eq!(fec.rate(), 1.0);
assert_eq!(fec.coding_gain_db(), 0.0);
}
#[test]
fn conv_r12_gain() {
let fec = FecCode::Convolutional { rate: 0.5 };
assert!((fec.coding_gain_db() - 5.0).abs() < 1e-10);
}
#[test]
fn conv_r34_gain() {
let fec = FecCode::Convolutional { rate: 0.75 };
assert!((fec.coding_gain_db() - 3.5).abs() < 1e-10);
}
#[test]
fn turbo_r12_gain() {
let fec = FecCode::Turbo { rate: 0.5 };
assert!((fec.coding_gain_db() - 7.5).abs() < 1e-10);
}
#[test]
fn ldpc_r12_gain() {
let fec = FecCode::Ldpc { rate: 0.5 };
assert!((fec.coding_gain_db() - 8.0).abs() < 1e-10);
}
#[test]
fn ldpc_r34_gain() {
let fec = FecCode::Ldpc { rate: 0.75 };
assert!((fec.coding_gain_db() - 6.5).abs() < 1e-10);
}
#[test]
fn ldpc_r910_gain() {
let fec = FecCode::Ldpc { rate: 0.9 };
assert!((fec.coding_gain_db() - 5.0).abs() < 1e-10);
}
#[test]
fn ldpc_interpolated_gain() {
let fec = FecCode::Ldpc { rate: 0.625 };
let gain = fec.coding_gain_db();
assert!(
gain > 7.0 && gain < 8.0,
"Expected interpolated gain between 7.0 and 8.0, got {}",
gain
);
}
#[test]
fn custom_fec() {
let fec = FecCode::Custom {
rate: 0.8,
coding_gain_db: 6.0,
};
assert_eq!(fec.rate(), 0.8);
assert_eq!(fec.coding_gain_db(), 6.0);
}
#[test]
fn coded_modulation_spectral_efficiency() {
let cm = CodedModulation::new(Modulation::Qpsk, FecCode::Ldpc { rate: 0.75 });
assert!((cm.spectral_efficiency() - 1.5).abs() < 1e-10);
}
#[test]
fn coded_modulation_throughput() {
let cm = dvbs2_qpsk_r34();
let tp = cm.throughput_bps(36e6);
assert!((tp - 54e6).abs() < 1.0);
}
#[test]
fn coded_requires_less_eb_no() {
let uncoded = CodedModulation::new(Modulation::Qpsk, FecCode::Uncoded);
let coded = dvbs2_qpsk_r12();
let req_uncoded = uncoded.required_eb_no_db(1e-5).unwrap();
let req_coded = coded.required_eb_no_db(1e-5).unwrap();
assert!(
req_coded < req_uncoded,
"Coded should require less Eb/No: coded={:.1}, uncoded={:.1}",
req_coded,
req_uncoded
);
let diff = req_uncoded - req_coded;
assert!(
(diff - 8.0).abs() < 0.5,
"LDPC R=1/2 coding gain should be ~8 dB, got {:.1} dB",
diff
);
}
#[test]
fn coded_ber_better_than_uncoded() {
let uncoded = CodedModulation::new(Modulation::Qpsk, FecCode::Uncoded);
let coded = dvbs2_qpsk_r34();
let eb_no = 5.0; let ber_uncoded = uncoded.ber_from_db(eb_no);
let ber_coded = coded.ber_from_db(eb_no);
assert!(
ber_coded < ber_uncoded,
"Coded BER should be lower: coded={:.2e}, uncoded={:.2e}",
ber_coded,
ber_uncoded
);
}
#[test]
fn link_margin_positive() {
let cm = dvbs2_qpsk_r34();
let margin = cm.link_margin_db(10.0, 1e-5).unwrap();
assert!(
margin > 0.0,
"10 dB Eb/No should close with QPSK R=3/4 at BER=1e-5"
);
}
#[test]
fn link_margin_negative() {
let cm = CodedModulation::new(Modulation::Mqam(64), FecCode::Uncoded);
let margin = cm.link_margin_db(5.0, 1e-6).unwrap();
assert!(
margin < 0.0,
"5 dB Eb/No should NOT close for uncoded 64-QAM at BER=1e-6"
);
}
#[test]
fn dvbs2_presets_display() {
assert_eq!(format!("{}", dvbs2_qpsk_r12()), "QPSK + LDPC (R=0.5)");
assert_eq!(
format!("{}", dvbs2_8psk_r23()),
"8-PSK + LDPC (R=0.6666666666666666)"
);
}
#[test]
fn symbol_rate_coded() {
let cm = dvbs2_qpsk_r34();
let rs = cm.symbol_rate(54e6);
assert!((rs - 36e6).abs() < 1.0);
}
}