#[inline]
pub const fn lcg_step(seed: u32) -> u32 {
seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223)
}
#[inline]
pub fn lcg_uniform(seed: u32) -> f32 {
(seed as f32) * (2.0 / u32::MAX as f32) - 1.0
}
#[inline]
pub fn apply_iq_imbalance(norm: f32, phi_rad: f32, epsilon: f32) -> f32 {
let perturbation = (epsilon * 0.5) * cos_approx(2.0 * phi_rad);
(norm * (1.0 + perturbation)).max(0.0)
}
#[inline]
pub fn apply_dc_offset(norm: f32, phi_rad: f32, dc_i: f32, dc_q: f32) -> f32 {
let bias = dc_i * cos_approx(phi_rad) + dc_q * sin_approx(phi_rad);
(norm + bias).max(0.0)
}
#[inline]
pub fn apply_pa_compression(norm: f32, k3: f32) -> f32 {
let factor = 1.0 - k3 * norm * norm;
(norm * factor.max(0.0)).max(0.0)
}
#[inline]
pub fn quantization_noise_std(n_bits: u32) -> f32 {
let lsb = libm_pow2(1i32 - n_bits as i32);
lsb / 3.464_101_6 }
#[inline]
pub fn apply_quantization_noise(norm: f32, n_bits: u32, seed: u32) -> (f32, u32) {
let u_q = quantization_noise_std(n_bits);
let noise_seed = lcg_step(seed);
let w = lcg_uniform(noise_seed); let perturbed = (norm + u_q * w * 3.0).max(0.0);
(perturbed, noise_seed)
}
#[inline]
pub fn apply_phase_noise(norm: f32, sigma_phi: f32, seed: u32) -> (f32, u32) {
let noise_seed = lcg_step(seed);
let delta_phi = sigma_phi * lcg_uniform(noise_seed); let perturbation = norm * sin_approx(delta_phi).abs();
let perturbed = (norm + perturbation).max(0.0);
(perturbed, noise_seed)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScintillationClass {
Weak,
Moderate,
Strong,
}
#[inline]
pub const fn classify_s4(s4: f32) -> ScintillationClass {
if s4 < 0.30 {
ScintillationClass::Weak
} else if s4 < 0.60 {
ScintillationClass::Moderate
} else {
ScintillationClass::Strong
}
}
#[inline]
pub fn apply_scintillation(norm: f32, s4: f32, seed: u32) -> (f32, u32, ScintillationClass) {
let noise_seed = lcg_step(seed);
let w = lcg_uniform(noise_seed);
let perturbed = (norm * (1.0 + s4 * w)).max(0.0);
let cls = classify_s4(s4);
(perturbed, noise_seed, cls)
}
#[inline]
pub fn doppler_residual_floor(f_d_hz: f32, k_v_hz: f32, nominal_norm: f32) -> f32 {
if k_v_hz <= 0.0 {
return nominal_norm;
}
let theta_ss = core::f32::consts::TAU * f_d_hz.abs() / k_v_hz;
(nominal_norm + theta_ss * nominal_norm).max(nominal_norm)
}
#[derive(Debug, Clone, Copy)]
pub struct ImpairmentVector {
pub iq_imbalance_epsilon: f32,
pub dc_offset_i: f32,
pub dc_offset_q: f32,
pub pa_k3: f32,
pub adc_bits: u32,
pub phase_noise_sigma: f32,
pub scintillation_s4: f32,
}
impl ImpairmentVector {
pub const RTL_SDR: Self = Self {
iq_imbalance_epsilon: 0.012,
dc_offset_i: 0.015,
dc_offset_q: 0.010,
pa_k3: 0.0,
adc_bits: 8,
phase_noise_sigma: 0.08,
scintillation_s4: 0.0,
};
pub const USRP_X310: Self = Self {
iq_imbalance_epsilon: 0.001,
dc_offset_i: 0.001,
dc_offset_q: 0.001,
pa_k3: 0.0,
adc_bits: 14,
phase_noise_sigma: 0.015,
scintillation_s4: 0.0,
};
pub const COLOSSEUM_NODE: Self = Self {
iq_imbalance_epsilon: 0.004,
dc_offset_i: 0.003,
dc_offset_q: 0.002,
pa_k3: 0.12,
adc_bits: 12,
phase_noise_sigma: 0.025,
scintillation_s4: 0.0,
};
pub const ESA_L_BAND_MODERATE: Self = Self {
iq_imbalance_epsilon: 0.002,
dc_offset_i: 0.001,
dc_offset_q: 0.001,
pa_k3: 0.0,
adc_bits: 14,
phase_noise_sigma: 0.020,
scintillation_s4: 0.40,
};
pub const ESA_L_BAND_STRONG: Self = Self {
iq_imbalance_epsilon: 0.002,
dc_offset_i: 0.001,
dc_offset_q: 0.001,
pa_k3: 0.0,
adc_bits: 14,
phase_noise_sigma: 0.020,
scintillation_s4: 0.70,
};
pub const NONE: Self = Self {
iq_imbalance_epsilon: 0.0,
dc_offset_i: 0.0,
dc_offset_q: 0.0,
pa_k3: 0.0,
adc_bits: 0,
phase_noise_sigma: 0.0,
scintillation_s4: 0.0,
};
}
pub fn apply_all(norm: f32, phi_rad: f32, seed: u32, imp: ImpairmentVector) -> (f32, u32) {
let mut r = norm;
let mut s = seed;
if imp.pa_k3 > 0.0 {
r = apply_pa_compression(r, imp.pa_k3);
}
if imp.scintillation_s4 > 0.0 {
let (rr, ss, _) = apply_scintillation(r, imp.scintillation_s4, s);
r = rr;
s = ss;
}
if imp.dc_offset_i.abs() > 0.0 || imp.dc_offset_q.abs() > 0.0 {
r = apply_dc_offset(r, phi_rad, imp.dc_offset_i, imp.dc_offset_q);
}
if imp.iq_imbalance_epsilon > 0.0 {
r = apply_iq_imbalance(r, phi_rad, imp.iq_imbalance_epsilon);
}
if imp.phase_noise_sigma > 0.0 {
let (rr, ss) = apply_phase_noise(r, imp.phase_noise_sigma, s);
r = rr;
s = ss;
}
if imp.adc_bits > 0 {
let (rr, ss) = apply_quantization_noise(r, imp.adc_bits, s);
r = rr;
s = ss;
}
(r.max(0.0), s)
}
#[inline]
pub fn sin_approx(x: f32) -> f32 {
use core::f32::consts::{PI, TAU};
let mut x = x;
for _ in 0..64 {
if x <= PI { break; }
x -= TAU;
}
for _ in 0..64 {
if x >= -PI { break; }
x += TAU;
}
let b = 4.0 / PI;
let c = -4.0 / (PI * PI);
let y = b * x + c * x * x.abs();
0.225 * (y * y.abs() - y) + y
}
#[inline]
pub fn cos_approx(x: f32) -> f32 {
sin_approx(x + core::f32::consts::FRAC_PI_2)
}
#[inline]
fn libm_pow2(n: i32) -> f32 {
if n < -126 { return 0.0; }
if n > 127 { return f32::MAX; }
f32::from_bits(((n + 127) as u32) << 23)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iq_imbalance_bounded() {
let norm = 0.1_f32;
let epsilon = 0.08_f32;
for k in 0..16u32 {
let phi = k as f32 * (core::f32::consts::PI / 8.0);
let r = apply_iq_imbalance(norm, phi, epsilon);
let max_delta = (epsilon * 0.5 + 1e-6) * norm;
assert!(
(r - norm).abs() <= max_delta,
"IQ imbalance exceeded bound: r={r}, norm={norm}, delta={}, max={}",
(r - norm).abs(), max_delta
);
}
}
#[test]
fn dc_offset_small_positive() {
let norm = 0.1_f32;
let r = apply_dc_offset(norm, 0.0, 0.01, 0.0);
let delta = (r - norm).abs();
assert!(delta < 0.02, "DC offset perturbation too large: {delta}");
}
#[test]
fn pa_compression_reduces_norm() {
let norm = 0.5_f32;
let k3 = 0.30_f32;
let r = apply_pa_compression(norm, k3);
assert!(r <= norm, "PA compression should reduce or equal input: {r} vs {norm}");
assert!(r > 0.0, "PA compression should not go negative: {r}");
}
#[test]
fn pa_compression_identity_at_zero() {
let r = apply_pa_compression(0.0, 0.30);
assert!(r == 0.0);
}
#[test]
fn quantization_noise_std_n8() {
let u = quantization_noise_std(8);
assert!(u > 1e-3 && u < 1e-2, "8-bit u_q out of expected range: {u}");
}
#[test]
fn quantization_noise_std_n14() {
let u8 = quantization_noise_std(8);
let u14 = quantization_noise_std(14);
assert!(u14 < u8 * 0.1, "14-bit should be << 8-bit: {u14} vs {u8}");
}
#[test]
fn phase_noise_bounded() {
let norm = 0.1_f32;
let sigma = 0.05_f32;
let (r, _) = apply_phase_noise(norm, sigma, 42);
assert!((r - norm).abs() < sigma + 1e-4, "Phase noise exceeded sigma bound");
}
#[test]
fn scintillation_weak_small_perturbation() {
let norm = 0.1_f32;
let s4 = 0.20_f32; let (r, _, cls) = apply_scintillation(norm, s4, 12345);
assert_eq!(cls, ScintillationClass::Weak);
assert!((r - norm).abs() <= s4 * norm + 1e-6);
}
#[test]
fn scintillation_strong_classifies_correctly() {
let (_, _, cls) = apply_scintillation(0.1, 0.70, 999);
assert_eq!(cls, ScintillationClass::Strong);
}
#[test]
fn doppler_floor_elevated() {
let nominal = 0.05_f32;
let elevated = doppler_residual_floor(1.58, 200.0, nominal);
assert!(elevated > nominal, "Doppler should elevate floor: {elevated} vs {nominal}");
}
#[test]
fn apply_all_none_is_identity() {
let norm = 0.1_f32;
let (r, _) = apply_all(norm, 0.0, 42, ImpairmentVector::NONE);
assert!((r - norm).abs() < 1e-6, "Zero impairment should be identity: {r} vs {norm}");
}
#[test]
fn apply_all_rtl_sdr_bounded() {
let norm = 0.1_f32;
let (r, _) = apply_all(norm, 1.0, 7777, ImpairmentVector::RTL_SDR);
assert!((r - norm).abs() < 0.3 * norm + 0.05,
"RTL-SDR impairment too large: {r} vs {norm}");
}
#[test]
fn lcg_period_deterministic() {
let s0 = 123456789u32;
let s1 = lcg_step(s0);
let s2 = lcg_step(s1);
let s1b = lcg_step(s0);
assert_eq!(s1, s1b, "LCG must be deterministic");
assert_ne!(s1, s2, "Consecutive LCG states must differ");
}
#[test]
fn sin_approx_zero_and_halfpi() {
let s0 = sin_approx(0.0);
let s_halfpi = sin_approx(core::f32::consts::FRAC_PI_2);
assert!(s0.abs() < 0.01, "sin(0) ≈ 0: got {s0}");
assert!((s_halfpi - 1.0).abs() < 0.01, "sin(π/2) ≈ 1: got {s_halfpi}");
}
#[test]
fn classify_s4_boundaries() {
assert_eq!(classify_s4(0.10), ScintillationClass::Weak);
assert_eq!(classify_s4(0.45), ScintillationClass::Moderate);
assert_eq!(classify_s4(0.75), ScintillationClass::Strong);
}
}