pub const OBLIQUITY_J2000: f64 = 0.409_092_804_22;
const COS_EPS: f64 = 0.917_482_062_070_108_2;
const SIN_EPS: f64 = 0.397_777_155_929_776_9;
#[inline]
pub fn bary_to_helio(bary: &[f64; 6], sun: &[f64; 6]) -> [f64; 6] {
[
bary[0] - sun[0],
bary[1] - sun[1],
bary[2] - sun[2],
bary[3] - sun[3],
bary[4] - sun[4],
bary[5] - sun[5],
]
}
#[inline]
pub fn helio_to_bary(helio: &[f64; 6], sun: &[f64; 6]) -> [f64; 6] {
[
helio[0] + sun[0],
helio[1] + sun[1],
helio[2] + sun[2],
helio[3] + sun[3],
helio[4] + sun[4],
helio[5] + sun[5],
]
}
pub fn equatorial_to_ecliptic(state: &[f64; 6]) -> [f64; 6] {
let [x, y, z, vx, vy, vz] = *state;
[
x,
COS_EPS * y + SIN_EPS * z,
-SIN_EPS * y + COS_EPS * z,
vx,
COS_EPS * vy + SIN_EPS * vz,
-SIN_EPS * vy + COS_EPS * vz,
]
}
pub fn ecliptic_to_equatorial(state: &[f64; 6]) -> [f64; 6] {
let [x, y, z, vx, vy, vz] = *state;
[
x,
COS_EPS * y - SIN_EPS * z,
SIN_EPS * y + COS_EPS * z,
vx,
COS_EPS * vy - SIN_EPS * vz,
SIN_EPS * vy + COS_EPS * vz,
]
}
pub fn rotate_matrix_eq_to_ecl(m: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let r = rotation_matrix_eq_to_ecl();
mat6x6_multiply(&mat6x6_multiply(&r, m), &transpose6x6(&r))
}
fn rotation_matrix_eq_to_ecl() -> [[f64; 6]; 6] {
let mut r = [[0.0f64; 6]; 6];
r[0][0] = 1.0;
r[1][1] = COS_EPS;
r[1][2] = SIN_EPS;
r[2][1] = -SIN_EPS;
r[2][2] = COS_EPS;
r[3][3] = 1.0;
r[4][4] = COS_EPS;
r[4][5] = SIN_EPS;
r[5][4] = -SIN_EPS;
r[5][5] = COS_EPS;
r
}
fn transpose6x6(m: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let mut t = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
t[i][j] = m[j][i];
}
}
t
}
fn mat6x6_multiply(a: &[[f64; 6]; 6], b: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let mut c = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
for k in 0..6 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
pub fn cartesian_to_spherical(dx: [f64; 3], dv: [f64; 3]) -> [f64; 6] {
let [x, y, z] = dx;
let [vx, vy, vz] = dv;
let rho_sq = x * x + y * y + z * z;
let rho = rho_sq.sqrt();
let xy_sq = x * x + y * y;
let xy = xy_sq.sqrt();
let mut ra = y.atan2(x);
if ra < 0.0 {
ra += std::f64::consts::TAU;
}
let dec = (z / rho).asin();
let drho = (x * vx + y * vy + z * vz) / rho;
let dra = if xy_sq > 0.0 {
(x * vy - y * vx) / xy_sq
} else {
0.0
};
let ddec = if xy > 0.0 {
(vz * rho_sq - z * (x * vx + y * vy + z * vz)) / (rho_sq * xy)
} else {
0.0
};
[rho, ra, dec, drho, dra, ddec]
}
pub fn cartesian_to_spherical_jacobian(dx: [f64; 3], dv: [f64; 3]) -> [[f64; 6]; 6] {
let [x, y, z] = dx;
let [vx, vy, vz] = dv;
let rho_sq = x * x + y * y + z * z;
let rho = rho_sq.sqrt();
let rho3 = rho_sq * rho;
let xy_sq = x * x + y * y;
let xy = xy_sq.sqrt();
let s = x * vx + y * vy + z * vz; let a = x * vy - y * vx;
let mut jac = [[0.0f64; 6]; 6];
jac[0][0] = x / rho;
jac[0][1] = y / rho;
jac[0][2] = z / rho;
if xy_sq > 0.0 {
jac[1][0] = -y / xy_sq;
jac[1][1] = x / xy_sq;
}
if xy > 0.0 {
jac[2][0] = -x * z / (rho_sq * xy);
jac[2][1] = -y * z / (rho_sq * xy);
jac[2][2] = xy / rho_sq;
}
jac[3][0] = (vx * rho_sq - x * s) / rho3;
jac[3][1] = (vy * rho_sq - y * s) / rho3;
jac[3][2] = (vz * rho_sq - z * s) / rho3;
jac[3][3] = x / rho;
jac[3][4] = y / rho;
jac[3][5] = z / rho;
if xy_sq > 0.0 {
let xy4 = xy_sq * xy_sq;
jac[4][0] = (vy * xy_sq - 2.0 * x * a) / xy4;
jac[4][1] = (-vx * xy_sq - 2.0 * y * a) / xy4;
jac[4][3] = -y / xy_sq;
jac[4][4] = x / xy_sq;
}
if xy > 0.0 {
let f = vz * rho_sq - z * s;
let g = rho_sq * xy;
let dgdx = x * (2.0 * xy_sq + rho_sq) / xy;
let dgdy = y * (2.0 * xy_sq + rho_sq) / xy;
let dgdz = 2.0 * z * xy;
let dfdx = 2.0 * x * vz - z * vx;
let dfdy = 2.0 * y * vz - z * vy;
let dfdz = vz * z - s;
let g2 = g * g;
jac[5][0] = (dfdx * g - f * dgdx) / g2;
jac[5][1] = (dfdy * g - f * dgdy) / g2;
jac[5][2] = (dfdz * g - f * dgdz) / g2;
jac[5][3] = -z * x / g;
jac[5][4] = -z * y / g;
jac[5][5] = xy_sq / g; }
jac
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_eq_ecl() {
let state = [1.0, 0.5, -0.3, 0.01, 0.005, -0.003];
let ecl = equatorial_to_ecliptic(&state);
let back = ecliptic_to_equatorial(&ecl);
for i in 0..6 {
assert!((state[i] - back[i]).abs() < 1e-14, "element {i} mismatch");
}
}
#[test]
fn spherical_basic() {
let dx = [2.0, 0.0, 0.0];
let dv = [0.0, 0.0, 0.0];
let sph = cartesian_to_spherical(dx, dv);
assert!((sph[0] - 2.0).abs() < 1e-14, "rho");
assert!(sph[1].abs() < 1e-14, "ra");
assert!(sph[2].abs() < 1e-14, "dec");
}
fn numerical_partial(dx: [f64; 3], dv: [f64; 3], k: usize, j: usize, h: f64) -> f64 {
let mut dx_p = dx;
let mut dx_m = dx;
let mut dv_p = dv;
let mut dv_m = dv;
if j < 3 {
dx_p[j] += h;
dx_m[j] -= h;
} else {
dv_p[j - 3] += h;
dv_m[j - 3] -= h;
}
let plus = cartesian_to_spherical(dx_p, dv_p);
let minus = cartesian_to_spherical(dx_m, dv_m);
let mut d = plus[k] - minus[k];
if k == 1 {
d = (d + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
}
d / (2.0 * h)
}
fn max_row_err(
analytic: &[[f64; 6]; 6],
dx: [f64; 3],
dv: [f64; 3],
row: usize,
h: f64,
) -> f64 {
let mut worst = 0.0f64;
for j in 0..6 {
let num = numerical_partial(dx, dv, row, j, h);
let ana = analytic[row][j];
let err = (num - ana).abs();
if err > worst {
worst = err;
}
}
worst
}
#[test]
fn jacobian_matches_finite_differences_generic() {
let dx = [0.7, -0.4, 0.3];
let dv = [0.02, 0.015, -0.008];
let jac = cartesian_to_spherical_jacobian(dx, dv);
let h = 1e-5;
for row in 0..6 {
let err = max_row_err(&jac, dx, dv, row, h);
assert!(err < 5e-8, "row {row}: max |analytic - FD| = {err:.3e}");
}
}
#[test]
fn jacobian_matches_finite_differences_near_equator() {
let dx = [1.5, 0.8, 0.02];
let dv = [0.01, -0.005, 0.003];
let jac = cartesian_to_spherical_jacobian(dx, dv);
let h = 1e-5;
for row in 0..6 {
let err = max_row_err(&jac, dx, dv, row, h);
assert!(err < 5e-8, "row {row}: max |analytic - FD| = {err:.3e}");
}
}
#[test]
fn jacobian_pole_is_sanitized() {
let dx = [0.0, 0.0, 1.0];
let dv = [0.0, 0.0, 0.1];
let jac = cartesian_to_spherical_jacobian(dx, dv);
for row in 0..6 {
for col in 0..6 {
assert!(
jac[row][col].is_finite(),
"jac[{row}][{col}] = {} is not finite",
jac[row][col]
);
}
}
}
}