use crate::astro::dynamics::Position;
use crate::coordinates::frames::GCRS;
use qtty::Meter;
pub trait Correction: Send + Sync {
fn name(&self) -> &str;
fn apply_to_range(
&self,
range: Meter,
rx_gcrs_km: Position<GCRS>,
sat_gcrs_km: Position<GCRS>,
) -> Meter;
}
pub struct CorrectionRegistry {
corrections: Vec<Box<dyn Correction>>,
}
impl CorrectionRegistry {
pub fn new() -> Self {
Self {
corrections: Vec::new(),
}
}
pub fn push(&mut self, c: Box<dyn Correction>) {
self.corrections.push(c);
}
pub fn len(&self) -> usize {
self.corrections.len()
}
pub fn is_empty(&self) -> bool {
self.corrections.is_empty()
}
pub fn apply(
&self,
range: Meter,
rx_gcrs_km: Position<GCRS>,
sat_gcrs_km: Position<GCRS>,
) -> Meter {
let mut r = range;
for c in &self.corrections {
r = c.apply_to_range(r, rx_gcrs_km, sat_gcrs_km);
}
r
}
pub fn names(&self) -> Vec<&str> {
self.corrections.iter().map(|c| c.name()).collect()
}
}
impl Default for CorrectionRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct PhaseCenterOffset {
offset_gcrs_km: [f64; 3],
}
impl PhaseCenterOffset {
pub fn new(offset_gcrs_km: [f64; 3]) -> Self {
Self { offset_gcrs_km }
}
pub fn from_metres(offset_m: [f64; 3]) -> Self {
Self {
offset_gcrs_km: [
offset_m[0] / 1000.0,
offset_m[1] / 1000.0,
offset_m[2] / 1000.0,
],
}
}
}
impl Correction for PhaseCenterOffset {
fn name(&self) -> &str {
"PhaseCenterOffset"
}
fn apply_to_range(
&self,
range: Meter,
rx_gcrs_km: Position<GCRS>,
sat_gcrs_km: Position<GCRS>,
) -> Meter {
let rx = [
rx_gcrs_km.x().value(),
rx_gcrs_km.y().value(),
rx_gcrs_km.z().value(),
];
let sat = [
sat_gcrs_km.x().value(),
sat_gcrs_km.y().value(),
sat_gcrs_km.z().value(),
];
let los = [sat[0] - rx[0], sat[1] - rx[1], sat[2] - rx[2]];
let los_mag = (los[0] * los[0] + los[1] * los[1] + los[2] * los[2]).sqrt();
if los_mag == 0.0 {
return range;
}
let los_hat = [los[0] / los_mag, los[1] / los_mag, los[2] / los_mag];
let pco_dot = self.offset_gcrs_km[0] * los_hat[0]
+ self.offset_gcrs_km[1] * los_hat[1]
+ self.offset_gcrs_km[2] * los_hat[2];
Meter::new(range.value() - pco_dot * 1000.0)
}
}
pub struct ShapiroDelay;
const GM_M3_S2: f64 = 3.986_004_418e14;
const C_M_S: f64 = 299_792_458.0;
const TWO_GM_OVER_C2_M: f64 = 2.0 * GM_M3_S2 / (C_M_S * C_M_S);
impl Correction for ShapiroDelay {
fn name(&self) -> &str {
"ShapiroDelay"
}
fn apply_to_range(
&self,
range: Meter,
rx_gcrs_km: Position<GCRS>,
sat_gcrs_km: Position<GCRS>,
) -> Meter {
let rx = [
rx_gcrs_km.x().value(),
rx_gcrs_km.y().value(),
rx_gcrs_km.z().value(),
];
let sat = [
sat_gcrs_km.x().value(),
sat_gcrs_km.y().value(),
sat_gcrs_km.z().value(),
];
let r_rx = (rx[0] * rx[0] + rx[1] * rx[1] + rx[2] * rx[2]).sqrt() * 1_000.0; let r_sat = (sat[0] * sat[0] + sat[1] * sat[1] + sat[2] * sat[2]).sqrt() * 1_000.0;
let rho = range.value();
let sum = r_rx + r_sat;
let den = sum - rho;
if den > 0.0 {
Meter::new(rho + TWO_GM_OVER_C2_M * ((sum + rho) / den).ln())
} else {
range
}
}
}
pub struct EarthTideDisplacement {
displacement_gcrs_km: [f64; 3],
}
impl EarthTideDisplacement {
pub fn from_km(dx: f64, dy: f64, dz: f64) -> Self {
Self {
displacement_gcrs_km: [dx, dy, dz],
}
}
pub fn from_metres(dx_m: f64, dy_m: f64, dz_m: f64) -> Self {
Self::from_km(dx_m / 1000.0, dy_m / 1000.0, dz_m / 1000.0)
}
pub fn zero() -> Self {
Self::from_km(0.0, 0.0, 0.0)
}
}
impl Correction for EarthTideDisplacement {
fn name(&self) -> &str {
"EarthTideDisplacement"
}
fn apply_to_range(
&self,
range: Meter,
rx_gcrs_km: Position<GCRS>,
sat_gcrs_km: Position<GCRS>,
) -> Meter {
let rx = [
rx_gcrs_km.x().value(),
rx_gcrs_km.y().value(),
rx_gcrs_km.z().value(),
];
let sat = [
sat_gcrs_km.x().value(),
sat_gcrs_km.y().value(),
sat_gcrs_km.z().value(),
];
let los = [sat[0] - rx[0], sat[1] - rx[1], sat[2] - rx[2]];
let los_mag = (los[0] * los[0] + los[1] * los[1] + los[2] * los[2]).sqrt();
if los_mag == 0.0 {
return range;
}
let los_hat = [los[0] / los_mag, los[1] / los_mag, los[2] / los_mag];
let disp_los = self.displacement_gcrs_km[0] * los_hat[0]
+ self.displacement_gcrs_km[1] * los_hat[1]
+ self.displacement_gcrs_km[2] * los_hat[2];
Meter::new(range.value() - disp_los * 1000.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinates::frames::GCRS;
#[test]
fn shapiro_positive_and_small() {
let rx = Position::<GCRS>::new(6_378.0, 0.0, 0.0);
let sat = Position::<GCRS>::new(26_560.0, 0.0, 0.0);
let rho_m = Meter::new((26_560.0 - 6_378.0) * 1_000.0);
let corrected = ShapiroDelay.apply_to_range(rho_m, rx, sat);
let delay = corrected.value() - rho_m.value();
assert!(delay > 0.005 && delay < 0.025, "Shapiro delay={delay:.4}m");
}
#[test]
fn earth_tide_zero_is_noop() {
let rx = Position::<GCRS>::new(7_000.0, 0.0, 0.0);
let sat = Position::<GCRS>::new(26_560.0, 1_000.0, 500.0);
let rho = Meter::new(19_000_000.0);
let etd = EarthTideDisplacement::zero();
assert_eq!(etd.apply_to_range(rho, rx, sat).value(), rho.value());
}
#[test]
fn registry_applies_in_order() {
struct Add(f64);
impl Correction for Add {
fn name(&self) -> &str {
"add"
}
fn apply_to_range(&self, r: Meter, _: Position<GCRS>, _: Position<GCRS>) -> Meter {
Meter::new(r.value() + self.0)
}
}
let origin = Position::<GCRS>::new(0.0, 0.0, 0.0);
let mut reg = CorrectionRegistry::new();
reg.push(Box::new(Add(1.0)));
reg.push(Box::new(Add(2.0)));
assert_eq!(reg.apply(Meter::new(0.0), origin, origin).value(), 3.0);
assert_eq!(reg.len(), 2);
}
#[test]
fn pco_along_los_subtracts_offset() {
let pco = PhaseCenterOffset::new([0.001, 0.0, 0.0]); let rx = Position::<GCRS>::new(0.0, 0.0, 0.0);
let sat = Position::<GCRS>::new(1_000.0, 0.0, 0.0);
let corrected = pco.apply_to_range(Meter::new(1_000_000.0), rx, sat);
assert!((corrected.value() - (1_000_000.0 - 1.0)).abs() < 1e-9);
}
}