use crate::coordinates::frames::ICRF;
use crate::time::JulianDate;
use affn::{Displacement, Velocity};
use qtty::*;
type KmPerDay = Per<Kilometer, Day>;
type KmPerDayQ = qtty::Quantity<KmPerDay>;
const SECONDS_PER_DAY: f64 = qtty::time::SECONDS_PER_DAY;
#[inline]
fn jd_to_et(jd_tdb: JulianDate) -> Seconds {
(jd_tdb - JulianDate::J2000).to::<Second>()
}
#[inline]
fn xyz_coeffs(record: &[f64], ncoeff: usize) -> (&[f64], &[f64], &[f64]) {
(
&record[2..2 + ncoeff],
&record[2 + ncoeff..2 + 2 * ncoeff],
&record[2 + 2 * ncoeff..2 + 3 * ncoeff],
)
}
#[inline]
fn locate_params(
init: Seconds,
intlen: Seconds,
n_records: usize,
jd_tdb: JulianDate,
) -> (usize, f64, Seconds) {
let et = jd_to_et(jd_tdb);
let idx = ((et - init) / intlen).value() as usize;
let idx = idx.min(n_records - 1);
(idx, et.value(), intlen)
}
#[inline]
fn record_tau(record: &[f64], et: f64) -> (f64, Seconds) {
let mid = Seconds::new(record[0]);
let radius = Seconds::new(record[1]);
let tau = (et - mid.value()) / radius.value();
(tau, radius)
}
#[inline]
fn eval_position(record: &[f64], ncoeff: usize, tau: f64) -> Displacement<ICRF, Kilometer> {
let (cx, cy, cz) = xyz_coeffs(record, ncoeff);
Displacement::new(
Kilometers::new(cheby::evaluate(cx, tau)),
Kilometers::new(cheby::evaluate(cy, tau)),
Kilometers::new(cheby::evaluate(cz, tau)),
)
}
#[inline]
fn eval_velocity(
record: &[f64],
ncoeff: usize,
tau: f64,
radius: Seconds,
) -> Velocity<ICRF, KmPerDay> {
let (cx, cy, cz) = xyz_coeffs(record, ncoeff);
let scale = SECONDS_PER_DAY / radius.value();
Velocity::new(
KmPerDayQ::new(cheby::evaluate_derivative(cx, tau) * scale),
KmPerDayQ::new(cheby::evaluate_derivative(cy, tau) * scale),
KmPerDayQ::new(cheby::evaluate_derivative(cz, tau) * scale),
)
}
#[inline]
fn eval_both(
record: &[f64],
ncoeff: usize,
tau: f64,
radius: Seconds,
) -> (Displacement<ICRF, Kilometer>, Velocity<ICRF, KmPerDay>) {
let (cx, cy, cz) = xyz_coeffs(record, ncoeff);
let scale = SECONDS_PER_DAY / radius.value();
let (px, vx) = cheby::evaluate_both(cx, tau);
let (py, vy) = cheby::evaluate_both(cy, tau);
let (pz, vz) = cheby::evaluate_both(cz, tau);
(
Displacement::new(
Kilometers::new(px),
Kilometers::new(py),
Kilometers::new(pz),
),
Velocity::new(
KmPerDayQ::new(vx * scale),
KmPerDayQ::new(vy * scale),
KmPerDayQ::new(vz * scale),
),
)
}
pub struct SegmentDescriptor {
pub init: Seconds,
pub intlen: Seconds,
pub ncoeff: usize,
pub n_records: usize,
pub record_fn: fn(usize) -> &'static [f64],
}
#[inline]
fn locate(seg: &SegmentDescriptor, jd_tdb: JulianDate) -> (&'static [f64], f64, Seconds) {
let (idx, et, intlen) = locate_params(seg.init, seg.intlen, seg.n_records, jd_tdb);
let record = (seg.record_fn)(idx);
let (tau, radius) = record_tau(record, et);
let _ = intlen;
(record, tau, radius)
}
impl SegmentDescriptor {
#[inline]
pub fn position(&self, jd_tdb: JulianDate) -> Displacement<ICRF, Kilometer> {
let (record, tau, _) = locate(self, jd_tdb);
eval_position(record, self.ncoeff, tau)
}
#[inline]
pub fn velocity(&self, jd_tdb: JulianDate) -> Velocity<ICRF, KmPerDay> {
let (record, tau, radius) = locate(self, jd_tdb);
eval_velocity(record, self.ncoeff, tau, radius)
}
#[inline]
pub fn position_velocity(
&self,
jd_tdb: JulianDate,
) -> (Displacement<ICRF, Kilometer>, Velocity<ICRF, KmPerDay>) {
let (record, tau, radius) = locate(self, jd_tdb);
eval_both(record, self.ncoeff, tau, radius)
}
}
pub struct DynSegmentDescriptor {
pub init: Seconds,
pub intlen: Seconds,
pub ncoeff: usize,
pub rsize: usize,
pub n_records: usize,
pub data: Vec<f64>,
}
impl DynSegmentDescriptor {
pub fn from_spk(seg: &crate::data::spk::SegmentData) -> Self {
Self {
init: Seconds::new(seg.init),
intlen: Seconds::new(seg.intlen),
ncoeff: seg.ncoeff,
rsize: seg.rsize,
n_records: seg.n_records,
data: seg.records.clone(),
}
}
#[inline]
fn record(&self, i: usize) -> &[f64] {
let start = i * self.rsize;
&self.data[start..start + self.rsize]
}
#[inline]
fn locate(&self, jd_tdb: JulianDate) -> (&[f64], f64, Seconds) {
let (idx, et, _) = locate_params(self.init, self.intlen, self.n_records, jd_tdb);
let record = self.record(idx);
let (tau, radius) = record_tau(record, et);
(record, tau, radius)
}
#[inline]
pub fn position(&self, jd_tdb: JulianDate) -> Displacement<ICRF, Kilometer> {
let (record, tau, _) = self.locate(jd_tdb);
eval_position(record, self.ncoeff, tau)
}
#[inline]
pub fn velocity(&self, jd_tdb: JulianDate) -> Velocity<ICRF, KmPerDay> {
let (record, tau, radius) = self.locate(jd_tdb);
eval_velocity(record, self.ncoeff, tau, radius)
}
#[inline]
pub fn position_velocity(
&self,
jd_tdb: JulianDate,
) -> (Displacement<ICRF, Kilometer>, Velocity<ICRF, KmPerDay>) {
let (record, tau, radius) = self.locate(jd_tdb);
eval_both(record, self.ncoeff, tau, radius)
}
}
#[cfg(test)]
mod tests {
use super::*;
const JD_J2000: f64 = 2451545.0;
fn make_desc(cx0: f64, cy0: f64, cz0: f64) -> DynSegmentDescriptor {
let ncoeff = 2usize;
let rsize = 2 + 3 * ncoeff; let intlen_days = 1000.0_f64;
let intlen_secs = intlen_days * SECONDS_PER_DAY;
let init_secs = 0.0_f64;
let mid = init_secs + intlen_secs / 2.0;
let radius = intlen_secs / 2.0;
let data = vec![mid, radius, cx0, 0.0, cy0, 0.0, cz0, 0.0];
DynSegmentDescriptor {
init: Seconds::new(init_secs),
intlen: Seconds::new(intlen_secs),
ncoeff,
rsize,
n_records: 1,
data,
}
}
fn jd_mid() -> JulianDate {
JulianDate::new(JD_J2000 + 500.0)
}
fn jd_quarter() -> JulianDate {
JulianDate::new(JD_J2000 + 250.0)
}
#[test]
fn dyn_desc_position_at_mid() {
let desc = make_desc(1000.0, 2000.0, 3000.0);
let pos = desc.position(jd_mid());
assert!(
(pos.x().value() - 1000.0).abs() < 1e-6,
"x = {}",
pos.x().value()
);
assert!(
(pos.y().value() - 2000.0).abs() < 1e-6,
"y = {}",
pos.y().value()
);
assert!(
(pos.z().value() - 3000.0).abs() < 1e-6,
"z = {}",
pos.z().value()
);
}
#[test]
fn dyn_desc_position_is_finite() {
let desc = make_desc(500.0, -300.0, 150.0);
let pos = desc.position(jd_quarter());
assert!(pos.x().value().is_finite());
assert!(pos.y().value().is_finite());
assert!(pos.z().value().is_finite());
}
#[test]
fn dyn_desc_position_at_boundary_does_not_panic() {
let desc = make_desc(100.0, 200.0, 300.0);
let jd_start = JulianDate::new(JD_J2000); let pos = desc.position(jd_start);
assert!(pos.x().value().is_finite());
}
#[test]
fn dyn_desc_velocity_is_finite() {
let desc = make_desc(1000.0, 2000.0, 3000.0);
let vel = desc.velocity(jd_mid());
assert!(vel.x().value().is_finite());
assert!(vel.y().value().is_finite());
assert!(vel.z().value().is_finite());
}
#[test]
fn dyn_desc_velocity_at_tau0_with_linear_coeff() {
let ncoeff = 2usize;
let rsize = 8usize;
let intlen_secs = 1000.0 * SECONDS_PER_DAY;
let radius = intlen_secs / 2.0;
let data = vec![
intlen_secs / 2.0, radius, 0.0,
1.0, 0.0,
2.0, 0.0,
3.0, ];
let desc = DynSegmentDescriptor {
init: Seconds::new(0.0),
intlen: Seconds::new(intlen_secs),
ncoeff,
rsize,
n_records: 1,
data,
};
let vel = desc.velocity(jd_mid());
let scale = SECONDS_PER_DAY / radius;
assert!((vel.x().value() - 1.0 * scale).abs() < 1e-6);
assert!((vel.y().value() - 2.0 * scale).abs() < 1e-6);
assert!((vel.z().value() - 3.0 * scale).abs() < 1e-6);
}
#[test]
fn dyn_desc_position_velocity_consistent() {
let desc = make_desc(1500.0, -800.0, 400.0);
let jd = jd_quarter();
let pos_only = desc.position(jd);
let vel_only = desc.velocity(jd);
let (pos_both, vel_both) = desc.position_velocity(jd);
assert!((pos_only.x().value() - pos_both.x().value()).abs() < 1e-10);
assert!((pos_only.y().value() - pos_both.y().value()).abs() < 1e-10);
assert!((pos_only.z().value() - pos_both.z().value()).abs() < 1e-10);
assert!((vel_only.x().value() - vel_both.x().value()).abs() < 1e-10);
assert!((vel_only.y().value() - vel_both.y().value()).abs() < 1e-10);
assert!((vel_only.z().value() - vel_both.z().value()).abs() < 1e-10);
}
#[test]
fn dyn_desc_from_spk_roundtrip() {
use crate::data::spk::SegmentData;
let ncoeff = 3usize;
let rsize = 2 + 3 * ncoeff; let records = vec![0.0; rsize]; let seg = SegmentData {
init: 0.0,
intlen: 86400.0,
rsize,
ncoeff,
n_records: 1,
records,
};
let desc = DynSegmentDescriptor::from_spk(&seg);
assert_eq!(desc.ncoeff, ncoeff);
assert_eq!(desc.rsize, rsize);
assert_eq!(desc.n_records, 1);
assert_eq!(desc.data.len(), rsize);
}
static STATIC_RECORD: [f64; 8] = [
500.0 * 86400.0, 500.0 * 86400.0, 1000.0,
0.0, 2000.0,
0.0, 3000.0,
0.0, ];
fn static_record_fn(_i: usize) -> &'static [f64] {
&STATIC_RECORD
}
fn make_static_desc() -> SegmentDescriptor {
SegmentDescriptor {
init: Seconds::new(0.0),
intlen: Seconds::new(1000.0 * SECONDS_PER_DAY),
ncoeff: 2,
n_records: 1,
record_fn: static_record_fn,
}
}
#[test]
fn static_desc_position_at_mid() {
let desc = make_static_desc();
let jd = jd_mid(); let pos = desc.position(jd);
assert!(
(pos.x().value() - 1000.0).abs() < 1e-6,
"x={}",
pos.x().value()
);
assert!(
(pos.y().value() - 2000.0).abs() < 1e-6,
"y={}",
pos.y().value()
);
assert!(
(pos.z().value() - 3000.0).abs() < 1e-6,
"z={}",
pos.z().value()
);
}
#[test]
fn static_desc_position_is_finite() {
let desc = make_static_desc();
let pos = desc.position(jd_quarter());
assert!(pos.x().value().is_finite());
assert!(pos.y().value().is_finite());
assert!(pos.z().value().is_finite());
}
#[test]
fn static_desc_velocity_is_finite() {
let desc = make_static_desc();
let vel = desc.velocity(jd_mid());
assert!(vel.x().value().is_finite());
assert!(vel.y().value().is_finite());
assert!(vel.z().value().is_finite());
}
#[test]
fn static_desc_velocity_zero_at_constant_segment() {
let desc = make_static_desc();
let vel = desc.velocity(jd_mid());
assert!(vel.x().value().abs() < 1e-9, "vx={}", vel.x().value());
assert!(vel.y().value().abs() < 1e-9, "vy={}", vel.y().value());
assert!(vel.z().value().abs() < 1e-9, "vz={}", vel.z().value());
}
#[test]
fn static_desc_position_velocity_consistent() {
let desc = make_static_desc();
let jd = jd_quarter();
let pos_only = desc.position(jd);
let vel_only = desc.velocity(jd);
let (pos_both, vel_both) = desc.position_velocity(jd);
assert!((pos_only.x().value() - pos_both.x().value()).abs() < 1e-10);
assert!((pos_only.y().value() - pos_both.y().value()).abs() < 1e-10);
assert!((pos_only.z().value() - pos_both.z().value()).abs() < 1e-10);
assert!((vel_only.x().value() - vel_both.x().value()).abs() < 1e-10);
assert!((vel_only.y().value() - vel_both.y().value()).abs() < 1e-10);
assert!((vel_only.z().value() - vel_both.z().value()).abs() < 1e-10);
}
#[test]
fn static_desc_position_at_boundary() {
let desc = make_static_desc();
let jd_start = JulianDate::new(JD_J2000); let pos = desc.position(jd_start);
assert!(pos.x().value().is_finite());
}
#[test]
fn dyn_desc_multi_record_second_interval() {
let ncoeff = 2usize;
let rsize = 8usize;
let intlen_secs = 500.0 * SECONDS_PER_DAY;
let mid0 = intlen_secs / 2.0;
let rad = intlen_secs / 2.0;
let mid1 = intlen_secs + intlen_secs / 2.0;
let data = vec![
mid0, rad, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, mid1, rad, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, ];
let desc = DynSegmentDescriptor {
init: Seconds::new(0.0),
intlen: Seconds::new(intlen_secs),
ncoeff,
rsize,
n_records: 2,
data,
};
let jd_second = JulianDate::new(JD_J2000 + 750.0);
let pos = desc.position(jd_second);
let et_s = 750.0 * SECONDS_PER_DAY;
let tau = (et_s - mid1) / rad;
let expected_x = 200.0; let _ = tau;
let _ = expected_x;
assert!(pos.x().value().is_finite());
}
}