pub mod body;
pub mod earth;
pub mod epoch;
pub mod frame;
pub mod horizons;
pub mod moon;
pub mod planets;
pub mod rotation;
pub mod sun;
#[cfg(feature = "wasm")]
pub mod wasm;
use nalgebra::{Matrix3, Rotation3, UnitQuaternion, Vector3};
pub type SimpleEci = frame::Vec3<frame::SimpleEci>;
pub type SimpleEcef = frame::Vec3<frame::SimpleEcef>;
pub type Gcrs = frame::Vec3<frame::Gcrs>;
pub type Rsw = frame::Vec3<frame::Rsw>;
pub fn rsw_quaternion(pos: &Vector3<f64>, vel: &Vector3<f64>) -> Option<UnitQuaternion<f64>> {
let r_len = pos.norm();
if r_len < 1e-10 {
return None;
}
let r = pos / r_len;
let w_raw = pos.cross(vel);
let w_len = w_raw.norm();
if w_len < 1e-10 {
return None;
}
let w = w_raw / w_len;
let s = w.cross(&r);
let mat = Matrix3::from_columns(&[r, s, w]);
Some(UnitQuaternion::from_rotation_matrix(
&Rotation3::from_matrix_unchecked(mat),
))
}
pub fn body_quat_to_rsw(
pos: &Vector3<f64>,
vel: &Vector3<f64>,
q_body_eci: &UnitQuaternion<f64>,
) -> Option<UnitQuaternion<f64>> {
let q_rsw_eci = rsw_quaternion(pos, vel)?;
Some(q_rsw_eci.inverse() * q_body_eci)
}
#[cfg(test)]
mod tests {
use super::*;
fn eci_to_ecef(eci: &SimpleEci, era: f64) -> SimpleEcef {
frame::Rotation::<frame::SimpleEci, frame::SimpleEcef>::from_era(era).transform(eci)
}
fn ecef_to_eci(ecef: &SimpleEcef, era: f64) -> SimpleEci {
frame::Rotation::<frame::SimpleEcef, frame::SimpleEci>::from_era(era).transform(ecef)
}
#[test]
fn test_simple_eci_ecef_zero_era() {
let eci = SimpleEci::new(7000.0, 1000.0, 500.0);
let ecef = eci_to_ecef(&eci, 0.0);
let eps = 1e-10;
assert!((ecef.x() - eci.x()).abs() < eps);
assert!((ecef.y() - eci.y()).abs() < eps);
assert!((ecef.z() - eci.z()).abs() < eps);
}
#[test]
fn test_simple_eci_ecef_90deg() {
let era = std::f64::consts::FRAC_PI_2;
let eci = SimpleEci::new(1.0, 0.0, 0.0);
let ecef = eci_to_ecef(&eci, era);
let eps = 1e-10;
assert!(ecef.x().abs() < eps);
assert!((ecef.y() - (-1.0)).abs() < eps);
assert!(ecef.z().abs() < eps);
let eci2 = SimpleEci::new(0.0, 1.0, 0.0);
let ecef2 = eci_to_ecef(&eci2, era);
assert!((ecef2.x() - 1.0).abs() < eps);
assert!(ecef2.y().abs() < eps);
assert!(ecef2.z().abs() < eps);
}
#[test]
fn test_simple_eci_ecef_roundtrip() {
let original = SimpleEci::new(6700.0, 1500.0, 3200.0);
let era = 1.234;
let roundtrip = ecef_to_eci(&eci_to_ecef(&original, era), era);
let eps = 1e-10;
assert!((roundtrip.x() - original.x()).abs() < eps);
assert!((roundtrip.y() - original.y()).abs() < eps);
assert!((roundtrip.z() - original.z()).abs() < eps);
}
#[test]
fn test_simple_eci_ecef_magnitude_preserved() {
let eci = SimpleEci::new(6700.0, 1500.0, 3200.0);
let era = 2.5;
let ecef = eci_to_ecef(&eci, era);
let eps = 1e-10;
assert!((eci.magnitude() - ecef.magnitude()).abs() < eps);
}
#[test]
fn rsw_quaternion_equatorial_prograde_is_identity() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 7.5, 0.0);
let q = rsw_quaternion(&pos, &vel).unwrap();
let identity = UnitQuaternion::identity();
assert!(
q.angle_to(&identity) < 1e-10,
"equatorial prograde RSW-to-ECI should be identity, got angle {}",
q.angle_to(&identity)
);
}
#[test]
fn rsw_quaternion_equatorial_prograde_basis_vectors() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 7.5, 0.0);
let q = rsw_quaternion(&pos, &vel).unwrap();
let r_eci = q * Vector3::new(1.0, 0.0, 0.0); let s_eci = q * Vector3::new(0.0, 1.0, 0.0); let w_eci = q * Vector3::new(0.0, 0.0, 1.0);
let eps = 1e-10;
assert!((r_eci - Vector3::new(1.0, 0.0, 0.0)).norm() < eps);
assert!((s_eci - Vector3::new(0.0, 1.0, 0.0)).norm() < eps);
assert!((w_eci - Vector3::new(0.0, 0.0, 1.0)).norm() < eps);
}
#[test]
fn rsw_quaternion_inclined_orbit_basis_vectors() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 0.0, 7.5);
let q = rsw_quaternion(&pos, &vel).unwrap();
let r_eci = q * Vector3::new(1.0, 0.0, 0.0); let s_eci = q * Vector3::new(0.0, 1.0, 0.0); let w_eci = q * Vector3::new(0.0, 0.0, 1.0);
let eps = 1e-10;
assert!(
(r_eci - Vector3::new(1.0, 0.0, 0.0)).norm() < eps,
"R: {r_eci}"
);
assert!(
(s_eci - Vector3::new(0.0, 0.0, 1.0)).norm() < eps,
"S: {s_eci}"
);
assert!(
(w_eci - Vector3::new(0.0, -1.0, 0.0)).norm() < eps,
"W: {w_eci}"
);
}
#[test]
fn rsw_quaternion_degenerate_zero_pos() {
let pos = Vector3::new(0.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 7.5, 0.0);
assert!(rsw_quaternion(&pos, &vel).is_none());
}
#[test]
fn rsw_quaternion_degenerate_parallel() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(1.0, 0.0, 0.0);
assert!(rsw_quaternion(&pos, &vel).is_none());
}
#[test]
fn body_quat_to_rsw_identity_is_rsw_aligned() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 7.5, 0.0);
let q_rsw = rsw_quaternion(&pos, &vel).unwrap();
let result = body_quat_to_rsw(&pos, &vel, &q_rsw).unwrap();
assert!(result.angle_to(&UnitQuaternion::identity()) < 1e-10);
}
#[test]
fn body_quat_to_rsw_90deg_yaw() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(0.0, 7.5, 0.0);
let q_rsw = rsw_quaternion(&pos, &vel).unwrap();
let yaw_in_rsw =
UnitQuaternion::from_axis_angle(&Vector3::z_axis(), std::f64::consts::FRAC_PI_2);
let q_body_eci = q_rsw * yaw_in_rsw;
let result = body_quat_to_rsw(&pos, &vel, &q_body_eci).unwrap();
assert!(result.angle_to(&yaw_in_rsw) < 1e-10);
}
#[test]
fn body_quat_to_rsw_degenerate() {
let pos = Vector3::new(7000.0, 0.0, 0.0);
let vel = Vector3::new(1.0, 0.0, 0.0);
let q = UnitQuaternion::identity();
assert!(body_quat_to_rsw(&pos, &vel, &q).is_none());
}
#[test]
fn body_quat_to_rsw_eci_fixed_body_rotates_with_orbit() {
let r = 7000.0;
let v = 7.5;
let q_body_eci = UnitQuaternion::identity();
let q0 = body_quat_to_rsw(
&Vector3::new(r, 0.0, 0.0),
&Vector3::new(0.0, v, 0.0),
&q_body_eci,
)
.unwrap();
let q90 = body_quat_to_rsw(
&Vector3::new(0.0, r, 0.0),
&Vector3::new(-v, 0.0, 0.0),
&q_body_eci,
)
.unwrap();
let q180 = body_quat_to_rsw(
&Vector3::new(-r, 0.0, 0.0),
&Vector3::new(0.0, -v, 0.0),
&q_body_eci,
)
.unwrap();
let eps = 1e-10;
assert!(
(q0.angle_to(&q90) - std::f64::consts::FRAC_PI_2).abs() < eps,
"0°→90°: expected π/2, got {}",
q0.angle_to(&q90)
);
assert!(
(q0.angle_to(&q180) - std::f64::consts::PI).abs() < eps,
"0°→180°: expected π, got {}",
q0.angle_to(&q180)
);
}
#[test]
fn body_quat_to_rsw_nadir_pointing_is_identity() {
let r = 7000.0;
let v = 7.5;
for theta in [0.0_f64, 0.3, 1.2, 2.5, 4.7] {
let pos = Vector3::new(r * theta.cos(), r * theta.sin(), 0.0);
let vel = Vector3::new(-v * theta.sin(), v * theta.cos(), 0.0);
let q_rsw = rsw_quaternion(&pos, &vel).unwrap();
let result = body_quat_to_rsw(&pos, &vel, &q_rsw).unwrap();
assert!(
result.angle_to(&UnitQuaternion::identity()) < 1e-10,
"nadir at θ={theta}: expected identity, got angle {}",
result.angle_to(&UnitQuaternion::identity())
);
}
}
#[test]
fn body_quat_to_rsw_roundtrip_with_rsw_quaternion() {
let pos = Vector3::new(3000.0, 5000.0, 2000.0);
let vel = Vector3::new(-2.0, 1.5, 6.0);
let q_body_eci = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(1.0, 2.0, 3.0)),
1.234,
);
let q_rsw = rsw_quaternion(&pos, &vel).unwrap();
let q_body_rsw = body_quat_to_rsw(&pos, &vel, &q_body_eci).unwrap();
let reconstructed = q_rsw * q_body_rsw;
assert!(reconstructed.angle_to(&q_body_eci) < 1e-10);
}
}