use super::errors::{JplephemError, Result};
#[derive(Debug, Clone)]
pub struct ChebyshevPolynomial {
coefficients: Vec<f64>,
}
impl ChebyshevPolynomial {
pub fn new(coefficients: Vec<f64>) -> Self {
Self { coefficients }
}
pub fn evaluate(&self, x: f64) -> f64 {
if self.coefficients.is_empty() {
return 0.0;
}
let n = self.coefficients.len();
if n == 1 {
return self.coefficients[0];
}
let mut b_k_plus_1 = 0.0;
let mut b_k_plus_2 = 0.0;
for i in (1..n).rev() {
let b_k = 2.0 * x * b_k_plus_1 - b_k_plus_2 + self.coefficients[i];
b_k_plus_2 = b_k_plus_1;
b_k_plus_1 = b_k;
}
self.coefficients[0] + x * b_k_plus_1 - b_k_plus_2
}
pub fn derivative(&self, x: f64) -> f64 {
if self.coefficients.len() <= 1 {
return 0.0;
}
let mut result = 0.0;
for i in 1..self.coefficients.len() {
let n = i as f64;
result += self.coefficients[i] * n * Self::chebyshev_u(i - 1, x);
}
result
}
fn chebyshev_u(n: usize, x: f64) -> f64 {
match n {
0 => 1.0,
1 => 2.0 * x,
_ => {
let mut u_prev2 = 1.0;
let mut u_prev1 = 2.0 * x;
let mut u_n = 0.0;
for _ in 2..=n {
u_n = 2.0 * x * u_prev1 - u_prev2;
u_prev2 = u_prev1;
u_prev1 = u_n;
}
u_n
}
}
}
pub fn degree(&self) -> usize {
self.coefficients.len().saturating_sub(1)
}
pub fn coefficients(&self) -> &[f64] {
&self.coefficients
}
}
pub fn normalize_time(time: f64, midpoint: f64, radius: f64) -> Result<f64> {
if radius <= 0.0 {
return Err(JplephemError::Other(
"Invalid radius for time normalization: must be positive".to_string(),
));
}
let normalized = (time - midpoint) / radius;
if !(-1.0 - 1e-9..=1.0 + 1e-9).contains(&normalized) {
return Err(JplephemError::OutOfRangeError {
jd: time,
start_jd: midpoint - radius,
end_jd: midpoint + radius,
});
}
Ok(normalized.clamp(-1.0, 1.0))
}
pub fn rescale_derivative(deriv_normalized: f64, radius: f64) -> Result<f64> {
if radius <= 0.0 {
return Err(JplephemError::Other(
"Invalid radius for derivative rescaling: must be positive".to_string(),
));
}
Ok(deriv_normalized / radius)
}
#[derive(Debug, Clone)]
pub struct ChebyshevRecords {
init: f64,
intlen: f64,
record_size: usize,
n_records: usize,
n_coeffs: usize,
component_count: usize,
coefficients: Vec<f64>,
}
impl ChebyshevRecords {
pub fn load(array: &[f64], component_count: usize) -> Result<Self> {
if array.len() < 4 {
return Err(JplephemError::InvalidFormat(
"Segment data too small for a Chebyshev record array".to_string(),
));
}
let n = array.len();
let init = array[n - 4];
let intlen = array[n - 3];
let record_size = array[n - 2] as usize;
let n_records = array[n - 1] as usize;
if record_size < 2 + component_count {
return Err(JplephemError::InvalidFormat(format!(
"Invalid record size for {component_count} components: {record_size}"
)));
}
let expected_size = n_records * record_size + 4;
if n != expected_size {
return Err(JplephemError::InvalidFormat(format!(
"Inconsistent array size: expected {expected_size}, got {n}"
)));
}
Ok(Self {
init,
intlen,
record_size,
n_records,
n_coeffs: (record_size - 2) / component_count,
component_count,
coefficients: array[0..n - 4].to_vec(),
})
}
pub fn init(&self) -> f64 {
self.init
}
pub fn interval_length(&self) -> f64 {
self.intlen
}
pub fn len(&self) -> usize {
self.n_records
}
pub fn is_empty(&self) -> bool {
self.n_records == 0
}
pub fn coefficient_count(&self) -> usize {
self.n_coeffs
}
pub fn component_count(&self) -> usize {
self.component_count
}
pub fn record_index(&self, et: f64) -> Result<usize> {
let out_of_range = || JplephemError::OutOfRangeError {
jd: super::spk::seconds_to_jd(et),
start_jd: super::spk::seconds_to_jd(self.init),
end_jd: super::spk::seconds_to_jd(self.init + self.intlen * self.n_records as f64),
};
let elapsed = et - self.init;
if elapsed < 0.0 || self.n_records == 0 {
return Err(out_of_range());
}
let index = (elapsed / self.intlen).floor() as usize;
match index.cmp(&self.n_records) {
std::cmp::Ordering::Less => Ok(index),
std::cmp::Ordering::Equal => Ok(self.n_records - 1),
std::cmp::Ordering::Greater => Err(out_of_range()),
}
}
pub fn record(&self, index: usize) -> Result<ChebyshevRecord<'_>> {
let start = index * self.record_size;
if index >= self.n_records || start + self.record_size > self.coefficients.len() {
return Err(JplephemError::InvalidFormat(
"Record index out of bounds".to_string(),
));
}
Ok(ChebyshevRecord {
midpoint: self.coefficients[start],
radius: self.coefficients[start + 1],
coefficients: &self.coefficients[start + 2..start + self.record_size],
n_coeffs: self.n_coeffs,
component_count: self.component_count,
})
}
pub fn record_at(&self, et: f64) -> Result<ChebyshevRecord<'_>> {
self.record(self.record_index(et)?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChebyshevRecord<'a> {
midpoint: f64,
radius: f64,
coefficients: &'a [f64],
n_coeffs: usize,
component_count: usize,
}
impl ChebyshevRecord<'_> {
pub fn midpoint(&self) -> f64 {
self.midpoint
}
pub fn radius(&self) -> f64 {
self.radius
}
pub fn polynomial(&self, index: usize) -> Result<ChebyshevPolynomial> {
if index >= self.component_count {
return Err(JplephemError::InvalidFormat(format!(
"Component {index} out of range for a record of {} components",
self.component_count
)));
}
let start = index * self.n_coeffs;
Ok(ChebyshevPolynomial::new(
self.coefficients[start..start + self.n_coeffs].to_vec(),
))
}
pub fn normalized_time(&self, et: f64) -> Result<f64> {
normalize_time(et, self.midpoint, self.radius)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chebyshev_constant() {
let poly = ChebyshevPolynomial::new(vec![5.0]);
assert_eq!(poly.evaluate(-1.0), 5.0);
assert_eq!(poly.evaluate(0.0), 5.0);
assert_eq!(poly.evaluate(1.0), 5.0);
assert_eq!(poly.derivative(0.0), 0.0);
}
#[test]
fn test_chebyshev_linear() {
let poly = ChebyshevPolynomial::new(vec![3.0, 2.0]);
assert!((poly.evaluate(-1.0) - 1.0).abs() < 1e-12);
assert!((poly.evaluate(0.0) - 3.0).abs() < 1e-12);
assert!((poly.evaluate(1.0) - 5.0).abs() < 1e-12);
assert!((poly.derivative(0.0) - 2.0).abs() < 1e-12);
}
#[test]
fn test_chebyshev_quadratic() {
let poly = ChebyshevPolynomial::new(vec![3.0, 2.0, 1.0]);
assert!((poly.evaluate(-1.0) - 2.0).abs() < 1e-12);
assert!((poly.evaluate(0.0) - 2.0).abs() < 1e-12);
assert!((poly.evaluate(1.0) - 6.0).abs() < 1e-12);
assert!((poly.derivative(-1.0) - (-2.0)).abs() < 1e-12);
assert!((poly.derivative(0.0) - 2.0).abs() < 1e-12);
assert!((poly.derivative(1.0) - 6.0).abs() < 1e-12);
}
#[test]
fn test_x_squared_approximation() {
let poly = ChebyshevPolynomial::new(vec![0.5, 0.0, 0.5]);
for i in 0..=10 {
let x = -1.0 + i as f64 * 0.2;
let expected = x * x;
assert!(
(expected - poly.evaluate(x)).abs() < 1e-12,
"Bad approximation at x={x}"
);
}
}
#[test]
fn test_time_normalization() {
let mid = 100.0;
let rad = 10.0;
assert_eq!(normalize_time(100.0, mid, rad).unwrap(), 0.0);
assert_eq!(normalize_time(90.0, mid, rad).unwrap(), -1.0);
assert_eq!(normalize_time(110.0, mid, rad).unwrap(), 1.0);
assert_eq!(normalize_time(95.0, mid, rad).unwrap(), -0.5);
assert!(normalize_time(79.0, mid, rad).is_err());
assert!(normalize_time(121.0, mid, rad).is_err());
assert!(normalize_time(100.0, mid, 0.0).is_err());
}
#[test]
fn test_derivative_rescaling() {
assert_eq!(rescale_derivative(5.0, 10.0).unwrap(), 0.5);
assert!(rescale_derivative(5.0, 0.0).is_err());
}
}