use crate::astro::time::model::{Instant, InstantRepr};
use crate::astro::time::civil::j2000_seconds_from_split;
use crate::constants::{KM_TO_M, OMEGA_E_DOT_RAD_S, US_TO_S};
use crate::frame::ItrfPositionM;
use crate::id::GnssSatelliteId;
use crate::sp3::{Sp3, Sp3State};
use crate::validate;
use crate::{Error, Result};
impl Sp3 {
pub fn epochs_j2000_seconds(&self) -> Vec<f64> {
self.epochs
.iter()
.filter_map(instant_to_j2000_seconds)
.collect()
}
pub fn position(&self, sat: GnssSatelliteId, epoch: Instant) -> Result<Sp3State> {
if epoch.scale != self.header.time_scale {
return Err(Error::InvalidInput(format!(
"SP3 query time scale {} does not match product time scale {}",
epoch.scale.abbrev(),
self.header.time_scale.abbrev()
)));
}
let query = instant_to_j2000_seconds(&epoch).ok_or(Error::EpochOutOfRange)?;
self.position_at_j2000_seconds(sat, query)
}
pub fn position_at_j2000_seconds(&self, sat: GnssSatelliteId, query: f64) -> Result<Sp3State> {
let mut pos_x: Vec<f64> = Vec::new();
let mut pos_kx: Vec<f64> = Vec::new();
let mut pos_ky: Vec<f64> = Vec::new();
let mut pos_kz: Vec<f64> = Vec::new();
let mut clk_nodes: Vec<(f64, f64, bool)> = Vec::new();
for (idx, ep) in self.epochs.iter().enumerate() {
let xi = match instant_to_j2000_seconds(ep) {
Some(v) => v.floor(),
None => continue,
};
let Some(raw) = self.interp_raw[idx].get(&sat) else {
continue;
};
pos_x.push(xi);
pos_kx.push(raw.km[0]);
pos_ky.push(raw.km[1]);
pos_kz.push(raw.km[2]);
if let Some(clk_us) = raw.clock_us {
clk_nodes.push((xi, clk_us, raw.clock_event));
}
}
interpolate_precise_state(sat, &pos_x, &pos_kx, &pos_ky, &pos_kz, &clk_nodes, query)
}
}
pub(super) fn interpolate_precise_state(
sat: GnssSatelliteId,
pos_x: &[f64],
pos_kx: &[f64],
pos_ky: &[f64],
pos_kz: &[f64],
clk_nodes: &[(f64, f64, bool)],
query: f64,
) -> Result<Sp3State> {
let query = validate::finite(query, "query_j2000_s").map_err(map_query_input)?;
if pos_x.is_empty() {
return Err(Error::UnknownSatellite(sat));
}
if pos_x.len() < 2 {
return Err(Error::EpochOutOfRange);
}
validate_strictly_increasing_nodes(pos_x)?;
let nominal = nominal_positive_spacing(pos_x).ok_or(Error::EpochOutOfRange)?;
let first = pos_x[0];
let last = pos_x[pos_x.len() - 1];
if query < first - nominal || query > last + nominal {
return Err(Error::EpochOutOfRange);
}
let gap_thresh = 1.5 * nominal;
let mut bi = 0usize;
while bi + 1 < pos_x.len() && pos_x[bi + 1] <= query {
bi += 1;
}
if bi + 1 < pos_x.len() {
let (lo, hi) = (pos_x[bi], pos_x[bi + 1]);
if hi - lo > gap_thresh && query > lo + nominal && query < hi - nominal {
return Err(Error::EpochOutOfRange);
}
}
let (x_m, y_m, z_m) = interpolate_position_neville(pos_x, pos_kx, pos_ky, pos_kz, query);
let clock_s = interpolate_clock(clk_nodes, query);
Ok(Sp3State {
position: ItrfPositionM::new(x_m, y_m, z_m).expect("valid ITRF position"),
clock_s,
velocity: None,
clock_rate_s_s: None,
flags: crate::sp3::Sp3Flags::default(),
})
}
fn map_query_input(error: validate::FieldError) -> Error {
Error::InvalidInput(format!("{} {}", error.field(), error.reason()))
}
fn nominal_positive_spacing(x: &[f64]) -> Option<f64> {
let nominal = x
.windows(2)
.map(|w| w[1] - w[0])
.filter(|&d| d > 0.0)
.fold(f64::INFINITY, f64::min);
if nominal.is_finite() {
Some(nominal)
} else {
None
}
}
fn validate_strictly_increasing_nodes(x: &[f64]) -> Result<()> {
for window in x.windows(2) {
if window[1] <= window[0] {
return Err(Error::InvalidInput(
"SP3 interpolation epochs must be strictly increasing".to_string(),
));
}
}
Ok(())
}
fn interpolate_clock(clk_nodes: &[(f64, f64, bool)], query: f64) -> Option<f64> {
if clk_nodes.len() < 2 {
return None;
}
let mut sub_start = 0usize;
let mut chosen: Option<(usize, usize)> = None; for i in 0..clk_nodes.len() {
let is_break = clk_nodes[i].2 && i > sub_start;
if is_break {
if range_contains_query(clk_nodes, sub_start, i, query) {
chosen = Some((sub_start, i));
}
sub_start = i;
}
}
if chosen.is_none() && range_contains_query(clk_nodes, sub_start, clk_nodes.len(), query) {
chosen = Some((sub_start, clk_nodes.len()));
}
let (start, end) = match chosen {
Some(r) => r,
None => nearest_subarc(clk_nodes, query)?,
};
if end - start < 2 {
return None;
}
let x: Vec<f64> = clk_nodes[start..end].iter().map(|n| n.0).collect();
let y: Vec<f64> = clk_nodes[start..end].iter().map(|n| n.1).collect();
Some(eval_cubic_spline(&x, &y, query) * US_TO_S)
}
fn range_contains_query(nodes: &[(f64, f64, bool)], start: usize, end: usize, query: f64) -> bool {
if end <= start {
return false;
}
let lo = nodes[start].0;
let hi = nodes[end - 1].0;
query >= lo && query <= hi
}
#[allow(clippy::needless_range_loop)]
fn nearest_subarc(nodes: &[(f64, f64, bool)], query: f64) -> Option<(usize, usize)> {
if nodes.is_empty() {
return None;
}
let mut bounds: Vec<(usize, usize)> = Vec::new();
let mut sub_start = 0usize;
for i in 0..nodes.len() {
if nodes[i].2 && i > sub_start {
bounds.push((sub_start, i));
sub_start = i;
}
}
bounds.push((sub_start, nodes.len()));
bounds
.into_iter()
.filter(|&(s, e)| e - s >= 2)
.min_by(|&(s1, e1), &(s2, e2)| {
let d1 = span_distance(nodes, s1, e1, query);
let d2 = span_distance(nodes, s2, e2, query);
d1.partial_cmp(&d2).unwrap_or(core::cmp::Ordering::Equal)
})
}
fn span_distance(nodes: &[(f64, f64, bool)], start: usize, end: usize, query: f64) -> f64 {
let lo = nodes[start].0;
let hi = nodes[end - 1].0;
if query < lo {
lo - query
} else if query > hi {
query - hi
} else {
0.0
}
}
pub(super) fn instant_to_j2000_seconds(instant: &Instant) -> Option<f64> {
match instant.repr {
InstantRepr::JulianDate(split) => {
Some(j2000_seconds_from_split(split.jd_whole, split.fraction))
}
InstantRepr::Nanos(ns) => {
let _ = ns;
None
}
}
}
const NEVILLE_POINTS: usize = 11;
fn interpolate_position_neville(
x: &[f64],
kx: &[f64],
ky: &[f64],
kz: &[f64],
query: f64,
) -> (f64, f64, f64) {
let n = x.len();
let nominal = nominal_positive_spacing(x).unwrap_or(1.0);
let gap_thresh = 1.5 * nominal;
let mut pivot = 0usize;
while pivot + 1 < n && x[pivot + 1] <= query {
pivot += 1;
}
if pivot + 1 < n && (x[pivot + 1] - x[pivot]) > gap_thresh && query >= x[pivot + 1] - nominal {
pivot += 1;
}
let mut run_lo = pivot;
while run_lo > 0 && (x[run_lo] - x[run_lo - 1]) <= gap_thresh {
run_lo -= 1;
}
let mut run_hi = pivot + 1;
while run_hi < n && (x[run_hi] - x[run_hi - 1]) <= gap_thresh {
run_hi += 1;
}
let run_len = run_hi - run_lo;
let win = NEVILLE_POINTS.min(run_len);
let half = (NEVILLE_POINTS / 2) as isize;
let mut start = pivot as isize - half;
if start < run_lo as isize {
start = run_lo as isize;
}
if start + win as isize > run_hi as isize {
start = run_hi as isize - win as isize;
}
let start = start as usize;
let mut t = [0.0f64; NEVILLE_POINTS];
let mut px = [0.0f64; NEVILLE_POINTS];
let mut py = [0.0f64; NEVILLE_POINTS];
let mut pz = [0.0f64; NEVILLE_POINTS];
for j in 0..win {
let k = start + j;
let tj = x[k] - query;
let (s, c) = (OMEGA_E_DOT_RAD_S * tj).sin_cos();
t[j] = tj;
px[j] = c * kx[k] - s * ky[k];
py[j] = s * kx[k] + c * ky[k];
pz[j] = kz[k];
}
let x_km = neville(&t[..win], &px[..win]);
let y_km = neville(&t[..win], &py[..win]);
let z_km = neville(&t[..win], &pz[..win]);
(x_km * KM_TO_M, y_km * KM_TO_M, z_km * KM_TO_M)
}
fn neville(x: &[f64], y: &[f64]) -> f64 {
let n = y.len();
let mut c: [f64; NEVILLE_POINTS] = [0.0; NEVILLE_POINTS];
c[..n].copy_from_slice(&y[..n]);
for j in 1..n {
for i in 0..(n - j) {
c[i] = (x[i + j] * c[i] - x[i] * c[i + 1]) / (x[i + j] - x[i]);
}
}
c[0]
}
fn eval_cubic_spline(x: &[f64], y: &[f64], query: f64) -> f64 {
let n = x.len();
debug_assert_eq!(n, y.len());
debug_assert!(n >= 2);
let dydx = solve_not_a_knot_slopes(x, y);
let (c0, c1, c2, c3) = hermite_segment_coeffs(x, y, &dydx);
evaluate_ppoly(x, &c0, &c1, &c2, &c3, query)
}
fn solve_not_a_knot_slopes(x: &[f64], y: &[f64]) -> Vec<f64> {
let n = x.len();
let mut dx = vec![0.0; n - 1];
let mut slope = vec![0.0; n - 1];
for i in 0..n - 1 {
dx[i] = x[i + 1] - x[i];
slope[i] = (y[i + 1] - y[i]) / dx[i];
}
if n == 2 {
return vec![slope[0], slope[0]];
}
if n == 3 {
return solve_n3_parabola(&dx, &slope, y);
}
let mut d = vec![0.0; n];
let mut du = vec![0.0; n - 1];
let mut dl = vec![0.0; n - 1];
let mut b = vec![0.0; n];
for i in 1..n - 1 {
d[i] = 2.0 * (dx[i - 1] + dx[i]); du[i] = dx[i - 1]; dl[i - 1] = dx[i]; b[i] = 3.0 * (dx[i] * slope[i - 1] + dx[i - 1] * slope[i]);
}
{
let dd = x[2] - x[0];
d[0] = dx[1]; du[0] = dd; b[0] = ((dx[0] + 2.0 * dd) * dx[1] * slope[0] + dx[0] * dx[0] * slope[1]) / dd;
}
{
let dd = x[n - 1] - x[n - 3];
d[n - 1] = dx[n - 2]; dl[n - 2] = dd; b[n - 1] = (dx[n - 2] * dx[n - 2] * slope[n - 3]
+ (2.0 * dd + dx[n - 2]) * dx[n - 3] * slope[n - 2])
/ dd;
}
dgtsv(dl, d, du, b)
}
fn solve_n3_parabola(dx: &[f64], slope: &[f64], _y: &[f64]) -> Vec<f64> {
let mut a = [
[1.0, 1.0, 0.0],
[dx[1], 2.0 * (dx[0] + dx[1]), dx[0]],
[0.0, 1.0, 1.0],
];
let mut b = [
2.0 * slope[0],
3.0 * (dx[0] * slope[1] + dx[1] * slope[0]),
2.0 * slope[1],
];
gesv3(&mut a, &mut b);
b.to_vec()
}
fn dgtsv(mut dl: Vec<f64>, mut d: Vec<f64>, mut du: Vec<f64>, mut b: Vec<f64>) -> Vec<f64> {
let n = d.len();
if n == 1 {
b[0] /= d[0];
return b;
}
for i in 0..n.saturating_sub(2) {
if d[i].abs() >= dl[i].abs() {
let fact = dl[i] / d[i];
d[i + 1] = (-fact).mul_add(du[i], d[i + 1]);
b[i + 1] = (-fact).mul_add(b[i], b[i + 1]);
dl[i] = 0.0;
} else {
let fact = d[i] / dl[i];
d[i] = dl[i];
let temp = d[i + 1];
d[i + 1] = (-fact).mul_add(temp, du[i]);
dl[i] = du[i + 1];
du[i + 1] = -fact * dl[i];
du[i] = temp;
let tb = b[i];
b[i] = b[i + 1];
b[i + 1] = (-fact).mul_add(b[i + 1], tb);
}
}
if n > 1 {
let i = n - 2;
if d[i].abs() >= dl[i].abs() {
let fact = dl[i] / d[i];
d[i + 1] = (-fact).mul_add(du[i], d[i + 1]);
b[i + 1] = (-fact).mul_add(b[i], b[i + 1]);
} else {
let fact = d[i] / dl[i];
d[i] = dl[i];
let temp = d[i + 1];
d[i + 1] = (-fact).mul_add(temp, du[i]);
du[i] = temp;
let tb = b[i];
b[i] = b[i + 1];
b[i + 1] = (-fact).mul_add(b[i + 1], tb);
}
}
b[n - 1] /= d[n - 1];
if n > 1 {
b[n - 2] = (-du[n - 2]).mul_add(b[n - 1], b[n - 2]) / d[n - 2];
}
for i in (0..n.saturating_sub(2)).rev() {
let t = (-du[i]).mul_add(b[i + 1], b[i]);
b[i] = (-dl[i]).mul_add(b[i + 2], t) / d[i];
}
b
}
#[allow(clippy::needless_range_loop)]
fn gesv3(a: &mut [[f64; 3]; 3], b: &mut [f64; 3]) {
let mut perm = [0usize, 1, 2];
for k in 0..3 {
let mut piv = k;
let mut best = a[k][k].abs();
for r in (k + 1)..3 {
let v = a[r][k].abs();
if v > best {
best = v;
piv = r;
}
}
if piv != k {
a.swap(k, piv);
perm.swap(k, piv);
}
for r in (k + 1)..3 {
let factor = a[r][k] / a[k][k];
a[r][k] = factor;
for c in (k + 1)..3 {
a[r][c] = (-factor).mul_add(a[k][c], a[r][c]);
}
}
}
let pb = [b[perm[0]], b[perm[1]], b[perm[2]]];
let mut yv = [0.0; 3];
for r in 0..3 {
let mut s = pb[r];
for c in 0..r {
s = (-a[r][c]).mul_add(yv[c], s);
}
yv[r] = s;
}
for r in (0..3).rev() {
let mut s = yv[r];
for c in (r + 1)..3 {
s = (-a[r][c]).mul_add(b[c], s);
}
b[r] = s / a[r][r];
}
}
fn hermite_segment_coeffs(
x: &[f64],
y: &[f64],
dydx: &[f64],
) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
let n = x.len();
let mut c0 = vec![0.0; n - 1];
let mut c1 = vec![0.0; n - 1];
let mut c2 = vec![0.0; n - 1];
let mut c3 = vec![0.0; n - 1];
for i in 0..n - 1 {
let dxr = x[i + 1] - x[i];
let slope = (y[i + 1] - y[i]) / dxr;
let t = (dydx[i] + dydx[i + 1] - 2.0 * slope) / dxr;
c0[i] = t / dxr;
c1[i] = (slope - dydx[i]) / dxr - t;
c2[i] = dydx[i];
c3[i] = y[i];
}
(c0, c1, c2, c3)
}
fn evaluate_ppoly(x: &[f64], c0: &[f64], c1: &[f64], c2: &[f64], c3: &[f64], query: f64) -> f64 {
let n = x.len();
let last = n - 2;
let interval = if query.is_nan() {
return f64::NAN;
} else if query < x[0] {
0
} else if query > x[n - 1] {
last
} else {
if query == x[n - 1] {
last
} else {
let mut lo = 0usize;
let mut hi = n - 1;
while hi - lo > 1 {
let mid = (lo + hi) / 2;
if x[mid] <= query {
lo = mid;
} else {
hi = mid;
}
}
lo
}
};
let s = query - x[interval];
let mut res = 0.0;
let mut z = 1.0;
res += c3[interval] * z;
z *= s;
res += c2[interval] * z;
z *= s;
res += c1[interval] * z;
z *= s;
res += c0[interval] * z;
res
}
#[cfg(all(test, sidereon_repo_tests))]
pub(super) fn eval_cubic_spline_for_test(x: &[f64], y: &[f64], query: f64) -> f64 {
eval_cubic_spline(x, y, query)
}
#[cfg(all(test, sidereon_repo_tests))]
mod interp_tests;