use std::sync::Arc;
use nalgebra::Vector3;
use super::daf::DAF;
use super::errors::{JplephemError, Result};
const MAXTRM: usize = 25;
#[derive(Debug, Clone)]
struct MdaRecord {
tl: f64,
g: Vec<f64>,
refpos: [f64; 3],
refvel: [f64; 3],
dt: [Vec<f64>; 3],
kqmax1: usize,
kq: [usize; 3],
}
#[derive(Debug, Clone)]
pub struct Type21Data {
maxdim: usize,
dlsize: usize,
n_records: usize,
epoch_table: Vec<f64>,
epoch_dir: Vec<f64>,
record_data: Vec<f64>,
}
impl Type21Data {
pub fn load(daf: &Arc<DAF>, start_i: usize, end_i: usize) -> Result<Self> {
let maxdim_raw = daf.read_array(end_i, end_i)?;
let maxdim = maxdim_raw[0] as usize;
if maxdim > MAXTRM {
return Err(JplephemError::InvalidFormat(format!(
"Type 21 MAXDIM {maxdim} exceeds maximum supported {MAXTRM}"
)));
}
if maxdim == 0 {
return Err(JplephemError::InvalidFormat(
"Type 21 MAXDIM is zero".to_string(),
));
}
let dlsize = 4 * maxdim + 11;
let n_records_raw = daf.read_array(end_i - 1, end_i - 1)?;
let n_records = n_records_raw[0] as usize;
if n_records == 0 {
return Err(JplephemError::InvalidFormat(
"Type 21 segment has zero records".to_string(),
));
}
let epoch_dir_count = if n_records > 100 {
(n_records - 1) / 100
} else {
0
};
let record_end = start_i + n_records * dlsize - 1;
let record_data = daf.read_array(start_i, record_end)?;
let epoch_start = start_i + n_records * dlsize;
let epoch_end = epoch_start + n_records - 1;
let epoch_table = daf.read_array(epoch_start, epoch_end)?;
let epoch_dir = if epoch_dir_count > 0 {
let dir_start = end_i - 1 - epoch_dir_count;
let dir_end = end_i - 2;
daf.read_array(dir_start, dir_end)?
} else {
Vec::new()
};
let expected_size = n_records * dlsize + n_records + epoch_dir_count + 2;
let actual_size = end_i - start_i + 1;
if actual_size != expected_size {
return Err(JplephemError::InvalidFormat(format!(
"Type 21 segment size mismatch: expected {expected_size}, got {actual_size} \
(n_records={n_records}, dlsize={dlsize}, maxdim={maxdim}, epoch_dir={epoch_dir_count})"
)));
}
Ok(Type21Data {
maxdim,
dlsize,
n_records,
epoch_table,
epoch_dir,
record_data,
})
}
fn find_record_index(&self, et: f64) -> usize {
let (search_start, search_end) = if !self.epoch_dir.is_empty() {
let mut bracket_end = self.n_records;
let mut bracket_start = self.epoch_dir.len() * 100 + 1;
for (i, &dir_epoch) in self.epoch_dir.iter().enumerate() {
if dir_epoch > et {
bracket_end = (i + 1) * 100;
bracket_start = i * 100 + 1;
break;
}
}
(bracket_start, bracket_end)
} else {
(1, self.n_records)
};
let mut record_index = search_end;
for i in (search_start - 1)..search_end {
if self.epoch_table[i] > et {
record_index = i + 1;
break;
}
}
record_index
}
fn parse_record(&self, record_index: usize) -> Result<MdaRecord> {
let offset = (record_index - 1) * self.dlsize;
let rec = &self.record_data[offset..offset + self.dlsize];
let maxdim = self.maxdim;
let tl = rec[0];
let g = rec[1..1 + maxdim].to_vec();
let refpos = [rec[maxdim + 1], rec[maxdim + 3], rec[maxdim + 5]];
let refvel = [rec[maxdim + 2], rec[maxdim + 4], rec[maxdim + 6]];
let dt_flat = &rec[maxdim + 7..4 * maxdim + 7];
let dt_x = dt_flat[0..maxdim].to_vec();
let dt_y = dt_flat[maxdim..2 * maxdim].to_vec();
let dt_z = dt_flat[2 * maxdim..3 * maxdim].to_vec();
let kqmax1 = rec[4 * maxdim + 7] as usize;
let kq = [
rec[4 * maxdim + 8] as usize,
rec[4 * maxdim + 9] as usize,
rec[4 * maxdim + 10] as usize,
];
if kqmax1 > maxdim + 1 {
return Err(JplephemError::InvalidFormat(format!(
"Type 21 KQMAX1 ({kqmax1}) exceeds MAXDIM+1 ({})",
maxdim + 1
)));
}
Ok(MdaRecord {
tl,
g,
refpos,
refvel,
dt: [dt_x, dt_y, dt_z],
kqmax1,
kq,
})
}
pub fn compute(&self, et: f64) -> Result<(Vector3<f64>, Vector3<f64>)> {
let record_index = self.find_record_index(et);
let rec = self.parse_record(record_index)?;
evaluate_mda(&rec, et)
}
}
fn evaluate_mda(rec: &MdaRecord, et: f64) -> Result<(Vector3<f64>, Vector3<f64>)> {
let delta = et - rec.tl;
let kqmax1 = rec.kqmax1;
if kqmax1 < 2 {
return Err(JplephemError::InvalidFormat(format!(
"Type 21 KQMAX1 ({kqmax1}) must be >= 2"
)));
}
let mq2 = kqmax1 - 2;
let mut ks = kqmax1 - 1;
let mut fc = vec![0.0; MAXTRM + 3];
let mut wc = vec![0.0; MAXTRM + 3];
fc[0] = 1.0;
let mut tp = delta;
for j in 1..=mq2 {
if rec.g[j - 1] == 0.0 {
return Err(JplephemError::InvalidFormat(format!(
"Type 21 zero stepsize at index {j}"
)));
}
fc[j] = tp / rec.g[j - 1];
wc[j - 1] = delta / rec.g[j - 1];
tp = delta + rec.g[j - 1];
}
let mut w = vec![0.0; MAXTRM + 3];
for j in 1..=kqmax1 {
w[j - 1] = 1.0 / (j as f64);
}
let mut jx = 0usize;
let mut ks1 = ks as isize - 1;
while ks >= 2 {
jx += 1;
for j in 1..=jx {
w[j + ks - 1] = fc[j] * w[j + ks1 as usize - 1] - wc[j - 1] * w[j + ks - 1];
}
ks -= 1;
ks1 -= 1;
}
let mut position = [0.0f64; 3];
for (i, pos) in position.iter_mut().enumerate() {
let kqq = rec.kq[i];
let mut sum = 0.0;
for j in (1..=kqq).rev() {
sum += rec.dt[i][j - 1] * w[j + ks - 1];
}
*pos = rec.refpos[i] + delta * (rec.refvel[i] + delta * sum);
}
for j in 1..=jx {
w[j + ks - 1] = fc[j] * w[j + ks1 as usize - 1] - wc[j - 1] * w[j + ks - 1];
}
ks -= 1;
let mut velocity = [0.0f64; 3];
for (i, vel) in velocity.iter_mut().enumerate() {
let kqq = rec.kq[i];
let mut sum = 0.0;
for j in (1..=kqq).rev() {
sum += rec.dt[i][j - 1] * w[j + ks - 1];
}
*vel = rec.refvel[i] + delta * sum;
}
Ok((
Vector3::new(position[0], position[1], position[2]),
Vector3::new(velocity[0], velocity[1], velocity[2]),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_record(maxdim: usize) -> MdaRecord {
MdaRecord {
tl: 0.0,
g: vec![1.0; maxdim],
refpos: [100.0, 200.0, 300.0],
refvel: [0.0, 0.0, 0.0],
dt: [vec![0.0; maxdim], vec![0.0; maxdim], vec![0.0; maxdim]],
kqmax1: 2,
kq: [1, 1, 1],
}
}
#[test]
fn test_evaluate_mda_stationary() {
let rec = make_test_record(15);
let (pos, vel) = evaluate_mda(&rec, 0.0).unwrap();
assert!((pos.x - 100.0).abs() < 1e-12);
assert!((pos.y - 200.0).abs() < 1e-12);
assert!((pos.z - 300.0).abs() < 1e-12);
assert!(vel.x.abs() < 1e-12);
assert!(vel.y.abs() < 1e-12);
assert!(vel.z.abs() < 1e-12);
}
#[test]
fn test_evaluate_mda_uniform_motion() {
let rec = MdaRecord {
tl: 1000.0,
g: vec![1.0; 15],
refpos: [100.0, 200.0, 300.0],
refvel: [10.0, -5.0, 3.0],
dt: [vec![0.0; 15], vec![0.0; 15], vec![0.0; 15]],
kqmax1: 2,
kq: [1, 1, 1],
};
let dt = 10.0;
let (pos, vel) = evaluate_mda(&rec, 1000.0 + dt).unwrap();
assert!(
(pos.x - (100.0 + 10.0 * dt)).abs() < 1e-10,
"x: {} vs {}",
pos.x,
100.0 + 10.0 * dt
);
assert!(
(pos.y - (200.0 - 5.0 * dt)).abs() < 1e-10,
"y: {} vs {}",
pos.y,
200.0 - 5.0 * dt
);
assert!(
(pos.z - (300.0 + 3.0 * dt)).abs() < 1e-10,
"z: {} vs {}",
pos.z,
300.0 + 3.0 * dt
);
assert!((vel.x - 10.0).abs() < 1e-10);
assert!((vel.y - (-5.0)).abs() < 1e-10);
assert!((vel.z - 3.0).abs() < 1e-10);
}
#[test]
fn test_evaluate_mda_at_reference_epoch() {
let rec = MdaRecord {
tl: 5000.0,
g: vec![86400.0; 15],
refpos: [1.0e8, -2.0e7, 3.0e6],
refvel: [25.0, -10.0, 5.0],
dt: [vec![1e-5; 15], vec![2e-6; 15], vec![3e-7; 15]],
kqmax1: 8,
kq: [7, 7, 7],
};
let (pos, vel) = evaluate_mda(&rec, 5000.0).unwrap();
assert!((pos.x - 1.0e8).abs() < 1e-6);
assert!((pos.y - (-2.0e7)).abs() < 1e-6);
assert!((pos.z - 3.0e6).abs() < 1e-6);
assert!((vel.x - 25.0).abs() < 1e-10);
assert!((vel.y - (-10.0)).abs() < 1e-10);
assert!((vel.z - 5.0).abs() < 1e-10);
}
#[test]
fn test_evaluate_mda_zero_stepsize_error() {
let mut rec = make_test_record(15);
rec.kqmax1 = 4;
rec.kq = [3, 3, 3];
rec.g[0] = 0.0;
let result = evaluate_mda(&rec, 1.0);
assert!(result.is_err());
}
#[test]
fn test_evaluate_mda_kqmax1_too_small() {
let mut rec = make_test_record(15);
rec.kqmax1 = 1;
let result = evaluate_mda(&rec, 0.0);
assert!(result.is_err());
}
#[test]
fn test_type21_data_load_validation() {
let maxdim = 15;
let dlsize = 4 * maxdim + 11;
assert_eq!(dlsize, 71);
let maxdim = 20;
let dlsize = 4 * maxdim + 11;
assert_eq!(dlsize, 91);
}
#[test]
fn test_find_record_index_small() {
let data = Type21Data {
maxdim: 15,
dlsize: 71,
n_records: 5,
epoch_table: vec![100.0, 200.0, 300.0, 400.0, 500.0],
epoch_dir: vec![],
record_data: vec![0.0; 71 * 5],
};
assert_eq!(data.find_record_index(50.0), 1);
assert_eq!(data.find_record_index(150.0), 2);
assert_eq!(data.find_record_index(250.0), 3);
assert_eq!(data.find_record_index(600.0), 5);
}
}