#[derive(Debug, Clone, Copy)]
pub struct PlanetShape {
pub name: &'static str,
pub mu: f64,
r_eq: f64,
r_pol: f64,
pub flat_coeff: f64,
}
impl PlanetShape {
pub const fn new(name: &'static str, mu: f64, r_eq: f64, r_pol: f64, flat_coeff: f64) -> Self {
if !r_eq.is_finite() {
panic!("PlanetShape::new: r_eq must be finite (got NaN or Inf)");
}
if r_eq <= 0.0 {
panic!("PlanetShape::new: r_eq must be strictly positive (got <= 0)");
}
if !r_pol.is_finite() {
panic!("PlanetShape::new: r_pol must be finite (got NaN or Inf)");
}
if r_pol <= 0.0 {
panic!("PlanetShape::new: r_pol must be strictly positive (got <= 0)");
}
if r_pol > r_eq {
panic!(
"PlanetShape::new: r_pol must be <= r_eq (got a prolate ellipsoid; \
oblate or spherical only)"
);
}
Self {
name,
mu,
r_eq,
r_pol,
flat_coeff,
}
}
#[inline]
pub const fn r_eq(&self) -> f64 {
self.r_eq
}
#[inline]
pub const fn r_pol(&self) -> f64 {
self.r_pol
}
pub fn flat_inv(&self) -> f64 {
1.0 / self.flat_coeff
}
pub fn e_ellipsoid(&self) -> f64 {
let f = self.flat_coeff;
(2.0 * f - f * f).sqrt()
}
pub fn e_ellip_sq(&self) -> f64 {
let f = self.flat_coeff;
2.0 * f - f * f
}
pub fn mu_typed(
&self,
) -> astrodyn_quantities::dims::GravParam<astrodyn_quantities::frame::SelfPlanet> {
astrodyn_quantities::dims::GravParam::<astrodyn_quantities::frame::SelfPlanet>::from_si(
self.mu,
)
}
pub fn r_eq_typed(&self) -> uom::si::f64::Length {
use astrodyn_quantities::ext::F64Ext;
self.r_eq.m()
}
pub fn r_pol_typed(&self) -> uom::si::f64::Length {
use astrodyn_quantities::ext::F64Ext;
self.r_pol.m()
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "tests assert bit-exact accessor round-trips against literal-initialized constants"
)]
mod tests {
use crate::presets::*;
#[test]
fn earth_mu_typed_matches_f64_field() {
let mu = EARTH.mu_typed();
assert_eq!(mu.value, EARTH.mu);
}
#[test]
fn earth_r_eq_typed_matches_f64_field() {
use uom::si::length::meter;
let r_eq = EARTH.r_eq_typed();
assert_eq!(r_eq.get::<meter>(), EARTH.r_eq());
}
#[test]
fn earth_r_pol_typed_matches_f64_field() {
use uom::si::length::meter;
let r_pol = EARTH.r_pol_typed();
assert_eq!(r_pol.get::<meter>(), EARTH.r_pol());
}
#[test]
fn polar_radius_consistent_with_flattening() {
for planet in [EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
let expected_r_pol = planet.r_eq() * (1.0 - planet.flat_coeff);
let err = (planet.r_pol() - expected_r_pol).abs();
assert!(
err < 1.0, "{}: r_pol={} vs r_eq*(1-f)={}, err={}",
planet.name,
planet.r_pol(),
expected_r_pol,
err
);
}
}
#[test]
fn all_values_positive() {
for planet in [EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
assert!(planet.mu > 0.0, "{}: mu must be positive", planet.name);
assert!(
planet.r_eq() > 0.0,
"{}: r_eq must be positive",
planet.name
);
assert!(
planet.r_pol() > 0.0,
"{}: r_pol must be positive",
planet.name
);
assert!(
planet.flat_coeff > 0.0,
"{}: flattening must be positive",
planet.name
);
assert!(
planet.flat_coeff < 1.0,
"{}: flattening must be < 1",
planet.name
);
assert!(
planet.r_eq() >= planet.r_pol(),
"{}: r_eq must be >= r_pol",
planet.name
);
}
}
#[test]
fn earth_inverse_flattening() {
let inv_f = EARTH.flat_inv();
assert!(
(inv_f - 298.257223563).abs() < 1e-6,
"Earth 1/f: expected 298.257223563, got {}",
inv_f
);
}
#[test]
fn eccentricity_positive() {
for planet in [EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
let e = planet.e_ellipsoid();
assert!(e > 0.0 && e < 1.0, "{}: e={}", planet.name, e);
}
}
use crate::planet::PlanetShape;
const TEST_SHAPE: PlanetShape = PlanetShape::new(
"Testopia",
1.5e14,
6_400_000.0,
6_400_000.0 * (1.0 - 1.0 / 300.0),
1.0 / 300.0,
);
#[test]
fn shape_struct_field_access_round_trip() {
assert_eq!(TEST_SHAPE.name, "Testopia");
assert_eq!(TEST_SHAPE.mu, 1.5e14);
assert_eq!(TEST_SHAPE.r_eq(), 6_400_000.0);
assert_eq!(TEST_SHAPE.flat_coeff, 1.0 / 300.0);
}
#[test]
fn radius_accessors_distinct_and_consistent() {
for planet in [TEST_SHAPE, EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
assert!(
planet.r_eq() > planet.r_pol(),
"{}: r_eq={} not > r_pol={}",
planet.name,
planet.r_eq(),
planet.r_pol()
);
let derived_pol = planet.r_eq() * (1.0 - planet.flat_coeff);
let err = (derived_pol - planet.r_pol()).abs();
assert!(
err < 1.0,
"{}: r_pol drifted by {err} m from r_eq*(1-f)",
planet.name
);
}
}
#[test]
fn flattening_inverse_round_trip() {
let f = TEST_SHAPE.flat_coeff;
let inv = TEST_SHAPE.flat_inv();
assert!(
(inv * f - 1.0).abs() < 1e-14,
"flat_inv({f}) * f != 1: got {}",
inv * f
);
}
#[test]
fn mu_accessor_matches_typed_and_preserves_value() {
for planet in [TEST_SHAPE, EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
assert_eq!(planet.mu_typed().value, planet.mu);
}
}
#[test]
fn eccentricity_squared_matches_definition() {
for planet in [TEST_SHAPE, EARTH, MOON, SUN, MARS, JUPITER, SATURN] {
let f = planet.flat_coeff;
let expected = 2.0 * f - f * f;
assert!((planet.e_ellip_sq() - expected).abs() < 1e-15);
let e = planet.e_ellipsoid();
assert!((e * e - planet.e_ellip_sq()).abs() < 1e-15);
}
}
#[test]
fn shape_struct_is_copy() {
fn take_by_value(s: PlanetShape) -> f64 {
s.r_eq()
}
let s = TEST_SHAPE;
assert_eq!(take_by_value(s), TEST_SHAPE.r_eq());
assert_eq!(s.name, "Testopia");
}
#[test]
#[should_panic(expected = "r_eq must be finite")]
fn new_panics_on_nan_r_eq() {
let _ = PlanetShape::new("X", 1.0, f64::NAN, 1.0, 0.0);
}
#[test]
#[should_panic(expected = "r_eq must be finite")]
fn new_panics_on_inf_r_eq() {
let _ = PlanetShape::new("X", 1.0, f64::INFINITY, 1.0, 0.0);
}
#[test]
#[should_panic(expected = "r_eq must be strictly positive")]
fn new_panics_on_zero_r_eq() {
let _ = PlanetShape::new("X", 1.0, 0.0, 1.0, 0.0);
}
#[test]
#[should_panic(expected = "r_eq must be strictly positive")]
fn new_panics_on_negative_r_eq() {
let _ = PlanetShape::new("X", 1.0, -1.0, 1.0, 0.0);
}
#[test]
#[should_panic(expected = "r_pol must be finite")]
fn new_panics_on_nan_r_pol() {
let _ = PlanetShape::new("X", 1.0, 1.0, f64::NAN, 0.0);
}
#[test]
#[should_panic(expected = "r_pol must be strictly positive")]
fn new_panics_on_zero_r_pol() {
let _ = PlanetShape::new("X", 1.0, 1.0, 0.0, 0.0);
}
#[test]
#[should_panic(expected = "r_pol must be <= r_eq")]
fn new_panics_on_prolate_ellipsoid() {
let _ = PlanetShape::new("X", 1.0, 1.0, 2.0, 0.0);
}
#[test]
fn new_accepts_spherical_ellipsoid() {
let sphere = PlanetShape::new("Sphere", 1.0, 1.0, 1.0, 0.0);
assert_eq!(sphere.r_eq(), 1.0);
assert_eq!(sphere.r_pol(), 1.0);
}
}