use astrodynamics::constants::{J2_EARTH, MU_EARTH, RE_EARTH};
use astrodynamics::frames::transforms::{gcrs_to_itrs_matrix, itrs_to_gcrs_compute, mat3_vec3_mul};
use astrodynamics::math::least_squares::{
self, solve_trf, LeastSquaresProblem, SolveOptions, Status,
};
use astrodynamics::time::model::TimeScale;
use astrodynamics::time::scales::TimeScales;
use nalgebra::DVector;
use crate::constants::{M_PER_KM, OMEGA_E_DOT_RAD_S};
mod time;
use time::dt_seconds;
pub use time::CalendarEpoch;
const OMEGA_EARTH_RAD_S: f64 = OMEGA_E_DOT_RAD_S;
pub const MIN_SAMPLES: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Model {
#[default]
CircularSecular,
EccentricSecular,
}
#[derive(Debug, Clone, Copy)]
pub struct EcefSample {
pub epoch: CalendarEpoch,
pub x_m: f64,
pub y_m: f64,
pub z_m: f64,
}
impl EcefSample {
pub const fn new(epoch: CalendarEpoch, x_m: f64, y_m: f64, z_m: f64) -> Self {
Self {
epoch,
x_m,
y_m,
z_m,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Elements {
pub model: Model,
pub epoch: CalendarEpoch,
pub a_m: f64,
pub e: f64,
pub i_rad: f64,
pub raan_rad: f64,
pub raan_rate_rad_s: f64,
pub raan_rate_j2_rad_s: f64,
pub arg_lat_rad: f64,
pub mean_motion_rad_s: f64,
pub h: f64,
pub k: f64,
pub arg_perigee_rad: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FitStats {
pub rms_m: f64,
pub max_m: f64,
pub n_samples: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ReducedOrbit {
pub elements: Elements,
pub stats: FitStats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Frame {
Gcrs,
Ecef,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DriftEntry {
pub epoch: CalendarEpoch,
pub error_m: f64,
}
#[derive(Debug, Clone)]
pub struct DriftReport {
pub per_epoch: Vec<DriftEntry>,
pub max_m: f64,
pub rms_m: f64,
pub threshold_horizon: Option<CalendarEpoch>,
}
#[derive(Debug, Clone)]
pub enum ReducedOrbitError {
TooFewSamples {
got: usize,
required: usize,
},
InvalidWindow,
SingularPlaneFit,
RaanAmbiguous,
Singular(least_squares::SolveError),
FitDidNotConverge,
}
impl core::fmt::Display for ReducedOrbitError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ReducedOrbitError::TooFewSamples { got, required } => {
write!(f, "only {got} samples; need at least {required}")
}
ReducedOrbitError::InvalidWindow => {
write!(f, "the fit window is empty, inverted, or has no time span")
}
ReducedOrbitError::SingularPlaneFit => {
write!(
f,
"samples are collinear or coincident; orbital plane undefined"
)
}
ReducedOrbitError::RaanAmbiguous => {
write!(f, "near-equatorial orbit; ascending node (raan) undefined")
}
ReducedOrbitError::Singular(e) => write!(f, "degenerate fit geometry: {e}"),
ReducedOrbitError::FitDidNotConverge => {
write!(f, "least-squares refinement did not converge")
}
}
}
}
impl std::error::Error for ReducedOrbitError {}
impl From<least_squares::SolveError> for ReducedOrbitError {
fn from(e: least_squares::SolveError) -> Self {
ReducedOrbitError::Singular(e)
}
}
const MIN_INCLINATION_RAD: f64 = 1.0e-3;
const N_PARAMS: usize = 6;
fn unscale_params(x: &[f64], a_scale: f64, rate_scale: f64) -> [f64; N_PARAMS] {
[
x[0] * a_scale,
x[1],
x[2],
x[3] / rate_scale,
x[4],
x[5] / rate_scale,
]
}
const N_PARAMS_ECC: usize = 8;
fn unscale_params_ecc(x: &[f64], a_scale: f64, rate_scale: f64) -> [f64; N_PARAMS_ECC] {
[
x[0] * a_scale,
x[1],
x[2],
x[3] / rate_scale,
x[4],
x[5],
x[6],
x[7] / rate_scale,
]
}
const ECC_ZERO_EPS: f64 = 1.0e-12;
fn solve_kepler(m: f64, e: f64) -> f64 {
let two_pi = 2.0 * std::f64::consts::PI;
let m = m.rem_euclid(two_pi);
let mut ee = if e < 0.8 { m } else { std::f64::consts::PI };
for _ in 0..30 {
let f = ee - e * ee.sin() - m;
let fp = 1.0 - e * ee.cos();
let d = f / fp;
ee -= d;
if d.abs() < 1.0e-14 {
break;
}
}
ee
}
fn eval_gcrs_km_ecc(p: &[f64], dt: f64) -> [f64; 3] {
let a = p[0];
let i = p[1];
let raan = p[2] + p[3] * dt;
let h = p[4];
let k = p[5];
let lambda = p[6] + p[7] * dt;
let e = (h * h + k * k).sqrt();
if e < ECC_ZERO_EPS {
return eval_gcrs_km(&[a, i, p[2], p[3], p[6], p[7]], dt);
}
let omega = h.atan2(k);
let mm = lambda - omega;
let big_e = solve_kepler(mm, e);
let (se, ce) = big_e.sin_cos();
let r = a * (1.0 - e * ce);
let nu = (((1.0 - e * e).sqrt()) * se).atan2(ce - e);
let u = omega + nu;
rotate_in_plane_km([r * u.cos(), r * u.sin()], i, raan)
}
fn rotate_in_plane_km(xy: [f64; 2], i: f64, raan: f64) -> [f64; 3] {
let (si, ci) = i.sin_cos();
let (sr, cr) = raan.sin_cos();
let x1 = xy[0];
let y1 = xy[1] * ci;
let z1 = xy[1] * si;
[cr * x1 - sr * y1, sr * x1 + cr * y1, z1]
}
fn eval_gcrs_velocity_km_s_ecc(p: &[f64], dt: f64) -> [f64; 3] {
let a = p[0];
let i = p[1];
let raan_rate = p[3];
let h = p[4];
let k = p[5];
let n = p[7];
let raan = p[2] + raan_rate * dt;
let lambda = p[6] + n * dt;
let e = (h * h + k * k).sqrt();
if e < ECC_ZERO_EPS {
return eval_gcrs_velocity_km_s(&[a, i, p[2], p[3], p[6], p[7]], dt);
}
let omega = h.atan2(k);
let mm = lambda - omega;
let big_e = solve_kepler(mm, e);
let (se, ce) = big_e.sin_cos();
let edot = n / (1.0 - e * ce);
let beta = (1.0 - e * e).sqrt();
let x_pf = a * (ce - e);
let y_pf = a * beta * se;
let xdot_pf = -a * se * edot;
let ydot_pf = a * beta * ce * edot;
let (so, co) = omega.sin_cos();
let x1 = co * x_pf - so * y_pf;
let y1 = so * x_pf + co * y_pf;
let dx1 = co * xdot_pf - so * ydot_pf;
let dy1 = so * xdot_pf + co * ydot_pf;
let (si, ci) = i.sin_cos();
let (sr, cr) = raan.sin_cos();
let y1i = y1 * ci;
let dy1i = dy1 * ci;
let vx = cr * dx1 - sr * dy1i + raan_rate * (-sr * x1 - cr * y1i);
let vy = sr * dx1 + cr * dy1i + raan_rate * (cr * x1 - sr * y1i);
let vz = dy1 * si;
[vx, vy, vz]
}
fn eval_gcrs_km(p: &[f64], dt: f64) -> [f64; 3] {
let a = p[0];
let i = p[1];
let raan = p[2] + p[3] * dt;
let u = p[4] + p[5] * dt;
let (su, cu) = u.sin_cos();
let xp = a * cu;
let yp = a * su;
let (si, ci) = i.sin_cos();
let (sr, cr) = raan.sin_cos();
let x1 = xp;
let y1 = yp * ci;
let z1 = yp * si;
[cr * x1 - sr * y1, sr * x1 + cr * y1, z1]
}
fn eval_gcrs_velocity_km_s(p: &[f64], dt: f64) -> [f64; 3] {
let a = p[0];
let i = p[1];
let raan_rate = p[3];
let n = p[5];
let raan = p[2] + raan_rate * dt;
let u = p[4] + n * dt;
let (su, cu) = u.sin_cos();
let (si, ci) = i.sin_cos();
let (sr, cr) = raan.sin_cos();
let xp = a * cu;
let yp = a * su;
let dxp = -a * su * n;
let dyp = a * cu * n;
let x1 = xp;
let y1 = yp * ci;
let dx1 = dxp;
let dy1 = dyp * ci;
let vx = cr * dx1 - sr * dy1 + raan_rate * (-sr * x1 - cr * y1);
let vy = sr * dx1 + cr * dy1 + raan_rate * (cr * x1 - sr * y1);
let vz = dyp * si;
[vx, vy, vz]
}
struct GcrsSample {
dt: f64,
r_km: [f64; 3],
}
fn cross(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[
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: &[f64; 3]) -> f64 {
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
}
fn seed_params(samples: &[GcrsSample]) -> Result<[f64; N_PARAMS], ReducedOrbitError> {
let a_km = samples.iter().map(|s| norm(&s.r_km)).sum::<f64>() / samples.len() as f64;
if !a_km.is_finite() || a_km <= 0.0 {
return Err(ReducedOrbitError::SingularPlaneFit);
}
let mut h = [0.0_f64; 3];
for w in samples.windows(2) {
let c = cross(&w[0].r_km, &w[1].r_km);
h[0] += c[0];
h[1] += c[1];
h[2] += c[2];
}
let hn = norm(&h);
if !hn.is_finite() || hn <= a_km * a_km * 1.0e-9 {
return Err(ReducedOrbitError::SingularPlaneFit);
}
let hhat = [h[0] / hn, h[1] / hn, h[2] / hn];
let i = hhat[2].clamp(-1.0, 1.0).acos();
if i < MIN_INCLINATION_RAD || (std::f64::consts::PI - i) < MIN_INCLINATION_RAD {
return Err(ReducedOrbitError::RaanAmbiguous);
}
let node = [-hhat[1], hhat[0], 0.0];
let node_n = norm(&node);
if node_n <= 1.0e-12 {
return Err(ReducedOrbitError::RaanAmbiguous);
}
let raan0 = node[1].atan2(node[0]);
let nhat = [node[0] / node_n, node[1] / node_n, 0.0];
let r0 = &samples[0].r_km;
let cos_u = (r0[0] * nhat[0] + r0[1] * nhat[1] + r0[2] * nhat[2]) / norm(r0);
let p_axis = cross(&hhat, &nhat);
let sin_u = (r0[0] * p_axis[0] + r0[1] * p_axis[1] + r0[2] * p_axis[2]) / norm(r0);
let arg_lat0 = sin_u.atan2(cos_u);
let n = (MU_EARTH / (a_km * a_km * a_km)).sqrt();
let raan_rate = raan_rate_j2(n, i, a_km);
Ok([a_km, i, raan0, raan_rate, arg_lat0, n])
}
fn raan_rate_j2(n: f64, i: f64, a_km: f64) -> f64 {
let re_over_a = RE_EARTH / a_km;
-1.5 * n * J2_EARTH * re_over_a * re_over_a * i.cos()
}
pub fn fit(samples: &[EcefSample], scale: TimeScale) -> Result<ReducedOrbit, ReducedOrbitError> {
fit_with_model(samples, scale, Model::CircularSecular)
}
pub fn fit_with_model(
samples: &[EcefSample],
scale: TimeScale,
model: Model,
) -> Result<ReducedOrbit, ReducedOrbitError> {
match model {
Model::CircularSecular => fit_circular(samples, scale),
Model::EccentricSecular => fit_eccentric(samples, scale),
}
}
fn fit_circular(
samples: &[EcefSample],
scale: TimeScale,
) -> Result<ReducedOrbit, ReducedOrbitError> {
if samples.len() < MIN_SAMPLES {
return Err(ReducedOrbitError::TooFewSamples {
got: samples.len(),
required: MIN_SAMPLES,
});
}
let mut ordered: Vec<(TimeScales, &EcefSample)> = samples
.iter()
.map(|s| (s.epoch.time_scales(scale), s))
.collect();
ordered.sort_by(|a, b| {
(a.0.jd_whole + a.0.tt_fraction).total_cmp(&(b.0.jd_whole + b.0.tt_fraction))
});
let t0_cal = ordered[0].1.epoch;
let t0_ts = ordered[0].0;
let mut gcrs: Vec<GcrsSample> = Vec::with_capacity(samples.len());
for (ts, s) in &ordered {
let (x, y, z) =
itrs_to_gcrs_compute(s.x_m / M_PER_KM, s.y_m / M_PER_KM, s.z_m / M_PER_KM, ts);
let dt = dt_seconds(&t0_ts, ts);
gcrs.push(GcrsSample {
dt,
r_km: [x, y, z],
});
}
let dt_span = gcrs.iter().map(|s| s.dt).fold(f64::NEG_INFINITY, f64::max)
- gcrs.iter().map(|s| s.dt).fold(f64::INFINITY, f64::min);
if !dt_span.is_finite() || dt_span <= 0.0 {
return Err(ReducedOrbitError::InvalidWindow);
}
let seed = seed_params(&gcrs)?;
let a_scale = seed[0];
let rate_scale = dt_span; let seed_scaled = [
seed[0] / a_scale,
seed[1],
seed[2],
seed[3] * rate_scale,
seed[4],
seed[5] * rate_scale,
];
let m = 3 * gcrs.len();
let residual = {
let gcrs_ref: Vec<(f64, [f64; 3])> = gcrs.iter().map(|s| (s.dt, s.r_km)).collect();
move |x: &DVector<f64>| -> DVector<f64> {
let xs = x.as_slice();
let p = unscale_params(xs, a_scale, rate_scale);
let mut r = Vec::with_capacity(m);
for (dt, obs) in &gcrs_ref {
let model = eval_gcrs_km(&p, *dt);
r.push(model[0] - obs[0]);
r.push(model[1] - obs[1]);
r.push(model[2] - obs[2]);
}
DVector::from_vec(r)
}
};
let x0 = DVector::from_row_slice(&seed_scaled);
let problem = LeastSquaresProblem::new(residual, x0);
let opts = SolveOptions {
gtol: 1e-12,
ftol: 1e-12,
xtol: 1e-12,
max_nfev: 400,
};
let report = solve_trf(&problem, &opts)?;
let converged = matches!(
report.status,
Status::GradientTolerance | Status::CostTolerance | Status::StepTolerance
);
if !converged {
return Err(ReducedOrbitError::FitDidNotConverge);
}
let p = unscale_params(report.x.as_slice(), a_scale, rate_scale);
let res = &report.residual;
let n_samp = gcrs.len();
let mut sumsq = 0.0;
let mut maxsq = 0.0_f64;
for k in 0..n_samp {
let dx = res[3 * k] * M_PER_KM;
let dy = res[3 * k + 1] * M_PER_KM;
let dz = res[3 * k + 2] * M_PER_KM;
let e2 = dx * dx + dy * dy + dz * dz;
sumsq += e2;
if e2 > maxsq {
maxsq = e2;
}
}
let rms_m = (sumsq / n_samp as f64).sqrt();
let max_m = maxsq.sqrt();
let elements = Elements {
model: Model::CircularSecular,
epoch: t0_cal,
a_m: p[0] * M_PER_KM,
e: 0.0,
i_rad: p[1],
raan_rad: p[2],
raan_rate_rad_s: p[3],
raan_rate_j2_rad_s: raan_rate_j2(p[5], p[1], p[0]),
arg_lat_rad: p[4],
mean_motion_rad_s: p[5],
h: 0.0,
k: 0.0,
arg_perigee_rad: 0.0,
};
Ok(ReducedOrbit {
elements,
stats: FitStats {
rms_m,
max_m,
n_samples: n_samp,
},
})
}
fn to_gcrs_samples(
samples: &[EcefSample],
scale: TimeScale,
) -> Result<(CalendarEpoch, Vec<GcrsSample>, f64), ReducedOrbitError> {
if samples.len() < MIN_SAMPLES {
return Err(ReducedOrbitError::TooFewSamples {
got: samples.len(),
required: MIN_SAMPLES,
});
}
let mut ordered: Vec<(TimeScales, &EcefSample)> = samples
.iter()
.map(|s| (s.epoch.time_scales(scale), s))
.collect();
ordered.sort_by(|a, b| {
(a.0.jd_whole + a.0.tt_fraction).total_cmp(&(b.0.jd_whole + b.0.tt_fraction))
});
let t0_cal = ordered[0].1.epoch;
let t0_ts = ordered[0].0;
let mut gcrs: Vec<GcrsSample> = Vec::with_capacity(samples.len());
for (ts, s) in &ordered {
let (x, y, z) =
itrs_to_gcrs_compute(s.x_m / M_PER_KM, s.y_m / M_PER_KM, s.z_m / M_PER_KM, ts);
let dt = dt_seconds(&t0_ts, ts);
gcrs.push(GcrsSample {
dt,
r_km: [x, y, z],
});
}
let dt_span = gcrs.iter().map(|s| s.dt).fold(f64::NEG_INFINITY, f64::max)
- gcrs.iter().map(|s| s.dt).fold(f64::INFINITY, f64::min);
if !dt_span.is_finite() || dt_span <= 0.0 {
return Err(ReducedOrbitError::InvalidWindow);
}
Ok((t0_cal, gcrs, dt_span))
}
fn fit_eccentric(
samples: &[EcefSample],
scale: TimeScale,
) -> Result<ReducedOrbit, ReducedOrbitError> {
let (t0_cal, gcrs, dt_span) = to_gcrs_samples(samples, scale)?;
let seed_c = seed_params(&gcrs)?;
let a_scale = seed_c[0];
let rate_scale = dt_span;
let seed_scaled = [
1.0, seed_c[1], seed_c[2], seed_c[3] * rate_scale, 0.0, 0.0, seed_c[4], seed_c[5] * rate_scale, ];
let m = 3 * gcrs.len();
let residual = {
let gcrs_ref: Vec<(f64, [f64; 3])> = gcrs.iter().map(|s| (s.dt, s.r_km)).collect();
move |x: &DVector<f64>| -> DVector<f64> {
let xs = x.as_slice();
let p = unscale_params_ecc(xs, a_scale, rate_scale);
let mut r = Vec::with_capacity(m);
for (dt, obs) in &gcrs_ref {
let model = eval_gcrs_km_ecc(&p, *dt);
r.push(model[0] - obs[0]);
r.push(model[1] - obs[1]);
r.push(model[2] - obs[2]);
}
DVector::from_vec(r)
}
};
let x0 = DVector::from_row_slice(&seed_scaled);
let problem = LeastSquaresProblem::new(residual, x0);
let opts = SolveOptions {
gtol: 1e-12,
ftol: 1e-12,
xtol: 1e-12,
max_nfev: 400,
};
let report = solve_trf(&problem, &opts)?;
let converged = matches!(
report.status,
Status::GradientTolerance | Status::CostTolerance | Status::StepTolerance
);
if !converged {
return Err(ReducedOrbitError::FitDidNotConverge);
}
let p = unscale_params_ecc(report.x.as_slice(), a_scale, rate_scale);
let res = &report.residual;
let n_samp = gcrs.len();
let mut sumsq = 0.0;
let mut maxsq = 0.0_f64;
for k in 0..n_samp {
let dx = res[3 * k] * M_PER_KM;
let dy = res[3 * k + 1] * M_PER_KM;
let dz = res[3 * k + 2] * M_PER_KM;
let e2 = dx * dx + dy * dy + dz * dz;
sumsq += e2;
if e2 > maxsq {
maxsq = e2;
}
}
let rms_m = (sumsq / n_samp as f64).sqrt();
let max_m = maxsq.sqrt();
let h = p[4];
let k = p[5];
let e = (h * h + k * k).sqrt();
let arg_perigee_rad = if e < ECC_ZERO_EPS { 0.0 } else { h.atan2(k) };
let elements = Elements {
model: Model::EccentricSecular,
epoch: t0_cal,
a_m: p[0] * M_PER_KM,
e,
i_rad: p[1],
raan_rad: p[2],
raan_rate_rad_s: p[3],
raan_rate_j2_rad_s: raan_rate_j2(p[7], p[1], p[0]),
arg_lat_rad: p[6],
mean_motion_rad_s: p[7],
h,
k,
arg_perigee_rad,
};
Ok(ReducedOrbit {
elements,
stats: FitStats {
rms_m,
max_m,
n_samples: n_samp,
},
})
}
fn params_from_elements(e: &Elements) -> [f64; N_PARAMS] {
[
e.a_m / M_PER_KM,
e.i_rad,
e.raan_rad,
e.raan_rate_rad_s,
e.arg_lat_rad,
e.mean_motion_rad_s,
]
}
fn params_from_elements_ecc(e: &Elements) -> [f64; N_PARAMS_ECC] {
[
e.a_m / M_PER_KM,
e.i_rad,
e.raan_rad,
e.raan_rate_rad_s,
e.h,
e.k,
e.arg_lat_rad,
e.mean_motion_rad_s,
]
}
fn eval_position_km(elements: &Elements, dt: f64) -> [f64; 3] {
match elements.model {
Model::CircularSecular => eval_gcrs_km(¶ms_from_elements(elements), dt),
Model::EccentricSecular => eval_gcrs_km_ecc(¶ms_from_elements_ecc(elements), dt),
}
}
fn eval_velocity_km_s(elements: &Elements, dt: f64) -> [f64; 3] {
match elements.model {
Model::CircularSecular => eval_gcrs_velocity_km_s(¶ms_from_elements(elements), dt),
Model::EccentricSecular => {
eval_gcrs_velocity_km_s_ecc(¶ms_from_elements_ecc(elements), dt)
}
}
}
pub fn position(
elements: &Elements,
epoch: CalendarEpoch,
scale: TimeScale,
frame: Frame,
) -> [f64; 3] {
let t0_ts = elements.epoch.time_scales(scale);
let ts = epoch.time_scales(scale);
let dt = dt_seconds(&t0_ts, &ts);
let r_gcrs_km = eval_position_km(elements, dt);
match frame {
Frame::Gcrs => [
r_gcrs_km[0] * M_PER_KM,
r_gcrs_km[1] * M_PER_KM,
r_gcrs_km[2] * M_PER_KM,
],
Frame::Ecef => {
let mat = gcrs_to_itrs_matrix(&ts);
let r = mat3_vec3_mul(&mat, &r_gcrs_km);
[r[0] * M_PER_KM, r[1] * M_PER_KM, r[2] * M_PER_KM]
}
}
}
pub fn position_velocity(
elements: &Elements,
epoch: CalendarEpoch,
scale: TimeScale,
frame: Frame,
) -> ([f64; 3], [f64; 3]) {
let t0_ts = elements.epoch.time_scales(scale);
let ts = epoch.time_scales(scale);
let dt = dt_seconds(&t0_ts, &ts);
let r_gcrs_km = eval_position_km(elements, dt);
let v_gcrs_km_s = eval_velocity_km_s(elements, dt);
match frame {
Frame::Gcrs => {
let r = [
r_gcrs_km[0] * M_PER_KM,
r_gcrs_km[1] * M_PER_KM,
r_gcrs_km[2] * M_PER_KM,
];
let v = [
v_gcrs_km_s[0] * M_PER_KM,
v_gcrs_km_s[1] * M_PER_KM,
v_gcrs_km_s[2] * M_PER_KM,
];
(r, v)
}
Frame::Ecef => {
let mat = gcrs_to_itrs_matrix(&ts);
let r_itrs_km = mat3_vec3_mul(&mat, &r_gcrs_km);
let v_rot_km_s = mat3_vec3_mul(&mat, &v_gcrs_km_s);
let vx = v_rot_km_s[0] + OMEGA_EARTH_RAD_S * r_itrs_km[1];
let vy = v_rot_km_s[1] - OMEGA_EARTH_RAD_S * r_itrs_km[0];
let vz = v_rot_km_s[2];
let r = [
r_itrs_km[0] * M_PER_KM,
r_itrs_km[1] * M_PER_KM,
r_itrs_km[2] * M_PER_KM,
];
let v = [vx * M_PER_KM, vy * M_PER_KM, vz * M_PER_KM];
(r, v)
}
}
}
pub fn drift(
elements: &Elements,
truth: &[EcefSample],
scale: TimeScale,
threshold_m: f64,
) -> DriftReport {
let mut per_epoch = Vec::with_capacity(truth.len());
let mut sumsq = 0.0;
let mut max_m = 0.0_f64;
let mut threshold_horizon = None;
for s in truth {
let model = position(elements, s.epoch, scale, Frame::Ecef);
let dx = model[0] - s.x_m;
let dy = model[1] - s.y_m;
let dz = model[2] - s.z_m;
let err = (dx * dx + dy * dy + dz * dz).sqrt();
sumsq += err * err;
if err > max_m {
max_m = err;
}
if threshold_horizon.is_none() && err > threshold_m {
threshold_horizon = Some(s.epoch);
}
per_epoch.push(DriftEntry {
epoch: s.epoch,
error_m: err,
});
}
let rms_m = if per_epoch.is_empty() {
0.0
} else {
(sumsq / per_epoch.len() as f64).sqrt()
};
DriftReport {
per_epoch,
max_m,
rms_m,
threshold_horizon,
}
}
#[cfg(test)]
mod tests;