use crate::math::sqrt_f32;
pub const C_LIGHT_M_S: f32 = 299_792_458.0;
pub const MACH_1_SEA_LEVEL_M_S: f32 = 340.29;
#[derive(Debug, Clone, Copy)]
pub struct LorentzFactor {
pub v_r: f32,
pub beta: f32,
pub gamma: f32,
pub time_dilation: f32,
}
impl LorentzFactor {
pub fn from_velocity(v_r_m_s: f32) -> Self {
let beta = (v_r_m_s / C_LIGHT_M_S).abs().min(1.0 - 1e-7);
let gamma = 1.0 / sqrt_f32(1.0 - beta * beta);
Self {
v_r: v_r_m_s,
beta,
gamma,
time_dilation: 1.0 / gamma,
}
}
pub fn from_mach(mach: f32) -> Self {
Self::from_velocity(mach * MACH_1_SEA_LEVEL_M_S)
}
}
pub fn relativistic_doppler_hz(f0_hz: f32, lf: &LorentzFactor) -> f32 {
let beta = lf.beta;
let sign = if lf.v_r >= 0.0 { 1.0 } else { -1.0 };
if sign > 0.0 {
f0_hz * sqrt_f32((1.0 + beta) / (1.0 - beta).max(1e-9))
} else {
f0_hz * sqrt_f32((1.0 - beta) / (1.0 + beta).max(1e-9))
}
}
pub fn doppler_offset_hz(f0_hz: f32, lf: &LorentzFactor) -> f32 {
relativistic_doppler_hz(f0_hz, lf) - f0_hz
}
pub fn classical_doppler_hz(f0_hz: f32, v_r_m_s: f32) -> f32 {
f0_hz * (1.0 + v_r_m_s / C_LIGHT_M_S)
}
pub fn relativistic_correction_residual_hz(f0_hz: f32, lf: &LorentzFactor) -> f32 {
let f_rel = relativistic_doppler_hz(f0_hz, lf);
let f_class = classical_doppler_hz(f0_hz, lf.v_r);
f_rel - f_class
}
#[derive(Debug, Clone, Copy)]
pub struct HighDynamicsSettings {
pub lorentz: LorentzFactor,
pub w_min_corrected: u32,
pub rho_corrected: f32,
pub doppler_hz: f32,
pub relativistic_residual_hz: f32,
pub correction_significant: bool,
}
pub fn high_dynamics_settings(
v_r_m_s: f32,
f0_hz: f32,
w_min_nom: u32,
rho_nominal: f32,
) -> HighDynamicsSettings {
let lf = LorentzFactor::from_velocity(v_r_m_s);
let w_min_corrected = crate::math::round_f32((w_min_nom as f32) * lf.gamma) as u32;
let rho_corrected = rho_nominal * lf.time_dilation; let doppler_hz = doppler_offset_hz(f0_hz, &lf);
let relativistic_residual_hz = relativistic_correction_residual_hz(f0_hz, &lf);
let correction_significant = lf.beta > 3.0e-5;
HighDynamicsSettings {
lorentz: lf,
w_min_corrected,
rho_corrected,
doppler_hz,
relativistic_residual_hz,
correction_significant,
}
}
pub fn mach_for_relativistic_residual(f0_hz: f32, threshold_hz: f32) -> f32 {
if threshold_hz <= 0.0 || f0_hz <= 0.0 { return f32::INFINITY; }
let beta_min = sqrt_f32(2.0 * threshold_hz / f0_hz);
if beta_min >= 1.0 { return f32::INFINITY; }
let v_min = beta_min * C_LIGHT_M_S;
v_min / MACH_1_SEA_LEVEL_M_S
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lorentz_at_rest() {
let lf = LorentzFactor::from_velocity(0.0);
assert!((lf.beta).abs() < 1e-7, "rest: beta={}", lf.beta);
assert!((lf.gamma - 1.0).abs() < 1e-5, "rest: gamma={}", lf.gamma);
assert!((lf.time_dilation - 1.0).abs() < 1e-5);
}
#[test]
fn lorentz_mach5_beta_small() {
let lf = LorentzFactor::from_mach(5.0);
let expected_beta = 5.0 * MACH_1_SEA_LEVEL_M_S / C_LIGHT_M_S;
assert!((lf.beta - expected_beta).abs() < 1e-10,
"Mach 5 beta: {} vs expected {}", lf.beta, expected_beta);
assert!((lf.gamma - 1.0).abs() < 1e-8, "Mach 5 gamma ≈ 1.0");
}
#[test]
fn relativistic_doppler_approaches_increases_freq() {
let lf = LorentzFactor::from_velocity(1000.0); let f0 = 10e9_f32; let fr = relativistic_doppler_hz(f0, &lf);
assert!(fr > f0, "approaching: fr must be > f0: {:.2e}", fr);
}
#[test]
fn classical_doppler_consistent_at_low_velocity() {
let v = 300.0_f32; let f0 = 435e6_f32; let lf = LorentzFactor::from_velocity(v);
let f_rel = relativistic_doppler_hz(f0, &lf);
let f_class = classical_doppler_hz(f0, v);
let frac_diff = ((f_rel - f_class) / f0).abs();
assert!(frac_diff < 1e-12,
"classical and relativistic agree at low velocity: {:.2e}", frac_diff);
}
#[test]
fn high_dynamics_settings_mach10() {
let settings = high_dynamics_settings(
10.0 * MACH_1_SEA_LEVEL_M_S, 10e9_f32, 32, 3.5,
);
assert_eq!(settings.w_min_corrected, 32,
"Mach 10: correction negligible, W unchanged");
assert!((settings.rho_corrected - 3.5).abs() < 0.01,
"Mach 10: ρ correction negligible");
assert!(!settings.correction_significant,
"Mach 10 (beta ~ 1.1e-5) is below 3e-5 significance threshold");
}
#[test]
fn relativistic_residual_mach_calculation() {
let f0 = 10e9_f32; let mach_thresh = mach_for_relativistic_residual(f0, 1e3);
assert!(mach_thresh > 100.0 && mach_thresh < 1000.0,
"1 kHz threshold at 10 GHz: {:.1} Mach", mach_thresh);
let mach_hi = mach_for_relativistic_residual(f0, 1e6);
assert!(mach_hi > 1000.0, "1 MHz threshold: {:.1} Mach", mach_hi);
assert!(mach_for_relativistic_residual(0.0, 1.0).is_infinite());
assert!(mach_for_relativistic_residual(1e9, 0.0).is_infinite());
}
#[test]
fn doppler_offset_sign_convention() {
let lf_approach = LorentzFactor::from_velocity(1000.0);
let lf_recede = LorentzFactor::from_velocity(-1000.0);
let f0 = 1e9_f32;
assert!(doppler_offset_hz(f0, &lf_approach) > 0.0, "approach: positive offset");
assert!(doppler_offset_hz(f0, &lf_recede) < 0.0, "recede: negative offset");
}
}