use crate::rotational::RotationalState;
use crate::state::{TranslationalState, TranslationalStateTyped};
use astrodyn_math::{mat3_from_rows, GeodeticState, JeodQuat, OrbitalElements};
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::dims::GravParam;
use astrodyn_quantities::ext::Vec3Ext;
use astrodyn_quantities::frame::RootInertial;
use glam::{DMat3, DVec3};
use uom::si::angle::radian;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;
pub fn init_from_orbital_elements(
semi_major_axis: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
true_anomaly: f64,
mu: f64,
) -> TranslationalState {
assert!(
mu > 0.0,
"init_from_orbital_elements: mu must be positive, got {mu}"
);
assert!(
semi_major_axis.is_finite(),
"init_from_orbital_elements: semi_major_axis must be finite, got {semi_major_axis}"
);
assert!(
(0.0..1.0).contains(&eccentricity),
"init_from_orbital_elements: eccentricity must be in [0, 1), got {eccentricity}"
);
use astrodyn_quantities::frame::SelfPlanet;
let mut oe = OrbitalElements::<SelfPlanet>::default();
oe.semi_major_axis = semi_major_axis;
oe.e_mag = eccentricity;
oe.inclination = inclination;
oe.long_asc_node = raan;
oe.arg_periapsis = arg_periapsis;
oe.semiparam = semi_major_axis * (1.0 - eccentricity * eccentricity);
oe.true_anom = true_anomaly;
oe.nu_to_anomalies();
let (position, velocity) = oe
.to_cartesian(mu)
.expect("init_from_orbital_elements: to_cartesian failed");
TranslationalState { position, velocity }
}
pub fn init_from_semi_latus_rectum_true_anomaly(
semi_latus_rectum: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
true_anomaly: f64,
mu: f64,
) -> TranslationalState {
assert!(
mu > 0.0,
"init_from_semi_latus_rectum_true_anomaly: mu must be positive, got {mu}"
);
assert!(
semi_latus_rectum > 0.0 && semi_latus_rectum.is_finite(),
"init_from_semi_latus_rectum_true_anomaly: semi_latus_rectum must be positive and finite, \
got {semi_latus_rectum}"
);
assert!(
(0.0..1.0).contains(&eccentricity),
"init_from_semi_latus_rectum_true_anomaly: eccentricity must be in [0, 1), \
got {eccentricity}"
);
use astrodyn_quantities::frame::SelfPlanet;
let mut oe = OrbitalElements::<SelfPlanet>::default();
oe.semiparam = semi_latus_rectum;
oe.e_mag = eccentricity;
oe.inclination = inclination;
oe.long_asc_node = raan;
oe.arg_periapsis = arg_periapsis;
oe.true_anom = true_anomaly;
oe.nu_to_anomalies();
let (position, velocity) = oe
.to_cartesian(mu)
.expect("init_from_semi_latus_rectum_true_anomaly: to_cartesian failed");
TranslationalState { position, velocity }
}
pub fn init_from_orbital_elements_typed<P: astrodyn_quantities::frame::Planet>(
semi_major_axis: Length,
eccentricity: f64,
inclination: Angle,
raan: Angle,
arg_periapsis: Angle,
true_anomaly: Angle,
mu: GravParam<P>,
) -> TranslationalStateTyped<RootInertial> {
let untyped = init_from_orbital_elements(
semi_major_axis.get::<meter>(),
eccentricity,
inclination.get::<radian>(),
raan.get::<radian>(),
arg_periapsis.get::<radian>(),
true_anomaly.get::<radian>(),
mu.value, );
TranslationalStateTyped::<RootInertial> {
position: Position::<RootInertial>::from_raw_si(untyped.position),
velocity: Velocity::<RootInertial>::from_raw_si(untyped.velocity),
}
}
pub fn init_from_mean_anomaly(
semi_major_axis: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
mean_anomaly: f64,
mu: f64,
) -> TranslationalState {
assert!(
mu > 0.0,
"init_from_mean_anomaly: mu must be positive, got {mu}"
);
assert!(
semi_major_axis.is_finite(),
"init_from_mean_anomaly: semi_major_axis must be finite, got {semi_major_axis}"
);
assert!(
(0.0..1.0).contains(&eccentricity),
"init_from_mean_anomaly: eccentricity must be in [0, 1), got {eccentricity}"
);
use astrodyn_quantities::frame::SelfPlanet;
let mut oe = OrbitalElements::<SelfPlanet>::default();
oe.semi_major_axis = semi_major_axis;
oe.e_mag = eccentricity;
oe.inclination = inclination;
oe.long_asc_node = raan;
oe.arg_periapsis = arg_periapsis;
oe.semiparam = semi_major_axis * (1.0 - eccentricity * eccentricity);
oe.mean_anom = mean_anomaly;
oe.mean_anom_to_nu()
.expect("init_from_mean_anomaly: Kepler solver failed");
let (position, velocity) = oe
.to_cartesian(mu)
.expect("init_from_mean_anomaly: to_cartesian failed");
TranslationalState { position, velocity }
}
pub fn init_from_time_periapsis(
semi_major_axis: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
time_periapsis: f64,
mu: f64,
) -> TranslationalState {
assert!(
mu > 0.0,
"init_from_time_periapsis: mu must be positive, got {mu}"
);
assert!(
semi_major_axis > 0.0 && semi_major_axis.is_finite(),
"init_from_time_periapsis: semi_major_axis must be positive and finite, got {semi_major_axis}"
);
let mean_anomaly = time_periapsis * (mu / semi_major_axis).sqrt() / semi_major_axis;
init_from_mean_anomaly(
semi_major_axis,
eccentricity,
inclination,
raan,
arg_periapsis,
mean_anomaly,
mu,
)
}
fn sma_ecc_from_altitudes(r_eq: f64, alt_apoapsis: f64, alt_periapsis: f64) -> (f64, f64) {
let semi_major_axis = r_eq + 0.5 * (alt_apoapsis + alt_periapsis);
assert!(
semi_major_axis > 0.0 && semi_major_axis.is_finite(),
"sma_ecc_from_altitudes: derived semi_major_axis must be positive and finite, \
got {semi_major_axis} from r_eq={r_eq}, alt_apoapsis={alt_apoapsis}, \
alt_periapsis={alt_periapsis}; check the apo/peri altitudes in the deck"
);
let eccentricity = (alt_apoapsis - alt_periapsis) / (2.0 * semi_major_axis);
(semi_major_axis, eccentricity)
}
#[expect(
clippy::too_many_arguments,
reason = "JEOD orbital-element set is six elements plus r_eq and mu"
)]
pub fn init_from_altitudes_true_anomaly(
r_eq: f64,
alt_apoapsis: f64,
alt_periapsis: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
true_anomaly: f64,
mu: f64,
) -> TranslationalState {
let (semi_major_axis, eccentricity) = sma_ecc_from_altitudes(r_eq, alt_apoapsis, alt_periapsis);
init_from_orbital_elements(
semi_major_axis,
eccentricity,
inclination,
raan,
arg_periapsis,
true_anomaly,
mu,
)
}
#[expect(
clippy::too_many_arguments,
reason = "JEOD orbital-element set is six elements plus r_eq and mu"
)]
pub fn init_from_altitudes_time_periapsis(
r_eq: f64,
alt_apoapsis: f64,
alt_periapsis: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
time_periapsis: f64,
mu: f64,
) -> TranslationalState {
let (semi_major_axis, eccentricity) = sma_ecc_from_altitudes(r_eq, alt_apoapsis, alt_periapsis);
init_from_time_periapsis(
semi_major_axis,
eccentricity,
inclination,
raan,
arg_periapsis,
time_periapsis,
mu,
)
}
pub fn init_from_arg_latitude_radial_vel(
semi_major_axis: f64,
inclination: f64,
raan: f64,
arg_latitude: f64,
orb_radius: f64,
radial_vel: f64,
mu: f64,
) -> TranslationalState {
assert!(
mu > 0.0,
"init_from_arg_latitude_radial_vel: mu must be positive, got {mu}"
);
assert!(
semi_major_axis > 0.0 && semi_major_axis.is_finite(),
"init_from_arg_latitude_radial_vel: semi_major_axis must be positive and finite, \
got {semi_major_axis}"
);
assert!(
orb_radius > 0.0 && orb_radius.is_finite(),
"init_from_arg_latitude_radial_vel: orb_radius must be positive and finite, \
got {orb_radius}"
);
assert!(
radial_vel.is_finite(),
"init_from_arg_latitude_radial_vel: radial_vel must be finite, got {radial_vel}"
);
let ecos_e = (semi_major_axis - orb_radius) / semi_major_axis;
let esin_e = (radial_vel * orb_radius) / (mu * semi_major_axis).sqrt();
let ecc_sq = ecos_e * ecos_e + esin_e * esin_e;
let eccentricity = ecc_sq.sqrt();
let true_anomaly = if eccentricity >= 1.0e-14 {
let kcost = ecos_e - ecc_sq;
let ksint = (1.0 - ecc_sq).sqrt() * esin_e;
ksint.atan2(kcost)
} else {
0.0
};
let arg_periapsis = arg_latitude - true_anomaly;
init_from_orbital_elements(
semi_major_axis,
eccentricity,
inclination,
raan,
arg_periapsis,
true_anomaly,
mu,
)
}
pub fn init_from_lvlh(
lvlh_pos: DVec3,
lvlh_vel: DVec3,
ref_position: DVec3,
ref_velocity: DVec3,
) -> TranslationalState {
use astrodyn_quantities::frame::{Earth, PlanetInertial};
let lvlh = astrodyn_math::LvlhFrame::compute(
ref_position.m_at::<PlanetInertial<Earth>>(),
ref_velocity.m_per_s_at::<PlanetInertial<Earth>>(),
);
let frame = lvlh_reference_frame_state(
lvlh.t_parent_this,
lvlh.ang_vel_this,
ref_position,
ref_velocity,
);
init_trans_relative_to_frame(&frame, lvlh_pos, lvlh_vel)
}
fn lvlh_reference_frame_state(
t_parent_this: DMat3,
ang_vel_this: DVec3,
origin_position: DVec3,
origin_velocity: DVec3,
) -> astrodyn_frames::RefFrameState {
astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans {
position: origin_position,
velocity: origin_velocity,
},
rot: astrodyn_frames::RefFrameRot {
q_parent_this: JeodQuat::left_quat_from_transformation(&t_parent_this),
t_parent_this,
ang_vel_this,
},
}
}
pub fn init_trans_relative_to_frame(
frame: &astrodyn_frames::RefFrameState,
offset_position: DVec3,
offset_velocity: DVec3,
) -> TranslationalState {
let mut composed = astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans {
position: offset_position,
velocity: offset_velocity,
},
rot: astrodyn_frames::RefFrameRot::default(),
};
composed.incr_left(frame);
TranslationalState {
position: composed.trans.position,
velocity: composed.trans.velocity,
}
}
pub fn init_rot_relative_to_frame(
frame: &astrodyn_frames::RefFrameState,
q_frame_subject: JeodQuat,
ang_vel_frame_to_subject: DVec3,
) -> RotationalState {
let mut q_frame_subject = q_frame_subject;
q_frame_subject.normalize();
let mut composed = astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans::default(),
rot: astrodyn_frames::RefFrameRot {
q_parent_this: q_frame_subject,
t_parent_this: q_frame_subject.left_quat_to_transformation(),
ang_vel_this: ang_vel_frame_to_subject,
},
};
composed.incr_left(frame);
RotationalState {
quaternion: composed.rot.q_parent_this,
ang_vel_body: composed.rot.ang_vel_this,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LvlhAngularVelocityFrame {
#[default]
Body,
Lvlh,
}
pub fn init_rot_from_lvlh(
q_lvlh_body: JeodQuat,
ang_vel_lvlh_to_body: DVec3,
ang_vel_frame: LvlhAngularVelocityFrame,
ref_position: DVec3,
ref_velocity: DVec3,
) -> RotationalState {
let mut q_lvlh_body = q_lvlh_body;
q_lvlh_body.normalize();
use astrodyn_quantities::frame::{Earth, PlanetInertial};
let lvlh = astrodyn_math::LvlhFrame::compute(
ref_position.m_at::<PlanetInertial<Earth>>(),
ref_velocity.m_per_s_at::<PlanetInertial<Earth>>(),
);
let q_inertial_lvlh = JeodQuat::left_quat_from_transformation(&lvlh.t_parent_this);
let q_inertial_body = q_lvlh_body.multiply(&q_inertial_lvlh);
let t_lvlh_body = q_lvlh_body.left_quat_to_transformation();
let ang_vel_lvlh_to_body_in_body = match ang_vel_frame {
LvlhAngularVelocityFrame::Body => ang_vel_lvlh_to_body,
LvlhAngularVelocityFrame::Lvlh => t_lvlh_body * ang_vel_lvlh_to_body,
};
let ang_vel_inertial_lvlh_in_body = t_lvlh_body * lvlh.ang_vel_this;
let ang_vel_body = ang_vel_inertial_lvlh_in_body + ang_vel_lvlh_to_body_in_body;
RotationalState {
quaternion: q_inertial_body,
ang_vel_body,
}
}
pub fn init_from_ned(
geodetic: &GeodeticState,
ned_velocity: DVec3,
r_eq: f64,
r_pol: f64,
t_eci_pcpf: &DMat3,
omega_planet: DVec3,
) -> TranslationalState {
let pcpf_pos = geodetic.to_planet_fixed(r_eq, r_pol);
let t_pcpf_ned = compute_ned_rotation(geodetic.latitude, geodetic.longitude);
let pcpf_vel = t_pcpf_ned.transpose() * ned_velocity;
let t_pcpf_to_eci = t_eci_pcpf.transpose();
let position = t_pcpf_to_eci * pcpf_pos;
let velocity = t_pcpf_to_eci * pcpf_vel + omega_planet.cross(position);
TranslationalState { position, velocity }
}
pub fn ned_reference_frame_state(
geodetic: &GeodeticState,
r_eq: f64,
r_pol: f64,
t_eci_pcpf: &DMat3,
omega_planet: DVec3,
) -> astrodyn_frames::RefFrameState {
let pcpf_pos = geodetic.to_planet_fixed(r_eq, r_pol);
let t_pfix_ned = compute_ned_rotation(geodetic.latitude, geodetic.longitude);
let s_inertial_pfix = astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: astrodyn_frames::RefFrameRot {
q_parent_this: JeodQuat::left_quat_from_transformation(t_eci_pcpf),
t_parent_this: *t_eci_pcpf,
ang_vel_this: omega_planet,
},
};
let mut s_inertial_ned = astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans {
position: pcpf_pos,
velocity: DVec3::ZERO,
},
rot: astrodyn_frames::RefFrameRot {
q_parent_this: JeodQuat::left_quat_from_transformation(&t_pfix_ned),
t_parent_this: t_pfix_ned,
ang_vel_this: DVec3::ZERO,
},
};
s_inertial_ned.incr_left(&s_inertial_pfix);
s_inertial_ned
}
pub fn init_rot_from_ned(
q_ned_body: JeodQuat,
ang_vel_ned_to_body: DVec3,
geodetic: &GeodeticState,
r_eq: f64,
r_pol: f64,
t_eci_pcpf: &DMat3,
omega_planet: DVec3,
) -> RotationalState {
let frame = ned_reference_frame_state(geodetic, r_eq, r_pol, t_eci_pcpf, omega_planet);
init_rot_relative_to_frame(&frame, q_ned_body, ang_vel_ned_to_body)
}
pub fn compute_ned_rotation(lat: f64, lon: f64) -> DMat3 {
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let sin_lon = lon.sin();
let cos_lon = lon.cos();
let north = DVec3::new(-sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat);
let east = DVec3::new(-sin_lon, cos_lon, 0.0);
let down = DVec3::new(-cos_lat * cos_lon, -cos_lat * sin_lon, -sin_lat);
mat3_from_rows(north, east, down)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
const EARTH_MU: f64 = 3.986_004_415e14; const EARTH_R_EQ: f64 = 6_378_137.0; const EARTH_R_POL: f64 = EARTH_R_EQ * (1.0 - 1.0 / 298.257_223_563);
#[test]
fn circular_orbit_from_elements() {
let alt = 400_000.0; let r = EARTH_R_EQ + alt;
let a = r; let e = 0.0;
let inc = 0.0; let raan = 0.0;
let argp = 0.0;
let nu = 0.0;
let state = init_from_orbital_elements(a, e, inc, raan, argp, nu, EARTH_MU);
let r_mag = state.position.length();
assert!(
(r_mag - r).abs() < 1e-6,
"Position magnitude: expected {}, got {}, error = {} m",
r,
r_mag,
(r_mag - r).abs()
);
let v_circ = (EARTH_MU / r).sqrt();
let v_mag = state.velocity.length();
assert!(
(v_mag - v_circ).abs() < 1e-6,
"Velocity magnitude: expected {}, got {}, error = {} m/s",
v_circ,
v_mag,
(v_mag - v_circ).abs()
);
}
#[test]
fn iss_reference_state_from_elements() {
let init = astrodyn_verif_jeod_fixtures::orbital_init::load_orbital_init(
"ISS",
"trans_Orbit_inertial_body_set01",
);
let expected =
astrodyn_verif_jeod_fixtures::reference_state::load_reference_state("ISS", "inertial");
let t_peri = init
.time_periapsis
.expect("ISS set01 should have time_periapsis");
let state = init_from_time_periapsis(
init.semi_major_axis
.expect("ISS set01 should have semi_major_axis"),
init.eccentricity
.expect("ISS set01 should have eccentricity"),
init.inclination,
init.ascending_node,
init.arg_periapsis
.expect("ISS set01 should have arg_periapsis"),
t_peri,
EARTH_MU,
);
let pos_err = (state.position - expected.position).length();
let vel_err = (state.velocity - expected.velocity).length();
println!("ISS position error: {:.2} m", pos_err);
println!("ISS velocity error: {:.6} m/s", vel_err);
println!(
"Computed pos: [{:.2}, {:.2}, {:.2}]",
state.position.x, state.position.y, state.position.z
);
println!(
"Expected pos: [{:.2}, {:.2}, {:.2}]",
expected.position.x, expected.position.y, expected.position.z
);
assert!(
pos_err < 1000.0,
"ISS position error {:.2} m exceeds 1 km tolerance",
pos_err
);
assert!(
vel_err < 1.0,
"ISS velocity error {:.6} m/s exceeds 1 m/s tolerance",
vel_err
);
}
#[test]
fn lvlh_zero_offset_returns_reference() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v, 0.0);
let state = init_from_lvlh(DVec3::ZERO, DVec3::ZERO, ref_pos, ref_vel);
let pos_err = (state.position - ref_pos).length();
let vel_err = (state.velocity - ref_vel).length();
assert!(
pos_err < 1e-10,
"LVLH zero offset position error: {} m",
pos_err
);
assert!(
vel_err < 1e-10,
"LVLH zero offset velocity error: {} m/s",
vel_err
);
}
#[test]
fn lvlh_round_trip() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let lvlh_offset_pos = DVec3::new(100.0, 20.0, 50.0); let lvlh_offset_vel = DVec3::new(0.1, 0.05, -0.02);
let state = init_from_lvlh(lvlh_offset_pos, lvlh_offset_vel, ref_pos, ref_vel);
use astrodyn_quantities::frame::{Earth, PlanetInertial};
let lvlh = astrodyn_math::LvlhFrame::compute(
ref_pos.m_at::<PlanetInertial<Earth>>(),
ref_vel.m_per_s_at::<PlanetInertial<Earth>>(),
);
let t = lvlh.t_parent_this;
let delta_pos = state.position - ref_pos;
let delta_vel = state.velocity - ref_vel;
let recovered_lvlh_pos = t * delta_pos;
let recovered_lvlh_vel = t * delta_vel - lvlh.ang_vel_this.cross(recovered_lvlh_pos);
let pos_err = (recovered_lvlh_pos - lvlh_offset_pos).length();
let vel_err = (recovered_lvlh_vel - lvlh_offset_vel).length();
assert!(
pos_err < 1e-10,
"LVLH round-trip position error: {} m",
pos_err
);
assert!(
vel_err < 1e-10,
"LVLH round-trip velocity error: {} m/s",
vel_err
);
}
fn frame_state(
position: DVec3,
velocity: DVec3,
t_parent_this: DMat3,
ang_vel_this: DVec3,
) -> astrodyn_frames::RefFrameState {
astrodyn_frames::RefFrameState {
trans: astrodyn_frames::RefFrameTrans { position, velocity },
rot: astrodyn_frames::RefFrameRot {
q_parent_this: JeodQuat::left_quat_from_transformation(&t_parent_this),
t_parent_this,
ang_vel_this,
},
}
}
#[test]
fn init_trans_relative_to_frame_identity_is_plain_add() {
let frame = frame_state(
DVec3::new(1.0, 2.0, 3.0),
DVec3::new(0.1, 0.2, 0.3),
DMat3::IDENTITY,
DVec3::ZERO,
);
let out = init_trans_relative_to_frame(
&frame,
DVec3::new(10.0, 20.0, 30.0),
DVec3::new(1.0, 1.0, 1.0),
);
assert!((out.position - DVec3::new(11.0, 22.0, 33.0)).length() < 1e-12);
assert!((out.velocity - DVec3::new(1.1, 1.2, 1.3)).length() < 1e-12);
}
#[test]
fn init_trans_relative_to_frame_includes_omega_cross_r() {
let omega = 0.001;
let r = 100.0;
let frame = frame_state(
DVec3::ZERO,
DVec3::ZERO,
DMat3::IDENTITY,
DVec3::new(0.0, 0.0, omega),
);
let out = init_trans_relative_to_frame(&frame, DVec3::new(r, 0.0, 0.0), DVec3::ZERO);
let expected = DVec3::new(0.0, omega * r, 0.0);
assert!(
(out.velocity - expected).length() < 1e-12,
"ω×r term missing: got {:?}, expected {:?}",
out.velocity,
expected
);
}
#[test]
fn init_rot_relative_to_frame_composes_attitude_and_rate() {
let t_parent_this = DMat3::from_axis_angle(DVec3::Z, 0.3);
let w_frame = DVec3::new(0.0, 0.0, 0.002);
let frame = frame_state(DVec3::ZERO, DVec3::ZERO, t_parent_this, w_frame);
let out = init_rot_relative_to_frame(&frame, JeodQuat::identity(), DVec3::ZERO);
let q_frame = JeodQuat::left_quat_from_transformation(&t_parent_this);
let dot = out.quaternion.scalar() * q_frame.scalar()
+ out.quaternion.vector().dot(q_frame.vector());
assert!(dot.abs() > 1.0 - 1e-12, "attitude not forwarded");
assert!((out.ang_vel_body - w_frame).length() < 1e-12);
}
#[test]
fn ned_equator_prime_meridian() {
let geodetic = GeodeticState {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
};
let t_eci_pcpf = DMat3::IDENTITY;
let state = init_from_ned(
&geodetic,
DVec3::ZERO, EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
DVec3::ZERO, );
assert!(
(state.position.x - EARTH_R_EQ).abs() < 1e-6,
"Position X: expected {}, got {}",
EARTH_R_EQ,
state.position.x
);
assert!(
state.position.y.abs() < 1e-6,
"Position Y: expected 0, got {}",
state.position.y
);
assert!(
state.position.z.abs() < 1e-6,
"Position Z: expected 0, got {}",
state.position.z
);
}
#[test]
fn ned_rot_frame_origin_velocity_agrees_with_init_from_ned() {
let geodetic = GeodeticState {
latitude: 28.6082_f64.to_radians(),
longitude: (-80.6040_f64).to_radians(),
altitude: 3.0,
};
let t_eci_pcpf = DMat3::from_axis_angle(DVec3::Z, 1.234);
let omega_planet = DVec3::new(0.0, 0.0, 7.292_115e-5);
let frame = ned_reference_frame_state(
&geodetic,
EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
omega_planet,
);
let via_init = init_from_ned(
&geodetic,
DVec3::ZERO,
EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
omega_planet,
);
assert!(
(frame.trans.position - via_init.position).length() < 1e-6,
"NED frame origin position disagrees with init_from_ned: {:?} vs {:?}",
frame.trans.position,
via_init.position
);
assert!(
(frame.trans.velocity - via_init.velocity).length() < 1e-9,
"NED frame origin velocity disagrees with init_from_ned: {:?} vs {:?}",
frame.trans.velocity,
via_init.velocity
);
}
#[test]
fn ned_rot_aligned_body_recovers_planet_rate() {
let geodetic = GeodeticState {
latitude: 28.6082_f64.to_radians(),
longitude: (-80.6040_f64).to_radians(),
altitude: 3.0,
};
let t_eci_pcpf =
DMat3::from_axis_angle(DVec3::Z, 1.234) * DMat3::from_axis_angle(DVec3::X, 0.3);
let omega_planet = DVec3::new(0.0, 0.0, 7.292_115e-5);
let rot = init_rot_from_ned(
JeodQuat::identity(),
DVec3::ZERO,
&geodetic,
EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
omega_planet,
);
assert!(
(rot.ang_vel_body.length() - omega_planet.length()).abs() < 1e-15,
"inertial body rate magnitude {} != |omega_planet| {}",
rot.ang_vel_body.length(),
omega_planet.length()
);
let t_inertial_body = rot.quaternion.left_quat_to_transformation();
let omega_inertial = t_eci_pcpf.transpose() * omega_planet;
let expected_body = t_inertial_body * omega_inertial;
assert!(
(rot.ang_vel_body - expected_body).length() < 1e-18,
"body-frame ω {:?} != T_inertial_body·ω_inertial {:?}",
rot.ang_vel_body,
expected_body
);
assert!(
rot.quaternion.vector().length() > 1e-3,
"expected a non-trivial inertial→body attitude at 28.6N/-80.6E"
);
}
#[test]
fn elements_round_trip() {
let a = 7_000_000.0; let e = 0.01;
let inc = 51.6_f64.to_radians();
let raan = 30.0_f64.to_radians();
let argp = 45.0_f64.to_radians();
let nu = 60.0_f64.to_radians();
let state = init_from_orbital_elements(a, e, inc, raan, argp, nu, EARTH_MU);
use astrodyn_quantities::frame::{Earth, PlanetInertial};
let oe = OrbitalElements::<Earth>::from_cartesian_typed(
astrodyn_quantities::ext::F64Ext::m3_per_s2_for::<Earth>(EARTH_MU),
state.position.m_at::<PlanetInertial<Earth>>(),
state.velocity.m_per_s_at::<PlanetInertial<Earth>>(),
)
.expect("from_cartesian_typed failed");
assert!(
(oe.semi_major_axis - a).abs() / a < 1e-10,
"semi_major_axis: expected {}, got {}, rel_err = {}",
a,
oe.semi_major_axis,
(oe.semi_major_axis - a).abs() / a
);
assert!(
(oe.e_mag - e).abs() < 1e-10,
"eccentricity: expected {}, got {}, error = {}",
e,
oe.e_mag,
(oe.e_mag - e).abs()
);
assert!(
(oe.inclination - inc).abs() < 1e-10,
"inclination: expected {}, got {}, error = {}",
inc,
oe.inclination,
(oe.inclination - inc).abs()
);
assert!(
(oe.long_asc_node - raan).abs() < 1e-10,
"RAAN: expected {}, got {}, error = {}",
raan,
oe.long_asc_node,
(oe.long_asc_node - raan).abs()
);
assert!(
(oe.arg_periapsis - argp).abs() < 1e-10,
"arg_periapsis: expected {}, got {}, error = {}",
argp,
oe.arg_periapsis,
(oe.arg_periapsis - argp).abs()
);
assert!(
(oe.true_anom - nu).abs() < 1e-10,
"true_anomaly: expected {}, got {}, error = {}",
nu,
oe.true_anom,
(oe.true_anom - nu).abs()
);
}
#[test]
fn mean_anomaly_agrees_with_true_anomaly_for_circular() {
let a = EARTH_R_EQ + 400_000.0;
let e = 0.0;
let inc = 0.0;
let raan = 0.0;
let argp = 0.0;
let nu = 1.0;
let state_true = init_from_orbital_elements(a, e, inc, raan, argp, nu, EARTH_MU);
let state_mean = init_from_mean_anomaly(a, e, inc, raan, argp, nu, EARTH_MU);
let pos_err = (state_true.position - state_mean.position).length();
let vel_err = (state_true.velocity - state_mean.velocity).length();
assert!(
pos_err < 1e-6,
"Circular orbit: true vs mean anomaly position error = {} m",
pos_err
);
assert!(
vel_err < 1e-6,
"Circular orbit: true vs mean anomaly velocity error = {} m/s",
vel_err
);
}
#[test]
fn ned_rotation_orthonormal() {
let test_cases = [
(0.0, 0.0), (PI / 4.0, PI / 3.0), (-PI / 6.0, -PI / 2.0), (PI / 2.0 - 0.01, 1.0), ];
for (lat, lon) in test_cases {
let t = compute_ned_rotation(lat, lon);
let product = t * t.transpose();
let diff = product - DMat3::IDENTITY;
assert!(
diff.x_axis.length() < 1e-14,
"NED rotation not orthonormal at lat={}, lon={}",
lat,
lon
);
assert!(diff.y_axis.length() < 1e-14);
assert!(diff.z_axis.length() < 1e-14);
assert!(
(t.determinant() - 1.0).abs() < 1e-14,
"NED rotation determinant != 1 at lat={}, lon={}",
lat,
lon
);
}
}
#[test]
fn ned_north_velocity_at_equator() {
let geodetic = GeodeticState {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
};
let t_eci_pcpf = DMat3::IDENTITY;
let ned_vel = DVec3::new(1000.0, 0.0, 0.0); let state = init_from_ned(
&geodetic,
ned_vel,
EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
DVec3::ZERO,
);
assert!(
state.velocity.x.abs() < 1e-6,
"Vel X: expected 0, got {}",
state.velocity.x
);
assert!(
state.velocity.y.abs() < 1e-6,
"Vel Y: expected 0, got {}",
state.velocity.y
);
assert!(
(state.velocity.z - 1000.0).abs() < 1e-6,
"Vel Z: expected 1000, got {}",
state.velocity.z
);
}
#[test]
fn ned_omega_cross_r_contribution() {
let geodetic = GeodeticState {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
};
let t_eci_pcpf = DMat3::IDENTITY;
let omega_earth = 7.292_115_0e-5; let omega = DVec3::new(0.0, 0.0, omega_earth);
let state = init_from_ned(
&geodetic,
DVec3::ZERO,
EARTH_R_EQ,
EARTH_R_POL,
&t_eci_pcpf,
omega,
);
let expected_vy = omega_earth * EARTH_R_EQ;
assert!(
state.velocity.x.abs() < 1e-6,
"Vel X: expected 0, got {}",
state.velocity.x
);
assert!(
(state.velocity.y - expected_vy).abs() < 1e-3,
"Vel Y: expected {:.1}, got {:.1}",
expected_vy,
state.velocity.y
);
assert!(
state.velocity.z.abs() < 1e-6,
"Vel Z: expected 0, got {}",
state.velocity.z
);
}
#[test]
fn lvlh_with_inclined_orbit() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let state = init_from_lvlh(DVec3::ZERO, DVec3::ZERO, ref_pos, ref_vel);
assert!(
(state.position - ref_pos).length() < 1e-10,
"Inclined LVLH zero offset position error"
);
assert!(
(state.velocity - ref_vel).length() < 1e-10,
"Inclined LVLH zero offset velocity error"
);
let lvlh_pos = DVec3::new(0.0, 0.0, 1000.0);
let state_nadir = init_from_lvlh(lvlh_pos, DVec3::ZERO, ref_pos, ref_vel);
let r_offset = state_nadir.position.length();
assert!(
r_offset < r,
"1 km nadir offset should reduce position magnitude: {} vs {}",
r_offset,
r
);
let delta = (state_nadir.position - ref_pos).length();
assert!(
(delta - 1000.0).abs() < 1e-6,
"Offset magnitude: expected 1000 m, got {} m",
delta
);
}
#[test]
fn typed_orbital_init_matches_untyped_bit_for_bit() {
use astrodyn_quantities::ext::F64Ext;
use uom::si::angle::radian;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;
let alt = 400_000.0;
let r = EARTH_R_EQ + alt;
let a = r;
let e = 0.0;
let inc = 0.0;
let raan = 0.0;
let argp = 0.0;
let nu = 0.0;
let untyped = init_from_orbital_elements(a, e, inc, raan, argp, nu, EARTH_MU);
let typed = init_from_orbital_elements_typed(
Length::new::<meter>(a),
e,
Angle::new::<radian>(inc),
Angle::new::<radian>(raan),
Angle::new::<radian>(argp),
Angle::new::<radian>(nu),
EARTH_MU.m3_per_s2_for::<astrodyn_quantities::frame::Earth>(),
);
assert_eq!(typed.position.raw_si(), untyped.position);
assert_eq!(typed.velocity.raw_si(), untyped.velocity);
}
fn lvlh_frame_at(ref_pos: DVec3, ref_vel: DVec3) -> astrodyn_math::LvlhFrame {
use astrodyn_quantities::frame::{Earth, PlanetInertial};
astrodyn_math::LvlhFrame::compute(
ref_pos.m_at::<PlanetInertial<Earth>>(),
ref_vel.m_per_s_at::<PlanetInertial<Earth>>(),
)
}
#[test]
fn lvlh_rot_identity_attitude_zero_rate_recovers_lvlh_frame() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let lvlh = lvlh_frame_at(ref_pos, ref_vel);
let expected_q = JeodQuat::left_quat_from_transformation(&lvlh.t_parent_this);
let state = init_rot_from_lvlh(
JeodQuat::identity(),
DVec3::ZERO,
LvlhAngularVelocityFrame::Body,
ref_pos,
ref_vel,
);
let dq: f64 = (0..4)
.map(|i| {
let a = state.quaternion.data[i];
let b = expected_q.data[i];
(a - b).powi(2)
})
.sum::<f64>()
.sqrt();
assert!(
dq < 1e-14,
"identity LVLH→body should match LVLH frame quaternion exactly: dq = {dq}"
);
let ang_err = (state.ang_vel_body - lvlh.ang_vel_this).length();
assert!(
ang_err < 1e-14,
"identity LVLH→body should recover LVLH ang vel: err = {ang_err}"
);
}
#[test]
fn lvlh_rot_inverse_rate_zeros_inertial_ang_vel() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v, 0.0);
let lvlh = lvlh_frame_at(ref_pos, ref_vel);
let cancel = -lvlh.ang_vel_this;
let state = init_rot_from_lvlh(
JeodQuat::identity(),
cancel,
LvlhAngularVelocityFrame::Lvlh,
ref_pos,
ref_vel,
);
let mag = state.ang_vel_body.length();
assert!(
mag < 1e-14,
"cancelling LVLH-relative rate should null inertial ang vel: |w| = {mag}"
);
}
#[test]
fn lvlh_rot_nontrivial_attitude_round_trips() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let inc = 28.5_f64.to_radians();
let ref_pos = DVec3::new(r * 0.6, r * 0.8, 0.0);
let ref_vel = DVec3::new(-v * 0.8 * inc.cos(), v * 0.6 * inc.cos(), v * inc.sin());
let axis = DVec3::new(1.0, 2.0, 3.0).normalize();
let q_lvlh_body = JeodQuat::left_quat_from_eigen_rotation(1.2, axis);
let w_lvlh_body_in_body = DVec3::new(0.01, -0.02, 0.03);
let state = init_rot_from_lvlh(
q_lvlh_body,
w_lvlh_body_in_body,
LvlhAngularVelocityFrame::Body,
ref_pos,
ref_vel,
);
let lvlh = lvlh_frame_at(ref_pos, ref_vel);
let q_inertial_lvlh = JeodQuat::left_quat_from_transformation(&lvlh.t_parent_this);
let mut recovered = state.quaternion.multiply(&q_inertial_lvlh.conjugate());
recovered.normalize();
let dq: f64 = (0..4)
.map(|i| {
let a = recovered.data[i];
let b = q_lvlh_body.data[i];
(a - b).powi(2)
})
.sum::<f64>()
.sqrt();
assert!(
dq < 1e-12,
"recovered Q_lvlh_body should match input: dq = {dq}, recovered = {:?}, input = {:?}",
recovered.data,
q_lvlh_body.data
);
let t_lvlh_body = q_lvlh_body.left_quat_to_transformation();
let recovered_rate = state.ang_vel_body - t_lvlh_body * lvlh.ang_vel_this;
let rate_err = (recovered_rate - w_lvlh_body_in_body).length();
assert!(
rate_err < 1e-14,
"recovered LVLH-relative rate should match input: err = {rate_err}"
);
}
#[test]
fn lvlh_rot_body_vs_lvlh_rate_frame_agree_when_rotated_back() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v, 0.0);
let axis = DVec3::new(0.0, 0.0, 1.0);
let q_lvlh_body = JeodQuat::left_quat_from_eigen_rotation(0.5, axis);
let w_in_lvlh = DVec3::new(0.001, -0.002, 0.003);
let t_lvlh_body = q_lvlh_body.left_quat_to_transformation();
let w_in_body = t_lvlh_body * w_in_lvlh;
let s_lvlh = init_rot_from_lvlh(
q_lvlh_body,
w_in_lvlh,
LvlhAngularVelocityFrame::Lvlh,
ref_pos,
ref_vel,
);
let s_body = init_rot_from_lvlh(
q_lvlh_body,
w_in_body,
LvlhAngularVelocityFrame::Body,
ref_pos,
ref_vel,
);
let dq: f64 = (0..4)
.map(|i| {
let a = s_lvlh.quaternion.data[i];
let b = s_body.quaternion.data[i];
(a - b).powi(2)
})
.sum::<f64>()
.sqrt();
assert!(dq < 1e-14, "quaternion mismatch across rate-frame: {dq}");
let dw = (s_lvlh.ang_vel_body - s_body.ang_vel_body).length();
assert!(dw < 1e-14, "ang vel mismatch across rate-frame: {dw}");
}
#[test]
fn lvlh_rot_renormalizes_off_unit_input_consistently() {
let r = EARTH_R_EQ + 400_000.0;
let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let ref_pos = DVec3::new(r, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let axis = DVec3::new(1.0, 2.0, 3.0).normalize();
let q_unit = JeodQuat::left_quat_from_eigen_rotation(0.5, axis);
let mut q_off = q_unit;
for d in q_off.data.iter_mut() {
*d *= 1.001;
}
let w_in_lvlh = DVec3::new(0.001, -0.002, 0.003);
let s_unit = init_rot_from_lvlh(
q_unit,
w_in_lvlh,
LvlhAngularVelocityFrame::Lvlh,
ref_pos,
ref_vel,
);
let s_off = init_rot_from_lvlh(
q_off,
w_in_lvlh,
LvlhAngularVelocityFrame::Lvlh,
ref_pos,
ref_vel,
);
let dq: f64 = (0..4)
.map(|i| (s_unit.quaternion.data[i] - s_off.quaternion.data[i]).powi(2))
.sum::<f64>()
.sqrt();
assert!(
dq < 1e-14,
"off-unit input must produce the same attitude as the pre-normalized input: dq = {dq}"
);
let dw = (s_unit.ang_vel_body - s_off.ang_vel_body).length();
assert!(
dw < 1e-14,
"off-unit input must produce the same ang vel as the pre-normalized input: dw = {dw}"
);
let lvlh = lvlh_frame_at(ref_pos, ref_vel);
let t_lvlh_body_unit = q_unit.left_quat_to_transformation();
let expected_w = t_lvlh_body_unit * lvlh.ang_vel_this + t_lvlh_body_unit * w_in_lvlh;
let err_unit = (s_unit.ang_vel_body - expected_w).length();
let err_off = (s_off.ang_vel_body - expected_w).length();
assert!(
err_unit < 1e-14,
"ang vel must match the renormalized-matrix formula (unit input): err = {err_unit}"
);
assert!(
err_off < 1e-14,
"ang vel must match the renormalized-matrix formula (off-unit input): err = {err_off}"
);
}
#[test]
fn slr_true_anomaly_matches_orbital_elements_within_roundoff() {
let p = 6_732_889.984_55;
let e = 0.00129073350;
let inc = 51.670450765_f64.to_radians();
let raan = 49.708417385_f64.to_radians();
let argp = 100.582445989_f64.to_radians();
let nu = 299.884499026_f64.to_radians();
let slr = init_from_semi_latus_rectum_true_anomaly(p, e, inc, raan, argp, nu, EARTH_MU);
let a = p / (1.0 - e * e);
let sma = init_from_orbital_elements(a, e, inc, raan, argp, nu, EARTH_MU);
let pos_err = (slr.position - sma.position).length();
let vel_err = (slr.velocity - sma.velocity).length();
assert!(
pos_err < 1e-6,
"slr vs sma position roundoff too large: {pos_err} m"
);
assert!(
vel_err < 1e-9,
"slr vs sma velocity roundoff too large: {vel_err} m/s"
);
}
#[test]
fn slr_true_anomaly_position_magnitude_matches_conic() {
let p = 6_700_000.0;
let e = 0.01;
let nu = 1.3_f64; let state = init_from_semi_latus_rectum_true_anomaly(p, e, 0.5, 0.3, 0.7, nu, EARTH_MU);
let r_expected = p / (1.0 + e * nu.cos());
let r_actual = state.position.length();
assert!(
(r_actual - r_expected).abs() < 1e-6,
"conic radius: expected {r_expected}, got {r_actual}"
);
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_05_panics_on_zero_mu_in_slr_true_anomaly_init() {
let _ =
init_from_semi_latus_rectum_true_anomaly(6_700_000.0, 0.01, 0.0, 0.0, 0.0, 0.0, 0.0);
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_05_panics_on_zero_mu_in_orbital_elements_init() {
let _ = init_from_orbital_elements(
EARTH_R_EQ + 400_000.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0, );
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_05_panics_on_negative_mu_in_mean_anomaly_init() {
let _ = init_from_mean_anomaly(EARTH_R_EQ + 400_000.0, 0.01, 0.0, 0.0, 0.0, 0.0, -EARTH_MU);
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_05_panics_on_zero_mu_in_time_periapsis_init() {
let _ = init_from_time_periapsis(EARTH_R_EQ + 400_000.0, 0.01, 0.0, 0.0, 0.0, 0.0, 0.0);
}
#[test]
fn ba_13_altitudes_true_anomaly_matches_sma_ecc_derivation() {
let alt_apo = 363_454.582_64;
let alt_peri = 346_073.820_40;
let a = EARTH_R_EQ + 0.5 * (alt_apo + alt_peri);
let e = (alt_apo - alt_peri) / (2.0 * a);
let via_alt = init_from_altitudes_true_anomaly(
EARTH_R_EQ, alt_apo, alt_peri, 0.9, 0.86, 1.75, 5.23, EARTH_MU,
);
let via_sma = init_from_orbital_elements(a, e, 0.9, 0.86, 1.75, 5.23, EARTH_MU);
assert_eq!(via_alt.position, via_sma.position);
assert_eq!(via_alt.velocity, via_sma.velocity);
}
#[test]
#[should_panic(expected = "eccentricity must be in [0, 1)")]
fn ba_13_panics_on_swapped_altitudes() {
let _ = init_from_altitudes_true_anomaly(
EARTH_R_EQ, 346_073.0, 363_454.0, 0.9, 0.86, 1.75, 5.23, EARTH_MU,
);
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_13_panics_on_zero_mu_in_altitudes_time_periapsis_init() {
let _ = init_from_altitudes_time_periapsis(
EARTH_R_EQ, 363_454.0, 346_073.0, 0.9, 0.86, 1.75, 4581.96, 0.0,
);
}
#[test]
fn ba_14_arg_latitude_radial_vel_matches_iss_set06_deck() {
let a = 6_732_901.201_52; let i = 51.670450765_f64.to_radians();
let raan = 49.708417385_f64.to_radians();
let arg_lat = 400.466945015_f64.to_radians();
let r = 6_728_562.764_55; let rdot = -8.61072308;
let ecos_e = (a - r) / a;
let esin_e = (rdot * r) / (EARTH_MU * a).sqrt();
let ecc_sq = ecos_e * ecos_e + esin_e * esin_e;
let e = ecc_sq.sqrt();
let kcost = ecos_e - ecc_sq;
let ksint = (1.0 - ecc_sq).sqrt() * esin_e;
let nu = ksint.atan2(kcost);
let argp = arg_lat - nu;
let via_set06 = init_from_arg_latitude_radial_vel(a, i, raan, arg_lat, r, rdot, EARTH_MU);
let via_elem = init_from_orbital_elements(a, e, i, raan, argp, nu, EARTH_MU);
assert_eq!(via_set06.position, via_elem.position);
assert_eq!(via_set06.velocity, via_elem.velocity);
let r_actual = via_set06.position.length();
assert!(
(r_actual - r).abs() < 1e-6,
"set06 recovered radius: expected {r} m, got {r_actual} m"
);
}
#[test]
fn ba_14_arg_latitude_radial_vel_circular_sets_nu_zero() {
let a = EARTH_R_EQ + 400_000.0;
let i = 0.5;
let raan = 0.3;
let arg_lat = 1.1;
let via_set06 = init_from_arg_latitude_radial_vel(a, i, raan, arg_lat, a, 0.0, EARTH_MU);
let via_elem = init_from_orbital_elements(a, 0.0, i, raan, arg_lat, 0.0, EARTH_MU);
assert_eq!(via_set06.position, via_elem.position);
assert_eq!(via_set06.velocity, via_elem.velocity);
}
#[test]
#[should_panic(expected = "mu must be positive")]
fn ba_14_panics_on_zero_mu_in_arg_latitude_radial_vel_init() {
let _ = init_from_arg_latitude_radial_vel(
EARTH_R_EQ + 400_000.0,
0.5,
0.3,
1.1,
EARTH_R_EQ + 400_000.0,
0.0,
0.0,
);
}
}