use crate::cio::gcrs_to_itrs_matrix;
use crate::eop::EopSeries;
use crate::ephem::{moon_position, sun_position};
use crate::forces::{
drag_accel, lense_thirring_accel, relativistic_accel, srp_accel, third_body_accel, MU_MOON,
MU_SUN,
};
use crate::fusion::ukf::inverse;
use crate::gravity_sh::SphericalHarmonicField;
use crate::integrator::{integrate_dopri, Tolerance};
use crate::precession::{julian_centuries_tt, mat_vec, transpose, Mat3};
use crate::tides::tidal_acceleration;
use crate::timescales::SECONDS_PER_DAY;
type Vec3 = [f64; 3];
pub const MODULE_NAME: &str = "precise-od";
fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn norm(a: Vec3) -> f64 {
dot(a, a).sqrt()
}
fn unit(a: Vec3) -> Vec3 {
let n = norm(a);
if n == 0.0 {
[0.0, 0.0, 0.0]
} else {
[a[0] / n, a[1] / n, a[2] / n]
}
}
pub fn ric_from_state(r: Vec3, v: Vec3) -> Mat3 {
let r_hat = unit(r);
let n_hat = unit(cross(r, v));
let t_hat = cross(n_hat, r_hat); [r_hat, t_hat, n_hat]
}
pub fn to_rtn(w: Vec3, r: Vec3, v: Vec3) -> Vec3 {
mat_vec(&ric_from_state(r, v), w)
}
#[derive(Clone, Copy, Debug)]
pub struct Observation {
pub t: f64,
pub pos: Vec3,
pub sigma: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct EmpiricalAccel {
pub radial: [f64; 3],
pub transverse: [f64; 3],
pub normal: [f64; 3],
pub radial_2cpr: [f64; 2],
pub transverse_2cpr: [f64; 2],
pub normal_2cpr: [f64; 2],
}
#[derive(Clone, Copy, Debug)]
pub struct EstimatedParams {
pub r0: Vec3,
pub v0: Vec3,
pub cr: Option<f64>,
pub empirical: Option<EmpiricalAccel>,
}
#[derive(Clone, Copy, Debug)]
pub struct OdReport {
pub rms_3d: f64,
pub rms_rtn: Vec3,
pub n_obs: usize,
pub n_edited: usize,
pub n_params: usize,
pub iterations: usize,
pub converged: bool,
pub params: EstimatedParams,
}
fn arg_of_latitude(r: Vec3, v: Vec3) -> f64 {
let h = cross(r, v);
let n_hat = unit(h); let node = cross([0.0, 0.0, 1.0], h); let p_hat = if norm(node) < 1e-9 * norm(h) {
unit(cross(n_hat, [0.0, 1.0, 0.0])) } else {
unit(node)
};
let q_hat = cross(n_hat, p_hat); dot(r, q_hat).atan2(dot(r, p_hat))
}
pub(crate) fn empirical_accel(emp: &EmpiricalAccel, r: Vec3, v: Vec3) -> Vec3 {
let ric = ric_from_state(r, v); let u = arg_of_latitude(r, v);
let (cu, su) = (u.cos(), u.sin());
let (c2u, s2u) = ((2.0 * u).cos(), (2.0 * u).sin());
let comp = |c: [f64; 3], c2: [f64; 2]| c[0] + c[1] * cu + c[2] * su + c2[0] * c2u + c2[1] * s2u;
let rtn = [
comp(emp.radial, emp.radial_2cpr),
comp(emp.transverse, emp.transverse_2cpr),
comp(emp.normal, emp.normal_2cpr),
];
mat_vec(&transpose(&ric), rtn)
}
pub trait ForceModel: Clone {
fn accel_rv(&self, t: f64, r: Vec3, v: Vec3) -> Vec3;
fn cr(&self) -> f64;
fn set_cr(&mut self, cr: f64);
fn set_empirical(&mut self, empirical: Option<EmpiricalAccel>);
fn dynamics_matrix(&self, t: f64, r: Vec3, v: Vec3) -> [[f64; 6]; 6] {
let mut a_mat = [[0.0; 6]; 6];
for i in 0..3 {
a_mat[i][i + 3] = 1.0;
}
let hr = 1.0;
let hv = 1.0e-3;
for j in 0..3 {
let (mut rp, mut rm) = (r, r);
rp[j] += hr;
rm[j] -= hr;
let (ap, am) = (self.accel_rv(t, rp, v), self.accel_rv(t, rm, v));
let (mut vp, mut vm) = (v, v);
vp[j] += hv;
vm[j] -= hv;
let (apv, amv) = (self.accel_rv(t, r, vp), self.accel_rv(t, r, vm));
for i in 0..3 {
a_mat[3 + i][j] = (ap[i] - am[i]) / (2.0 * hr);
a_mat[3 + i][3 + j] = (apv[i] - amv[i]) / (2.0 * hv);
}
}
a_mat
}
}
#[derive(Clone, Debug)]
pub struct PreciseForceModel {
pub geopotential: SphericalHarmonicField,
pub epoch_jd_tt: f64,
pub sun: bool,
pub moon: bool,
pub srp: bool,
pub cr: f64,
pub area_over_mass: f64,
pub drag: bool,
pub cd_area_over_mass: f64,
pub relativity: bool,
pub lense_thirring: bool,
pub tides: bool,
pub empirical: Option<EmpiricalAccel>,
pub eop: Option<EopSeries>,
}
impl PreciseForceModel {
pub fn from_field(geopotential: SphericalHarmonicField, epoch_jd_tt: f64) -> Self {
Self {
geopotential,
epoch_jd_tt,
sun: false,
moon: false,
srp: false,
cr: 1.0,
area_over_mass: 0.0,
drag: false,
cd_area_over_mass: 0.0,
relativity: false,
lense_thirring: false,
tides: false,
empirical: None,
eop: None,
}
}
pub fn egm2008(nmax: usize, epoch_jd_tt: f64) -> Self {
Self::from_field(SphericalHarmonicField::egm2008_truncated(nmax), epoch_jd_tt)
}
pub fn third_body(mut self, sun: bool, moon: bool) -> Self {
self.sun = sun;
self.moon = moon;
self
}
pub fn solar_radiation(mut self, cr: f64, area_over_mass: f64) -> Self {
self.srp = true;
self.cr = cr;
self.area_over_mass = area_over_mass;
self
}
pub fn drag(mut self, cd_area_over_mass: f64) -> Self {
self.drag = true;
self.cd_area_over_mass = cd_area_over_mass;
self
}
pub fn relativity(mut self) -> Self {
self.relativity = true;
self
}
pub fn lense_thirring(mut self) -> Self {
self.lense_thirring = true;
self
}
pub fn tides(mut self) -> Self {
self.tides = true;
self
}
pub fn with_empirical(mut self, empirical: EmpiricalAccel) -> Self {
self.empirical = Some(empirical);
self
}
pub fn with_eop(mut self, eop: EopSeries) -> Self {
self.eop = Some(eop);
self
}
pub fn frame_args(&self, jd_tt: f64) -> (f64, f64, f64) {
match &self.eop {
Some(eop) => eop.frame_args_tt(jd_tt),
None => (jd_tt, 0.0, 0.0),
}
}
fn frame(&self, jd_tt: f64) -> Mat3 {
let (jd_ut1, xp, yp) = self.frame_args(jd_tt);
gcrs_to_itrs_matrix(jd_tt, jd_ut1, xp, yp)
}
fn accel_with(
&self,
jd_tt: f64,
m: &Mat3,
sun: Option<Vec3>,
moon: Option<Vec3>,
r: Vec3,
v: Vec3,
) -> Vec3 {
let r_ecef = mat_vec(m, r);
let a_ecef = self.geopotential.acceleration(r_ecef);
let mut a = mat_vec(&transpose(m), a_ecef);
let mut add = |p: Vec3| {
a = [a[0] + p[0], a[1] + p[1], a[2] + p[2]];
};
if self.sun {
if let Some(s) = sun {
add(third_body_accel(r, s, MU_SUN));
}
}
if self.moon {
if let Some(mn) = moon {
add(third_body_accel(r, mn, MU_MOON));
}
}
if self.srp {
if let Some(s) = sun {
add(srp_accel(r, s, self.cr, self.area_over_mass));
}
}
if self.drag {
add(drag_accel(r, v, self.cd_area_over_mass));
}
if self.relativity {
add(relativistic_accel(r, v));
}
if self.lense_thirring {
add(lense_thirring_accel(r, v));
}
if self.tides {
add(tidal_acceleration(r, jd_tt));
}
if let Some(emp) = self.empirical {
add(empirical_accel(&emp, r, v));
}
a
}
fn ephem(&self, jd_tt: f64) -> (Option<Vec3>, Option<Vec3>) {
let tjc = julian_centuries_tt(jd_tt);
let sun = (self.sun || self.srp).then(|| sun_position(tjc));
let moon = self.moon.then(|| moon_position(tjc));
(sun, moon)
}
}
impl ForceModel for PreciseForceModel {
fn accel_rv(&self, t: f64, r: Vec3, v: Vec3) -> Vec3 {
let jd_tt = self.epoch_jd_tt + t / SECONDS_PER_DAY;
let m = self.frame(jd_tt);
let (sun, moon) = self.ephem(jd_tt);
self.accel_with(jd_tt, &m, sun, moon, r, v)
}
fn cr(&self) -> f64 {
self.cr
}
fn set_cr(&mut self, cr: f64) {
self.cr = cr;
}
fn set_empirical(&mut self, empirical: Option<EmpiricalAccel>) {
self.empirical = empirical;
}
fn dynamics_matrix(&self, t: f64, r: Vec3, v: Vec3) -> [[f64; 6]; 6] {
let jd_tt = self.epoch_jd_tt + t / SECONDS_PER_DAY;
let m = self.frame(jd_tt);
let (sun, moon) = self.ephem(jd_tt);
let accel = |r: Vec3, v: Vec3| self.accel_with(jd_tt, &m, sun, moon, r, v);
let mut a_mat = [[0.0; 6]; 6];
for i in 0..3 {
a_mat[i][i + 3] = 1.0;
}
let hr = 1.0;
let hv = 1.0e-3;
for j in 0..3 {
let (mut rp, mut rm) = (r, r);
rp[j] += hr;
rm[j] -= hr;
let (ap, am) = (accel(rp, v), accel(rm, v));
let (mut vp, mut vm) = (v, v);
vp[j] += hv;
vm[j] -= hv;
let (apv, amv) = (accel(r, vp), accel(r, vm));
for i in 0..3 {
a_mat[3 + i][j] = (ap[i] - am[i]) / (2.0 * hr); a_mat[3 + i][3 + j] = (apv[i] - amv[i]) / (2.0 * hv); }
}
a_mat
}
}
pub fn propagate<F: ForceModel>(
fm: &F,
r0: Vec3,
v0: Vec3,
t_end: f64,
tol: &Tolerance,
) -> (Vec3, Vec3) {
let f = |t: f64, y: &[f64]| {
let a = fm.accel_rv(t, [y[0], y[1], y[2]], [y[3], y[4], y[5]]);
vec![y[3], y[4], y[5], a[0], a[1], a[2]]
};
let y0 = vec![r0[0], r0[1], r0[2], v0[0], v0[1], v0[2]];
let h0 = (t_end / 1000.0).max(1.0).min(t_end.max(1e-3));
let sol = integrate_dopri(&f, 0.0, &y0, t_end, h0, tol);
(
[sol.y[0], sol.y[1], sol.y[2]],
[sol.y[3], sol.y[4], sol.y[5]],
)
}
pub fn propagate_with_stm<F: ForceModel>(
fm: &F,
r0: Vec3,
v0: Vec3,
t_end: f64,
tol: &Tolerance,
) -> (Vec3, Vec3, [[f64; 6]; 6]) {
let mut y0 = vec![0.0; 42];
y0[0..3].copy_from_slice(&r0);
y0[3..6].copy_from_slice(&v0);
for i in 0..6 {
y0[6 + i * 6 + i] = 1.0; }
if t_end == 0.0 {
let mut phi = [[0.0; 6]; 6];
for (i, row) in phi.iter_mut().enumerate() {
row[i] = 1.0;
}
return (r0, v0, phi);
}
let f = |t: f64, y: &[f64]| stm_rhs(fm, t, y);
let h0 = (t_end / 1000.0).max(1.0).min(t_end.max(1e-3));
let sol = integrate_dopri(&f, 0.0, &y0, t_end, h0, tol);
(
[sol.y[0], sol.y[1], sol.y[2]],
[sol.y[3], sol.y[4], sol.y[5]],
phi_from_augmented(&sol.y),
)
}
fn stm_rhs<F: ForceModel>(fm: &F, t: f64, y: &[f64]) -> Vec<f64> {
let r = [y[0], y[1], y[2]];
let v = [y[3], y[4], y[5]];
let a = fm.accel_rv(t, r, v);
let a_mat = fm.dynamics_matrix(t, r, v);
let mut dy = vec![0.0; 42];
dy[0..3].copy_from_slice(&v);
dy[3..6].copy_from_slice(&a);
for i in 0..6 {
for j in 0..6 {
let mut s = 0.0;
for (k, arow) in a_mat[i].iter().enumerate() {
s += arow * y[6 + k * 6 + j];
}
dy[6 + i * 6 + j] = s;
}
}
dy
}
fn phi_from_augmented(y: &[f64]) -> [[f64; 6]; 6] {
let mut phi = [[0.0; 6]; 6];
for (i, row) in phi.iter_mut().enumerate() {
for (j, e) in row.iter_mut().enumerate() {
*e = y[6 + i * 6 + j];
}
}
phi
}
fn propagate_with_stm_samples<F: ForceModel>(
fm: &F,
r0: Vec3,
v0: Vec3,
times: &[f64],
tol: &Tolerance,
) -> Vec<([f64; 6], [[f64; 6]; 6])> {
let mut y = vec![0.0; 42];
y[0..3].copy_from_slice(&r0);
y[3..6].copy_from_slice(&v0);
for i in 0..6 {
y[6 + i * 6 + i] = 1.0;
}
let f = |t: f64, yy: &[f64]| stm_rhs(fm, t, yy);
let mut t_prev = 0.0;
let mut out = Vec::with_capacity(times.len());
for &t in times {
if t > t_prev {
let dt = t - t_prev;
let h0 = (dt / 100.0).max(1.0).min(dt);
let sol = integrate_dopri(&f, t_prev, &y, t, h0, tol);
y = sol.y;
t_prev = t;
}
let state6 = [y[0], y[1], y[2], y[3], y[4], y[5]];
out.push((state6, phi_from_augmented(&y)));
}
out
}
pub fn propagate_samples<F: ForceModel>(
fm: &F,
r0: Vec3,
v0: Vec3,
times: &[f64],
tol: &Tolerance,
) -> Vec<Vec3> {
let f = |t: f64, y: &[f64]| {
let a = fm.accel_rv(t, [y[0], y[1], y[2]], [y[3], y[4], y[5]]);
vec![y[3], y[4], y[5], a[0], a[1], a[2]]
};
let mut y = vec![r0[0], r0[1], r0[2], v0[0], v0[1], v0[2]];
let mut t_prev = 0.0;
let mut out = Vec::with_capacity(times.len());
for &t in times {
if t > t_prev {
let dt = t - t_prev;
let h0 = (dt / 100.0).max(1.0).min(dt);
let sol = integrate_dopri(&f, t_prev, &y, t, h0, tol);
y = sol.y;
t_prev = t;
}
out.push([y[0], y[1], y[2]]);
}
out
}
#[derive(Clone, Debug)]
pub struct FitConfig {
pub estimate_cr: bool,
pub estimate_empirical: bool,
pub estimate_empirical_2cpr: bool,
pub empirical_sigma: f64,
pub max_iter: usize,
pub outlier_sigma: f64,
pub tol: Tolerance,
}
impl Default for FitConfig {
fn default() -> Self {
Self {
estimate_cr: false,
estimate_empirical: false,
estimate_empirical_2cpr: false,
empirical_sigma: 1e-7,
max_iter: 20,
outlier_sigma: 0.0,
tol: Tolerance {
rtol: 1e-11,
atol: 1e-9,
..Tolerance::default()
},
}
}
}
fn emp_get(e: &EmpiricalAccel, k: usize) -> f64 {
match k {
0..=2 => e.radial[k],
3..=5 => e.transverse[k - 3],
6..=8 => e.normal[k - 6],
9..=10 => e.radial_2cpr[k - 9],
11..=12 => e.transverse_2cpr[k - 11],
_ => e.normal_2cpr[k - 13],
}
}
fn emp_set(e: &mut EmpiricalAccel, k: usize, v: f64) {
match k {
0..=2 => e.radial[k] = v,
3..=5 => e.transverse[k - 3] = v,
6..=8 => e.normal[k - 6] = v,
9..=10 => e.radial_2cpr[k - 9] = v,
11..=12 => e.transverse_2cpr[k - 11] = v,
_ => e.normal_2cpr[k - 13] = v,
}
}
pub fn fit<F: ForceModel>(
template: &F,
initial: EstimatedParams,
obs: &[Observation],
cfg: &FitConfig,
) -> Option<OdReport> {
if obs.len() < 3 {
return None;
}
let mut order: Vec<usize> = (0..obs.len()).collect();
order.sort_by(|&a, &b| {
obs[a]
.t
.partial_cmp(&obs[b].t)
.unwrap_or(std::cmp::Ordering::Equal)
});
let obs: Vec<Observation> = order.iter().map(|&i| obs[i]).collect();
let times: Vec<f64> = obs.iter().map(|o| o.t).collect();
let n_obs_total = obs.len();
let n_emp = if cfg.estimate_empirical {
if cfg.estimate_empirical_2cpr {
15
} else {
9
}
} else {
0
};
let emp_base = 6 + cfg.estimate_cr as usize;
let n_params = emp_base + n_emp;
let mut r0 = initial.r0;
let mut v0 = initial.v0;
let mut cr = initial.cr.unwrap_or(template.cr());
let mut emp = initial.empirical.unwrap_or_default();
let mut edited = vec![false; n_obs_total];
let mut did_edit = false;
let mut iterations = 0;
let mut converged = false;
for it in 0..cfg.max_iter {
iterations = it + 1;
let mut fm = template.clone();
fm.set_cr(cr);
fm.set_empirical(if cfg.estimate_empirical {
Some(emp)
} else {
initial.empirical
});
let preds = propagate_with_stm_samples(&fm, r0, v0, ×, &cfg.tol);
let cr_partial: Option<Vec<Vec3>> = if cfg.estimate_cr {
let dcr = 1e-3;
let (mut fmp, mut fmm) = (fm.clone(), fm.clone());
fmp.set_cr(cr + dcr);
fmm.set_cr(cr - dcr);
let pp = propagate_samples(&fmp, r0, v0, ×, &cfg.tol);
let pm = propagate_samples(&fmm, r0, v0, ×, &cfg.tol);
Some(
pp.iter()
.zip(&pm)
.map(|(a, b)| {
[
(a[0] - b[0]) / (2.0 * dcr),
(a[1] - b[1]) / (2.0 * dcr),
(a[2] - b[2]) / (2.0 * dcr),
]
})
.collect(),
)
} else {
None
};
let emp_partials: Vec<Vec<Vec3>> = if cfg.estimate_empirical {
let nominal = propagate_samples(&fm, r0, v0, ×, &cfg.tol);
let damp = 1e-9;
(0..n_emp)
.map(|k| {
let mut ep = emp;
emp_set(&mut ep, k, emp_get(&emp, k) + damp);
let mut fmp = fm.clone();
fmp.set_empirical(Some(ep));
let pp = propagate_samples(&fmp, r0, v0, ×, &cfg.tol);
pp.iter()
.zip(&nominal)
.map(|(a, b)| {
[
(a[0] - b[0]) / damp,
(a[1] - b[1]) / damp,
(a[2] - b[2]) / damp,
]
})
.collect()
})
.collect()
} else {
Vec::new()
};
let mut ata = vec![vec![0.0; n_params]; n_params];
let mut atb = vec![0.0; n_params];
for (i, ob) in obs.iter().enumerate() {
if edited[i] {
continue;
}
let w = 1.0 / (ob.sigma * ob.sigma);
let (state6, phi) = &preds[i];
let resid = [
ob.pos[0] - state6[0],
ob.pos[1] - state6[1],
ob.pos[2] - state6[2],
];
for axis in 0..3 {
let mut row = vec![0.0; n_params];
row[..6].copy_from_slice(&phi[axis][..6]);
if let Some(cp) = &cr_partial {
row[6] = cp[i][axis];
}
if cfg.estimate_empirical {
for k in 0..n_emp {
row[emp_base + k] = emp_partials[k][i][axis];
}
}
for p in 0..n_params {
atb[p] += row[p] * w * resid[axis];
for q in 0..n_params {
ata[p][q] += row[p] * w * row[q];
}
}
}
}
if cfg.estimate_empirical && cfg.empirical_sigma > 0.0 {
let wa = 1.0 / (cfg.empirical_sigma * cfg.empirical_sigma);
for k in 0..n_emp {
ata[emp_base + k][emp_base + k] += wa;
atb[emp_base + k] += wa * (0.0 - emp_get(&emp, k));
}
}
let ata_inv = inverse(&ata)?;
let dx: Vec<f64> = (0..n_params)
.map(|p| (0..n_params).map(|q| ata_inv[p][q] * atb[q]).sum())
.collect();
for k in 0..3 {
r0[k] += dx[k];
v0[k] += dx[3 + k];
}
if cfg.estimate_cr {
cr += dx[6];
}
if cfg.estimate_empirical {
for k in 0..n_emp {
let cur = emp_get(&emp, k);
emp_set(&mut emp, k, cur + dx[emp_base + k]);
}
}
let dpos = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
let dvel = (dx[3] * dx[3] + dx[4] * dx[4] + dx[5] * dx[5]).sqrt();
if dpos < 1e-4 && dvel < 1e-7 {
if cfg.outlier_sigma > 0.0 && !did_edit {
did_edit = true;
let resid3d: Vec<f64> = preds
.iter()
.enumerate()
.map(|(i, (s, _))| {
let o = obs[i].pos;
((o[0] - s[0]).powi(2) + (o[1] - s[1]).powi(2) + (o[2] - s[2]).powi(2))
.sqrt()
})
.collect();
let mut any_new = false;
for _pass in 0..3 {
let (mut sum, mut cnt) = (0.0, 0usize);
for (i, &d) in resid3d.iter().enumerate() {
if !edited[i] {
sum += d * d;
cnt += 1;
}
}
if cnt == 0 {
break;
}
let rms = (sum / cnt as f64).sqrt();
let thresh = cfg.outlier_sigma * rms;
let mut marked = false;
for (i, &d) in resid3d.iter().enumerate() {
if !edited[i] && d > thresh {
edited[i] = true;
marked = true;
any_new = true;
}
}
if !marked {
break;
}
}
if any_new {
continue; }
}
converged = true;
break;
}
}
let mut fm = template.clone();
fm.set_cr(cr);
fm.set_empirical(if cfg.estimate_empirical {
Some(emp)
} else {
initial.empirical
});
let preds = propagate_with_stm_samples(&fm, r0, v0, ×, &cfg.tol);
let (mut sum3d, mut used) = (0.0, 0usize);
let mut sum_rtn = [0.0; 3];
for (i, ob) in obs.iter().enumerate() {
if edited[i] {
continue;
}
let (state6, _) = &preds[i];
let resid = [
ob.pos[0] - state6[0],
ob.pos[1] - state6[1],
ob.pos[2] - state6[2],
];
sum3d += resid[0] * resid[0] + resid[1] * resid[1] + resid[2] * resid[2];
let rv = [state6[0], state6[1], state6[2]];
let vv = [state6[3], state6[4], state6[5]];
let rtn = to_rtn(resid, rv, vv);
for k in 0..3 {
sum_rtn[k] += rtn[k] * rtn[k];
}
used += 1;
}
let used_f = used.max(1) as f64;
let rms_3d = (sum3d / used_f).sqrt();
let rms_rtn = [
(sum_rtn[0] / used_f).sqrt(),
(sum_rtn[1] / used_f).sqrt(),
(sum_rtn[2] / used_f).sqrt(),
];
Some(OdReport {
rms_3d,
rms_rtn,
n_obs: used,
n_edited: n_obs_total - used,
n_params,
iterations,
converged,
params: EstimatedParams {
r0,
v0,
cr: cfg.estimate_cr.then_some(cr),
empirical: if cfg.estimate_empirical {
Some(emp)
} else {
initial.empirical
},
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_rtn_radial_displacement_is_pure_radial() {
let r = [6.9e6, 1.0e6, 2.0e6];
let v = [-1.0e3, 7.0e3, 1.0e3];
let r_hat = unit(r);
let d = [r_hat[0] * 3.0, r_hat[1] * 3.0, r_hat[2] * 3.0];
let rtn = to_rtn(d, r, v);
assert!((rtn[0] - 3.0).abs() < 1e-9, "radial {rtn:?}");
assert!(
rtn[1].abs() < 1e-9 && rtn[2].abs() < 1e-9,
"off-radial leak {rtn:?}"
);
}
}