use kshana::body::Body;
use kshana::ephem_provider::EphemerisProvider;
use kshana::forces::MU_SUN;
use kshana::radiometric::{
delta_dor, dual_freq_plasma_calibration, one_way_doppler, one_way_range, shapiro_delay,
solar_plasma_delay, two_way_doppler, two_way_range, Band,
};
use kshana::timescales::{TwoPartJd, SECONDS_PER_DAY};
const C_M_PER_S: f64 = 299_792_458.0;
const DSN_DOPPLER_FLOOR_M_PER_S: f64 = 5.0e-5;
const DSN_RANGE_FLOOR_M: f64 = 1.0;
const DSN_DELTA_DOR_FLOOR_RAD: f64 = 1.0e-9;
const PROBE_JD: f64 = 2_459_580.5;
const PROBE_JD_DOPPLER: f64 = 0.5;
#[derive(Debug)]
struct ConstantVelocityEphemeris {
t0_jd: f64,
r0: [f64; 3],
v: [f64; 3],
}
impl EphemerisProvider for ConstantVelocityEphemeris {
fn relative_position(&self, _target: &Body, _center: &Body, jd_tdb: f64) -> Option<[f64; 3]> {
let dt_s = (jd_tdb - self.t0_jd) * SECONDS_PER_DAY;
Some([
self.r0[0] + self.v[0] * dt_s,
self.r0[1] + self.v[1] * dt_s,
self.r0[2] + self.v[2] * dt_s,
])
}
}
fn norm(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[test]
fn range_precision_far_below_dsn_ranging_floor() {
let r0 = [1.8e11, 4.0e10, -2.5e10];
let ephem = ConstantVelocityEphemeris {
t0_jd: PROBE_JD,
r0,
v: [0.0, 0.0, 0.0],
};
let station = [0.0, 0.0, 0.0];
let t_rx = TwoPartJd::from_f64(PROBE_JD);
let two_way =
two_way_range(station, &Body::mars(), &Body::sun(), t_rx, &ephem).expect("synthetic range");
let one_way =
one_way_range(station, &Body::mars(), &Body::sun(), t_rx, &ephem).expect("synthetic range");
let closed_form_round_trip =
2.0 * norm([station[0] - r0[0], station[1] - r0[1], station[2] - r0[2]]);
let err_vs_closed_form = (two_way - closed_form_round_trip).abs();
let err_vs_two_one_way = (two_way - 2.0 * one_way).abs();
assert!(
err_vs_closed_form < 1e-3,
"two-way range model error {err_vs_closed_form} m vs closed form is not sub-mm"
);
assert!(
err_vs_two_one_way < 1e-3,
"two-way range vs 2×one-way error {err_vs_two_one_way} m is not sub-mm"
);
let worse = err_vs_closed_form.max(err_vs_two_one_way);
assert!(
worse * 1000.0 < DSN_RANGE_FLOOR_M,
"range model error {worse} m must be ≥1000× below the ~{DSN_RANGE_FLOOR_M} m DSN ranging floor"
);
}
#[test]
fn doppler_precision_far_below_dsn_doppler_floor() {
let speed = 9_000.0_f64; let ephem = ConstantVelocityEphemeris {
t0_jd: PROBE_JD_DOPPLER,
r0: [2.0e11, 0.0, 0.0],
v: [speed, 0.0, 0.0],
};
let station = [0.0, 0.0, 0.0];
let t_rx = TwoPartJd::from_f64(PROBE_JD_DOPPLER);
let f_x = Band::X.downlink_hz();
let rdot_analytic = speed / (1.0 + speed / C_M_PER_S);
let f_d_one =
one_way_doppler(station, &Body::mars(), &Body::sun(), t_rx, f_x, &ephem).expect("doppler");
let v_recovered_one = -C_M_PER_S * f_d_one / f_x;
let err_one = (v_recovered_one - rdot_analytic).abs();
let f_ul = Band::X.uplink_hz();
let f_d_two = two_way_doppler(
station,
&Body::mars(),
&Body::sun(),
t_rx,
f_ul,
1.0,
&ephem,
)
.expect("two-way doppler");
let v_recovered_two = -C_M_PER_S * f_d_two / (2.0 * f_ul);
let err_two = (v_recovered_two - rdot_analytic).abs();
let naive_gap = (rdot_analytic - speed).abs();
assert!(
err_one < naive_gap / 100.0,
"model Doppler error {err_one} m/s vs the retarded closed form should be ≪ the {naive_gap} m/s naive-vs-retarded gap"
);
assert!(
err_one < DSN_DOPPLER_FLOOR_M_PER_S,
"one-way Doppler velocity error {err_one} m/s exceeds the ~{DSN_DOPPLER_FLOOR_M_PER_S} m/s DSN floor"
);
assert!(
err_two < DSN_DOPPLER_FLOOR_M_PER_S,
"two-way Doppler velocity error {err_two} m/s exceeds the ~{DSN_DOPPLER_FLOOR_M_PER_S} m/s DSN floor"
);
let worse = err_one.max(err_two);
assert!(
worse * 3.0 < DSN_DOPPLER_FLOOR_M_PER_S,
"Doppler model velocity error {worse} m/s must be ≥3× below the DSN ~{DSN_DOPPLER_FLOOR_M_PER_S} m/s floor"
);
}
#[test]
fn delta_dor_precision_far_below_dsn_angular_floor() {
let baseline = [8.0e6, 0.0, 0.0];
let baseline_len = norm(baseline);
let quasar = [0.0, 0.0, 1.0];
let dtheta = 5.0e-7_f64; let sc_unit = [dtheta.sin(), 0.0, dtheta.cos()];
let r = 2.27e11; let sc_pos = [sc_unit[0] * r, sc_unit[1] * r, sc_unit[2] * r];
let dtau_model = delta_dor(sc_pos, quasar, baseline);
let dtau_analytic = -(baseline[0] * dtheta.sin()) / C_M_PER_S;
let delay_err = (dtau_model - dtau_analytic).abs();
let angle_err_rad = C_M_PER_S * delay_err / baseline_len;
assert!(
angle_err_rad < DSN_DELTA_DOR_FLOOR_RAD,
"Δ-DOR model angular error {angle_err_rad} rad exceeds the ~{DSN_DELTA_DOR_FLOOR_RAD} rad DSN floor"
);
assert!(
angle_err_rad * 1.0e6 < DSN_DELTA_DOR_FLOOR_RAD,
"Δ-DOR angular error {angle_err_rad} rad must be ≥1e6× below the {DSN_DELTA_DOR_FLOOR_RAD} rad DSN floor"
);
}
#[test]
fn plasma_calibration_recovers_injected_delay_well_below_one_percent() {
let f_x = Band::X.downlink_hz();
let f_ka = Band::Ka.downlink_hz();
let tec = 2.5e18_f64;
let injected_x = solar_plasma_delay(f_x, tec);
let injected_ka = solar_plasma_delay(f_ka, tec);
let recovered_x = dual_freq_plasma_calibration(injected_x, injected_ka, f_x, f_ka);
let rel_err = (recovered_x - injected_x).abs() / injected_x;
assert!(
rel_err < 1e-2,
"dual-frequency plasma calibration relative error {rel_err} exceeds 1%"
);
assert!(
rel_err < 1e-9,
"noise-free plasma calibration should recover the injected delay essentially exactly, got rel err {rel_err}"
);
let injected_x_range_m = injected_x * C_M_PER_S;
assert!(
injected_x_range_m > 0.0 && injected_x_range_m.is_finite(),
"injected X-band plasma range delay {injected_x_range_m} m should be a positive finite magnitude"
);
}
#[test]
fn shapiro_delay_in_published_microsecond_band() {
const AU_M: f64 = 1.495_978_707e11;
let r_sun = 6.957e8; let b = 3.0 * r_sun; let earth = [-AU_M, b, 0.0];
let mars = [1.524 * AU_M, b, 0.0];
let one_way = shapiro_delay(earth, mars, MU_SUN);
let round_trip = 2.0 * one_way;
assert!(
(100e-6..=250e-6).contains(&round_trip),
"Earth–Mars round-trip Shapiro {} µs not in the published ~100–250 µs band",
round_trip * 1e6
);
}