Added equinoctial elements. Also a little cleanup

This commit is contained in:
connor committed 2026-09-30 16:07:54 -04:00
1 parent b089b206f5
commit 1f02cac6a9
8 files changed
+654 -14

No files matched your search

+35 -1
View File
@@ -8,6 +8,7 @@ use std::hint::black_box;
use astrokit::body::Body; use astrokit::body::Body;
use astrokit::cartesian::Cartesian; use astrokit::cartesian::Cartesian;
use astrokit::equinoctial::Equinoctial;
use astrokit::frames::Frame; use astrokit::frames::Frame;
use astrokit::kepler::{self, Degeneracy, Keplerian}; use astrokit::kepler::{self, Degeneracy, Keplerian};
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
@@ -121,11 +122,44 @@ fn bench_invariants(c: &mut Criterion) {
group.finish(); group.finish();
} }
/// Equinoctial against Keplerian on the same states.
///
/// The interesting comparison is GEO: Keplerian takes its doubly-degenerate
/// branch there and equinoctial takes no branch at all.
fn bench_equinoctial(c: &mut Criterion) {
let mut group = c.benchmark_group("equinoctial");
let leo = leo();
let geo = geo();
group.bench_function("from_cartesian/leo", |b| {
b.iter(|| Equinoctial::from_cartesian(&black_box(leo)))
});
group.bench_function("from_cartesian/geo", |b| {
b.iter(|| Equinoctial::from_cartesian(&black_box(geo)))
});
let elements = Equinoctial::from_cartesian(&leo);
group.bench_function("to_cartesian", |b| {
b.iter(|| black_box(elements).to_cartesian())
});
group.bench_function("round_trip", |b| {
b.iter(|| Equinoctial::from_cartesian(&black_box(leo)).to_cartesian())
});
let keplerian = leo.keplerian();
group.bench_function("from_keplerian", |b| {
b.iter(|| Equinoctial::from_keplerian(&black_box(keplerian)))
});
group.finish();
}
criterion_group!( criterion_group!(
benches, benches,
bench_scalar_relations, bench_scalar_relations,
bench_anomalies, bench_anomalies,
bench_conversions, bench_conversions,
bench_invariants bench_invariants,
bench_equinoctial
); );
criterion_main!(benches); criterion_main!(benches);
+3 -3
View File
@@ -117,9 +117,9 @@ fn bench_one_period(c: &mut Criterion) {
/// The cost of a state transition matrix relative to a plain propagation. /// The cost of a state transition matrix relative to a plain propagation.
/// ///
/// Stage 1 measured 10.6x for a 6x6 on a two-body arc. Central differences would /// Central differences would need thirteen propagations for a 6x6 and lose about
/// need thirteen propagations and lose about half the digits, so anything near /// half the available digits. This tracks the ratio as the force models get
/// that ratio is a win; this tracks it as the force models get heavier. /// heavier.
#[cfg(feature = "autodiff")] #[cfg(feature = "autodiff")]
fn bench_state_transition_matrix(c: &mut Criterion) { fn bench_state_transition_matrix(c: &mut Criterion) {
use nalgebra::{Matrix6, Vector6}; use nalgebra::{Matrix6, Vector6};
+3 -3
View File
@@ -10,9 +10,9 @@ also differentiate.
The crate is still being built to a milestone plan: The crate is still being built to a milestone plan:
- **M1** — the study-critical subset: two-body and spherical-harmonic gravity, - **M1** — the core subset: two-body and spherical-harmonic gravity, drag,
drag, third-body, cannonball SRP, GCRF/ITRF frames, ground stations and the third-body, cannonball SRP, GCRF/ITRF frames, ground stations and the four
four measurement types, with a validation suite against Orekit reference measurement types, with a validation suite against Orekit reference
trajectories. trajectories.
- **M2** — general-purpose completion: higher-degree gravity fields, more - **M2** — general-purpose completion: higher-degree gravity fields, more
density models, box-wing SRP and eclipses, maneuvers, covariance propagation, density models, box-wing SRP and eclipses, maneuvers, covariance propagation,
+603
View File
@@ -0,0 +1,603 @@
//! Modified equinoctial elements.
//!
//! Unlike [`Keplerian`], these have no degenerate case on a circular or equatorial
//! orbit. The one singularity is exactly retrograde motion, `i = 180`, which this
//! module refuses rather than returning a diverging `tan(i/2)`.
//!
//! Conversions to and from Cartesian go directly, through the equinoctial basis
//! vectors, not via [`Keplerian`] — routing through the degenerate representation
//! would defeat the purpose.
//!
//! Longitudes are *true*, matching [`Keplerian`]'s use of true anomaly.
//!
//! Walker, Ireland and Owens, "A set of modified equinoctial orbit elements",
//! Celestial Mechanics 36 (1985).
use nalgebra::Vector3;
use ordinary_diffeq::scalar::Scalar;
use crate::cartesian::Cartesian;
use crate::frames::Frame;
use crate::kepler::{self, wrap_two_pi, Degeneracy, Keplerian};
use crate::state::{HasPosition, HasVelocity, State};
/// How close to exactly retrograde an orbit may be before the prograde
/// convention is refused.
///
/// `h` and `k` carry `tan(i/2)`, which diverges at `i = 180`. At this tolerance
/// they reach about 1e7, which is representable but already meaningless.
pub const RETROGRADE_TOLERANCE: f64 = 1e-7;
/// Modified equinoctial orbital elements.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Equinoctial<T: Scalar> {
/// Semi-latus rectum `a(1 - e^2)`, m.
pub semi_latus_rectum: T,
/// `e cos(aop + raan)`.
pub f: T,
/// `e sin(aop + raan)`.
pub g: T,
/// `tan(i/2) cos(raan)`.
pub h: T,
/// `tan(i/2) sin(raan)`.
pub k: T,
/// True longitude `raan + aop + true anomaly`, rad.
pub true_longitude: T,
/// The frame these describe motion in. Supplies `mu`.
pub frame: Frame,
}
impl<T: Scalar> Equinoctial<T> {
/// Elements from a Cartesian state, without going through [`Keplerian`].
///
/// # Panics
///
/// If the frame is not inertial, or if the orbit is within
/// [`RETROGRADE_TOLERANCE`] of exactly retrograde.
pub fn from_cartesian(state: &Cartesian<T>) -> Self {
state.require_inertial("converting to equinoctial elements");
let (position, velocity) = (state.position, state.velocity);
let mu = state.frame.mu();
let momentum = position.cross(&velocity);
let normal = momentum.normalize();
// h and k come straight from the orbit normal: for a prograde orbit the
// normal is [sin i sin raan, -sin i cos raan, cos i], and
// tan(i/2) = sin i / (1 + cos i).
let denominator = T::lift(1.0) + normal[2];
assert!(
denominator.re() > RETROGRADE_TOLERANCE,
"equinoctial elements in the prograde convention are singular at \
i = 180 degrees; this orbit is within {RETROGRADE_TOLERANCE:e} of it"
);
let h = -normal[1] / denominator;
let k = normal[0] / denominator;
// The semi-latus rectum needs no eccentricity: p = |h|^2 / mu.
let semi_latus_rectum = momentum.norm_squared() / mu;
// f and g are the eccentricity vector's components in the equinoctial
// basis, and the true longitude is the position's angle in it.
let (f_hat, g_hat) = Self::basis(h, k);
let eccentricity_vector = (position
* (velocity.norm_squared() - T::lift(mu) / position.norm())
- velocity * position.dot(&velocity))
/ T::lift(mu);
Self {
semi_latus_rectum,
f: eccentricity_vector.dot(&f_hat),
g: eccentricity_vector.dot(&g_hat),
h,
k,
true_longitude: wrap_two_pi(position.dot(&g_hat).atan2(position.dot(&f_hat))),
frame: state.frame,
}
}
/// The equinoctial basis vectors, in the frame's axes.
///
/// `f_hat` is the direction true longitude is measured from and `g_hat`
/// completes the orbital plane. Both are unit vectors and their cross product
/// is the orbit normal.
fn basis(h: T, k: T) -> (Vector3<T>, Vector3<T>) {
let one = T::lift(1.0);
let scale = one / (one + h.powi(2) + k.powi(2));
(
Vector3::new(
(one - k.powi(2) + h.powi(2)) * scale,
h * k * 2.0 * scale,
-k * 2.0 * scale,
),
Vector3::new(
h * k * 2.0 * scale,
(one + k.powi(2) - h.powi(2)) * scale,
h * 2.0 * scale,
),
)
}
/// Checks that the basis is orthonormal and right-handed about the orbit
/// normal implied by `h` and `k`. Used by the tests.
#[cfg(test)]
fn basis_normal(h: T, k: T) -> Vector3<T> {
let (f_hat, g_hat) = Self::basis(h, k);
f_hat.cross(&g_hat)
}
/// A Cartesian state, in the frame these elements carry.
pub fn to_cartesian(&self) -> Cartesian<T> {
let one = T::lift(1.0);
let mu = self.frame.mu();
let (sin_l, cos_l) = (self.true_longitude.sin(), self.true_longitude.cos());
let alpha_squared = self.h.powi(2) - self.k.powi(2);
let s_squared = one + self.h.powi(2) + self.k.powi(2);
let w = one + self.f * cos_l + self.g * sin_l;
let radius = self.semi_latus_rectum / w;
let speed_scale = (T::lift(mu) / self.semi_latus_rectum).sqrt();
let hk2 = self.h * self.k * 2.0;
let position = Vector3::new(
cos_l + alpha_squared * cos_l + hk2 * sin_l,
sin_l - alpha_squared * sin_l + hk2 * cos_l,
(self.h * sin_l - self.k * cos_l) * 2.0,
) * (radius / s_squared);
let velocity = Vector3::new(
-(sin_l + alpha_squared * sin_l - hk2 * cos_l + self.g - hk2 * self.f
+ alpha_squared * self.g),
-(-cos_l + alpha_squared * cos_l + hk2 * sin_l - self.f
+ hk2 * self.g
+ alpha_squared * self.f),
(self.h * cos_l + self.k * sin_l + self.f * self.h + self.g * self.k) * 2.0,
) * (speed_scale / s_squared);
Cartesian::new(position, velocity, self.frame)
}
/// Elements from a Keplerian set, by the defining relations.
///
/// # Panics
///
/// If the orbit is within [`RETROGRADE_TOLERANCE`] of exactly retrograde.
pub fn from_keplerian(elements: &Keplerian<T>) -> Self {
assert!(
(core::f64::consts::PI - elements.inclination.re()).abs() > RETROGRADE_TOLERANCE,
"equinoctial elements in the prograde convention are singular at \
i = 180 degrees"
);
let longitude_of_periapsis = elements.aop + elements.raan;
let tangent = (elements.inclination * 0.5).tan();
Self {
semi_latus_rectum: elements.semi_major_axis
* (T::lift(1.0) - elements.eccentricity.powi(2)),
f: elements.eccentricity * longitude_of_periapsis.cos(),
g: elements.eccentricity * longitude_of_periapsis.sin(),
h: tangent * elements.raan.cos(),
k: tangent * elements.raan.sin(),
true_longitude: wrap_two_pi(longitude_of_periapsis + elements.true_anomaly),
frame: elements.frame,
}
}
/// The Keplerian set these describe.
///
/// Where the Keplerian set is degenerate the usual substitutions apply and
/// [`Keplerian::degeneracy`] says so, exactly as
/// [`Keplerian::from_cartesian`] would report it.
pub fn to_keplerian(&self) -> Keplerian<T> {
self.to_cartesian().keplerian()
}
/// Semi-major axis, m. Negative on a hyperbolic orbit.
pub fn semi_major_axis(&self) -> T {
self.semi_latus_rectum / (T::lift(1.0) - self.eccentricity().powi(2))
}
/// Eccentricity, dimensionless.
pub fn eccentricity(&self) -> T {
(self.f.powi(2) + self.g.powi(2)).sqrt()
}
/// Inclination, rad.
pub fn inclination(&self) -> T {
(self.h.powi(2) + self.k.powi(2)).sqrt().atan() * 2.0
}
/// Right ascension of the ascending node, rad. Undefined for an equatorial
/// orbit, where `h` and `k` are both zero.
pub fn raan(&self) -> T {
self.k.atan2(self.h)
}
/// Orbital period, s.
pub fn period(&self) -> T {
kepler::period(self.semi_major_axis(), self.frame.mu())
}
/// Mean motion, rad/s.
pub fn mean_motion(&self) -> T {
kepler::mean_motion(self.semi_major_axis(), self.frame.mu())
}
}
impl<T: Scalar> Keplerian<T> {
/// These elements as a non-singular set.
pub fn equinoctial(&self) -> Equinoctial<T> {
Equinoctial::from_keplerian(self)
}
}
impl<T: Scalar> Cartesian<T> {
/// The equinoctial elements of this state.
pub fn equinoctial(&self) -> Equinoctial<T> {
Equinoctial::from_cartesian(self)
}
}
/// Starts a [`State`] from equinoctial elements, for any layout with a position
/// and a velocity.
pub trait FromEquinoctial<const D: usize>: HasPosition<D> + HasVelocity<D> {
/// Starts a [`State`] from equinoctial elements.
fn with_equinoctial<T: Scalar>(self, elements: Equinoctial<T>) -> State<T, D, Self> {
elements.to_cartesian().to_state(self)
}
}
impl<const D: usize, L: HasPosition<D> + HasVelocity<D>> FromEquinoctial<D> for L {}
/// Whether a Keplerian set's degeneracy makes its angles untrustworthy.
///
/// A convenience for choosing a representation: if this is true, the equinoctial
/// set is the one to work in.
pub fn prefers_equinoctial(degeneracy: Degeneracy) -> bool {
degeneracy != Degeneracy::None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::Body;
use crate::frames::Orientation;
use crate::state::Orbit;
use approx::assert_relative_eq;
const MU: f64 = Body::EARTH.mu;
fn inclined_elliptical() -> Cartesian<f64> {
Keplerian {
semi_major_axis: 2.0e7,
eccentricity: 0.3,
inclination: 45.0_f64.to_radians(),
raan: 30.0_f64.to_radians(),
aop: 60.0_f64.to_radians(),
true_anomaly: 120.0_f64.to_radians(),
frame: Frame::EARTH_J2000,
degeneracy: Degeneracy::None,
}
.to_cartesian()
}
fn circular_equatorial(radius: f64) -> Cartesian<f64> {
Cartesian::new(
Vector3::new(radius, 0.0, 0.0),
Vector3::new(0.0, kepler::circular_speed(radius, MU), 0.0),
Frame::EARTH_J2000,
)
}
fn leo() -> Cartesian<f64> {
Cartesian::new(
Vector3::new(
4.263868426884883e6,
5.146189057155391e6,
1.1310208421331816e6,
),
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
Frame::EARTH_J2000,
)
}
/// The invariant that was wrong: the basis vectors' cross product has to be
/// the orbit normal that `h` and `k` describe, not some other unit vector.
#[test]
fn test_basis_is_right_handed_about_the_orbit_normal() {
for (inclination, raan) in [
(45.0_f64, 30.0_f64),
(0.0, 0.0),
(98.0, 200.0),
(170.0, 45.0),
] {
let (inclination, raan) = (inclination.to_radians(), raan.to_radians());
let tangent: f64 = (inclination / 2.0).tan();
let (h, k) = (tangent * raan.cos(), tangent * raan.sin());
let expected = Vector3::new(
raan.sin() * inclination.sin(),
-raan.cos() * inclination.sin(),
inclination.cos(),
);
assert_relative_eq!(
Equinoctial::<f64>::basis_normal(h, k),
expected,
max_relative = 1e-12,
epsilon = 1e-14
);
}
}
#[test]
fn test_cartesian_round_trips() {
for state in [inclined_elliptical(), leo(), circular_equatorial(4.2164e7)] {
let back = state.equinoctial().to_cartesian();
// An equatorial orbit's z components are both about 1e-9 m, so a purely
// relative comparison on them is meaningless.
assert_relative_eq!(
back.position,
state.position,
max_relative = 1e-11,
epsilon = 1e-6
);
assert_relative_eq!(
back.velocity,
state.velocity,
max_relative = 1e-11,
epsilon = 1e-9
);
assert_eq!(back.frame, state.frame);
}
}
/// The direct Cartesian conversion has to agree with the route through
/// Keplerian, where Keplerian is trustworthy.
#[test]
fn test_direct_conversion_agrees_with_the_keplerian_route() {
let state = inclined_elliptical();
let direct = state.equinoctial();
let via_keplerian = state.keplerian().equinoctial();
assert_relative_eq!(
direct.semi_latus_rectum,
via_keplerian.semi_latus_rectum,
max_relative = 1e-11
);
assert_relative_eq!(
direct.f,
via_keplerian.f,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
direct.g,
via_keplerian.g,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
direct.h,
via_keplerian.h,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
direct.k,
via_keplerian.k,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
direct.true_longitude,
via_keplerian.true_longitude,
max_relative = 1e-11
);
}
/// The point of the module: a circular equatorial orbit is a degenerate
/// Keplerian set and an ordinary equinoctial one.
#[test]
fn test_geo_is_degenerate_in_keplerian_and_not_in_equinoctial() {
let state = circular_equatorial(4.2164e7);
assert_eq!(state.keplerian().degeneracy, Degeneracy::CircularEquatorial);
assert!(prefers_equinoctial(state.keplerian().degeneracy));
// Circular and equatorial is just the origin of the f-g-h-k space.
let elements = state.equinoctial();
assert_relative_eq!(elements.f, 0.0, epsilon = 1e-15);
assert_relative_eq!(elements.g, 0.0, epsilon = 1e-15);
assert_relative_eq!(elements.h, 0.0, epsilon = 1e-15);
assert_relative_eq!(elements.k, 0.0, epsilon = 1e-15);
assert_relative_eq!(elements.semi_latus_rectum, 4.2164e7, max_relative = 1e-12);
assert_relative_eq!(elements.true_longitude, 0.0, epsilon = 1e-12);
}
#[test]
fn test_derived_quantities_match_the_keplerian_set() {
let state = inclined_elliptical();
let keplerian = state.keplerian();
let equinoctial = state.equinoctial();
assert_relative_eq!(
equinoctial.semi_major_axis(),
keplerian.semi_major_axis,
max_relative = 1e-11
);
assert_relative_eq!(
equinoctial.eccentricity(),
keplerian.eccentricity,
max_relative = 1e-11
);
assert_relative_eq!(
equinoctial.inclination(),
keplerian.inclination,
max_relative = 1e-11
);
assert_relative_eq!(
equinoctial.raan(),
keplerian.raan,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
equinoctial.period(),
keplerian.period(),
max_relative = 1e-11
);
assert_relative_eq!(
equinoctial.mean_motion(),
keplerian.mean_motion(),
max_relative = 1e-11
);
}
#[test]
fn test_keplerian_round_trips_through_equinoctial() {
let original = inclined_elliptical().keplerian();
let back = original.equinoctial().to_keplerian();
assert_relative_eq!(
back.semi_major_axis,
original.semi_major_axis,
max_relative = 1e-11
);
assert_relative_eq!(
back.eccentricity,
original.eccentricity,
max_relative = 1e-11
);
assert_relative_eq!(back.inclination, original.inclination, max_relative = 1e-11);
assert_relative_eq!(back.raan, original.raan, max_relative = 1e-11);
assert_relative_eq!(
back.aop,
original.aop,
max_relative = 1e-11,
epsilon = 1e-12
);
assert_relative_eq!(
back.true_anomaly,
original.true_anomaly,
max_relative = 1e-11
);
}
#[test]
fn test_state_from_equinoctial() {
let elements = inclined_elliptical().equinoctial();
let y = Orbit.with_equinoctial(elements);
let back = y.cartesian(Frame::EARTH_J2000).equinoctial();
assert_relative_eq!(back.f, elements.f, max_relative = 1e-11, epsilon = 1e-12);
assert_relative_eq!(
back.true_longitude,
elements.true_longitude,
max_relative = 1e-11
);
}
#[test]
#[should_panic(expected = "requires an inertial frame")]
fn test_a_rotating_frame_is_refused() {
let rotating = Cartesian::new(
leo().position,
leo().velocity,
Frame::EARTH_J2000.with_orientation(Orientation::BodyFixed),
);
let _ = rotating.equinoctial();
}
#[test]
#[should_panic(expected = "singular at i = 180")]
fn test_exactly_retrograde_is_refused() {
let radius = 7.0e6;
// Equatorial, going the wrong way round: i = 180 exactly.
let retrograde = Cartesian::new(
Vector3::new(radius, 0.0, 0.0),
Vector3::new(0.0, -kepler::circular_speed(radius, MU), 0.0),
Frame::EARTH_J2000,
);
let _ = retrograde.equinoctial();
}
/// A slightly retrograde orbit is fine — only the pole itself is singular.
#[test]
fn test_near_retrograde_is_fine() {
let radius = 7.0e6;
let speed = kepler::circular_speed(radius, MU);
// i = 170 degrees.
let inclination = 170.0_f64.to_radians();
let state = Cartesian::new(
Vector3::new(radius, 0.0, 0.0),
Vector3::new(0.0, speed * inclination.cos(), speed * inclination.sin()),
Frame::EARTH_J2000,
);
let elements = state.equinoctial();
assert_relative_eq!(elements.inclination(), inclination, max_relative = 1e-10);
let back = elements.to_cartesian();
assert_relative_eq!(back.position, state.position, max_relative = 1e-10);
}
/// Equinoctial elements are differentiable where Keplerian angles are not:
/// on a circular orbit, `d(aop)/d(state)` is meaningless, but `d(f)/d(state)`
/// is perfectly well behaved.
#[cfg(feature = "autodiff")]
#[test]
fn test_elements_differentiate_on_a_circular_orbit() {
use nalgebra::{SVector, Vector6};
use num_dual::DualSVec64;
let state = circular_equatorial(4.2164e7);
let packed = Vector6::new(
state.position[0],
state.position[1],
state.position[2],
state.velocity[0],
state.velocity[1],
state.velocity[2],
);
let (value, gradient): (f64, SVector<f64, 6>) = num_dual::gradient(
|y: Vector6<DualSVec64<6>>| {
Cartesian::new(
y.fixed_rows::<3>(0).into_owned(),
y.fixed_rows::<3>(3).into_owned(),
Frame::EARTH_J2000,
)
.equinoctial()
.f
},
&packed,
);
assert_relative_eq!(value, 0.0, epsilon = 1e-15);
// Every partial is finite, which is the property that matters.
for partial in gradient.iter() {
assert!(partial.is_finite(), "gradient has a non-finite entry");
}
// And it agrees with central differences.
let f_of = |y: Vector6<f64>| {
Cartesian::new(
y.fixed_rows::<3>(0).into_owned(),
y.fixed_rows::<3>(3).into_owned(),
Frame::EARTH_J2000,
)
.equinoctial()
.f
};
for i in 0..6 {
let h = 1e-4 * packed[i].abs().max(1.0);
let mut plus = packed;
let mut minus = packed;
plus[i] += h;
minus[i] -= h;
let difference = (f_of(plus) - f_of(minus)) / (2.0 * h);
assert_relative_eq!(
gradient[i],
difference,
max_relative = 1e-5,
epsilon = 1e-16
);
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ pub struct Frame {
} }
impl Frame { impl Frame {
/// Earth-centered, J2000-oriented. The frame the study's scenarios work in. /// Earth-centered, J2000-oriented.
pub const EARTH_J2000: Self = Self { pub const EARTH_J2000: Self = Self {
center: Body::EARTH, center: Body::EARTH,
orientation: Orientation::J2000, orientation: Orientation::J2000,
+5 -5
View File
@@ -123,7 +123,7 @@ fn angle_about<T: Scalar>(from: Vector3<T>, to: Vector3<T>, unit_normal: Vector3
} }
/// Wraps an angle into `[0, 2pi)`. /// Wraps an angle into `[0, 2pi)`.
fn wrap_two_pi<T: Scalar>(angle: T) -> T { pub(crate) fn wrap_two_pi<T: Scalar>(angle: T) -> T {
let turns = (angle.re() / TAU).floor(); let turns = (angle.re() / TAU).floor();
angle - TAU * turns angle - TAU * turns
} }
@@ -144,8 +144,8 @@ fn wrap_two_pi<T: Scalar>(angle: T) -> T {
/// - **circular and equatorial** — both are undefined, so `raan` and `aop` are /// - **circular and equatorial** — both are undefined, so `raan` and `aop` are
/// zero and `true_anomaly` carries the true longitude /// zero and `true_anomaly` carries the true longitude
/// ///
/// GEO hits the third case and near-circular LEO the first, so these are the /// A geostationary orbit hits the third case and a near-circular one the first,
/// common cases here rather than corner cases. /// so these are common rather than corner cases.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Keplerian<T: Scalar> { pub struct Keplerian<T: Scalar> {
/// Semi-major axis, m. /// Semi-major axis, m.
@@ -483,8 +483,8 @@ mod tests {
); );
} }
/// GEO is circular and equatorial, the doubly-singular case, and it is one of /// A geostationary orbit is circular and equatorial: the doubly-singular case,
/// the study's scenarios rather than a corner case. /// and a thoroughly ordinary orbit.
#[test] #[test]
fn test_geo_is_reported_as_circular_equatorial() { fn test_geo_is_reported_as_circular_equatorial() {
let radius = 4.2164e7; let radius = 4.2164e7;
+2
View File
@@ -12,6 +12,7 @@
pub mod body; pub mod body;
pub mod cartesian; pub mod cartesian;
pub mod dynamics; pub mod dynamics;
pub mod equinoctial;
pub mod frames; pub mod frames;
pub mod kepler; pub mod kepler;
pub mod propagation; pub mod propagation;
@@ -25,6 +26,7 @@ pub mod prelude {
pub use crate::body::Body; pub use crate::body::Body;
pub use crate::cartesian::{Cartesian, FromCartesian}; pub use crate::cartesian::{Cartesian, FromCartesian};
pub use crate::dynamics::Dynamics::*; pub use crate::dynamics::Dynamics::*;
pub use crate::equinoctial::{Equinoctial, FromEquinoctial};
pub use crate::frames::{Frame, Orientation}; pub use crate::frames::{Frame, Orientation};
pub use crate::kepler::{Degeneracy, Keplerian, TranslationalLayout}; pub use crate::kepler::{Degeneracy, Keplerian, TranslationalLayout};
pub use crate::propagation::{Accuracy, Propagation}; pub use crate::propagation::{Accuracy, Propagation};
+2 -1
View File
@@ -756,7 +756,8 @@ mod tests {
assert_eq!(speed::<f64, 14, Full>(&full), 13.0); assert_eq!(speed::<f64, 14, Full>(&full), 13.0);
} }
/// The `Orbit + k estimated parameters` case the filters will need. /// The `Orbit + k estimated parameters` case, for a filter that estimates
/// more than the trajectory.
#[test] #[test]
fn test_user_defined_layout_with_estimated_parameters() { fn test_user_defined_layout_with_estimated_parameters() {
define_state! { define_state! {