Added full cartesian support with (the beginning of) frames
This commit is contained in:
1 parent
412d96dde0
commit
b089b206f5
11 files changed
+731
-167
No files matched your search
+20
-17
@@ -6,31 +6,36 @@
|
|||||||
|
|
||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use astrokit::body::Body;
|
||||||
|
use astrokit::cartesian::Cartesian;
|
||||||
|
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};
|
||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
|
|
||||||
const MU: f64 = 3.986004415e14;
|
const MU: f64 = Body::EARTH.mu;
|
||||||
|
|
||||||
/// Inclined, near-circular LEO. Just outside the degeneracy tolerance, so it
|
/// Inclined, near-circular LEO. Just outside the degeneracy tolerance, so it
|
||||||
/// takes the general branch.
|
/// takes the general branch.
|
||||||
fn leo() -> (Vector3<f64>, Vector3<f64>) {
|
fn leo() -> Cartesian<f64> {
|
||||||
(
|
Cartesian::new(
|
||||||
Vector3::new(
|
Vector3::new(
|
||||||
4.263868426884883e6,
|
4.263868426884883e6,
|
||||||
5.146189057155391e6,
|
5.146189057155391e6,
|
||||||
1.1310208421331816e6,
|
1.1310208421331816e6,
|
||||||
),
|
),
|
||||||
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
|
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GEO: circular and equatorial, so it takes the doubly-degenerate branch.
|
/// GEO: circular and equatorial, so it takes the doubly-degenerate branch.
|
||||||
fn geo() -> (Vector3<f64>, Vector3<f64>) {
|
fn geo() -> Cartesian<f64> {
|
||||||
let radius = 4.2164e7;
|
let radius = 4.2164e7;
|
||||||
(
|
Cartesian::new(
|
||||||
Vector3::new(radius, 0.0, 0.0),
|
Vector3::new(radius, 0.0, 0.0),
|
||||||
Vector3::new(0.0, kepler::circular_speed(radius, MU), 0.0),
|
Vector3::new(0.0, kepler::circular_speed(radius, MU), 0.0),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +78,14 @@ fn bench_anomalies(c: &mut Criterion) {
|
|||||||
fn bench_conversions(c: &mut Criterion) {
|
fn bench_conversions(c: &mut Criterion) {
|
||||||
let mut group = c.benchmark_group("kepler/conversions");
|
let mut group = c.benchmark_group("kepler/conversions");
|
||||||
|
|
||||||
let (r, v) = leo();
|
let leo = leo();
|
||||||
group.bench_function("from_cartesian/leo", |b| {
|
group.bench_function("from_cartesian/leo", |b| {
|
||||||
b.iter(|| Keplerian::from_cartesian(black_box(r), black_box(v), black_box(MU)))
|
b.iter(|| Keplerian::from_cartesian(&black_box(leo)))
|
||||||
});
|
});
|
||||||
|
|
||||||
let (r_geo, v_geo) = geo();
|
let geo = geo();
|
||||||
group.bench_function("from_cartesian/geo", |b| {
|
group.bench_function("from_cartesian/geo", |b| {
|
||||||
b.iter(|| Keplerian::from_cartesian(black_box(r_geo), black_box(v_geo), black_box(MU)))
|
b.iter(|| Keplerian::from_cartesian(&black_box(geo)))
|
||||||
});
|
});
|
||||||
|
|
||||||
let elements = Keplerian {
|
let elements = Keplerian {
|
||||||
@@ -90,30 +95,28 @@ fn bench_conversions(c: &mut Criterion) {
|
|||||||
raan: 0.5,
|
raan: 0.5,
|
||||||
aop: 1.0,
|
aop: 1.0,
|
||||||
true_anomaly: 2.0,
|
true_anomaly: 2.0,
|
||||||
|
frame: Frame::EARTH_J2000,
|
||||||
degeneracy: Degeneracy::None,
|
degeneracy: Degeneracy::None,
|
||||||
};
|
};
|
||||||
group.bench_function("to_cartesian", |b| {
|
group.bench_function("to_cartesian", |b| {
|
||||||
b.iter(|| black_box(elements).to_cartesian(black_box(MU)))
|
b.iter(|| black_box(elements).to_cartesian())
|
||||||
});
|
});
|
||||||
|
|
||||||
group.bench_function("round_trip", |b| {
|
group.bench_function("round_trip", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| Keplerian::from_cartesian(&black_box(leo)).to_cartesian())
|
||||||
let elements = Keplerian::from_cartesian(black_box(r), black_box(v), MU);
|
|
||||||
black_box(elements.to_cartesian(MU))
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
group.finish();
|
group.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_invariants(c: &mut Criterion) {
|
fn bench_invariants(c: &mut Criterion) {
|
||||||
let mut group = c.benchmark_group("kepler/invariants");
|
let mut group = c.benchmark_group("kepler/invariants");
|
||||||
let (r, v) = leo();
|
let leo = leo();
|
||||||
|
|
||||||
group.bench_function("specific_energy", |b| {
|
group.bench_function("specific_energy", |b| {
|
||||||
b.iter(|| kepler::specific_energy(black_box(r), black_box(v), black_box(MU)))
|
b.iter(|| black_box(leo).specific_energy())
|
||||||
});
|
});
|
||||||
group.bench_function("specific_angular_momentum", |b| {
|
group.bench_function("specific_angular_momentum", |b| {
|
||||||
b.iter(|| kepler::specific_angular_momentum(black_box(r), black_box(v)))
|
b.iter(|| black_box(leo).specific_angular_momentum())
|
||||||
});
|
});
|
||||||
group.finish();
|
group.finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fn velocity() -> Vector3<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn two_body() -> Vec<Dynamics> {
|
fn two_body() -> Vec<Dynamics> {
|
||||||
vec![TwoBody { mu: MU }]
|
vec![TwoBody { body: Body::EARTH }]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One derivative evaluation, per layout. The layout should cost nothing.
|
/// One derivative evaluation, per layout. The layout should cost nothing.
|
||||||
@@ -45,7 +45,7 @@ fn bench_derivative(c: &mut Criterion) {
|
|||||||
|
|
||||||
let assembly = Assembly::new(
|
let assembly = Assembly::new(
|
||||||
dynamics::build::<f64, 6, Orbit>(&two_body()),
|
dynamics::build::<f64, 6, Orbit>(&two_body()),
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
let y = Orbit.with_position(position()).with_velocity(velocity());
|
let y = Orbit.with_position(position()).with_velocity(velocity());
|
||||||
group.bench_function("two_body/Orbit", |b| {
|
group.bench_function("two_body/Orbit", |b| {
|
||||||
@@ -54,7 +54,7 @@ fn bench_derivative(c: &mut Criterion) {
|
|||||||
|
|
||||||
let assembly = Assembly::new(
|
let assembly = Assembly::new(
|
||||||
dynamics::build::<f64, 7, OrbitMass>(&two_body()),
|
dynamics::build::<f64, 7, OrbitMass>(&two_body()),
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
let y = OrbitMass
|
let y = OrbitMass
|
||||||
.with_position(position())
|
.with_position(position())
|
||||||
@@ -66,7 +66,7 @@ fn bench_derivative(c: &mut Criterion) {
|
|||||||
|
|
||||||
let assembly = Assembly::new(
|
let assembly = Assembly::new(
|
||||||
dynamics::build::<f64, 14, Full>(&two_body()),
|
dynamics::build::<f64, 14, Full>(&two_body()),
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
let y = Full
|
let y = Full
|
||||||
.with_position(position())
|
.with_position(position())
|
||||||
|
|||||||
+30
-32
@@ -2,46 +2,38 @@
|
|||||||
//!
|
//!
|
||||||
//! Run with `cargo run --example two_body`.
|
//! Run with `cargo run --example two_body`.
|
||||||
|
|
||||||
use astrokit::kepler;
|
|
||||||
use astrokit::prelude::*;
|
use astrokit::prelude::*;
|
||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
|
|
||||||
/// Earth's gravitational parameter, m^3/s^2.
|
|
||||||
const MU: f64 = 3.986004415e14;
|
|
||||||
|
|
||||||
/// Semi-major axis of the test orbit, m.
|
|
||||||
const SMA: f64 = 6.7781363e6;
|
|
||||||
|
|
||||||
/// Earth's equatorial radius, m. Comes from a body once bodies land.
|
|
||||||
const EARTH_RADIUS: f64 = 6.378137e6;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let epoch = Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0);
|
let epoch = Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0);
|
||||||
let period = Duration::from_seconds(kepler::period(SMA, MU));
|
let frame = Frame::EARTH_J2000;
|
||||||
|
|
||||||
let y0 = Orbit
|
let initial = Cartesian::new(
|
||||||
.with_position(Vector3::new(
|
Vector3::new(
|
||||||
4.263868426884883e6,
|
4.263868426884883e6,
|
||||||
5.146189057155391e6,
|
5.146189057155391e6,
|
||||||
1.1310208421331816e6,
|
1.1310208421331816e6,
|
||||||
))
|
),
|
||||||
.with_velocity(Vector3::new(
|
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
|
||||||
-5923.454461876975,
|
frame,
|
||||||
4496.802639690076,
|
);
|
||||||
1870.3893008991558,
|
|
||||||
));
|
|
||||||
|
|
||||||
let forces = vec![TwoBody { mu: MU }];
|
let elements = initial.keplerian();
|
||||||
|
let period = Duration::from_seconds(initial.period());
|
||||||
|
let forces = vec![TwoBody { body: Body::EARTH }];
|
||||||
|
|
||||||
let mut propagation = Propagation::new(epoch, y0, &forces)
|
let mut propagation = Propagation::from_cartesian(epoch, initial, &forces)
|
||||||
.duration(period)
|
.duration(period)
|
||||||
.accuracy(Accuracy::High);
|
.accuracy(Accuracy::High);
|
||||||
let solution = propagation.run();
|
let solution = propagation.run();
|
||||||
|
|
||||||
let end = Orbit.state(*solution.states.last().unwrap());
|
let end = Orbit
|
||||||
|
.state(*solution.states.last().unwrap())
|
||||||
|
.cartesian(frame);
|
||||||
|
|
||||||
let elements = y0.keplerian(MU);
|
|
||||||
println!("epoch: {epoch}");
|
println!("epoch: {epoch}");
|
||||||
|
println!("body: {}", initial.frame.center.name());
|
||||||
println!("period: {period}");
|
println!("period: {period}");
|
||||||
println!(
|
println!(
|
||||||
"elements: a = {:.3} km, e = {:.6}, i = {:.3} deg ({:?})",
|
"elements: a = {:.3} km, e = {:.6}, i = {:.3} deg ({:?})",
|
||||||
@@ -50,24 +42,30 @@ fn main() {
|
|||||||
elements.inclination.to_degrees(),
|
elements.inclination.to_degrees(),
|
||||||
elements.degeneracy
|
elements.degeneracy
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
"altitude: {:.3} km",
|
||||||
|
initial.spherical_altitude() / 1e3
|
||||||
|
);
|
||||||
println!("steps: {}", solution.states.len() - 1);
|
println!("steps: {}", solution.states.len() - 1);
|
||||||
println!(
|
println!(
|
||||||
"position error: {:.3e} m",
|
"position error: {:.3e} m",
|
||||||
(end.position() - y0.position()).norm()
|
(end.position - initial.position).norm()
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"velocity error: {:.3e} m/s",
|
"velocity error: {:.3e} m/s",
|
||||||
(end.velocity() - y0.velocity()).norm()
|
(end.velocity - initial.velocity).norm()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"energy drift: {:.3e} J/kg",
|
||||||
|
end.specific_energy() - initial.specific_energy()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dense output: the state at any time in the arc, not only at a step.
|
// Dense output: the state at any time in the arc, not only at a step.
|
||||||
let half = Orbit.state(solution.interpolate(period.to_seconds() / 2.0));
|
let half = Orbit
|
||||||
|
.state(solution.interpolate(period.to_seconds() / 2.0))
|
||||||
|
.cartesian(frame);
|
||||||
println!(
|
println!(
|
||||||
"altitude at half a period: {:.3} km",
|
"half a period: {:.3} km altitude",
|
||||||
(half.position().norm() - EARTH_RADIUS) / 1e3
|
half.spherical_altitude() / 1e3
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"specific energy drift: {:.3e} J/kg",
|
|
||||||
end.specific_energy(MU) - y0.specific_energy(MU)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
//! Celestial bodies.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A celestial body: the gravitational parameter and shape that force models and
|
||||||
|
/// element conversions need.
|
||||||
|
///
|
||||||
|
/// Carried by a [`Frame`](crate::frames::Frame) and by the dynamics that need a
|
||||||
|
/// central body, so that no signature takes a bare `mu`. A propagation about the
|
||||||
|
/// Moon is then a matter of saying so rather than of working around an assumption.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Body {
|
||||||
|
/// NAIF integer id, for ANISE ephemeris and orientation lookups.
|
||||||
|
pub naif_id: i32,
|
||||||
|
/// Gravitational parameter, m^3/s^2.
|
||||||
|
pub mu: f64,
|
||||||
|
/// Equatorial radius, m.
|
||||||
|
pub equatorial_radius: f64,
|
||||||
|
/// Flattening, dimensionless. Zero for a sphere.
|
||||||
|
pub flattening: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
/// Earth.
|
||||||
|
///
|
||||||
|
/// `mu` is EGM96's. **It must agree with whatever gravity field a
|
||||||
|
/// propagation uses**: the point-mass term and the spherical harmonics are two
|
||||||
|
/// halves of one expansion, and taking `mu` from one model and the harmonics
|
||||||
|
/// from another is an inconsistency. Once a gravity
|
||||||
|
/// field is loaded it should supply its own `mu` and override this.
|
||||||
|
///
|
||||||
|
/// Radius and flattening are WGS84.
|
||||||
|
pub const EARTH: Self = Self {
|
||||||
|
naif_id: 399,
|
||||||
|
mu: 3.986004415e14,
|
||||||
|
equatorial_radius: 6378137.0,
|
||||||
|
flattening: 1.0 / 298.257223563,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The Moon. `mu` and radius from DE440; the figure is very nearly spherical.
|
||||||
|
pub const MOON: Self = Self {
|
||||||
|
naif_id: 301,
|
||||||
|
mu: 4.902800118e12,
|
||||||
|
equatorial_radius: 1737400.0,
|
||||||
|
flattening: 0.0012,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The Sun, treated as spherical.
|
||||||
|
pub const SUN: Self = Self {
|
||||||
|
naif_id: 10,
|
||||||
|
mu: 1.32712440018e20,
|
||||||
|
equatorial_radius: 6.957e8,
|
||||||
|
flattening: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A spherical body, for a test case or a body whose figure does not matter.
|
||||||
|
pub const fn spherical(naif_id: i32, mu: f64, radius: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
naif_id,
|
||||||
|
mu,
|
||||||
|
equatorial_radius: radius,
|
||||||
|
flattening: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polar radius, m.
|
||||||
|
pub fn polar_radius(&self) -> f64 {
|
||||||
|
self.equatorial_radius * (1.0 - self.flattening)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A name for the recognized bodies, or a rendering of the NAIF id.
|
||||||
|
///
|
||||||
|
/// Diagnostics only. Provenance records the fields, which is the thing that
|
||||||
|
/// actually pins a run down.
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
match self.naif_id {
|
||||||
|
10 => "Sun".to_string(),
|
||||||
|
199 => "Mercury".to_string(),
|
||||||
|
299 => "Venus".to_string(),
|
||||||
|
301 => "Moon".to_string(),
|
||||||
|
399 => "Earth".to_string(),
|
||||||
|
499 => "Mars".to_string(),
|
||||||
|
599 => "Jupiter".to_string(),
|
||||||
|
699 => "Saturn".to_string(),
|
||||||
|
799 => "Uranus".to_string(),
|
||||||
|
899 => "Neptune".to_string(),
|
||||||
|
id => format!("NAIF {id}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_earth_matches_wgs84() {
|
||||||
|
assert_eq!(Body::EARTH.equatorial_radius, 6378137.0);
|
||||||
|
// WGS84's polar radius, to the millimeter.
|
||||||
|
assert_relative_eq!(Body::EARTH.polar_radius(), 6356752.314245, epsilon = 1e-3);
|
||||||
|
assert_eq!(Body::EARTH.name(), "Earth");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spherical_body_has_equal_radii() {
|
||||||
|
let body = Body::spherical(9999, 1.0e14, 1.0e6);
|
||||||
|
assert_eq!(body.polar_radius(), body.equatorial_radius);
|
||||||
|
assert_eq!(body.name(), "NAIF 9999");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bodies go into a run's provenance, so they have to round-trip.
|
||||||
|
#[test]
|
||||||
|
fn test_body_round_trips_through_json() {
|
||||||
|
let json = serde_json::to_string(&Body::EARTH).unwrap();
|
||||||
|
assert_eq!(serde_json::from_str::<Body>(&json).unwrap(), Body::EARTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gravitational parameters should agree with ANISE's, which come from
|
||||||
|
/// JPL kernels. ANISE works in km^3/s^2.
|
||||||
|
#[test]
|
||||||
|
fn test_gravitational_parameters_are_the_expected_magnitude() {
|
||||||
|
// Earth: EGM96 against the DE440 value, which differ in the 9th digit.
|
||||||
|
assert_relative_eq!(Body::EARTH.mu * 1e-9, 398600.4415, max_relative = 1e-10);
|
||||||
|
assert_relative_eq!(Body::MOON.mu * 1e-9, 4902.800118, max_relative = 1e-10);
|
||||||
|
assert_relative_eq!(Body::SUN.mu * 1e-9, 132712440018.0, max_relative = 1e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
//! A translational state as position, velocity and frame.
|
||||||
|
//!
|
||||||
|
//! This is the interchange representation. An integrated
|
||||||
|
//! [`State`] is layout-shaped and may carry mass, attitude or
|
||||||
|
//! estimated parameters; a `Cartesian` is the translational part of it together
|
||||||
|
//! with the frame it is expressed in, which is what every element conversion needs
|
||||||
|
//! and what no bare pair of vectors can supply.
|
||||||
|
|
||||||
|
use nalgebra::Vector3;
|
||||||
|
use ordinary_diffeq::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::frames::Frame;
|
||||||
|
use crate::kepler::{self, Keplerian};
|
||||||
|
use crate::state::{HasPosition, HasVelocity, State};
|
||||||
|
|
||||||
|
/// A position and velocity in a reference frame.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Cartesian<T: Scalar> {
|
||||||
|
/// Position, m.
|
||||||
|
pub position: Vector3<T>,
|
||||||
|
/// Velocity, m/s.
|
||||||
|
pub velocity: Vector3<T>,
|
||||||
|
/// The frame both are expressed in.
|
||||||
|
pub frame: Frame,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Scalar> Cartesian<T> {
|
||||||
|
/// A state from a position and velocity in `frame`.
|
||||||
|
pub fn new(position: Vector3<T>, velocity: Vector3<T>, frame: Frame) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
velocity,
|
||||||
|
frame,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distance from the frame's origin, m.
|
||||||
|
pub fn radius(&self) -> T {
|
||||||
|
self.position.norm()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Speed, m/s.
|
||||||
|
pub fn speed(&self) -> T {
|
||||||
|
self.velocity.norm()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Height above the central body's equatorial radius, m.
|
||||||
|
///
|
||||||
|
/// Spherical, not geodetic: it subtracts the equatorial radius rather than
|
||||||
|
/// solving for the distance to the reference ellipsoid. For the Earth the two
|
||||||
|
/// differ by up to about 21 km at the poles. Geodetic altitude arrives with
|
||||||
|
/// the frame transformations, which is also where it belongs, since it is only
|
||||||
|
/// meaningful in a body-fixed frame.
|
||||||
|
pub fn spherical_altitude(&self) -> T {
|
||||||
|
self.radius() - self.frame.center.equatorial_radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specific orbital energy, J/kg.
|
||||||
|
pub fn specific_energy(&self) -> T {
|
||||||
|
kepler::specific_energy(self.position, self.velocity, self.frame.mu())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specific angular momentum, m^2/s.
|
||||||
|
pub fn specific_angular_momentum(&self) -> Vector3<T> {
|
||||||
|
kepler::specific_angular_momentum(self.position, self.velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Keplerian elements of this state.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the frame is not inertial. Orbital elements describe inertial motion, and
|
||||||
|
/// a velocity in a rotating frame is not the inertial velocity — converting one
|
||||||
|
/// anyway would return a plausible-looking wrong answer.
|
||||||
|
pub fn keplerian(&self) -> Keplerian<T> {
|
||||||
|
Keplerian::from_cartesian(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Orbital period, s.
|
||||||
|
pub fn period(&self) -> T {
|
||||||
|
kepler::period(self.keplerian().semi_major_axis, self.frame.mu())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This state in a layout, with everything the layout also carries left zero.
|
||||||
|
pub fn to_state<const D: usize, L>(&self, layout: L) -> State<T, D, L>
|
||||||
|
where
|
||||||
|
L: HasPosition<D> + HasVelocity<D>,
|
||||||
|
{
|
||||||
|
layout
|
||||||
|
.with_position(self.position)
|
||||||
|
.with_velocity(self.velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts the frame is inertial, for the operations that require it.
|
||||||
|
pub(crate) fn require_inertial(&self, what: &str) {
|
||||||
|
assert!(
|
||||||
|
self.frame.is_inertial(),
|
||||||
|
"{what} requires an inertial frame, but this state is in {:?} about {}",
|
||||||
|
self.frame.orientation,
|
||||||
|
self.frame.center.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Scalar, const D: usize, L: HasPosition<D> + HasVelocity<D>> State<T, D, L> {
|
||||||
|
/// The translational part of this state, in `frame`.
|
||||||
|
///
|
||||||
|
/// A layout carries no frame of its own — a propagation holds that — so it has
|
||||||
|
/// to be supplied here.
|
||||||
|
pub fn cartesian(&self, frame: Frame) -> Cartesian<T> {
|
||||||
|
Cartesian::new(self.position(), self.velocity(), frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layouts that can be filled from a [`Cartesian`].
|
||||||
|
pub trait FromCartesian<const D: usize>: HasPosition<D> + HasVelocity<D> {
|
||||||
|
/// Starts a [`State`] from a Cartesian state, discarding its frame.
|
||||||
|
fn with_cartesian<T: Scalar>(self, state: &Cartesian<T>) -> State<T, D, Self> {
|
||||||
|
state.to_state(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const D: usize, L: HasPosition<D> + HasVelocity<D>> FromCartesian<D> for L {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::body::Body;
|
||||||
|
use crate::frames::Orientation;
|
||||||
|
use crate::state::{Full, Orbit, OrbitMass};
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radius_speed_and_altitude() {
|
||||||
|
let state = leo();
|
||||||
|
assert_relative_eq!(state.radius(), 6.778135e6, max_relative = 1e-5);
|
||||||
|
assert_relative_eq!(state.speed(), 7668.5, max_relative = 1e-3);
|
||||||
|
// A 400 km circular orbit, measured off the equatorial radius.
|
||||||
|
assert_relative_eq!(state.spherical_altitude(), 399998.0, max_relative = 1e-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invariants_read_mu_from_the_frame() {
|
||||||
|
let state = leo();
|
||||||
|
assert_relative_eq!(
|
||||||
|
state.specific_energy(),
|
||||||
|
kepler::specific_energy(state.position, state.velocity, Body::EARTH.mu),
|
||||||
|
max_relative = 1e-15
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.specific_angular_momentum(),
|
||||||
|
state.position.cross(&state.velocity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same Cartesian state about a different body gives different elements,
|
||||||
|
/// because the frame supplies `mu`. This is the whole reason for the type.
|
||||||
|
#[test]
|
||||||
|
fn test_the_frame_changes_the_elements() {
|
||||||
|
let about_earth = leo();
|
||||||
|
let about_moon = Cartesian::new(
|
||||||
|
about_earth.position,
|
||||||
|
about_earth.velocity,
|
||||||
|
Frame::MOON_J2000,
|
||||||
|
);
|
||||||
|
|
||||||
|
let earth = about_earth.keplerian();
|
||||||
|
let moon = about_moon.keplerian();
|
||||||
|
|
||||||
|
// Same geometry, different central mass, so a different semi-major axis.
|
||||||
|
assert_relative_eq!(earth.inclination, moon.inclination, max_relative = 1e-12);
|
||||||
|
assert!(earth.semi_major_axis != moon.semi_major_axis);
|
||||||
|
// Far too fast for the Moon to hold on to: hyperbolic, so sma is negative.
|
||||||
|
assert!(moon.semi_major_axis < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_trips_through_a_layout() {
|
||||||
|
let state = leo();
|
||||||
|
|
||||||
|
for_each_layout(&state);
|
||||||
|
|
||||||
|
fn for_each_layout(state: &Cartesian<f64>) {
|
||||||
|
let on_orbit = state.to_state(Orbit);
|
||||||
|
assert_eq!(on_orbit.position(), state.position);
|
||||||
|
assert_eq!(on_orbit.cartesian(state.frame), *state);
|
||||||
|
|
||||||
|
let on_mass = Orbit.with_cartesian(state);
|
||||||
|
assert_eq!(on_mass.velocity(), state.velocity);
|
||||||
|
|
||||||
|
// A wider layout takes the translational part and leaves the rest zero.
|
||||||
|
let on_full = state.to_state(Full);
|
||||||
|
assert_eq!(on_full.position(), state.position);
|
||||||
|
assert_eq!(on_full.mass(), 0.0);
|
||||||
|
assert_eq!(on_full.cartesian(state.frame), *state);
|
||||||
|
|
||||||
|
let on_orbit_mass = OrbitMass.with_cartesian(state).with_mass(500.0);
|
||||||
|
assert_eq!(on_orbit_mass.mass(), 500.0);
|
||||||
|
assert_eq!(on_orbit_mass.cartesian(state.frame), *state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "requires an inertial frame")]
|
||||||
|
fn test_elements_from_a_rotating_frame_are_refused() {
|
||||||
|
let rotating = Cartesian::new(
|
||||||
|
leo().position,
|
||||||
|
leo().velocity,
|
||||||
|
Frame::EARTH_J2000.with_orientation(Orientation::BodyFixed),
|
||||||
|
);
|
||||||
|
let _ = rotating.keplerian();
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-16
@@ -16,7 +16,7 @@
|
|||||||
//! use astrokit::dynamics::{build, Dynamics};
|
//! use astrokit::dynamics::{build, Dynamics};
|
||||||
//! use astrokit::state::Orbit;
|
//! use astrokit::state::Orbit;
|
||||||
//!
|
//!
|
||||||
//! let spec = vec![Dynamics::TwoBody { mu: 3.986004415e14 }];
|
//! let spec = vec![Dynamics::TwoBody { body: astrokit::body::Body::EARTH }];
|
||||||
//!
|
//!
|
||||||
//! // The same spec, at two scalars.
|
//! // The same spec, at two scalars.
|
||||||
//! let plain = build::<f64, 6, Orbit>(&spec);
|
//! let plain = build::<f64, 6, Orbit>(&spec);
|
||||||
@@ -29,8 +29,9 @@
|
|||||||
//! hidden behind a mode flag.
|
//! hidden behind a mode flag.
|
||||||
//!
|
//!
|
||||||
//! A side benefit: because a model is constructed at the propagated scalar, it
|
//! A side benefit: because a model is constructed at the propagated scalar, it
|
||||||
//! stores its constants already converted. [`models::TwoBody`] holds `mu` as a
|
//! stores its constants already converted. [`models::TwoBody`] holds its body's
|
||||||
//! `T`, so `T::lift` happens once at build time and never in the derivative.
|
//! `mu` as a `T`, so `T::lift` happens once at build time and never in the
|
||||||
|
//! derivative.
|
||||||
//!
|
//!
|
||||||
//! # Force or acceleration
|
//! # Force or acceleration
|
||||||
//!
|
//!
|
||||||
@@ -47,6 +48,8 @@ use ordinary_diffeq::scalar::Scalar;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::{Add, AddAssign};
|
use std::ops::{Add, AddAssign};
|
||||||
|
|
||||||
|
use crate::body::Body;
|
||||||
|
use crate::frames::Frame;
|
||||||
use crate::state::{HasPosition, HasVelocity, Layout, State};
|
use crate::state::{HasPosition, HasVelocity, Layout, State};
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
@@ -124,17 +127,20 @@ impl<T: Scalar> Add for Contribution<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// What a [`DynamicsModel`] needs that is not in the state vector.
|
/// What a [`DynamicsModel`] needs that is not in the state vector.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
/// The epoch the propagation started from. The `t` a model receives is
|
/// The epoch the propagation started from. The `t` a model receives is
|
||||||
/// seconds after this.
|
/// seconds after this.
|
||||||
pub epoch: Epoch,
|
pub epoch: Epoch,
|
||||||
|
/// The frame the state is expressed in, and so the central body a model can
|
||||||
|
/// assume without being told.
|
||||||
|
pub frame: Frame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// A context anchored at `epoch`.
|
/// A context anchored at `epoch`, in `frame`.
|
||||||
pub fn new(epoch: Epoch) -> Self {
|
pub fn new(epoch: Epoch, frame: Frame) -> Self {
|
||||||
Self { epoch }
|
Self { epoch, frame }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The epoch `t` seconds after the start of the propagation.
|
/// The epoch `t` seconds after the start of the propagation.
|
||||||
@@ -179,10 +185,13 @@ pub trait DynamicsModel<T: Scalar, const D: usize, L: Layout<D>> {
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Dynamics {
|
pub enum Dynamics {
|
||||||
/// Point-mass gravity of a central body.
|
/// Point-mass gravity of a body.
|
||||||
|
///
|
||||||
|
/// Named explicitly rather than taken from the propagation's frame, because an
|
||||||
|
/// n-body composition needs several and only one of them is the center.
|
||||||
TwoBody {
|
TwoBody {
|
||||||
/// Gravitational parameter, m^3/s^2.
|
/// The attracting body.
|
||||||
mu: f64,
|
body: Body,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +203,7 @@ impl Dynamics {
|
|||||||
L: HasPosition<D> + HasVelocity<D> + 'static,
|
L: HasPosition<D> + HasVelocity<D> + 'static,
|
||||||
{
|
{
|
||||||
match *self {
|
match *self {
|
||||||
Self::TwoBody { mu } => Box::new(models::TwoBody::new(mu)),
|
Self::TwoBody { body } => Box::new(models::TwoBody::new(body)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +255,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_spec_round_trips_through_json() {
|
fn test_spec_round_trips_through_json() {
|
||||||
let spec = vec![Dynamics::TwoBody { mu: 3.986004415e14 }];
|
let spec = vec![Dynamics::TwoBody { body: Body::EARTH }];
|
||||||
let json = serde_json::to_string(&spec).unwrap();
|
let json = serde_json::to_string(&spec).unwrap();
|
||||||
let back: Vec<Dynamics> = serde_json::from_str(&json).unwrap();
|
let back: Vec<Dynamics> = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(spec, back);
|
assert_eq!(spec, back);
|
||||||
@@ -256,7 +265,7 @@ mod tests {
|
|||||||
/// built at two different scalars.
|
/// built at two different scalars.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_one_spec_builds_at_two_scalars() {
|
fn test_one_spec_builds_at_two_scalars() {
|
||||||
let spec = vec![Dynamics::TwoBody { mu: 3.986004415e14 }];
|
let spec = vec![Dynamics::TwoBody { body: Body::EARTH }];
|
||||||
|
|
||||||
let plain = build::<f64, 6, Orbit>(&spec);
|
let plain = build::<f64, 6, Orbit>(&spec);
|
||||||
assert_eq!(plain.len(), 1);
|
assert_eq!(plain.len(), 1);
|
||||||
@@ -272,7 +281,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_context_advances_an_epoch() {
|
fn test_context_advances_an_epoch() {
|
||||||
let epoch = Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0);
|
let epoch = Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0);
|
||||||
let context = Context::new(epoch);
|
let context = Context::new(epoch, Frame::EARTH_J2000);
|
||||||
assert_eq!(context.epoch_at(0.0), epoch);
|
assert_eq!(context.epoch_at(0.0), epoch);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
context.epoch_at(3600.0),
|
context.epoch_at(3600.0),
|
||||||
@@ -283,11 +292,14 @@ mod tests {
|
|||||||
/// A model bounded on a capability works for any layout that has it.
|
/// A model bounded on a capability works for any layout that has it.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_two_body_acceleration_is_layout_agnostic() {
|
fn test_two_body_acceleration_is_layout_agnostic() {
|
||||||
let context = Context::new(Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0));
|
let context = Context::new(
|
||||||
|
Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
|
);
|
||||||
let mu = 3.986004415e14;
|
let mu = 3.986004415e14;
|
||||||
let r = Vector3::new(7.0e6, 0.0, 0.0);
|
let r = Vector3::new(7.0e6, 0.0, 0.0);
|
||||||
|
|
||||||
let model = models::TwoBody::<f64>::new(mu);
|
let model = models::TwoBody::<f64>::new(Body::EARTH);
|
||||||
let on_orbit = Orbit.with_position(r);
|
let on_orbit = Orbit.with_position(r);
|
||||||
let on_full = Full.with_position(r).with_mass(500.0);
|
let on_full = Full.with_position(r).with_mass(500.0);
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,31 @@
|
|||||||
use ordinary_diffeq::scalar::Scalar;
|
use ordinary_diffeq::scalar::Scalar;
|
||||||
|
|
||||||
use super::super::{Context, Contribution, DynamicsModel};
|
use super::super::{Context, Contribution, DynamicsModel};
|
||||||
|
use crate::body::Body;
|
||||||
use crate::state::{HasPosition, State};
|
use crate::state::{HasPosition, State};
|
||||||
|
|
||||||
/// Point-mass gravity of a single central body, at the origin of the state's
|
/// Point-mass gravity of a single body, at the origin of the state's frame.
|
||||||
/// frame.
|
|
||||||
///
|
///
|
||||||
/// `mu` is stored in the propagated scalar, converted once at construction, so
|
/// The body's `mu` is stored in the propagated scalar, converted once at
|
||||||
/// the derivative does no lifting.
|
/// construction, so the derivative does no lifting.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct TwoBody<T: Scalar> {
|
pub struct TwoBody<T: Scalar> {
|
||||||
|
body: Body,
|
||||||
mu: T,
|
mu: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Scalar> TwoBody<T> {
|
impl<T: Scalar> TwoBody<T> {
|
||||||
/// Point-mass gravity with gravitational parameter `mu`, in m^3/s^2.
|
/// Point-mass gravity of `body`.
|
||||||
pub fn new(mu: f64) -> Self {
|
pub fn new(body: Body) -> Self {
|
||||||
Self { mu: T::lift(mu) }
|
Self {
|
||||||
|
body,
|
||||||
|
mu: T::lift(body.mu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The attracting body.
|
||||||
|
pub fn body(&self) -> Body {
|
||||||
|
self.body
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The gravitational parameter, m^3/s^2.
|
/// The gravitational parameter, m^3/s^2.
|
||||||
@@ -42,6 +51,7 @@ impl<T: Scalar, const D: usize, L: HasPosition<D>> DynamicsModel<T, D, L> for Tw
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::frames::Frame;
|
||||||
use crate::state::Orbit;
|
use crate::state::Orbit;
|
||||||
use approx::assert_relative_eq;
|
use approx::assert_relative_eq;
|
||||||
use hifitime::Epoch;
|
use hifitime::Epoch;
|
||||||
@@ -51,8 +61,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_acceleration_points_at_the_origin() {
|
fn test_acceleration_points_at_the_origin() {
|
||||||
let context = Context::new(Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0));
|
let context = Context::new(
|
||||||
let model = TwoBody::<f64>::new(MU);
|
Epoch::from_gregorian_utc_hms(2026, 1, 1, 0, 0, 0),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
|
);
|
||||||
|
let model = TwoBody::<f64>::new(Body::EARTH);
|
||||||
let y = Orbit.with_position(Vector3::new(3.0e6, 4.0e6, 0.0));
|
let y = Orbit.with_position(Vector3::new(3.0e6, 4.0e6, 0.0));
|
||||||
|
|
||||||
let c = model.contribution(0.0, &y, &context);
|
let c = model.contribution(0.0, &y, &context);
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
//! Reference frames: where a state's origin is and how its axes are oriented.
|
||||||
|
//!
|
||||||
|
//! This module carries the frame *type*. Transformations between orientations —
|
||||||
|
//! J2000 to ITRF, topocentric, geodetic site coordinates — are a separate piece of
|
||||||
|
//! work and will land here on top of ANISE.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::body::Body;
|
||||||
|
|
||||||
|
/// How a frame's axes are oriented.
|
||||||
|
///
|
||||||
|
/// Provisional and deliberately coarse. The distinctions that matter for
|
||||||
|
/// sub-arcsecond work — ICRF against EME2000, ITRF against a body-fixed IAU frame
|
||||||
|
/// — arrive with the transformations, which is the point at which getting them
|
||||||
|
/// wrong would show up. Marked `#[non_exhaustive]` so refining it is not a
|
||||||
|
/// breaking change.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Orientation {
|
||||||
|
/// Inertial, aligned with the J2000 mean equator and equinox.
|
||||||
|
J2000,
|
||||||
|
/// Rotating with the central body.
|
||||||
|
BodyFixed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference frame: a central body and an orientation.
|
||||||
|
///
|
||||||
|
/// Held by every [`Cartesian`](crate::cartesian::Cartesian) and every element set,
|
||||||
|
/// which is what lets conversions read `mu` from the state rather than take it as
|
||||||
|
/// an argument.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Frame {
|
||||||
|
/// The body at the origin.
|
||||||
|
pub center: Body,
|
||||||
|
/// How the axes are oriented.
|
||||||
|
pub orientation: Orientation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
/// Earth-centered, J2000-oriented. The frame the study's scenarios work in.
|
||||||
|
pub const EARTH_J2000: Self = Self {
|
||||||
|
center: Body::EARTH,
|
||||||
|
orientation: Orientation::J2000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Earth-centered, rotating with the Earth.
|
||||||
|
pub const EARTH_FIXED: Self = Self {
|
||||||
|
center: Body::EARTH,
|
||||||
|
orientation: Orientation::BodyFixed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Moon-centered, J2000-oriented.
|
||||||
|
pub const MOON_J2000: Self = Self {
|
||||||
|
center: Body::MOON,
|
||||||
|
orientation: Orientation::J2000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A J2000-oriented frame centered on `center`.
|
||||||
|
pub const fn inertial(center: Body) -> Self {
|
||||||
|
Self {
|
||||||
|
center,
|
||||||
|
orientation: Orientation::J2000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frame rotating with `center`.
|
||||||
|
pub const fn body_fixed(center: Body) -> Self {
|
||||||
|
Self {
|
||||||
|
center,
|
||||||
|
orientation: Orientation::BodyFixed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The central body's gravitational parameter, m^3/s^2.
|
||||||
|
pub fn mu(&self) -> f64 {
|
||||||
|
self.center.mu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether orbital motion in this frame is unaccelerated by the frame itself.
|
||||||
|
///
|
||||||
|
/// Element conversions and the two-body relations are only meaningful in an
|
||||||
|
/// inertial frame; in a rotating one the velocity is not the inertial velocity.
|
||||||
|
pub fn is_inertial(&self) -> bool {
|
||||||
|
matches!(self.orientation, Orientation::J2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same frame with a different orientation.
|
||||||
|
pub fn with_orientation(&self, orientation: Orientation) -> Self {
|
||||||
|
Self {
|
||||||
|
orientation,
|
||||||
|
..*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Frame {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::EARTH_J2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frame_carries_the_central_bodys_mu() {
|
||||||
|
assert_eq!(Frame::EARTH_J2000.mu(), Body::EARTH.mu);
|
||||||
|
assert_eq!(Frame::MOON_J2000.mu(), Body::MOON.mu);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_only_inertial_frames_report_inertial() {
|
||||||
|
assert!(Frame::EARTH_J2000.is_inertial());
|
||||||
|
assert!(!Frame::EARTH_FIXED.is_inertial());
|
||||||
|
assert!(Frame::inertial(Body::SUN).is_inertial());
|
||||||
|
assert!(!Frame::body_fixed(Body::SUN).is_inertial());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_orientation_can_be_swapped_keeping_the_center() {
|
||||||
|
let fixed = Frame::EARTH_J2000.with_orientation(Orientation::BodyFixed);
|
||||||
|
assert_eq!(fixed, Frame::EARTH_FIXED);
|
||||||
|
assert_eq!(fixed.center, Body::EARTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frame_round_trips_through_json() {
|
||||||
|
let json = serde_json::to_string(&Frame::MOON_J2000).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<Frame>(&json).unwrap(),
|
||||||
|
Frame::MOON_J2000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
-82
@@ -15,6 +15,8 @@ use core::f64::consts::TAU;
|
|||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
use ordinary_diffeq::scalar::Scalar;
|
use ordinary_diffeq::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::cartesian::Cartesian;
|
||||||
|
use crate::frames::Frame;
|
||||||
use crate::state::{HasPosition, HasVelocity, State};
|
use crate::state::{HasPosition, HasVelocity, State};
|
||||||
|
|
||||||
/// How close to zero an eccentricity or inclination has to be before
|
/// How close to zero an eccentricity or inclination has to be before
|
||||||
@@ -158,6 +160,9 @@ pub struct Keplerian<T: Scalar> {
|
|||||||
pub aop: T,
|
pub aop: T,
|
||||||
/// True anomaly, rad.
|
/// True anomaly, rad.
|
||||||
pub true_anomaly: T,
|
pub true_anomaly: T,
|
||||||
|
/// The frame these describe motion in. Supplies `mu`, so no method here takes
|
||||||
|
/// one.
|
||||||
|
pub frame: Frame,
|
||||||
/// Which elements, if any, are not what they are named.
|
/// Which elements, if any, are not what they are named.
|
||||||
pub degeneracy: Degeneracy,
|
pub degeneracy: Degeneracy,
|
||||||
}
|
}
|
||||||
@@ -176,11 +181,17 @@ pub enum Degeneracy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Scalar> Keplerian<T> {
|
impl<T: Scalar> Keplerian<T> {
|
||||||
/// Elements from an inertial position and velocity.
|
/// Elements from a Cartesian state.
|
||||||
///
|
///
|
||||||
/// Vallado's RV2COE, with the three singular cases handled as
|
/// Vallado's RV2COE, with the three singular cases handled as [`Keplerian`]
|
||||||
/// [`Keplerian`] documents.
|
/// documents and the angles measured by `atan2` rather than `acos`.
|
||||||
pub fn from_cartesian(position: Vector3<T>, velocity: Vector3<T>, mu: f64) -> Self {
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the state's frame is not inertial.
|
||||||
|
pub fn from_cartesian(state: &Cartesian<T>) -> Self {
|
||||||
|
state.require_inertial("converting to Keplerian elements");
|
||||||
|
let (position, velocity, mu) = (state.position, state.velocity, state.frame.mu());
|
||||||
let zero = T::lift(0.0);
|
let zero = T::lift(0.0);
|
||||||
let r = position.norm();
|
let r = position.norm();
|
||||||
|
|
||||||
@@ -265,17 +276,19 @@ impl<T: Scalar> Keplerian<T> {
|
|||||||
raan,
|
raan,
|
||||||
aop,
|
aop,
|
||||||
true_anomaly,
|
true_anomaly,
|
||||||
|
frame: state.frame,
|
||||||
degeneracy,
|
degeneracy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An inertial position and velocity, in meters and meters per second.
|
/// A Cartesian state, in the frame these elements carry.
|
||||||
///
|
///
|
||||||
/// Inverts [`Keplerian::from_cartesian`] for a non-degenerate set. A
|
/// Inverts [`Keplerian::from_cartesian`] for a non-degenerate set. A degenerate
|
||||||
/// degenerate set round-trips too, because the substituted angles are zero
|
/// set round-trips too, because the substituted angles are zero where they are
|
||||||
/// where they are undefined and the perifocal construction then puts the
|
/// undefined and the perifocal construction then puts the argument of latitude
|
||||||
/// argument of latitude or true longitude in the right place.
|
/// or true longitude in the right place.
|
||||||
pub fn to_cartesian(&self, mu: f64) -> (Vector3<T>, Vector3<T>) {
|
pub fn to_cartesian(&self) -> Cartesian<T> {
|
||||||
|
let mu = self.frame.mu();
|
||||||
let one = T::lift(1.0);
|
let one = T::lift(1.0);
|
||||||
let semi_latus = self.semi_major_axis * (one - self.eccentricity.powi(2));
|
let semi_latus = self.semi_major_axis * (one - self.eccentricity.powi(2));
|
||||||
let radius = semi_latus / (one + self.eccentricity * self.true_anomaly.cos());
|
let radius = semi_latus / (one + self.eccentricity * self.true_anomaly.cos());
|
||||||
@@ -293,9 +306,10 @@ impl<T: Scalar> Keplerian<T> {
|
|||||||
T::lift(0.0),
|
T::lift(0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
(
|
Cartesian::new(
|
||||||
self.perifocal_to_inertial(in_plane),
|
self.perifocal_to_inertial(in_plane),
|
||||||
self.perifocal_to_inertial(velocity_in_plane),
|
self.perifocal_to_inertial(velocity_in_plane),
|
||||||
|
self.frame,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,13 +333,13 @@ impl<T: Scalar> Keplerian<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Orbital period, in seconds.
|
/// Orbital period, in seconds.
|
||||||
pub fn period(&self, mu: f64) -> T {
|
pub fn period(&self) -> T {
|
||||||
period(self.semi_major_axis, mu)
|
period(self.semi_major_axis, self.frame.mu())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mean motion, in radians per second.
|
/// Mean motion, in radians per second.
|
||||||
pub fn mean_motion(&self, mu: f64) -> T {
|
pub fn mean_motion(&self) -> T {
|
||||||
mean_motion(self.semi_major_axis, mu)
|
mean_motion(self.semi_major_axis, self.frame.mu())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eccentric anomaly, in radians.
|
/// Eccentric anomaly, in radians.
|
||||||
@@ -355,31 +369,20 @@ impl<T: Scalar> Keplerian<T> {
|
|||||||
/// these without doing anything.
|
/// these without doing anything.
|
||||||
pub trait TranslationalLayout<const D: usize>: HasPosition<D> + HasVelocity<D> {
|
pub trait TranslationalLayout<const D: usize>: HasPosition<D> + HasVelocity<D> {
|
||||||
/// Starts a [`State`] from Keplerian elements.
|
/// Starts a [`State`] from Keplerian elements.
|
||||||
fn with_keplerian<T: Scalar>(self, elements: Keplerian<T>, mu: f64) -> State<T, D, Self> {
|
fn with_keplerian<T: Scalar>(self, elements: Keplerian<T>) -> State<T, D, Self> {
|
||||||
let (position, velocity) = elements.to_cartesian(mu);
|
elements.to_cartesian().to_state(self)
|
||||||
self.with_position(position).with_velocity(velocity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const D: usize, L: HasPosition<D> + HasVelocity<D>> TranslationalLayout<D> for L {}
|
impl<const D: usize, L: HasPosition<D> + HasVelocity<D>> TranslationalLayout<D> for L {}
|
||||||
|
|
||||||
impl<T: Scalar, const D: usize, L: HasPosition<D> + HasVelocity<D>> State<T, D, L> {
|
impl<T: Scalar, const D: usize, L: HasPosition<D> + HasVelocity<D>> State<T, D, L> {
|
||||||
/// The Keplerian elements of this state, about a body of parameter `mu`.
|
/// The Keplerian elements of this state, in `frame`.
|
||||||
pub fn keplerian(&self, mu: f64) -> Keplerian<T> {
|
pub fn keplerian(&self, frame: Frame) -> Keplerian<T> {
|
||||||
Keplerian::from_cartesian(self.position(), self.velocity(), mu)
|
self.cartesian(frame).keplerian()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Orbital period, in seconds.
|
/// Specific angular momentum, in m^2/s. Needs no frame.
|
||||||
pub fn period(&self, mu: f64) -> T {
|
|
||||||
period(self.keplerian(mu).semi_major_axis, mu)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Specific orbital energy, in J/kg.
|
|
||||||
pub fn specific_energy(&self, mu: f64) -> T {
|
|
||||||
specific_energy(self.position(), self.velocity(), mu)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Specific angular momentum, in m^2/s.
|
|
||||||
pub fn specific_angular_momentum(&self) -> Vector3<T> {
|
pub fn specific_angular_momentum(&self) -> Vector3<T> {
|
||||||
specific_angular_momentum(self.position(), self.velocity())
|
specific_angular_momentum(self.position(), self.velocity())
|
||||||
}
|
}
|
||||||
@@ -388,21 +391,33 @@ impl<T: Scalar, const D: usize, L: HasPosition<D> + HasVelocity<D>> State<T, D,
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::cartesian::FromCartesian;
|
||||||
use crate::state::Orbit;
|
use crate::state::Orbit;
|
||||||
use approx::assert_relative_eq;
|
use approx::assert_relative_eq;
|
||||||
use core::f64::consts::PI;
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
const MU: f64 = 3.986004415e14;
|
/// Earth's gravitational parameter, for the free functions that still take one.
|
||||||
|
const MU: f64 = crate::body::Body::EARTH.mu;
|
||||||
|
|
||||||
/// The state the propagation tests use, an inclined near-circular LEO.
|
/// The state the propagation tests use, an inclined near-circular LEO.
|
||||||
fn leo() -> (Vector3<f64>, Vector3<f64>) {
|
fn leo() -> Cartesian<f64> {
|
||||||
(
|
Cartesian::new(
|
||||||
Vector3::new(
|
Vector3::new(
|
||||||
4.263868426884883e6,
|
4.263868426884883e6,
|
||||||
5.146189057155391e6,
|
5.146189057155391e6,
|
||||||
1.1310208421331816e6,
|
1.1310208421331816e6,
|
||||||
),
|
),
|
||||||
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
|
Vector3::new(-5923.454461876975, 4496.802639690076, 1870.3893008991558),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A circular orbit of the given radius in the equatorial plane.
|
||||||
|
fn circular_equatorial(radius: f64) -> Cartesian<f64> {
|
||||||
|
Cartesian::new(
|
||||||
|
Vector3::new(radius, 0.0, 0.0),
|
||||||
|
Vector3::new(0.0, circular_speed(radius, MU), 0.0),
|
||||||
|
Frame::EARTH_J2000,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,23 +433,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_circular_speed_gives_a_closed_orbit() {
|
fn test_circular_speed_gives_a_closed_orbit() {
|
||||||
let radius = 7.0e6;
|
let radius = 7.0e6;
|
||||||
let speed = circular_speed(radius, MU);
|
let elements = circular_equatorial(radius).keplerian();
|
||||||
let elements = Keplerian::from_cartesian(
|
|
||||||
Vector3::new(radius, 0.0, 0.0),
|
|
||||||
Vector3::new(0.0, speed, 0.0),
|
|
||||||
MU,
|
|
||||||
);
|
|
||||||
assert_relative_eq!(elements.semi_major_axis, radius, max_relative = 1e-12);
|
assert_relative_eq!(elements.semi_major_axis, radius, max_relative = 1e-12);
|
||||||
assert!(elements.eccentricity < 1e-15);
|
assert!(elements.eccentricity < 1e-15);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cartesian_round_trips_through_elements() {
|
fn test_cartesian_round_trips_through_elements() {
|
||||||
let (r, v) = leo();
|
let state = leo();
|
||||||
let elements = Keplerian::from_cartesian(r, v, MU);
|
let back = state.keplerian().to_cartesian();
|
||||||
let (r2, v2) = elements.to_cartesian(MU);
|
assert_relative_eq!(back.position, state.position, max_relative = 1e-12);
|
||||||
assert_relative_eq!(r2, r, max_relative = 1e-12);
|
assert_relative_eq!(back.velocity, state.velocity, max_relative = 1e-12);
|
||||||
assert_relative_eq!(v2, v, max_relative = 1e-12);
|
assert_eq!(back.frame, state.frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An inclined elliptical orbit, where no element is degenerate.
|
/// An inclined elliptical orbit, where no element is degenerate.
|
||||||
@@ -447,10 +457,10 @@ mod tests {
|
|||||||
raan: 30.0_f64.to_radians(),
|
raan: 30.0_f64.to_radians(),
|
||||||
aop: 60.0_f64.to_radians(),
|
aop: 60.0_f64.to_radians(),
|
||||||
true_anomaly: 120.0_f64.to_radians(),
|
true_anomaly: 120.0_f64.to_radians(),
|
||||||
|
frame: Frame::EARTH_J2000,
|
||||||
degeneracy: Degeneracy::None,
|
degeneracy: Degeneracy::None,
|
||||||
};
|
};
|
||||||
let (r, v) = elements.to_cartesian(MU);
|
let back = elements.to_cartesian().keplerian();
|
||||||
let back = Keplerian::from_cartesian(r, v, MU);
|
|
||||||
|
|
||||||
assert_eq!(back.degeneracy, Degeneracy::None);
|
assert_eq!(back.degeneracy, Degeneracy::None);
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(
|
||||||
@@ -478,11 +488,7 @@ mod tests {
|
|||||||
#[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;
|
||||||
let elements = Keplerian::from_cartesian(
|
let elements = circular_equatorial(radius).keplerian();
|
||||||
Vector3::new(radius, 0.0, 0.0),
|
|
||||||
Vector3::new(0.0, circular_speed(radius, MU), 0.0),
|
|
||||||
MU,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(elements.degeneracy, Degeneracy::CircularEquatorial);
|
assert_eq!(elements.degeneracy, Degeneracy::CircularEquatorial);
|
||||||
assert_relative_eq!(elements.semi_major_axis, radius, max_relative = 1e-12);
|
assert_relative_eq!(elements.semi_major_axis, radius, max_relative = 1e-12);
|
||||||
@@ -492,11 +498,12 @@ mod tests {
|
|||||||
assert_relative_eq!(elements.true_anomaly, 0.0, epsilon = 1e-12);
|
assert_relative_eq!(elements.true_anomaly, 0.0, epsilon = 1e-12);
|
||||||
|
|
||||||
// A quarter turn later the true longitude is 90 degrees.
|
// A quarter turn later the true longitude is 90 degrees.
|
||||||
let quarter = Keplerian::from_cartesian(
|
let quarter = Cartesian::new(
|
||||||
Vector3::new(0.0, radius, 0.0),
|
Vector3::new(0.0, radius, 0.0),
|
||||||
Vector3::new(-circular_speed(radius, MU), 0.0, 0.0),
|
Vector3::new(-circular_speed(radius, MU), 0.0, 0.0),
|
||||||
MU,
|
Frame::EARTH_J2000,
|
||||||
);
|
)
|
||||||
|
.keplerian();
|
||||||
assert_relative_eq!(quarter.true_anomaly, PI / 2.0, max_relative = 1e-12);
|
assert_relative_eq!(quarter.true_anomaly, PI / 2.0, max_relative = 1e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,11 +513,12 @@ mod tests {
|
|||||||
let radius = 7.0e6;
|
let radius = 7.0e6;
|
||||||
let speed = circular_speed(radius, MU);
|
let speed = circular_speed(radius, MU);
|
||||||
// Ascending node along +x, so at +x the argument of latitude is zero.
|
// Ascending node along +x, so at +x the argument of latitude is zero.
|
||||||
let elements = Keplerian::from_cartesian(
|
let elements = Cartesian::new(
|
||||||
Vector3::new(radius, 0.0, 0.0),
|
Vector3::new(radius, 0.0, 0.0),
|
||||||
Vector3::new(0.0, speed * 0.5, speed * 3.0_f64.sqrt() / 2.0),
|
Vector3::new(0.0, speed * 0.5, speed * 3.0_f64.sqrt() / 2.0),
|
||||||
MU,
|
Frame::EARTH_J2000,
|
||||||
);
|
)
|
||||||
|
.keplerian();
|
||||||
|
|
||||||
assert_eq!(elements.degeneracy, Degeneracy::Circular);
|
assert_eq!(elements.degeneracy, Degeneracy::Circular);
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(
|
||||||
@@ -554,6 +562,7 @@ mod tests {
|
|||||||
raan: 0.0,
|
raan: 0.0,
|
||||||
aop: 0.0,
|
aop: 0.0,
|
||||||
true_anomaly: 0.0,
|
true_anomaly: 0.0,
|
||||||
|
frame: Frame::EARTH_J2000,
|
||||||
degeneracy: Degeneracy::None,
|
degeneracy: Degeneracy::None,
|
||||||
};
|
};
|
||||||
assert_relative_eq!(elements.periapsis(), 1.4e7, max_relative = 1e-12);
|
assert_relative_eq!(elements.periapsis(), 1.4e7, max_relative = 1e-12);
|
||||||
@@ -565,21 +574,19 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_state_accessors() {
|
fn test_state_accessors() {
|
||||||
let (r, v) = leo();
|
let state = leo();
|
||||||
let y = Orbit.with_position(r).with_velocity(v);
|
let y = Orbit.with_cartesian(&state);
|
||||||
|
|
||||||
let elements = y.keplerian(MU);
|
let elements = y.keplerian(state.frame);
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(
|
||||||
y.period(MU),
|
elements.period(),
|
||||||
period(elements.semi_major_axis, MU),
|
period(elements.semi_major_axis, MU),
|
||||||
max_relative = 1e-12
|
max_relative = 1e-12
|
||||||
);
|
);
|
||||||
assert_relative_eq!(
|
assert_eq!(
|
||||||
y.specific_energy(MU),
|
y.specific_angular_momentum(),
|
||||||
specific_energy(r, v, MU),
|
state.position.cross(&state.velocity)
|
||||||
max_relative = 1e-12
|
|
||||||
);
|
);
|
||||||
assert_eq!(y.specific_angular_momentum(), r.cross(&v));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -591,10 +598,11 @@ mod tests {
|
|||||||
raan: 0.3,
|
raan: 0.3,
|
||||||
aop: 0.6,
|
aop: 0.6,
|
||||||
true_anomaly: 1.2,
|
true_anomaly: 1.2,
|
||||||
|
frame: Frame::EARTH_J2000,
|
||||||
degeneracy: Degeneracy::None,
|
degeneracy: Degeneracy::None,
|
||||||
};
|
};
|
||||||
let y = Orbit.with_keplerian(elements, MU);
|
let y = Orbit.with_keplerian(elements);
|
||||||
let back = y.keplerian(MU);
|
let back = y.keplerian(Frame::EARTH_J2000);
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(
|
||||||
back.semi_major_axis,
|
back.semi_major_axis,
|
||||||
elements.semi_major_axis,
|
elements.semi_major_axis,
|
||||||
@@ -616,8 +624,9 @@ mod tests {
|
|||||||
use anise::prelude::Orbit as AniseOrbit;
|
use anise::prelude::Orbit as AniseOrbit;
|
||||||
use hifitime::Epoch;
|
use hifitime::Epoch;
|
||||||
|
|
||||||
let (r, v) = leo();
|
let state = leo();
|
||||||
let ours = Keplerian::from_cartesian(r, v, MU);
|
let (r, v) = (state.position, state.velocity);
|
||||||
|
let ours = state.keplerian();
|
||||||
|
|
||||||
let frame = EARTH_J2000.with_mu_km3_s2(MU * 1e-9);
|
let frame = EARTH_J2000.with_mu_km3_s2(MU * 1e-9);
|
||||||
let theirs = AniseOrbit::new(
|
let theirs = AniseOrbit::new(
|
||||||
@@ -662,7 +671,7 @@ mod tests {
|
|||||||
max_relative = 1e-9
|
max_relative = 1e-9
|
||||||
);
|
);
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(
|
||||||
period(ours.semi_major_axis, MU),
|
ours.period(),
|
||||||
theirs.period().unwrap().to_seconds(),
|
theirs.period().unwrap().to_seconds(),
|
||||||
max_relative = 1e-9
|
max_relative = 1e-9
|
||||||
);
|
);
|
||||||
@@ -676,7 +685,8 @@ mod tests {
|
|||||||
use nalgebra::{SVector, Vector6};
|
use nalgebra::{SVector, Vector6};
|
||||||
use num_dual::DualSVec64;
|
use num_dual::DualSVec64;
|
||||||
|
|
||||||
let (r, v) = leo();
|
let leo = leo();
|
||||||
|
let (r, v) = (leo.position, leo.velocity);
|
||||||
let state = Vector6::new(r[0], r[1], r[2], v[0], v[1], v[2]);
|
let state = Vector6::new(r[0], r[1], r[2], v[0], v[1], v[2]);
|
||||||
|
|
||||||
// d(sma)/d(state), by automatic differentiation.
|
// d(sma)/d(state), by automatic differentiation.
|
||||||
@@ -684,25 +694,24 @@ mod tests {
|
|||||||
|y: Vector6<DualSVec64<6>>| {
|
|y: Vector6<DualSVec64<6>>| {
|
||||||
let position = y.fixed_rows::<3>(0).into_owned();
|
let position = y.fixed_rows::<3>(0).into_owned();
|
||||||
let velocity = y.fixed_rows::<3>(3).into_owned();
|
let velocity = y.fixed_rows::<3>(3).into_owned();
|
||||||
Keplerian::from_cartesian(position, velocity, MU).semi_major_axis
|
Cartesian::new(position, velocity, Frame::EARTH_J2000)
|
||||||
|
.keplerian()
|
||||||
|
.semi_major_axis
|
||||||
},
|
},
|
||||||
&state,
|
&state,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_relative_eq!(
|
assert_relative_eq!(sma, leo.keplerian().semi_major_axis, max_relative = 1e-12);
|
||||||
sma,
|
|
||||||
Keplerian::from_cartesian(r, v, MU).semi_major_axis,
|
|
||||||
max_relative = 1e-12
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cross-check the gradient against central differences, which agree only
|
// Cross-check the gradient against central differences, which agree only
|
||||||
// to about the square root of machine epsilon.
|
// to about the square root of machine epsilon.
|
||||||
let sma_of = |y: Vector6<f64>| {
|
let sma_of = |y: Vector6<f64>| {
|
||||||
Keplerian::from_cartesian(
|
Cartesian::new(
|
||||||
y.fixed_rows::<3>(0).into_owned(),
|
y.fixed_rows::<3>(0).into_owned(),
|
||||||
y.fixed_rows::<3>(3).into_owned(),
|
y.fixed_rows::<3>(3).into_owned(),
|
||||||
MU,
|
Frame::EARTH_J2000,
|
||||||
)
|
)
|
||||||
|
.keplerian()
|
||||||
.semi_major_axis
|
.semi_major_axis
|
||||||
};
|
};
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
#![cfg_attr(feature = "autodiff", doc = include_str!("../readme.md"))]
|
#![cfg_attr(feature = "autodiff", doc = include_str!("../readme.md"))]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
pub mod body;
|
||||||
|
pub mod cartesian;
|
||||||
pub mod dynamics;
|
pub mod dynamics;
|
||||||
|
pub mod frames;
|
||||||
pub mod kepler;
|
pub mod kepler;
|
||||||
pub mod propagation;
|
pub mod propagation;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
@@ -19,7 +22,10 @@ pub mod prelude {
|
|||||||
pub use crate::dynamics::{Context, Contribution, Dynamics, DynamicsModel};
|
pub use crate::dynamics::{Context, Contribution, Dynamics, DynamicsModel};
|
||||||
// The variants, so a script writes `TwoBody { mu }` rather than
|
// The variants, so a script writes `TwoBody { mu }` rather than
|
||||||
// `Dynamics::TwoBody { mu }`. Note this puts every model's name in scope.
|
// `Dynamics::TwoBody { mu }`. Note this puts every model's name in scope.
|
||||||
|
pub use crate::body::Body;
|
||||||
|
pub use crate::cartesian::{Cartesian, FromCartesian};
|
||||||
pub use crate::dynamics::Dynamics::*;
|
pub use crate::dynamics::Dynamics::*;
|
||||||
|
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};
|
||||||
pub use crate::state::{
|
pub use crate::state::{
|
||||||
|
|||||||
+41
-7
@@ -6,8 +6,10 @@ use ordinary_diffeq::integrator::bs3::BS3;
|
|||||||
use ordinary_diffeq::prelude::*;
|
use ordinary_diffeq::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::cartesian::Cartesian;
|
||||||
use crate::dynamics::{self, Context, Contribution, Dynamics, DynamicsModel};
|
use crate::dynamics::{self, Context, Contribution, Dynamics, DynamicsModel};
|
||||||
use crate::state::{HasPosition, HasVelocity, Layout, State};
|
use crate::frames::Frame;
|
||||||
|
use crate::state::{HasPosition, HasVelocity, Layout, Orbit, State};
|
||||||
|
|
||||||
/// How precisely the equations are solved.
|
/// How precisely the equations are solved.
|
||||||
///
|
///
|
||||||
@@ -189,6 +191,7 @@ pub struct Propagation<
|
|||||||
> {
|
> {
|
||||||
epoch: Epoch,
|
epoch: Epoch,
|
||||||
initial_state: State<T, D, L>,
|
initial_state: State<T, D, L>,
|
||||||
|
frame: Frame,
|
||||||
duration: Option<Duration>,
|
duration: Option<Duration>,
|
||||||
specs: Vec<Dynamics>,
|
specs: Vec<Dynamics>,
|
||||||
integrator: S,
|
integrator: S,
|
||||||
@@ -210,6 +213,7 @@ where
|
|||||||
Self {
|
Self {
|
||||||
epoch,
|
epoch,
|
||||||
initial_state,
|
initial_state,
|
||||||
|
frame: Frame::default(),
|
||||||
duration: None,
|
duration: None,
|
||||||
specs: dynamics.to_vec(),
|
specs: dynamics.to_vec(),
|
||||||
integrator: Vern7::new().a_tol(1e-10).r_tol(1e-10),
|
integrator: Vern7::new().a_tol(1e-10).r_tol(1e-10),
|
||||||
@@ -219,12 +223,40 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Propagation<T, 6, Orbit, Vern7<6>>
|
||||||
|
where
|
||||||
|
T: Scalar + 'static,
|
||||||
|
Vern7<6>: Integrator<6>,
|
||||||
|
{
|
||||||
|
/// A propagation of a translational state, in that state's own frame.
|
||||||
|
///
|
||||||
|
/// The shorthand for the common case: a [`Cartesian`] already carries its
|
||||||
|
/// frame, so neither the layout nor the frame has to be named.
|
||||||
|
///
|
||||||
|
/// Use [`Propagation::new`] with a wider layout when the state also carries
|
||||||
|
/// mass, attitude or estimated parameters.
|
||||||
|
pub fn from_cartesian(epoch: Epoch, state: Cartesian<T>, dynamics: &[Dynamics]) -> Self {
|
||||||
|
let frame = state.frame;
|
||||||
|
Self::new(epoch, state.to_state(Orbit), dynamics).frame(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, const D: usize, L, S> Propagation<T, D, L, S>
|
impl<T, const D: usize, L, S> Propagation<T, D, L, S>
|
||||||
where
|
where
|
||||||
T: Scalar + 'static,
|
T: Scalar + 'static,
|
||||||
L: HasPosition<D> + HasVelocity<D> + 'static,
|
L: HasPosition<D> + HasVelocity<D> + 'static,
|
||||||
S: Tolerances<D> + Copy,
|
S: Tolerances<D> + Copy,
|
||||||
{
|
{
|
||||||
|
/// The frame the state is expressed in.
|
||||||
|
///
|
||||||
|
/// Defaults to [`Frame::EARTH_J2000`]. The default lives here rather than in
|
||||||
|
/// the force models, so a propagation about another body is a matter of saying
|
||||||
|
/// so rather than of overriding an assumption in several places.
|
||||||
|
pub fn frame(mut self, frame: Frame) -> Self {
|
||||||
|
self.frame = frame;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// How long to propagate for.
|
/// How long to propagate for.
|
||||||
pub fn duration(mut self, duration: Duration) -> Self {
|
pub fn duration(mut self, duration: Duration) -> Self {
|
||||||
self.duration = Some(duration);
|
self.duration = Some(duration);
|
||||||
@@ -282,6 +314,7 @@ where
|
|||||||
Propagation {
|
Propagation {
|
||||||
epoch: self.epoch,
|
epoch: self.epoch,
|
||||||
initial_state: self.initial_state,
|
initial_state: self.initial_state,
|
||||||
|
frame: self.frame,
|
||||||
duration: self.duration,
|
duration: self.duration,
|
||||||
specs: self.specs,
|
specs: self.specs,
|
||||||
integrator,
|
integrator,
|
||||||
@@ -312,7 +345,7 @@ where
|
|||||||
|
|
||||||
let assembly = Assembly {
|
let assembly = Assembly {
|
||||||
models: dynamics::build::<T, D, L>(&self.specs),
|
models: dynamics::build::<T, D, L>(&self.specs),
|
||||||
context: Context::new(self.epoch),
|
context: Context::new(self.epoch, self.frame),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ode = ODE::new(
|
let ode = ODE::new(
|
||||||
@@ -331,6 +364,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::body::Body;
|
||||||
use crate::state::{Full, Orbit, OrbitMass};
|
use crate::state::{Full, Orbit, OrbitMass};
|
||||||
use approx::assert_relative_eq;
|
use approx::assert_relative_eq;
|
||||||
use nalgebra::{Vector3, Vector6};
|
use nalgebra::{Vector3, Vector6};
|
||||||
@@ -344,7 +378,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn two_body() -> Vec<Dynamics> {
|
fn two_body() -> Vec<Dynamics> {
|
||||||
vec![Dynamics::TwoBody { mu: MU }]
|
vec![Dynamics::TwoBody { body: Body::EARTH }]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leo() -> State<f64, 6, Orbit> {
|
fn leo() -> State<f64, 6, Orbit> {
|
||||||
@@ -565,7 +599,7 @@ mod tests {
|
|||||||
vec![Box::new(ConstantForce {
|
vec![Box::new(ConstantForce {
|
||||||
force: Vector3::new(10.0, 0.0, 0.0),
|
force: Vector3::new(10.0, 0.0, 0.0),
|
||||||
})],
|
})],
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
|
|
||||||
let y = OrbitMass
|
let y = OrbitMass
|
||||||
@@ -587,9 +621,9 @@ mod tests {
|
|||||||
Box::new(ConstantForce {
|
Box::new(ConstantForce {
|
||||||
force: Vector3::new(10.0, 0.0, 0.0),
|
force: Vector3::new(10.0, 0.0, 0.0),
|
||||||
}),
|
}),
|
||||||
Dynamics::TwoBody { mu: MU }.build(),
|
Dynamics::TwoBody { body: Body::EARTH }.build(),
|
||||||
],
|
],
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
|
|
||||||
let y = OrbitMass
|
let y = OrbitMass
|
||||||
@@ -612,7 +646,7 @@ mod tests {
|
|||||||
vec![Box::new(ConstantForce {
|
vec![Box::new(ConstantForce {
|
||||||
force: Vector3::new(10.0, 0.0, 0.0),
|
force: Vector3::new(10.0, 0.0, 0.0),
|
||||||
})],
|
})],
|
||||||
Context::new(epoch()),
|
Context::new(epoch(), Frame::EARTH_J2000),
|
||||||
);
|
);
|
||||||
let _ = state_derivative(0.0, leo().vector, &assembly);
|
let _ = state_derivative(0.0, leo().vector, &assembly);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user