use crate::ber;
use crate::modulation::Modulation;
#[must_use]
pub fn noise_floor_dbm(bandwidth_hz: f64, noise_figure_db: f64) -> f64 {
-174.0 + 10.0 * bandwidth_hz.log10() + noise_figure_db
}
#[must_use]
pub fn sensitivity_matched_filter_dbm(
modulation: &Modulation,
info_bit_rate_bps: f64,
_code_rate: f64,
noise_figure_db: f64,
target_ber: f64,
implementation_loss_db: f64,
) -> Option<f64> {
let required_eb_no_db = ber::required_eb_no_db(target_ber, modulation)?;
let sensitivity = -174.0
+ noise_figure_db
+ required_eb_no_db
+ 10.0 * info_bit_rate_bps.log10()
+ implementation_loss_db;
tracing::debug!(
required_eb_no_db,
noise_figure_db,
info_bit_rate_bps,
implementation_loss_db,
sensitivity_dbm = sensitivity,
"Matched filter sensitivity"
);
Some(sensitivity)
}
#[must_use]
pub fn sensitivity_bandpass_dbm(
modulation: &Modulation,
info_bit_rate_bps: f64,
code_rate: f64,
noise_figure_db: f64,
target_ber: f64,
implementation_loss_db: f64,
rolloff: f64,
) -> Option<f64> {
let matched = sensitivity_matched_filter_dbm(
modulation,
info_bit_rate_bps,
code_rate,
noise_figure_db,
target_ber,
implementation_loss_db,
)?;
let rolloff_penalty_db = 10.0 * (1.0 + rolloff).log10();
Some(matched + rolloff_penalty_db)
}
#[must_use]
pub fn sensitivity_dbm(
modulation: &Modulation,
info_bit_rate_bps: f64,
code_rate: f64,
noise_figure_db: f64,
target_ber: f64,
implementation_loss_db: f64,
_rolloff: f64,
) -> Option<f64> {
sensitivity_matched_filter_dbm(
modulation,
info_bit_rate_bps,
code_rate,
noise_figure_db,
target_ber,
implementation_loss_db,
)
}
#[must_use]
pub fn rolloff_penalty_db(rolloff: f64) -> f64 {
10.0 * (1.0 + rolloff).log10()
}
#[must_use]
pub fn sensitivity_from_snr_dbm(
bandwidth_hz: f64,
noise_figure_db: f64,
required_snr_db: f64,
implementation_loss_db: f64,
) -> f64 {
noise_floor_dbm(bandwidth_hz, noise_figure_db) + required_snr_db + implementation_loss_db
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modulation::Modulation;
#[test]
fn noise_floor_1mhz_3db_nf() {
let nf = noise_floor_dbm(1e6, 3.0);
assert!((nf - (-111.0)).abs() < 0.01);
}
#[test]
fn noise_floor_100mhz_0db_nf() {
let nf = noise_floor_dbm(100e6, 0.0);
assert!((nf - (-94.0)).abs() < 0.01);
}
#[test]
fn sensitivity_matched_bpsk_1mbps() {
let sens =
sensitivity_matched_filter_dbm(&Modulation::Bpsk, 1e6, 1.0, 3.0, 1e-5, 0.0).unwrap();
assert!(
sens > -103.0 && sens < -100.0,
"Expected ~-101.4 dBm, got {}",
sens
);
}
#[test]
fn sensitivity_bandpass_worse_than_matched() {
let matched =
sensitivity_matched_filter_dbm(&Modulation::Qpsk, 10e6, 0.75, 3.0, 1e-6, 0.0).unwrap();
let bandpass =
sensitivity_bandpass_dbm(&Modulation::Qpsk, 10e6, 0.75, 3.0, 1e-6, 0.0, 0.35).unwrap();
assert!(
bandpass > matched,
"Bandpass sensitivity should be worse than matched filter"
);
let diff = bandpass - matched;
assert!(
(diff - 1.303).abs() < 0.01,
"Expected ~1.3 dB penalty for α=0.35, got {:.3} dB",
diff
);
}
#[test]
fn rolloff_zero_no_penalty() {
let penalty = rolloff_penalty_db(0.0);
assert!((penalty - 0.0).abs() < 1e-10);
}
#[test]
fn rolloff_one_3db_penalty() {
let penalty = rolloff_penalty_db(1.0);
assert!((penalty - 3.0103).abs() < 0.001);
}
#[test]
fn rolloff_035_penalty() {
let penalty = rolloff_penalty_db(0.35);
assert!((penalty - 1.303).abs() < 0.01);
}
#[test]
fn rolloff_penalty_table() {
let cases = vec![
(0.00, 0.00),
(0.20, 0.79),
(0.25, 0.97),
(0.35, 1.30),
(0.50, 1.76),
(1.00, 3.01),
];
for (alpha, expected) in cases {
let penalty = rolloff_penalty_db(alpha);
assert!(
(penalty - expected).abs() < 0.01,
"α={}: expected {:.2} dB, got {:.2} dB",
alpha,
expected,
penalty
);
}
}
#[test]
fn sensitivity_legacy_wrapper() {
let legacy = sensitivity_dbm(&Modulation::Bpsk, 1e6, 1.0, 3.0, 1e-5, 0.0, 0.35).unwrap();
let matched =
sensitivity_matched_filter_dbm(&Modulation::Bpsk, 1e6, 1.0, 3.0, 1e-5, 0.0).unwrap();
assert!((legacy - matched).abs() < 1e-10);
}
#[test]
fn sensitivity_qpsk_10mbps() {
let sens =
sensitivity_matched_filter_dbm(&Modulation::Qpsk, 10e6, 0.75, 5.0, 1e-6, 2.0).unwrap();
assert!(
sens > -100.0 && sens < -75.0,
"Expected sensitivity in -100 to -75 dBm range, got {}",
sens
);
}
#[test]
fn sensitivity_from_snr_simple() {
let sens = sensitivity_from_snr_dbm(10e6, 3.0, 10.0, 1.0);
assert!((sens - (-90.0)).abs() < 0.01);
}
#[test]
fn higher_rate_needs_more_power() {
let sens_1m =
sensitivity_matched_filter_dbm(&Modulation::Bpsk, 1e6, 1.0, 3.0, 1e-5, 0.0).unwrap();
let sens_10m =
sensitivity_matched_filter_dbm(&Modulation::Bpsk, 10e6, 1.0, 3.0, 1e-5, 0.0).unwrap();
assert!(
sens_10m > sens_1m,
"Higher bit rate should require more power"
);
assert!(((sens_10m - sens_1m) - 10.0).abs() < 0.5);
}
}