Added equinoctial elements. Also a little cleanup
This commit is contained in:
1 parent
b089b206f5
commit
1f02cac6a9
8 files changed
+654
-14
No files matched your search
+35
-1
@@ -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);
|
||||||
@@ -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};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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! {
|
||||||
|
|||||||
Reference in new issue
Block a user