From 4688f40f5a924dc79cd05b04a6b588788200556c Mon Sep 17 00:00:00 2001 From: Helge Eichhorn Date: Wed, 24 Jan 2024 14:49:02 +0100 Subject: [PATCH] Refactor two-body state vector representation and expose from Python (#46) * Prototype exposing two-body states * Refactor two body state vector representations * Move time field into state * Refactor some more * Add Python type hints * Use explicit methods * Add Python example * More tests * Re-add TwoBody trait * Add Python smoke tests and PyO3 unit test * Test Python time wrapper * Test Python frames wrapper * Test Sun and barycenters * Test planets * Test all bodies * Test PyBody * Re-introduce `two_body` module and more tests * Add anomaly tests --- .github/workflows/rust.yml | 23 ++ .idea/lox-space.iml | 5 +- .idea/misc.xml | 6 + Cargo.lock | 80 ++-- Cargo.toml | 3 + crates/lox-space/examples/iss.rs | 15 +- crates/lox-space/src/prelude.rs | 6 +- crates/lox_core/Cargo.toml | 13 +- crates/lox_core/src/bodies.rs | 55 ++- crates/lox_core/src/coords.rs | 24 ++ crates/lox_core/src/coords/anomalies.rs | 44 +++ crates/lox_core/src/coords/states.rs | 464 ++++++++++++++++++++++ crates/lox_core/src/coords/two_body.rs | 338 ++++++++++++++++ crates/lox_core/src/frames.rs | 41 +- crates/lox_core/src/frames/iau.rs | 16 +- crates/lox_core/src/lib.rs | 2 +- crates/lox_core/src/math.rs | 10 + crates/lox_core/src/time/epochs.rs | 2 +- crates/lox_core/src/two_body.rs | 339 ---------------- crates/lox_core/src/two_body/elements.rs | 421 -------------------- crates/lox_py/Cargo.toml | 7 +- crates/lox_py/lox_space.pyi | 91 +++-- crates/lox_py/pyproject.toml | 9 + crates/lox_py/src/bodies.rs | 481 ++++++++++++++++++++--- crates/lox_py/src/coords.rs | 328 ++++++++++++++++ crates/lox_py/src/frames.rs | 101 +++++ crates/lox_py/src/lib.rs | 123 +----- crates/lox_py/src/time.rs | 156 ++++++++ crates/lox_py/tests/test_coords.py | 22 ++ tools/lox_gen/Cargo.lock | 11 +- 30 files changed, 2183 insertions(+), 1053 deletions(-) create mode 100644 .idea/misc.xml create mode 100644 crates/lox_core/src/coords.rs create mode 100644 crates/lox_core/src/coords/anomalies.rs create mode 100644 crates/lox_core/src/coords/states.rs create mode 100644 crates/lox_core/src/coords/two_body.rs delete mode 100644 crates/lox_core/src/two_body.rs delete mode 100644 crates/lox_core/src/two_body/elements.rs create mode 100644 crates/lox_py/src/coords.rs create mode 100644 crates/lox_py/src/frames.rs create mode 100644 crates/lox_py/src/time.rs create mode 100644 crates/lox_py/tests/test_coords.py diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 467b8bf9..bd564d79 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,6 +16,29 @@ jobs: - name: Run tests run: cargo test + python: + name: Python smoke tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Generate virtual environment + run: python -m venv .venv + - name: Build Python wrapper + uses: PyO3/maturin-action@v1 + with: + command: develop + working-directory: ./crates/lox_py + args: --extras dev + - name: Run test + run: | + source .venv/bin/activate + pytest crates/lox_py/tests + fmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/.idea/lox-space.iml b/.idea/lox-space.iml index 79dd551d..b2060bea 100644 --- a/.idea/lox-space.iml +++ b/.idea/lox-space.iml @@ -2,7 +2,7 @@ - + @@ -18,11 +18,12 @@ + - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..6e1ff8df --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 20b56ee5..0b872a6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,28 +90,35 @@ checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" [[package]] name = "divan" -version = "0.1.2" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab20f5802e0b897093184f5dc5d23447fa715604f238dc798f6da188b230019" +checksum = "5398159ee27f2b123d89b856bad61725442f37df5fb98c30cd570c318d594aee" dependencies = [ + "cfg-if", "clap", "condtype", "divan-macros", - "linkme", + "libc", "regex-lite", ] [[package]] name = "divan-macros" -version = "0.1.2" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5d6354551e0b5c451a948814fc47fe745a14eac7835c087d60162661019db4" +checksum = "5092f66eb3563a01e85552731ae82c04c934ff4efd7ad1a0deae7b948f4b3ec4" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + [[package]] name = "errno" version = "0.3.5" @@ -257,9 +264,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" [[package]] name = "glob" @@ -297,26 +304,6 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" -[[package]] -name = "linkme" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608" -dependencies = [ - "linkme-impl", -] - -[[package]] -name = "linkme-impl" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -345,6 +332,7 @@ name = "lox_core" version = "0.1.0" dependencies = [ "divan", + "dyn-clone", "fast_polynomial", "float_eq", "glam", @@ -366,8 +354,10 @@ dependencies = [ name = "lox_py" version = "0.1.0" dependencies = [ + "float_eq", "lox_core", "pyo3", + "rstest", "thiserror", ] @@ -537,9 +527,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", @@ -549,7 +539,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.7.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -557,9 +547,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" dependencies = [ "cfg-if", "indoc", @@ -574,9 +564,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" dependencies = [ "once_cell", "target-lexicon", @@ -584,9 +574,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" dependencies = [ "libc", "pyo3-build-config", @@ -594,9 +584,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -606,9 +596,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" dependencies = [ "heck", "proc-macro2", @@ -688,7 +678,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.2", + "regex-syntax", ] [[package]] @@ -699,7 +689,7 @@ checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax", ] [[package]] @@ -708,12 +698,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "regex-syntax" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 9d6637f8..333d4c50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,8 @@ authors = ["Helge Eichhorn and the lox-space contributors"] [workspace.dependencies] lox_core = { path = "./crates/lox_core" } +float_eq = "1.0.1" +proptest = "1.4.0" +rstest = "0.18.2" thiserror = "1.0" diff --git a/crates/lox-space/examples/iss.rs b/crates/lox-space/examples/iss.rs index 1e137215..66767908 100644 --- a/crates/lox-space/examples/iss.rs +++ b/crates/lox-space/examples/iss.rs @@ -14,20 +14,21 @@ fn main() { let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time); let position = DVec3::new(6068279.27, -1692843.94, -2516619.18) * 1e-3; let velocity = DVec3::new(-660.415582, 5495.938726, -5303.093233) * 1e-3; - let iss = Cartesian::new(epoch, Earth, position, velocity); + let iss_cartesian = Cartesian::new(epoch, Earth, Icrf, position, velocity); + let iss = Keplerian::from(iss_cartesian); - println!( - "ISS Orbit for Julian Day {}", - iss.epoch().days_since_j2000(), - ); + println!("ISS Orbit for Julian Day {}", iss.time().days_since_j2000(),); println!("============================="); - println!("Semi-major axis: {:.3} km", iss.semi_major()); + println!("Semi-major axis: {:.3} km", iss.semi_major_axis()); println!("Eccentricity: {:.6}", iss.eccentricity()); println!("Inclination: {:.3}°", iss.inclination().to_degrees()); println!( "Longitude of ascending node: {:.3}°", iss.ascending_node().to_degrees() ); - println!("Argument of perigee: {}°", iss.periapsis_arg().to_degrees()); + println!( + "Argument of perigee: {}°", + iss.periapsis_argument().to_degrees() + ); println!("True anomaly: {:.3}°", iss.true_anomaly().to_degrees()); } diff --git a/crates/lox-space/src/prelude.rs b/crates/lox-space/src/prelude.rs index 20f7e2b7..b9f9c620 100644 --- a/crates/lox-space/src/prelude.rs +++ b/crates/lox-space/src/prelude.rs @@ -7,8 +7,8 @@ */ pub use lox_core::bodies::*; - +pub use lox_core::coords::two_body::{Cartesian, Keplerian}; +pub use lox_core::coords::DVec3; +pub use lox_core::frames::*; pub use lox_core::time::dates::*; pub use lox_core::time::epochs::*; - -pub use lox_core::two_body::{Cartesian, DVec3, Keplerian, TwoBody}; diff --git a/crates/lox_core/Cargo.toml b/crates/lox_core/Cargo.toml index c3bdf10a..f66026a8 100644 --- a/crates/lox_core/Cargo.toml +++ b/crates/lox_core/Cargo.toml @@ -4,17 +4,18 @@ version = "0.1.0" edition = "2021" [dependencies] -float_eq = "1.0.1" -glam = "0.24.2" -num = "0.4.0" +float_eq.workspace = true thiserror.workspace = true +glam = "0.25.0" +num = "0.4.1" nom = "7.1.3" fast_polynomial = "0.1.0" +dyn-clone = "1.0.16" [dev-dependencies] -proptest = "1.1.0" -rstest = "0.18.2" -divan = "0.1.2" +proptest.workspace = true +rstest.workspace = true +divan = "0.1.11" [[bench]] name = "iau_frames" diff --git a/crates/lox_core/src/bodies.rs b/crates/lox_core/src/bodies.rs index d8420438..58d0c0dd 100644 --- a/crates/lox_core/src/bodies.rs +++ b/crates/lox_core/src/bodies.rs @@ -6,6 +6,7 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +use dyn_clone::{clone_trait_object, DynClone}; use std::f64::consts::PI; use std::fmt::{Display, Formatter}; @@ -91,6 +92,28 @@ macro_rules! body { } } + impl Display for $i { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } + } + }; + ($i:ident, $t:ident, $name:literal, $naif_id:literal) => { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct $i; + + impl $t for $i {} + + impl Body for $i { + fn id(&self) -> NaifId { + NaifId($naif_id) + } + + fn name(&self) -> &'static str { + $name + } + } + impl Display for $i { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name()) @@ -103,7 +126,8 @@ macro_rules! body { body! { Sun, 10 } // Planets. -pub trait Planet: PointMass + Spheroid {} +pub trait Planet: PointMass + Spheroid + DynClone {} +clone_trait_object!(Planet); body! { Mercury, Planet, 199 } body! { Venus, Planet, 299 } @@ -116,16 +140,19 @@ body! { Neptune, Planet, 899 } body! { Pluto, Planet, 999 } // Barycenters. -body! { SolarSystemBarycenter, "Solar System Barycenter", 0 } -body! { MercuryBarycenter, "Mercury Barycenter", 1 } -body! { VenusBarycenter, "Venus Barycenter", 2 } -body! { EarthBarycenter, "Earth Barycenter", 3 } -body! { MarsBarycenter, "Mars Barycenter", 4 } -body! { JupiterBarycenter, "Jupiter Barycenter", 5 } -body! { SaturnBarycenter, "Saturn Barycenter", 6 } -body! { UranusBarycenter, "Uranus Barycenter", 7 } -body! { NeptuneBarycenter, "Neptune Barycenter", 8 } -body! { PlutoBarycenter, "Pluto Barycenter", 9 } +pub trait Barycenter: PointMass + DynClone {} +clone_trait_object!(Barycenter); + +body! { SolarSystemBarycenter, Barycenter, "Solar System Barycenter", 0 } +body! { MercuryBarycenter, Barycenter, "Mercury Barycenter", 1 } +body! { VenusBarycenter, Barycenter, "Venus Barycenter", 2 } +body! { EarthBarycenter, Barycenter, "Earth Barycenter", 3 } +body! { MarsBarycenter, Barycenter, "Mars Barycenter", 4 } +body! { JupiterBarycenter, Barycenter, "Jupiter Barycenter", 5 } +body! { SaturnBarycenter, Barycenter, "Saturn Barycenter", 6 } +body! { UranusBarycenter, Barycenter, "Uranus Barycenter", 7 } +body! { NeptuneBarycenter, Barycenter, "Neptune Barycenter", 8 } +body! { PlutoBarycenter, Barycenter, "Pluto Barycenter", 9 } impl PointMass for SolarSystemBarycenter { fn gravitational_parameter(&self) -> f64 { @@ -134,7 +161,8 @@ impl PointMass for SolarSystemBarycenter { } // Satellites. -pub trait Satellite: PointMass + TriAxial {} +pub trait Satellite: PointMass + TriAxial + DynClone {} +clone_trait_object!(Satellite); body! { Moon, Satellite, 301 } body! { Phobos, Satellite, 401 } @@ -290,7 +318,8 @@ body! { Kerberos, 904 } body! { Styx, 905 } // Minor bodies. -pub trait MinorBody: PointMass + TriAxial {} +pub trait MinorBody: PointMass + TriAxial + DynClone {} +clone_trait_object!(MinorBody); body! {Gaspra, 9511010 } body! {Ida, 2431010 } diff --git a/crates/lox_core/src/coords.rs b/crates/lox_core/src/coords.rs new file mode 100644 index 00000000..a59cce1b --- /dev/null +++ b/crates/lox_core/src/coords.rs @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +pub use glam::DVec3; + +use crate::bodies::PointMass; +use crate::frames::ReferenceFrame; + +pub mod anomalies; +pub mod states; +pub mod two_body; + +pub trait CoordinateSystem { + type Origin: PointMass; + type Frame: ReferenceFrame; + + fn origin(&self) -> Self::Origin; + fn reference_frame(&self) -> Self::Frame; +} diff --git a/crates/lox_core/src/coords/anomalies.rs b/crates/lox_core/src/coords/anomalies.rs new file mode 100644 index 00000000..661f5ed6 --- /dev/null +++ b/crates/lox_core/src/coords/anomalies.rs @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +pub fn hyperbolic_to_true(hyperbolic_anomaly: f64, eccentricity: f64) -> f64 { + 2.0 * (((1.0 + eccentricity) / (eccentricity - 1.0)).sqrt() * (hyperbolic_anomaly / 2.0).tanh()) + .atan() +} + +pub fn eccentric_to_true(eccentric_anomaly: f64, eccentricity: f64) -> f64 { + 2.0 * (((1.0 + eccentricity) / (1.0 - eccentricity)).sqrt() * (eccentric_anomaly / 2.0).tan()) + .atan() +} + +#[cfg(test)] +mod tests { + use std::f64::consts::PI; + + use float_eq::assert_float_eq; + + use super::*; + + #[test] + fn test_hyperbolic() { + assert_float_eq!( + hyperbolic_to_true(PI / 2.0, 1.2), + 2.2797028138935547, + rel <= 1e-8 + ); + } + + #[test] + fn test_eccentric() { + assert_float_eq!( + eccentric_to_true(PI / 2.0, 0.2), + 1.7721542475852272, + rel <= 1e-8 + ); + } +} diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs new file mode 100644 index 00000000..94900e47 --- /dev/null +++ b/crates/lox_core/src/coords/states.rs @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use float_eq::float_eq; +use glam::{DMat3, DVec3}; + +use crate::math::{mod_two_pi, normalize_two_pi}; +use crate::time::epochs::Epoch; + +pub trait TwoBodyState { + fn time(&self) -> Epoch; + fn to_cartesian_state(&self, grav_param: f64) -> CartesianState; + fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState; +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct CartesianState { + time: Epoch, + position: DVec3, + velocity: DVec3, +} + +impl CartesianState { + pub fn new(time: Epoch, position: DVec3, velocity: DVec3) -> Self { + Self { + time, + position, + velocity, + } + } + + pub fn position(&self) -> DVec3 { + self.position + } + + pub fn velocity(&self) -> DVec3 { + self.velocity + } +} + +impl TwoBodyState for CartesianState { + fn time(&self) -> Epoch { + self.time + } + + fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { + *self + } + + fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState { + let r = self.position.length(); + let v = self.velocity.length(); + let h = self.position.cross(self.velocity); + let hm = h.length(); + let node = DVec3::Z.cross(h); + let e = eccentricity_vector(grav_param, self.position, self.velocity); + let eccentricity = e.length(); + let inclination = h.angle_between(DVec3::Z); + + let equatorial = is_equatorial(inclination); + let circular = is_circular(eccentricity); + + let semi_major = if circular { + hm.powi(2) / grav_param + } else { + -grav_param / (2.0 * (v.powi(2) / 2.0 - grav_param / r)) + }; + + let ascending_node; + let periapsis_arg; + let true_anomaly; + if equatorial && !circular { + ascending_node = 0.0; + periapsis_arg = azimuth(e); + true_anomaly = (h.dot(e.cross(self.position)) / hm).atan2(self.position.dot(e)); + } else if !equatorial && circular { + ascending_node = azimuth(node); + periapsis_arg = 0.0; + true_anomaly = (self.position.dot(h.cross(node)) / hm).atan2(self.position.dot(node)); + } else if equatorial && circular { + ascending_node = 0.0; + periapsis_arg = 0.0; + true_anomaly = azimuth(self.position); + } else { + if semi_major > 0.0 { + let e_se = self.position.dot(self.velocity) / (grav_param * semi_major).sqrt(); + let e_ce = (r * v.powi(2)) / grav_param - 1.0; + true_anomaly = + crate::coords::anomalies::eccentric_to_true(e_se.atan2(e_ce), eccentricity); + } else { + let e_sh = self.position.dot(self.velocity) / (-grav_param * semi_major).sqrt(); + let e_ch = (r * v.powi(2)) / grav_param - 1.0; + true_anomaly = crate::coords::anomalies::hyperbolic_to_true( + ((e_ch + e_sh) / (e_ch - e_sh)).ln() / 2.0, + eccentricity, + ); + } + ascending_node = azimuth(node); + let px = self.position.dot(node); + let py = self.position.dot(h.cross(node)) / hm; + periapsis_arg = py.atan2(px) - true_anomaly; + } + + KeplerianState::new( + self.time, + semi_major, + eccentricity, + inclination, + mod_two_pi(ascending_node), + mod_two_pi(periapsis_arg), + normalize_two_pi(true_anomaly, 0.0), + ) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct KeplerianState { + time: Epoch, + semi_major: f64, + eccentricity: f64, + inclination: f64, + ascending_node: f64, + periapsis_argument: f64, + true_anomaly: f64, +} + +impl KeplerianState { + pub fn new( + time: Epoch, + semi_major: f64, + eccentricity: f64, + inclination: f64, + ascending_node: f64, + periapsis_argument: f64, + true_anomaly: f64, + ) -> Self { + Self { + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_argument, + true_anomaly, + } + } + + pub fn semi_major_axis(&self) -> f64 { + self.semi_major + } + + pub fn eccentricity(&self) -> f64 { + self.eccentricity + } + + pub fn inclination(&self) -> f64 { + self.inclination + } + + pub fn ascending_node(&self) -> f64 { + self.ascending_node + } + + pub fn periapsis_argument(&self) -> f64 { + self.periapsis_argument + } + + pub fn true_anomaly(&self) -> f64 { + self.true_anomaly + } + + pub fn semi_latus_rectum(&self) -> f64 { + if float_eq!(self.eccentricity, 0.0, abs <= 1e-3) { + self.semi_major + } else { + self.semi_major * (1.0 - self.eccentricity.powi(2)) + } + } + + pub fn to_perifocal(&self, grav_param: f64) -> (DVec3, DVec3) { + let semi_latus = self.semi_latus_rectum(); + let (sin_nu, cos_nu) = self.true_anomaly.sin_cos(); + let sqrt_mu_p = (grav_param / semi_latus).sqrt(); + + let pos = + DVec3::new(cos_nu, sin_nu, 0.0) * (semi_latus / (1.0 + self.eccentricity * cos_nu)); + let vel = DVec3::new(-sin_nu, self.eccentricity + cos_nu, 0.0) * sqrt_mu_p; + + (pos, vel) + } +} + +impl TwoBodyState for KeplerianState { + fn time(&self) -> Epoch { + self.time + } + + fn to_cartesian_state(&self, grav_param: f64) -> CartesianState { + let (pos, vel) = self.to_perifocal(grav_param); + let rot = DMat3::from_rotation_z(self.ascending_node) + * DMat3::from_rotation_x(self.inclination) + * DMat3::from_rotation_z(self.periapsis_argument); + CartesianState::new(self.time, rot * pos, rot * vel) + } + + fn to_keplerian_state(&self, _grav_param: f64) -> KeplerianState { + *self + } +} + +fn azimuth(v: DVec3) -> f64 { + v.y.atan2(v.x) +} + +fn eccentricity_vector(grav_param: f64, pos: DVec3, vel: DVec3) -> DVec3 { + (pos * (vel.dot(vel) - grav_param / pos.length()) - vel * pos.dot(vel)) / grav_param +} + +fn is_equatorial(inclination: f64) -> bool { + float_eq!(inclination.abs(), 0.0, abs <= 1e-3) +} + +fn is_circular(eccentricity: f64) -> bool { + float_eq!(eccentricity, 0.0, abs <= 1e-3) +} + +#[cfg(test)] +mod tests { + use crate::time::epochs::TimeScale; + use float_eq::assert_float_eq; + use glam::DVec3; + + use super::*; + + #[test] + fn test_elliptic() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.9860047e14; + let semi_major = 24464560.0; + let eccentricity = 0.7311; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 3.10686; + let true_anomaly = 0.44369564302687126; + let pos = DVec3::new( + -0.107622532467967e7, + -0.676589636432773e7, + -0.332308783350379e6, + ); + let vel = DVec3::new( + 0.935685775154103e4, + -0.331234775037644e4, + -0.118801577532701e4, + ); + + let cartesian = CartesianState::new(time, pos, vel); + assert_eq!(cartesian.to_cartesian_state(grav_param), cartesian); + + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + assert_eq!(keplerian.to_keplerian_state(grav_param), keplerian); + + let cartesian1 = keplerian.to_cartesian_state(grav_param); + let keplerian1 = cartesian.to_keplerian_state(grav_param); + + assert_eq!(cartesian1.time(), time); + assert_eq!(keplerian1.time(), time); + + assert_float_eq!(pos.x, cartesian1.position.x, rel <= 1e-8); + assert_float_eq!(pos.y, cartesian1.position.y, rel <= 1e-8); + assert_float_eq!(pos.z, cartesian1.position.z, rel <= 1e-8); + assert_float_eq!(vel.x, cartesian1.velocity.x, rel <= 1e-8); + assert_float_eq!(vel.y, cartesian1.velocity.y, rel <= 1e-8); + assert_float_eq!(vel.z, cartesian1.velocity.z, rel <= 1e-8); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, rel <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } + + #[test] + fn test_circular() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.986004418e14; + let semi_major = 6778136.6; + let eccentricity = 0.0; + let inclination = 15f64.to_radians(); + let ascending_node = 20f64.to_radians(); + let periapsis_arg = 0.0; + let true_anomaly = 30f64.to_radians(); + let pos = DVec3::new(4396398.60746266, 5083838.45333733, 877155.42119322); + let vel = DVec3::new(-5797.06004014, 4716.60916063, 1718.86034246); + let cartesian = CartesianState::new(time, pos, vel); + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + + let cartesian1 = keplerian.to_cartesian_state(grav_param); + let keplerian1 = cartesian.to_keplerian_state(grav_param); + + assert_float_eq!(pos.x, cartesian1.position.x, rel <= 1e-8); + assert_float_eq!(pos.y, cartesian1.position.y, rel <= 1e-8); + assert_float_eq!(pos.z, cartesian1.position.z, rel <= 1e-8); + assert_float_eq!(vel.x, cartesian1.velocity.x, rel <= 1e-8); + assert_float_eq!(vel.y, cartesian1.velocity.y, rel <= 1e-8); + assert_float_eq!(vel.z, cartesian1.velocity.z, rel <= 1e-8); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, abs <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } + + #[test] + fn test_circular_orekit() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.9860047e14; + let semi_major = 24464560.0; + let eccentricity = 0.0; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 0.0; + let true_anomaly = 0.048363; + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + + let keplerian1 = keplerian + .to_cartesian_state(grav_param) + .to_keplerian_state(grav_param); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, abs <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } + + #[test] + fn test_hyperbolic_orekit() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.9860047e14; + let semi_major = -24464560.0; + let eccentricity = 1.7311; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 3.10686; + let true_anomaly = 0.12741601769795755; + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + + let keplerian1 = keplerian + .to_cartesian_state(grav_param) + .to_keplerian_state(grav_param); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, rel <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } + + #[test] + fn test_equatorial() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.9860047e14; + let semi_major = 24464560.0; + let eccentricity = 0.7311; + let inclination = 0.0; + let ascending_node = 0.0; + let periapsis_arg = 3.10686; + let true_anomaly = 0.44369564302687126; + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + + let keplerian1 = keplerian + .to_cartesian_state(grav_param) + .to_keplerian_state(grav_param); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, rel <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } + + #[test] + fn test_circular_equatorial() { + let time = Epoch::j2000(TimeScale::TDB); + let grav_param = 3.9860047e14; + let semi_major = 24464560.0; + let eccentricity = 0.0; + let inclination = 0.0; + let ascending_node = 0.0; + let periapsis_arg = 0.0; + let true_anomaly = 0.44369564302687126; + let keplerian = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + + let keplerian1 = keplerian + .to_cartesian_state(grav_param) + .to_keplerian_state(grav_param); + + assert_float_eq!(semi_major, keplerian1.semi_major, rel <= 1e-8); + assert_float_eq!(eccentricity, keplerian1.eccentricity, abs <= 1e-8); + assert_float_eq!(inclination, keplerian1.inclination, rel <= 1e-8); + assert_float_eq!(ascending_node, keplerian1.ascending_node, rel <= 1e-8); + assert_float_eq!(periapsis_arg, keplerian1.periapsis_argument, rel <= 1e-8); + assert_float_eq!(true_anomaly, keplerian1.true_anomaly, rel <= 1e-8); + } +} diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs new file mode 100644 index 00000000..8a15cf5a --- /dev/null +++ b/crates/lox_core/src/coords/two_body.rs @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use glam::DVec3; + +use crate::bodies::PointMass; +use crate::coords::states::{CartesianState, KeplerianState, TwoBodyState}; +use crate::coords::CoordinateSystem; +use crate::frames::{InertialFrame, ReferenceFrame}; +use crate::time::epochs::Epoch; + +pub trait TwoBody +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + fn to_cartesian(&self) -> Cartesian; + + fn to_keplerian(&self) -> Keplerian; +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Cartesian +where + T: PointMass + Copy, + S: ReferenceFrame + Copy, +{ + state: CartesianState, + origin: T, + frame: S, +} + +impl Cartesian +where + T: PointMass + Copy, + S: ReferenceFrame + Copy, +{ + pub fn new(time: Epoch, origin: T, frame: S, position: DVec3, velocity: DVec3) -> Self { + let state = CartesianState::new(time, position, velocity); + Self { + state, + origin, + frame, + } + } + + pub fn time(&self) -> Epoch { + self.state.time() + } + + pub fn position(&self) -> DVec3 { + self.state.position() + } + + pub fn velocity(&self) -> DVec3 { + self.state.position() + } +} + +impl TwoBody for Cartesian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + fn to_cartesian(&self) -> Cartesian { + *self + } + + fn to_keplerian(&self) -> Keplerian { + Keplerian::from(*self) + } +} + +impl CoordinateSystem for Cartesian +where + T: PointMass + Copy, + S: ReferenceFrame + Copy, +{ + type Origin = T; + type Frame = S; + + fn origin(&self) -> Self::Origin { + self.origin + } + + fn reference_frame(&self) -> Self::Frame { + self.frame + } +} + +impl From> for Cartesian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + fn from(keplerian: Keplerian) -> Self { + let grav_param = keplerian.origin.gravitational_parameter(); + let state = keplerian.state.to_cartesian_state(grav_param); + Cartesian { + state, + origin: keplerian.origin, + frame: keplerian.frame, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Keplerian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + state: KeplerianState, + origin: T, + frame: S, +} + +impl Keplerian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + #[allow(clippy::too_many_arguments)] + pub fn new( + time: Epoch, + origin: T, + frame: S, + semi_major: f64, + eccentricity: f64, + inclination: f64, + ascending_node: f64, + periapsis_arg: f64, + true_anomaly: f64, + ) -> Self { + let state = KeplerianState::new( + time, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + Self { + state, + origin, + frame, + } + } + + pub fn time(&self) -> Epoch { + self.state.time() + } + + pub fn semi_major_axis(&self) -> f64 { + self.state.semi_major_axis() + } + + pub fn eccentricity(&self) -> f64 { + self.state.eccentricity() + } + + pub fn inclination(&self) -> f64 { + self.state.inclination() + } + + pub fn ascending_node(&self) -> f64 { + self.state.ascending_node() + } + + pub fn periapsis_argument(&self) -> f64 { + self.state.periapsis_argument() + } + + pub fn true_anomaly(&self) -> f64 { + self.state.true_anomaly() + } +} + +impl TwoBody for Keplerian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + fn to_cartesian(&self) -> Cartesian { + Cartesian::from(*self) + } + + fn to_keplerian(&self) -> Keplerian { + *self + } +} + +impl CoordinateSystem for Keplerian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + type Origin = T; + type Frame = S; + + fn origin(&self) -> Self::Origin { + self.origin + } + + fn reference_frame(&self) -> Self::Frame { + self.frame + } +} + +impl From> for Keplerian +where + T: PointMass + Copy, + S: InertialFrame + Copy, +{ + fn from(cartesian: Cartesian) -> Self { + let grav_param = cartesian.origin.gravitational_parameter(); + let state = cartesian.state.to_keplerian_state(grav_param); + Self { + state, + origin: cartesian.origin, + frame: cartesian.frame, + } + } +} + +#[cfg(test)] +mod tests { + use float_eq::assert_float_eq; + + use crate::bodies::Earth; + use crate::frames::Icrf; + use crate::time::dates::{Date, Time}; + use crate::time::epochs::TimeScale; + + use super::*; + + #[test] + fn test_cartesian() { + let date = Date::new(2023, 3, 25).expect("Date should be valid"); + let time = Time::new(21, 8, 0).expect("Time should be valid"); + let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time); + let pos = DVec3::new( + -0.107622532467967e7, + -0.676589636432773e7, + -0.332308783350379e6, + ) * 1e-3; + let vel = DVec3::new( + 0.935685775154103e4, + -0.331234775037644e4, + -0.118801577532701e4, + ) * 1e-3; + + let cartesian = Cartesian::new(epoch, Earth, Icrf, pos, vel); + assert_eq!(cartesian.to_cartesian(), cartesian); + + let cartesian1 = cartesian.to_keplerian().to_cartesian(); + + assert_eq!(cartesian1.time(), epoch); + assert_eq!(cartesian1.origin(), Earth); + assert_eq!(cartesian1.reference_frame(), Icrf); + + assert_float_eq!(cartesian.position().x, cartesian1.position().x, rel <= 1e-8); + assert_float_eq!(cartesian.position().y, cartesian1.position().y, rel <= 1e-8); + assert_float_eq!(cartesian.position().z, cartesian1.position().z, rel <= 1e-8); + assert_float_eq!(cartesian.velocity().x, cartesian1.velocity().x, rel <= 1e-6); + assert_float_eq!(cartesian.velocity().y, cartesian1.velocity().y, rel <= 1e-6); + assert_float_eq!(cartesian.velocity().z, cartesian1.velocity().z, rel <= 1e-6); + } + + #[test] + fn test_keplerian() { + let date = Date::new(2023, 3, 25).expect("Date should be valid"); + let time = Time::new(21, 8, 0).expect("Time should be valid"); + let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time); + let semi_major = 24464560.0e-3; + let eccentricity = 0.7311; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 3.10686; + let true_anomaly = 0.44369564302687126; + + let keplerian = Keplerian::new( + epoch, + Earth, + Icrf, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + assert_eq!(keplerian.to_keplerian(), keplerian); + + let keplerian1 = keplerian.to_cartesian().to_keplerian(); + + assert_eq!(keplerian1.time(), epoch); + assert_eq!(keplerian1.origin(), Earth); + assert_eq!(keplerian1.reference_frame(), Icrf); + + assert_float_eq!( + keplerian.semi_major_axis(), + keplerian1.semi_major_axis(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.eccentricity(), + keplerian1.eccentricity(), + abs <= 1e-6 + ); + assert_float_eq!( + keplerian.inclination(), + keplerian1.inclination(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.ascending_node(), + keplerian1.ascending_node(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.periapsis_argument(), + keplerian1.periapsis_argument(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.true_anomaly(), + keplerian1.true_anomaly(), + rel <= 1e-6 + ); + } +} diff --git a/crates/lox_core/src/frames.rs b/crates/lox_core/src/frames.rs index 3dc8cd24..434d6b09 100644 --- a/crates/lox_core/src/frames.rs +++ b/crates/lox_core/src/frames.rs @@ -15,15 +15,29 @@ pub mod iau; // TODO: Replace with proper `Epoch` type type Epoch = f64; -pub trait ReferenceFrame { - fn is_inertial(&self) -> bool; +pub trait ReferenceFrame {} + +pub trait InertialFrame: ReferenceFrame { + fn is_inertial(&self) -> bool { + true + } + + fn is_rotating(&self) -> bool { + false + } +} + +pub trait RotatingFrame: ReferenceFrame { + fn is_inertial(&self) -> bool { + false + } fn is_rotating(&self) -> bool { - !self.is_inertial() + true } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Icrf; impl Display for Icrf { @@ -32,11 +46,8 @@ impl Display for Icrf { } } -impl ReferenceFrame for Icrf { - fn is_inertial(&self) -> bool { - true - } -} +impl ReferenceFrame for Icrf {} +impl InertialFrame for Icrf {} pub fn rotation_matrix_derivative(m: DMat3, v: DVec3) -> DMat3 { let sx = DVec3::new(0.0, v.z, v.y); @@ -111,3 +122,15 @@ where self.rotation_from(frame, t).transpose() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_icrf() { + assert!(Icrf.is_inertial()); + assert!(!Icrf.is_rotating()); + assert_eq!(format!("{}", Icrf), "ICRF"); + } +} diff --git a/crates/lox_core/src/frames/iau.rs b/crates/lox_core/src/frames/iau.rs index 91d700ae..2a788dd6 100644 --- a/crates/lox_core/src/frames/iau.rs +++ b/crates/lox_core/src/frames/iau.rs @@ -6,22 +6,25 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; use glam::{DMat3, DVec3}; use crate::bodies::RotationalElements; -use crate::frames::{Epoch, FromFrame, Icrf, ReferenceFrame, Rotation}; +use crate::frames::{Epoch, FromFrame, Icrf, ReferenceFrame, RotatingFrame, Rotation}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct BodyFixed(pub T); -impl ReferenceFrame for BodyFixed { - fn is_inertial(&self) -> bool { - false +impl Display for BodyFixed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "IAU_{}", &self.0.name().to_uppercase()) } } +impl ReferenceFrame for BodyFixed {} +impl RotatingFrame for BodyFixed {} + impl FromFrame for BodyFixed { fn rotation_from(&self, _: Icrf, t: Epoch) -> Rotation { let (right_ascension, declination, prime_meridian) = T::rotational_elements(t); @@ -49,6 +52,7 @@ mod tests { #[test] fn test_bodyfixed() { let iau_jupiter = BodyFixed(Jupiter); + assert_eq!(format!("{}", iau_jupiter), "IAU_JUPITER"); assert!(!iau_jupiter.is_inertial()); assert!(iau_jupiter.is_rotating()); diff --git a/crates/lox_core/src/lib.rs b/crates/lox_core/src/lib.rs index 0a10ad1e..4e170f47 100644 --- a/crates/lox_core/src/lib.rs +++ b/crates/lox_core/src/lib.rs @@ -8,11 +8,11 @@ pub mod bodies; pub mod communications; +pub mod coords; pub mod ephemeris; pub mod errors; pub mod frames; pub mod time; -pub mod two_body; pub mod types; mod earth; diff --git a/crates/lox_core/src/math.rs b/crates/lox_core/src/math.rs index 365146f5..13c9b5c3 100644 --- a/crates/lox_core/src/math.rs +++ b/crates/lox_core/src/math.rs @@ -24,6 +24,16 @@ pub(crate) fn arcsec_to_rad(arcsec: Arcsec) -> Radians { arcsec * RADIANS_IN_ARCSECOND } +/// Modulus after division by 2π, returning in the range [0,2π). +pub fn mod_two_pi(a: f64) -> f64 { + let w = a % (2.0 * PI); + if w < 0.0 { + w + 2.0 * PI + } else { + w + } +} + #[cfg(test)] mod tests { use float_eq::assert_float_eq; diff --git a/crates/lox_core/src/time/epochs.rs b/crates/lox_core/src/time/epochs.rs index 3e4b337d..c8de3c80 100644 --- a/crates/lox_core/src/time/epochs.rs +++ b/crates/lox_core/src/time/epochs.rs @@ -16,7 +16,7 @@ use crate::time::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER use crate::time::dates::Calendar::ProlepticJulian; use crate::time::dates::{Date, DateTime, Time}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TimeScale { TAI, TCB, diff --git a/crates/lox_core/src/two_body.rs b/crates/lox_core/src/two_body.rs deleted file mode 100644 index 9b6ed890..00000000 --- a/crates/lox_core/src/two_body.rs +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (c) 2023. Helge Eichhorn and the LOX contributors - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - */ - -pub use glam::DVec3; - -use crate::bodies::PointMass; -use crate::time::epochs::Epoch; -use crate::two_body::elements::{cartesian_to_keplerian, keplerian_to_cartesian}; - -pub mod elements; - -type Elements = (f64, f64, f64, f64, f64, f64); - -pub trait TwoBody { - type Center; - fn epoch(&self) -> Epoch; - fn center(&self) -> Self::Center; - fn position(&self) -> DVec3; - fn velocity(&self) -> DVec3; - fn cartesian(&self) -> (DVec3, DVec3); - fn keplerian(&self) -> Elements; - fn semi_major(&self) -> f64; - fn eccentricity(&self) -> f64; - fn inclination(&self) -> f64; - fn ascending_node(&self) -> f64; - fn periapsis_arg(&self) -> f64; - fn true_anomaly(&self) -> f64; -} - -#[derive(Debug, Clone)] -pub struct Cartesian { - epoch: Epoch, - center: T, - position: DVec3, - velocity: DVec3, -} - -impl Cartesian { - pub fn new(epoch: Epoch, center: T, position: DVec3, velocity: DVec3) -> Self { - Self { - epoch, - center, - position, - velocity, - } - } -} - -impl TwoBody for Cartesian { - type Center = T; - - fn epoch(&self) -> Epoch { - self.epoch - } - - fn center(&self) -> Self::Center { - self.center - } - - fn position(&self) -> DVec3 { - self.position - } - - fn velocity(&self) -> DVec3 { - self.velocity - } - - fn cartesian(&self) -> (DVec3, DVec3) { - (self.position, self.velocity) - } - - fn keplerian(&self) -> Elements { - let mu = self.center.gravitational_parameter(); - cartesian_to_keplerian(mu, self.position, self.velocity) - } - - fn semi_major(&self) -> f64 { - self.keplerian().0 - } - - fn eccentricity(&self) -> f64 { - self.keplerian().1 - } - - fn inclination(&self) -> f64 { - self.keplerian().2 - } - - fn ascending_node(&self) -> f64 { - self.keplerian().3 - } - - fn periapsis_arg(&self) -> f64 { - self.keplerian().4 - } - - fn true_anomaly(&self) -> f64 { - self.keplerian().5 - } -} - -impl From> for Cartesian { - fn from(value: Keplerian) -> Self { - let epoch = value.epoch; - let center = value.center; - let (pos, vel) = value.cartesian(); - Cartesian::new(epoch, center, pos, vel) - } -} - -#[derive(Debug, Clone)] -pub struct Keplerian { - epoch: Epoch, - center: T, - semi_major: f64, - eccentricity: f64, - inclination: f64, - ascending_node: f64, - periapsis_arg: f64, - true_anomaly: f64, -} - -impl Keplerian { - pub fn new(epoch: Epoch, center: T, elements: Elements) -> Self { - let (semi_major, eccentricity, inclination, ascending_node, periapsis_arg, true_anomaly) = - elements; - Self { - epoch, - center, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - } - } -} - -impl TwoBody for Keplerian { - type Center = T; - - fn epoch(&self) -> Epoch { - self.epoch - } - - fn center(&self) -> Self::Center { - self.center - } - - fn position(&self) -> DVec3 { - self.cartesian().0 - } - - fn velocity(&self) -> DVec3 { - self.cartesian().1 - } - - fn cartesian(&self) -> (DVec3, DVec3) { - let mu = self.center.gravitational_parameter(); - keplerian_to_cartesian( - mu, - self.semi_major, - self.eccentricity, - self.inclination, - self.ascending_node, - self.periapsis_arg, - self.true_anomaly, - ) - } - - fn keplerian(&self) -> Elements { - ( - self.semi_major, - self.eccentricity, - self.inclination, - self.ascending_node, - self.periapsis_arg, - self.true_anomaly, - ) - } - - fn semi_major(&self) -> f64 { - self.semi_major - } - - fn eccentricity(&self) -> f64 { - self.eccentricity - } - - fn inclination(&self) -> f64 { - self.inclination - } - - fn ascending_node(&self) -> f64 { - self.ascending_node - } - - fn periapsis_arg(&self) -> f64 { - self.periapsis_arg - } - - fn true_anomaly(&self) -> f64 { - self.true_anomaly - } -} - -impl From> for Keplerian { - fn from(value: Cartesian) -> Self { - let epoch = value.epoch; - let center = value.center; - let elements = value.keplerian(); - Self::new(epoch, center, elements) - } -} - -#[cfg(test)] -mod tests { - use std::ops::Mul; - - use float_eq::assert_float_eq; - - use crate::bodies::Earth; - use crate::time::dates::{Date, Time}; - use crate::time::epochs::TimeScale; - - use super::*; - - #[test] - fn test_two_body() { - let date = Date::new(2023, 3, 25).expect("Date should be valid"); - let time = Time::new(21, 8, 0).expect("Time should be valid"); - let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time); - let semi_major = 24464560.0e-3; - let eccentricity = 0.7311; - let inclination = 0.122138; - let ascending_node = 1.00681; - let periapsis_arg = 3.10686; - let true_anomaly = 0.44369564302687126; - let pos = DVec3::new( - -0.107622532467967e7, - -0.676589636432773e7, - -0.332308783350379e6, - ) - .mul(1e-3); - let vel = DVec3::new( - 0.935685775154103e4, - -0.331234775037644e4, - -0.118801577532701e4, - ) - .mul(1e-3); - - let cartesian = Cartesian::new(epoch, Earth, pos, vel); - - assert_eq!( - cartesian.cartesian(), - (cartesian.position(), cartesian.velocity()) - ); - assert_eq!(cartesian.epoch(), epoch); - assert_eq!(cartesian.center(), Earth); - assert_eq!(cartesian.position(), pos); - assert_eq!(cartesian.velocity(), vel); - assert_float_eq!(cartesian.semi_major(), semi_major, rel <= 1e-6); - assert_float_eq!(cartesian.eccentricity(), eccentricity, rel <= 1e-6); - assert_float_eq!(cartesian.inclination(), inclination, rel <= 1e-6); - assert_float_eq!(cartesian.ascending_node(), ascending_node, rel <= 1e-6); - assert_float_eq!(cartesian.periapsis_arg(), periapsis_arg, rel <= 1e-6); - assert_float_eq!(cartesian.true_anomaly(), true_anomaly, rel <= 1e-6); - - let keplerian = Keplerian::new( - epoch, - Earth, - ( - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ), - ); - - assert_eq!( - keplerian.keplerian(), - ( - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly - ) - ); - assert_eq!(keplerian.epoch(), epoch); - assert_eq!(keplerian.center(), Earth); - assert_float_eq!(keplerian.position().x, pos.x, rel <= 1e-8); - assert_float_eq!(keplerian.position().y, pos.y, rel <= 1e-8); - assert_float_eq!(keplerian.position().z, pos.z, rel <= 1e-8); - assert_float_eq!(keplerian.velocity().x, vel.x, rel <= 1e-6); - assert_float_eq!(keplerian.velocity().y, vel.y, rel <= 1e-6); - assert_float_eq!(keplerian.velocity().z, vel.z, rel <= 1e-6); - assert_float_eq!(keplerian.semi_major(), semi_major, rel <= 1e-6); - assert_float_eq!(keplerian.eccentricity(), eccentricity, rel <= 1e-6); - assert_float_eq!(keplerian.inclination(), inclination, rel <= 1e-6); - assert_float_eq!(keplerian.ascending_node(), ascending_node, rel <= 1e-6); - assert_float_eq!(keplerian.periapsis_arg(), periapsis_arg, rel <= 1e-6); - assert_float_eq!(keplerian.true_anomaly(), true_anomaly, rel <= 1e-6); - - let cartesian1 = Cartesian::from(keplerian.clone()); - let keplerian1 = Keplerian::from(cartesian.clone()); - - assert_float_eq!(cartesian.position.x, cartesian1.position.x, rel <= 1e-8); - assert_float_eq!(cartesian.position.y, cartesian1.position.y, rel <= 1e-8); - assert_float_eq!(cartesian.position.z, cartesian1.position.z, rel <= 1e-8); - assert_float_eq!(cartesian.velocity.x, cartesian1.velocity.x, rel <= 1e-6); - assert_float_eq!(cartesian.velocity.y, cartesian1.velocity.y, rel <= 1e-6); - assert_float_eq!(cartesian.velocity.z, cartesian1.velocity.z, rel <= 1e-6); - - assert_float_eq!(keplerian.semi_major, keplerian1.semi_major, rel <= 1e-2); - assert_float_eq!(keplerian.eccentricity, keplerian1.eccentricity, abs <= 1e-6); - assert_float_eq!(keplerian.inclination, keplerian1.inclination, rel <= 1e-6); - assert_float_eq!( - keplerian.ascending_node, - keplerian1.ascending_node, - rel <= 1e-6 - ); - assert_float_eq!( - keplerian.periapsis_arg, - keplerian1.periapsis_arg, - rel <= 1e-6 - ); - assert_float_eq!(keplerian.true_anomaly, keplerian1.true_anomaly, rel <= 1e-6); - } -} diff --git a/crates/lox_core/src/two_body/elements.rs b/crates/lox_core/src/two_body/elements.rs deleted file mode 100644 index 5287a7bb..00000000 --- a/crates/lox_core/src/two_body/elements.rs +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright (c) 2023. Helge Eichhorn and the LOX contributors - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - */ - -use crate::math::normalize_two_pi; -use float_eq::float_eq; -use glam::{DMat3, DVec3}; -use std::f64::consts::PI; - -pub fn keplerian_to_perifocal( - semi_latus: f64, - eccentricity: f64, - true_anomaly: f64, - grav_param: f64, -) -> (DVec3, DVec3) { - let (sin_nu, cos_nu) = true_anomaly.sin_cos(); - let sqrt_mu_p = (grav_param / semi_latus).sqrt(); - - let pos = DVec3::new(cos_nu, sin_nu, 0.0) * (semi_latus / (1.0 + eccentricity * cos_nu)); - let vel = DVec3::new(-sin_nu, eccentricity + cos_nu, 0.0) * sqrt_mu_p; - - (pos, vel) -} - -pub fn keplerian_to_cartesian( - grav_param: f64, - semi_major: f64, - eccentricity: f64, - inclination: f64, - ascending_node: f64, - periapsis_arg: f64, - true_anomaly: f64, -) -> (DVec3, DVec3) { - let semi_latus = semi_latus_rectum(semi_major, eccentricity); - let (pos, vel) = keplerian_to_perifocal(semi_latus, eccentricity, true_anomaly, grav_param); - let rot = DMat3::from_rotation_z(ascending_node) - * DMat3::from_rotation_x(inclination) - * DMat3::from_rotation_z(periapsis_arg); - (rot * pos, rot * vel) -} - -pub fn semi_latus_rectum(semi_major: f64, eccentricity: f64) -> f64 { - if float_eq!(eccentricity, 0.0, abs <= 1e-3) { - semi_major - } else { - semi_major * (1.0 - eccentricity.powi(2)) - } -} - -type Keplerian = (f64, f64, f64, f64, f64, f64); - -pub fn cartesian_to_keplerian(grav_param: f64, pos: DVec3, vel: DVec3) -> Keplerian { - let r = pos.length(); - let v = vel.length(); - let h = pos.cross(vel); - let hm = h.length(); - let node = DVec3::Z.cross(h); - let e = eccentricity_vector(grav_param, pos, vel); - let eccentricity = e.length(); - let inclination = h.angle_between(DVec3::Z); - - let equatorial = is_equatorial(inclination); - let circular = is_circular(eccentricity); - - let semi_major = if circular { - hm.powi(2) / grav_param - } else { - -grav_param / (2.0 * (v.powi(2) / 2.0 - grav_param / r)) - }; - - let ascending_node; - let periapsis_arg; - let true_anomaly; - if equatorial && !circular { - ascending_node = 0.0; - periapsis_arg = azimuth(e); - true_anomaly = (h.dot(e.cross(pos)) / hm).atan2(pos.dot(e)); - } else if !equatorial && circular { - ascending_node = azimuth(node); - periapsis_arg = 0.0; - true_anomaly = (pos.dot(h.cross(node)) / hm).atan2(pos.dot(node)); - } else if equatorial && circular { - ascending_node = 0.0; - periapsis_arg = 0.0; - true_anomaly = azimuth(pos); - } else { - if semi_major > 0.0 { - let e_se = pos.dot(vel) / (grav_param * semi_major).sqrt(); - let e_ce = (r * v.powi(2)) / grav_param - 1.0; - true_anomaly = eccentric_to_true(e_se.atan2(e_ce), eccentricity); - } else { - let e_sh = pos.dot(vel) / (-grav_param * semi_major).sqrt(); - let e_ch = (r * v.powi(2)) / grav_param - 1.0; - true_anomaly = - hyperbolic_to_true(((e_ch + e_sh) / (e_ch - e_sh)).ln() / 2.0, eccentricity); - } - ascending_node = azimuth(node); - let px = pos.dot(node); - let py = pos.dot(h.cross(node)) / hm; - periapsis_arg = py.atan2(px) - true_anomaly; - } - - ( - semi_major, - eccentricity, - inclination, - mod_two_pi(ascending_node), - mod_two_pi(periapsis_arg), - normalize_two_pi(true_anomaly, 0.0), - ) -} - -fn mod_two_pi(a: f64) -> f64 { - let w = a % (2.0 * PI); - if w < 0.0 { - w + 2.0 * PI - } else { - w - } -} - -fn hyperbolic_to_true(e: f64, ecc: f64) -> f64 { - 2.0 * (((1.0 + ecc) / (ecc - 1.0)).sqrt() * (e / 2.0).tanh()).atan() -} - -fn eccentric_to_true(e: f64, ecc: f64) -> f64 { - 2.0 * (((1.0 + ecc) / (1.0 - ecc)).sqrt() * (e / 2.0).tan()).atan() -} - -fn azimuth(v: DVec3) -> f64 { - v.y.atan2(v.x) -} - -fn eccentricity_vector(grav_param: f64, pos: DVec3, vel: DVec3) -> DVec3 { - (pos * (vel.dot(vel) - grav_param / pos.length()) - vel * pos.dot(vel)) / grav_param -} - -fn is_equatorial(inclination: f64) -> bool { - float_eq!(inclination.abs(), 0.0, abs <= 1e-3) -} - -fn is_circular(eccentricity: f64) -> bool { - float_eq!(eccentricity, 0.0, abs <= 1e-3) -} - -#[cfg(test)] -mod tests { - use super::*; - use float_eq::assert_float_eq; - use glam::DVec3; - - #[test] - fn test_perifocal() { - let semi_latus = 1.13880762905224e7; - let eccentricity = 0.7311; - let true_anomaly = 0.44369564302687126; - let grav_param = 3.9860047e14; - - let pos = DVec3::new(6194863.12535486, 2944437.90016286, 0.0); - let vel = DVec3::new(-2539.71254827, 9668.69568539, 0.0); - let (pos1, vel1) = - keplerian_to_perifocal(semi_latus, eccentricity, true_anomaly, grav_param); - - assert_float_eq!(pos.x, pos1.x, rel <= 1e-8); - assert_float_eq!(pos.y, pos1.y, rel <= 1e-8); - assert_float_eq!(pos.z, pos1.z, rel <= 1e-8); - assert_float_eq!(vel.x, vel1.x, rel <= 1e-8); - assert_float_eq!(vel.y, vel1.y, rel <= 1e-8); - assert_float_eq!(vel.z, vel1.z, rel <= 1e-8); - } - - #[test] - fn test_elliptic() { - let grav_param = 3.9860047e14; - let semi_major = 24464560.0; - let eccentricity = 0.7311; - let inclination = 0.122138; - let ascending_node = 1.00681; - let periapsis_arg = 3.10686; - let true_anomaly = 0.44369564302687126; - let pos = DVec3::new( - -0.107622532467967e7, - -0.676589636432773e7, - -0.332308783350379e6, - ); - let vel = DVec3::new( - 0.935685775154103e4, - -0.331234775037644e4, - -0.118801577532701e4, - ); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - let (pos1, vel1) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - assert_float_eq!(pos.x, pos1.x, rel <= 1e-8); - assert_float_eq!(pos.y, pos1.y, rel <= 1e-8); - assert_float_eq!(pos.z, pos1.z, rel <= 1e-8); - assert_float_eq!(vel.x, vel1.x, rel <= 1e-8); - assert_float_eq!(vel.y, vel1.y, rel <= 1e-8); - assert_float_eq!(vel.z, vel1.z, rel <= 1e-8); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, rel <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } - - #[test] - fn test_circular() { - let grav_param = 3.986004418e14; - let semi_major = 6778136.6; - let eccentricity = 0.0; - let inclination = 15f64.to_radians(); - let ascending_node = 20f64.to_radians(); - let periapsis_arg = 0.0; - let true_anomaly = 30f64.to_radians(); - let pos = DVec3::new(4396398.60746266, 5083838.45333733, 877155.42119322); - let vel = DVec3::new(-5797.06004014, 4716.60916063, 1718.86034246); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - let (pos1, vel1) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - assert_float_eq!(pos.x, pos1.x, rel <= 1e-8); - assert_float_eq!(pos.y, pos1.y, rel <= 1e-8); - assert_float_eq!(pos.z, pos1.z, rel <= 1e-8); - assert_float_eq!(vel.x, vel1.x, rel <= 1e-4); - assert_float_eq!(vel.y, vel1.y, rel <= 1e-4); - assert_float_eq!(vel.z, vel1.z, rel <= 1e-4); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, abs <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } - - #[test] - fn test_circular_orekit() { - let grav_param = 3.9860047e14; - let semi_major = 24464560.0; - let eccentricity = 0.0; - let inclination = 0.122138; - let ascending_node = 1.00681; - let periapsis_arg = 0.0; - let true_anomaly = 0.048363; - - let (pos, vel) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, abs <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } - - #[test] - fn test_hyperbolic_orekit() { - let grav_param = 3.9860047e14; - let semi_major = -24464560.0; - let eccentricity = 1.7311; - let inclination = 0.122138; - let ascending_node = 1.00681; - let periapsis_arg = 3.10686; - let true_anomaly = 0.12741601769795755; - - let (pos, vel) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, abs <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } - - #[test] - fn test_equatorial() { - let grav_param = 3.9860047e14; - let semi_major = 24464560.0; - let eccentricity = 0.7311; - let inclination = 0.0; - let ascending_node = 0.0; - let periapsis_arg = 3.10686; - let true_anomaly = 0.44369564302687126; - - let (pos, vel) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, abs <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } - - #[test] - fn test_circular_equatorial() { - let grav_param = 3.9860047e14; - let semi_major = 24464560.0; - let eccentricity = 0.0; - let inclination = 0.0; - let ascending_node = 0.0; - let periapsis_arg = 0.0; - let true_anomaly = 0.44369564302687126; - - let (pos, vel) = keplerian_to_cartesian( - grav_param, - semi_major, - eccentricity, - inclination, - ascending_node, - periapsis_arg, - true_anomaly, - ); - - let ( - semi_major1, - eccentricity1, - inclination1, - ascending_node1, - periapsis_arg1, - true_anomaly1, - ) = cartesian_to_keplerian(grav_param, pos, vel); - - assert_float_eq!(semi_major, semi_major1, rel <= 1e-8); - assert_float_eq!(eccentricity, eccentricity1, abs <= 1e-8); - assert_float_eq!(inclination, inclination1, rel <= 1e-8); - assert_float_eq!(ascending_node, ascending_node1, rel <= 1e-8); - assert_float_eq!(periapsis_arg, periapsis_arg1, rel <= 1e-8); - assert_float_eq!(true_anomaly, true_anomaly1, rel <= 1e-8); - } -} diff --git a/crates/lox_py/Cargo.toml b/crates/lox_py/Cargo.toml index 87d8ab13..007dbd6f 100644 --- a/crates/lox_py/Cargo.toml +++ b/crates/lox_py/Cargo.toml @@ -12,6 +12,11 @@ name = "lox_space" crate-type = ["cdylib"] [dependencies] -pyo3 = "0.20.0" +pyo3 = "0.20.2" lox_core.workspace = true +float_eq.workspace = true thiserror.workspace = true + +[dev-dependencies] +pyo3 = { version = "0.20.2", features = ["auto-initialize"] } +rstest.workspace = true diff --git a/crates/lox_py/lox_space.pyi b/crates/lox_py/lox_space.pyi index 135b64e2..cc6386a8 100644 --- a/crates/lox_py/lox_space.pyi +++ b/crates/lox_py/lox_space.pyi @@ -1,72 +1,105 @@ +class Epoch: + def __init__( + self, + scale: str, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + milli: int = 0, + micro: int = 0, + nano: int = 0, + pico: int = 0, + femto: int = 0, + atto: int = 0, + ): ... + class Sun: def __init__(self): ... - def id(self) -> int: ... - def name(self) -> str: ... - def gravitational_parameter(self) -> float: ... - def mean_radius(self) -> float: ... - def polar_radius(self) -> float: ... - def equatorial_radius(self) -> float: ... class Barycenter: def __init__(self, name: str): ... - def id(self) -> int: ... - def name(self) -> str: ... - def gravitational_parameter(self) -> float: ... class Planet: def __init__(self, name: str): ... - def id(self) -> int: ... - def name(self) -> str: ... - def gravitational_parameter(self) -> float: ... - def mean_radius(self) -> float: ... - def polar_radius(self) -> float: ... - def equatorial_radius(self) -> float: ... class Satellite: def __init__(self, name: str): ... - def id(self) -> int: ... - def name(self) -> str: ... - def gravitational_parameter(self) -> float: ... - def mean_radius(self) -> float: ... - def polar_radius(self) -> float: ... - def subplanetary_radius(self) -> float: ... - def along_orbit_radius(self) -> float: ... class MinorBody: def __init__(self, name: str): ... - def id(self) -> int: ... - def name(self) -> str: ... - def gravitational_parameter(self) -> float: ... - def mean_radius(self) -> float: ... - def polar_radius(self) -> float: ... - def subplanetary_radius(self) -> float: ... - def along_orbit_radius(self) -> float: ... + +class Cartesian: + def __init__( + self, + time: Epoch, + body: Sun | Barycenter | Planet | Satellite | MinorBody, + frame: str, + x: float, + y: float, + z: float, + vx: float, + vy: float, + vz: float, + ): ... + def time(self) -> Epoch: ... + def reference_frame(self) -> str: ... + def origin(self) -> Sun | Barycenter | Planet | Satellite | MinorBody: ... + def position(self) -> tuple[float, float, float]: ... + def velocity(self) -> tuple[float, float, float]: ... + def to_keplerian(self) -> Keplerian: ... + +class Keplerian: + def __init__( + self, + time: Epoch, + body: Sun | Barycenter | Planet | Satellite | MinorBody, + frame: str, + semi_major_axis: float, + eccentricity: float, + inclination: float, + ascending_node: float, + periapsis_argument: float, + true_anomaly: float, + ): ... + def time(self) -> Epoch: ... + def reference_frame(self) -> str: ... + def origin(self) -> Sun | Barycenter | Planet | Satellite | MinorBody: ... + def semi_major_axis(self) -> float: ... + def eccentricity(self) -> float: ... + def inclination(self) -> float: ... + def ascending_node(self) -> float: ... + def periapsis_argument(self) -> float: ... + def true_anomaly(self) -> float: ... + def to_cartesian(self) -> Cartesian: ... diff --git a/crates/lox_py/pyproject.toml b/crates/lox_py/pyproject.toml index dfaec801..698a7e54 100644 --- a/crates/lox_py/pyproject.toml +++ b/crates/lox_py/pyproject.toml @@ -11,6 +11,15 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version"] +dependencies = [ + "numpy~=1.26" +] + +[project.optional-dependencies] +dev = [ + "black", + "pytest", +] [tool.maturin] features = ["pyo3/extension-module"] diff --git a/crates/lox_py/src/bodies.rs b/crates/lox_py/src/bodies.rs index 01fae1a8..3ae8cee1 100644 --- a/crates/lox_py/src/bodies.rs +++ b/crates/lox_py/src/bodies.rs @@ -6,6 +6,7 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use lox_core::bodies::*; @@ -13,56 +14,58 @@ use lox_core::bodies::*; use crate::LoxPyError; #[pyclass(name = "Sun")] +#[derive(Clone, Default)] pub struct PySun; #[pymethods] impl PySun { #[new] - fn new() -> Self { + pub fn new() -> Self { Self } - fn __repr__(&self) -> PyResult { - Ok("Sun()".to_string()) + fn __repr__(&self) -> &str { + "Sun()" } - fn __str__(&self) -> PyResult { - Ok("Sun".to_string()) + fn __str__(&self) -> &str { + "Sun" } - fn id(&self) -> i32 { + pub fn id(&self) -> i32 { Sun.id().0 } - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { Sun.name() } - fn gravitational_parameter(&self) -> f64 { + pub fn gravitational_parameter(&self) -> f64 { Sun.gravitational_parameter() } - fn mean_radius(&self) -> f64 { + pub fn mean_radius(&self) -> f64 { Sun.mean_radius() } - fn polar_radius(&self) -> f64 { + pub fn polar_radius(&self) -> f64 { Sun.polar_radius() } - fn equatorial_radius(&self) -> f64 { + pub fn equatorial_radius(&self) -> f64 { Sun.equatorial_radius() } } #[pyclass(name = "Barycenter")] -pub struct PyBarycenter(Box); +#[derive(Clone)] +pub struct PyBarycenter(Box); #[pymethods] impl PyBarycenter { #[new] - fn new(name: &str) -> Result { - let barycenter: Option> = match name { + pub fn new(name: &str) -> Result { + let barycenter: Option> = match name { "ssb" | "SSB" | "solar system barycenter" | "Solar System Barycenter" => { Some(Box::new(SolarSystemBarycenter)) } @@ -83,34 +86,35 @@ impl PyBarycenter { } } - fn __repr__(&self) -> PyResult { - Ok(format!("Barycenter(\"{}\")", self.name())) + fn __repr__(&self) -> String { + format!("Barycenter(\"{}\")", self.name()) } - fn __str__(&self) -> PyResult { - Ok(self.name().to_string()) + fn __str__(&self) -> &str { + self.name() } - fn id(&self) -> i32 { + pub fn id(&self) -> i32 { self.0.id().0 } - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { self.0.name() } - fn gravitational_parameter(&self) -> f64 { + pub fn gravitational_parameter(&self) -> f64 { self.0.gravitational_parameter() } } #[pyclass(name = "Planet")] +#[derive(Clone)] pub struct PyPlanet(Box); #[pymethods] impl PyPlanet { #[new] - fn new(name: &str) -> Result { + pub fn new(name: &str) -> Result { let planet: Option> = match name { "mercury" | "Mercury" => Some(Box::new(Mercury)), "venus" | "Venus" => Some(Box::new(Venus)), @@ -129,46 +133,47 @@ impl PyPlanet { } } - fn __repr__(&self) -> PyResult { - Ok(format!("Planet(\"{}\")", self.name())) + fn __repr__(&self) -> String { + format!("Planet(\"{}\")", self.name()) } - fn __str__(&self) -> PyResult { - Ok(self.name().to_string()) + fn __str__(&self) -> &str { + self.name() } - fn id(&self) -> i32 { + pub fn id(&self) -> i32 { self.0.id().0 } - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { self.0.name() } - fn gravitational_parameter(&self) -> f64 { + pub fn gravitational_parameter(&self) -> f64 { self.0.gravitational_parameter() } - fn mean_radius(&self) -> f64 { + pub fn mean_radius(&self) -> f64 { self.0.mean_radius() } - fn polar_radius(&self) -> f64 { + pub fn polar_radius(&self) -> f64 { self.0.polar_radius() } - fn equatorial_radius(&self) -> f64 { + pub fn equatorial_radius(&self) -> f64 { self.0.equatorial_radius() } } #[pyclass(name = "Satellite")] +#[derive(Clone)] pub struct PySatellite(Box); #[pymethods] impl PySatellite { #[new] - fn new(name: &str) -> Result { + pub fn new(name: &str) -> Result { let satellite: Option> = match name { "moon" | "Moon" | "luna" | "Luna" => Some(Box::new(Moon)), "phobos" | "Phobos" => Some(Box::new(Phobos)), @@ -218,50 +223,51 @@ impl PySatellite { } } - fn __repr__(&self) -> PyResult { - Ok(format!("Satellite(\"{}\")", self.name())) + fn __repr__(&self) -> String { + format!("Satellite(\"{}\")", self.name()) } - fn __str__(&self) -> PyResult { - Ok(self.name().to_string()) + fn __str__(&self) -> &str { + self.name() } - fn id(&self) -> i32 { + pub fn id(&self) -> i32 { self.0.id().0 } - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { self.0.name() } - fn gravitational_parameter(&self) -> f64 { + pub fn gravitational_parameter(&self) -> f64 { self.0.gravitational_parameter() } - fn mean_radius(&self) -> f64 { + pub fn mean_radius(&self) -> f64 { self.0.mean_radius() } - fn polar_radius(&self) -> f64 { + pub fn polar_radius(&self) -> f64 { self.0.polar_radius() } - fn subplanetary_radius(&self) -> f64 { + pub fn subplanetary_radius(&self) -> f64 { self.0.subplanetary_radius() } - fn along_orbit_radius(&self) -> f64 { + pub fn along_orbit_radius(&self) -> f64 { self.0.along_orbit_radius() } } #[pyclass(name = "MinorBody")] +#[derive(Clone)] pub struct PyMinorBody(Box); #[pymethods] impl PyMinorBody { #[new] - fn new(name: &str) -> Result { + pub fn new(name: &str) -> Result { let minor: Option> = match name { "ceres" | "Ceres" => Some(Box::new(Ceres)), "vesta" | "Vesta" => Some(Box::new(Vesta)), @@ -276,39 +282,402 @@ impl PyMinorBody { } } - fn __repr__(&self) -> PyResult { - Ok(format!("MinorBody(\"{}\")", self.name())) + fn __repr__(&self) -> String { + format!("MinorBody(\"{}\")", self.name()) } - fn __str__(&self) -> PyResult { - Ok(self.name().to_string()) + fn __str__(&self) -> &str { + self.name() } - fn id(&self) -> i32 { + pub fn id(&self) -> i32 { self.0.id().0 } - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { self.0.name() } - fn gravitational_parameter(&self) -> f64 { + pub fn gravitational_parameter(&self) -> f64 { self.0.gravitational_parameter() } - fn mean_radius(&self) -> f64 { + pub fn mean_radius(&self) -> f64 { self.0.mean_radius() } - fn polar_radius(&self) -> f64 { + pub fn polar_radius(&self) -> f64 { self.0.polar_radius() } - fn subplanetary_radius(&self) -> f64 { + pub fn subplanetary_radius(&self) -> f64 { self.0.subplanetary_radius() } - fn along_orbit_radius(&self) -> f64 { + pub fn along_orbit_radius(&self) -> f64 { self.0.along_orbit_radius() } } + +#[derive(Clone)] +pub enum PyBody { + Barycenter(PyBarycenter), + Sun(PySun), + Planet(PyPlanet), + Satellite(PySatellite), + MinorBody(PyMinorBody), +} + +impl From for PyObject { + fn from(body: PyBody) -> Self { + Python::with_gil(|py| match body { + PyBody::Barycenter(barycenter) => barycenter.clone().into_py(py), + PyBody::Sun(sun) => sun.clone().into_py(py), + PyBody::Planet(planet) => planet.clone().into_py(py), + PyBody::Satellite(satellite) => satellite.clone().into_py(py), + PyBody::MinorBody(minor_body) => minor_body.clone().into_py(py), + }) + } +} + +impl TryFrom for PyBody { + type Error = PyErr; + + fn try_from(body: PyObject) -> Result { + Python::with_gil(|py| { + if let Ok(body) = body.extract::(py) { + Ok(PyBody::Barycenter(body)) + } else if let Ok(body) = body.extract::(py) { + Ok(PyBody::Sun(body)) + } else if let Ok(body) = body.extract::(py) { + Ok(PyBody::Planet(body)) + } else if let Ok(body) = body.extract::(py) { + Ok(PyBody::Satellite(body)) + } else if let Ok(body) = body.extract::(py) { + Ok(PyBody::MinorBody(body)) + } else { + Err(PyValueError::new_err("Invalid body")) + } + }) + } +} + +impl Body for PyBody { + fn id(&self) -> NaifId { + match &self { + PyBody::Barycenter(barycenter) => NaifId(barycenter.id()), + PyBody::Sun(sun) => NaifId(sun.id()), + PyBody::Planet(planet) => NaifId(planet.id()), + PyBody::Satellite(satellite) => NaifId(satellite.id()), + PyBody::MinorBody(minor_body) => NaifId(minor_body.id()), + } + } + + fn name(&self) -> &'static str { + match &self { + PyBody::Barycenter(barycenter) => barycenter.name(), + PyBody::Sun(sun) => sun.name(), + PyBody::Planet(planet) => planet.name(), + PyBody::Satellite(satellite) => satellite.name(), + PyBody::MinorBody(minor_body) => minor_body.name(), + } + } +} + +impl PointMass for PyBody { + fn gravitational_parameter(&self) -> f64 { + match &self { + PyBody::Barycenter(barycenter) => barycenter.gravitational_parameter(), + PyBody::Sun(sun) => sun.gravitational_parameter(), + PyBody::Planet(planet) => planet.gravitational_parameter(), + PyBody::Satellite(satellite) => satellite.gravitational_parameter(), + PyBody::MinorBody(minor_body) => minor_body.gravitational_parameter(), + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[test] + fn test_sun() { + let sun = PySun::new(); + assert_eq!(sun.__repr__(), "Sun()"); + assert_eq!(sun.__str__(), "Sun"); + assert_eq!(sun.id(), Sun.id().0); + assert_eq!(sun.name(), Sun.name()); + assert_eq!(sun.gravitational_parameter(), Sun.gravitational_parameter()); + assert_eq!(sun.mean_radius(), Sun.mean_radius()); + assert_eq!(sun.polar_radius(), Sun.polar_radius()); + assert_eq!(sun.equatorial_radius(), Sun.equatorial_radius()); + } + + #[rstest] + #[case("Solar System Barycenter", SolarSystemBarycenter)] + #[case("SSB", SolarSystemBarycenter)] + #[case("Mercury Barycenter", MercuryBarycenter)] + #[case("Venus Barycenter", VenusBarycenter)] + #[case("Earth Barycenter", EarthBarycenter)] + #[case("Mars Barycenter", MarsBarycenter)] + #[case("Jupiter Barycenter", JupiterBarycenter)] + #[case("Saturn Barycenter", SaturnBarycenter)] + #[case("Uranus Barycenter", UranusBarycenter)] + #[case("Neptune Barycenter", NeptuneBarycenter)] + #[case("Pluto Barycenter", PlutoBarycenter)] + fn test_barycenter(#[case] name: &str, #[case] barycenter: impl Barycenter) { + let py_barycenter = PyBarycenter::new(name).expect("barycenter should be valid"); + assert_eq!( + py_barycenter.__repr__(), + format!("Barycenter(\"{}\")", barycenter.name()) + ); + assert_eq!(py_barycenter.__str__(), barycenter.name()); + assert_eq!(py_barycenter.name(), barycenter.name()); + let py_barycenter = + PyBarycenter::new(&name.to_lowercase()).expect("barycenter should be valid"); + assert_eq!( + py_barycenter.__repr__(), + format!("Barycenter(\"{}\")", barycenter.name()) + ); + assert_eq!(py_barycenter.__str__(), barycenter.name()); + assert_eq!(py_barycenter.name(), barycenter.name()); + assert_eq!(py_barycenter.id(), barycenter.id().0); + assert_eq!( + py_barycenter.gravitational_parameter(), + barycenter.gravitational_parameter() + ); + } + + #[test] + fn test_invalid_barycenter() { + let barycenter = PyBarycenter::new("Rupert Barycenter"); + assert!(barycenter.is_err()); + } + + #[rstest] + #[case("Mercury", Mercury)] + #[case("Venus", Venus)] + #[case("Earth", Earth)] + #[case("Mars", Mars)] + #[case("Jupiter", Jupiter)] + #[case("Saturn", Saturn)] + #[case("Uranus", Uranus)] + #[case("Neptune", Neptune)] + #[case("Pluto", Pluto)] + fn test_planet(#[case] name: &str, #[case] planet: impl Planet) { + let py_planet = PyPlanet::new(name).expect("planet should be valid"); + assert_eq!(py_planet.__repr__(), format!("Planet(\"{}\")", name)); + assert_eq!(py_planet.__str__(), name); + assert_eq!(py_planet.name(), name); + let py_planet = PyPlanet::new(&name.to_lowercase()).expect("planet should be valid"); + assert_eq!(py_planet.__repr__(), format!("Planet(\"{}\")", name)); + assert_eq!(py_planet.__str__(), name); + assert_eq!(py_planet.name(), name); + assert_eq!(py_planet.id(), planet.id().0); + assert_eq!( + py_planet.gravitational_parameter(), + planet.gravitational_parameter() + ); + assert_eq!(py_planet.mean_radius(), planet.mean_radius()); + assert_eq!(py_planet.polar_radius(), planet.polar_radius()); + assert_eq!(py_planet.equatorial_radius(), planet.equatorial_radius()); + } + + #[test] + fn test_invalid_planet() { + let planet = PyPlanet::new("Rupert"); + assert!(planet.is_err()); + } + + #[rstest] + #[case("Moon", Moon)] + #[case("Luna", Moon)] + #[case("Phobos", Phobos)] + #[case("Deimos", Deimos)] + #[case("Io", Io)] + #[case("Europa", Europa)] + #[case("Ganymede", Ganymede)] + #[case("Callisto", Callisto)] + #[case("Amalthea", Amalthea)] + #[case("Himalia", Himalia)] + #[case("Thebe", Thebe)] + #[case("Adrastea", Adrastea)] + #[case("Metis", Metis)] + #[case("Mimas", Mimas)] + #[case("Enceladus", Enceladus)] + #[case("Tethys", Tethys)] + #[case("Dione", Dione)] + #[case("Rhea", Rhea)] + #[case("Titan", Titan)] + #[case("Hyperion", Hyperion)] + #[case("Iapetus", Iapetus)] + #[case("Phoebe", Phoebe)] + #[case("Janus", Janus)] + #[case("Epimetheus", Epimetheus)] + #[case("Helene", Helene)] + #[case("Atlas", Atlas)] + #[case("Prometheus", Prometheus)] + #[case("Pandora", Pandora)] + #[case("Ariel", Ariel)] + #[case("Umbriel", Umbriel)] + #[case("Titania", Titania)] + #[case("Oberon", Oberon)] + #[case("Miranda", Miranda)] + #[case("Triton", Triton)] + #[case("Naiad", Naiad)] + #[case("Thalassa", Thalassa)] + #[case("Despina", Despina)] + #[case("Galatea", Galatea)] + #[case("Larissa", Larissa)] + #[case("Proteus", Proteus)] + #[case("Charon", Charon)] + fn test_satellite(#[case] name: &str, #[case] satellite: impl Satellite) { + let py_satellite = PySatellite::new(name).expect("satellite should be valid"); + assert_eq!( + py_satellite.__repr__(), + format!("Satellite(\"{}\")", satellite.name()) + ); + assert_eq!(py_satellite.__str__(), satellite.name()); + assert_eq!(py_satellite.name(), satellite.name()); + let py_satellite = + PySatellite::new(&name.to_lowercase()).expect("satellite should be valid"); + assert_eq!( + py_satellite.__repr__(), + format!("Satellite(\"{}\")", satellite.name()) + ); + assert_eq!(py_satellite.__str__(), satellite.name()); + assert_eq!(py_satellite.name(), satellite.name()); + assert_eq!(py_satellite.id(), satellite.id().0); + assert_eq!( + py_satellite.gravitational_parameter(), + satellite.gravitational_parameter() + ); + assert_eq!(py_satellite.mean_radius(), satellite.mean_radius()); + assert_eq!(py_satellite.polar_radius(), satellite.polar_radius()); + assert_eq!( + py_satellite.subplanetary_radius(), + satellite.subplanetary_radius() + ); + assert_eq!( + py_satellite.along_orbit_radius(), + satellite.along_orbit_radius() + ); + } + + #[test] + fn test_invalid_satellite() { + let satellite = PySatellite::new("Endor"); + assert!(satellite.is_err()); + } + + #[rstest] + #[case("Ceres", Ceres)] + #[case("Vesta", Vesta)] + #[case("Psyche", Psyche)] + #[case("Eros", Eros)] + #[case("Davida", Davida)] + fn test_minor_body(#[case] name: &str, #[case] minor_body: impl MinorBody) { + let py_minor_body = PyMinorBody::new(name).expect("minor body should be valid"); + assert_eq!( + py_minor_body.__repr__(), + format!("MinorBody(\"{}\")", minor_body.name()) + ); + assert_eq!(py_minor_body.__str__(), minor_body.name()); + assert_eq!(py_minor_body.name(), minor_body.name()); + let py_minor_body = + PyMinorBody::new(&name.to_lowercase()).expect("minor body should be valid"); + assert_eq!( + py_minor_body.__repr__(), + format!("MinorBody(\"{}\")", minor_body.name()) + ); + assert_eq!(py_minor_body.__str__(), minor_body.name()); + assert_eq!(py_minor_body.name(), minor_body.name()); + assert_eq!(py_minor_body.id(), minor_body.id().0); + assert_eq!( + py_minor_body.gravitational_parameter(), + minor_body.gravitational_parameter() + ); + assert_eq!(py_minor_body.mean_radius(), minor_body.mean_radius()); + assert_eq!(py_minor_body.polar_radius(), minor_body.polar_radius()); + assert_eq!( + py_minor_body.subplanetary_radius(), + minor_body.subplanetary_radius() + ); + assert_eq!( + py_minor_body.along_orbit_radius(), + minor_body.along_orbit_radius() + ); + } + + #[test] + fn test_invalid_minor_body() { + let minor_body = PyMinorBody::new("Bielefeld"); + assert!(minor_body.is_err()); + } + + #[test] + fn test_body() { + let sun = PyBody::Sun(PySun::new()); + assert_eq!(sun.id(), Sun.id()); + assert_eq!(sun.name(), Sun.name()); + assert_eq!(sun.gravitational_parameter(), Sun.gravitational_parameter()); + let sun1: PyBody = PyObject::from(sun.clone()) + .try_into() + .expect("sun is valid"); + assert_eq!(sun1.id(), sun.id()); + + let barycenter = PyBody::Barycenter(PyBarycenter::new("ssb").expect("barycenter is valid")); + assert_eq!(barycenter.id(), SolarSystemBarycenter.id()); + assert_eq!(barycenter.name(), SolarSystemBarycenter.name()); + assert_eq!( + barycenter.gravitational_parameter(), + SolarSystemBarycenter.gravitational_parameter() + ); + let barycenter1: PyBody = PyObject::from(barycenter.clone()) + .try_into() + .expect("barycenter is valid"); + assert_eq!(barycenter1.id(), barycenter.id()); + + let planet = PyBody::Planet(PyPlanet::new("earth").expect("planet is valid")); + assert_eq!(planet.id(), Earth.id()); + assert_eq!(planet.name(), Earth.name()); + assert_eq!( + planet.gravitational_parameter(), + Earth.gravitational_parameter() + ); + let planet1: PyBody = PyObject::from(planet.clone()) + .try_into() + .expect("planet is valid"); + assert_eq!(planet1.id(), planet.id()); + + let satellite = PyBody::Satellite(PySatellite::new("moon").expect("satellite is valid")); + assert_eq!(satellite.id(), Moon.id()); + assert_eq!(satellite.name(), Moon.name()); + assert_eq!( + satellite.gravitational_parameter(), + Moon.gravitational_parameter() + ); + let satellite1: PyBody = PyObject::from(satellite.clone()) + .try_into() + .expect("satellite is valid"); + assert_eq!(satellite1.id(), satellite.id()); + + let minor_body = PyBody::MinorBody(PyMinorBody::new("ceres").expect("minor body is valid")); + assert_eq!(minor_body.id(), Ceres.id()); + assert_eq!(minor_body.name(), Ceres.name()); + assert_eq!( + minor_body.gravitational_parameter(), + Ceres.gravitational_parameter() + ); + let minor_body1: PyBody = PyObject::from(minor_body.clone()) + .try_into() + .expect("minor_body is valid"); + assert_eq!(minor_body1.id(), minor_body.id()); + + let obj = Python::with_gil(|py| 1.into_py(py)); + let body = PyBody::try_from(obj); + assert!(body.is_err()); + } +} diff --git a/crates/lox_py/src/coords.rs b/crates/lox_py/src/coords.rs new file mode 100644 index 00000000..a0928c36 --- /dev/null +++ b/crates/lox_py/src/coords.rs @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::str::FromStr; + +use pyo3::prelude::*; + +use lox_core::bodies::PointMass; +use lox_core::coords::states::{CartesianState, KeplerianState, TwoBodyState}; +use lox_core::coords::DVec3; + +use crate::bodies::PyBody; +use crate::frames::PyFrame; +use crate::time::PyEpoch; + +#[pyclass(name = "Cartesian")] +pub struct PyCartesian { + state: CartesianState, + origin: PyBody, + frame: PyFrame, +} + +#[pymethods] +impl PyCartesian { + #[allow(clippy::too_many_arguments)] + #[new] + fn new( + time: &PyEpoch, + body: PyObject, + frame: &str, + x: f64, + y: f64, + z: f64, + vx: f64, + vy: f64, + vz: f64, + ) -> PyResult { + let origin: PyBody = body.try_into()?; + let frame = PyFrame::from_str(frame)?; + let state = CartesianState::new(time.0, DVec3::new(x, y, z), DVec3::new(vx, vy, vz)); + Ok(Self { + state, + origin, + frame, + }) + } + + fn time(&self) -> PyEpoch { + PyEpoch(self.state.time()) + } + + fn reference_frame(&self) -> String { + format!("{}", self.frame) + } + + fn origin(&self) -> PyObject { + self.origin.clone().into() + } + + fn position(&self) -> (f64, f64, f64) { + let position = self.state.position(); + (position.x, position.y, position.z) + } + + fn velocity(&self) -> (f64, f64, f64) { + let velocity = self.state.velocity(); + (velocity.x, velocity.y, velocity.z) + } + + fn to_keplerian(&self) -> PyKeplerian { + let mu = self.origin.gravitational_parameter(); + let state = self.state.to_keplerian_state(mu); + PyKeplerian { + origin: self.origin.clone(), + frame: self.frame.clone(), + state, + } + } +} + +#[pyclass(name = "Keplerian")] +pub struct PyKeplerian { + state: KeplerianState, + origin: PyBody, + frame: PyFrame, +} + +#[pymethods] +impl PyKeplerian { + #[new] + #[allow(clippy::too_many_arguments)] + fn new( + t: &PyEpoch, + body: PyObject, + frame: &str, + semi_major_axis: f64, + eccentricity: f64, + inclination: f64, + ascending_node: f64, + periapsis_argument: f64, + true_anomaly: f64, + ) -> PyResult { + let origin: PyBody = body.try_into()?; + let frame = PyFrame::from_str(frame)?; + let state = KeplerianState::new( + t.0, + semi_major_axis, + eccentricity, + inclination, + ascending_node, + periapsis_argument, + true_anomaly, + ); + Ok(Self { + state, + origin, + frame, + }) + } + + fn time(&self) -> PyEpoch { + PyEpoch(self.state.time()) + } + + fn reference_frame(&self) -> String { + format!("{}", self.frame) + } + + fn origin(&self) -> PyObject { + self.origin.clone().into() + } + + fn semi_major_axis(&self) -> f64 { + self.state.semi_major_axis() + } + + fn eccentricity(&self) -> f64 { + self.state.eccentricity() + } + + fn inclination(&self) -> f64 { + self.state.inclination() + } + + fn ascending_node(&self) -> f64 { + self.state.ascending_node() + } + + fn periapsis_argument(&self) -> f64 { + self.state.periapsis_argument() + } + + fn true_anomaly(&self) -> f64 { + self.state.true_anomaly() + } + + fn to_cartesian(&self) -> PyCartesian { + let mu = self.origin.gravitational_parameter(); + let state = self.state.to_cartesian_state(mu); + PyCartesian { + state, + origin: self.origin.clone(), + frame: self.frame.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use float_eq::assert_float_eq; + + use crate::bodies::PyPlanet; + + use super::*; + + #[test] + fn test_cartesian() { + let epoch = PyEpoch::new( + "TDB", + 2023, + 3, + 25, + Some(21), + Some(8), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + ) + .expect("time should be valid"); + let body = Python::with_gil(|py| { + PyPlanet::new("Earth") + .expect("body should be valid") + .into_py(py) + }); + let pos = DVec3::new( + -0.107622532467967e7, + -0.676589636432773e7, + -0.332308783350379e6, + ) * 1e-3; + let vel = DVec3::new( + 0.935685775154103e4, + -0.331234775037644e4, + -0.118801577532701e4, + ) * 1e-3; + + let cartesian = PyCartesian::new( + &epoch, + body.clone(), + "ICRF", + pos.x, + pos.y, + pos.z, + vel.x, + vel.y, + vel.z, + ) + .expect("cartesian state should be valid"); + let cartesian1 = cartesian.to_keplerian().to_cartesian(); + + let origin = + Python::with_gil(|py| body.extract::(py)).expect("origin should be a planet"); + let origin1 = Python::with_gil(|py| cartesian1.origin().extract::(py)) + .expect("origin should be a planet"); + assert_eq!(cartesian1.time(), epoch); + assert_eq!(origin1.name(), origin.name()); + assert_eq!(cartesian1.reference_frame(), "ICRF"); + + assert_float_eq!(cartesian.position().0, cartesian1.position().0, rel <= 1e-8); + assert_float_eq!(cartesian.position().1, cartesian1.position().1, rel <= 1e-8); + assert_float_eq!(cartesian.position().2, cartesian1.position().2, rel <= 1e-8); + assert_float_eq!(cartesian.velocity().0, cartesian1.velocity().0, rel <= 1e-6); + assert_float_eq!(cartesian.velocity().1, cartesian1.velocity().1, rel <= 1e-6); + assert_float_eq!(cartesian.velocity().2, cartesian1.velocity().2, rel <= 1e-6); + } + + #[test] + fn test_keplerian() { + let epoch = PyEpoch::new( + "TDB", + 2023, + 3, + 25, + Some(21), + Some(8), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + ) + .expect("time should be valid"); + let body = Python::with_gil(|py| { + PyPlanet::new("Earth") + .expect("body should be valid") + .into_py(py) + }); + let semi_major = 24464560.0e-3; + let eccentricity = 0.7311; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 3.10686; + let true_anomaly = 0.44369564302687126; + + let keplerian = PyKeplerian::new( + &epoch, + body.clone(), + "ICRF", + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ) + .expect("Keplerian state should be valid"); + let keplerian1 = keplerian.to_cartesian().to_keplerian(); + + let origin = + Python::with_gil(|py| body.extract::(py)).expect("origin should be a planet"); + let origin1 = Python::with_gil(|py| keplerian1.origin().extract::(py)) + .expect("origin should be a planet"); + assert_eq!(keplerian1.time(), epoch); + assert_eq!(origin1.name(), origin.name()); + assert_eq!(keplerian1.reference_frame(), "ICRF"); + + assert_float_eq!( + keplerian.semi_major_axis(), + keplerian1.semi_major_axis(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.eccentricity(), + keplerian1.eccentricity(), + abs <= 1e-6 + ); + assert_float_eq!( + keplerian.inclination(), + keplerian1.inclination(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.ascending_node(), + keplerian1.ascending_node(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.periapsis_argument(), + keplerian1.periapsis_argument(), + rel <= 1e-6 + ); + assert_float_eq!( + keplerian.true_anomaly(), + keplerian1.true_anomaly(), + rel <= 1e-6 + ); + } +} diff --git a/crates/lox_py/src/frames.rs b/crates/lox_py/src/frames.rs new file mode 100644 index 00000000..0353399c --- /dev/null +++ b/crates/lox_py/src/frames.rs @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use lox_core::bodies::{Earth, Jupiter, Mars, Mercury, Neptune, Pluto, Saturn, Uranus, Venus}; +use lox_core::frames::iau::BodyFixed; +use lox_core::frames::Icrf; + +use crate::LoxPyError; + +// TODO: Add other supported IAU frames +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum PyFrame { + Icrf(Icrf), + IauMercury(BodyFixed), + IauVenus(BodyFixed), + IauEarth(BodyFixed), + IauMars(BodyFixed), + IauJupiter(BodyFixed), + IauSaturn(BodyFixed), + IauUranus(BodyFixed), + IauNeptune(BodyFixed), + IauPluto(BodyFixed), +} + +impl Display for PyFrame { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + PyFrame::Icrf(frame) => write!(f, "{}", frame), + PyFrame::IauMercury(frame) => write!(f, "{}", frame), + PyFrame::IauVenus(frame) => write!(f, "{}", frame), + PyFrame::IauEarth(frame) => write!(f, "{}", frame), + PyFrame::IauMars(frame) => write!(f, "{}", frame), + PyFrame::IauJupiter(frame) => write!(f, "{}", frame), + PyFrame::IauSaturn(frame) => write!(f, "{}", frame), + PyFrame::IauUranus(frame) => write!(f, "{}", frame), + PyFrame::IauNeptune(frame) => write!(f, "{}", frame), + PyFrame::IauPluto(frame) => write!(f, "{}", frame), + } + } +} + +impl FromStr for PyFrame { + type Err = LoxPyError; + + fn from_str(name: &str) -> Result { + match name { + "icrf" | "ICRF" => Ok(PyFrame::Icrf(Icrf)), + "iau_mercury" | "IAU_MERCURY" => Ok(PyFrame::IauMercury(BodyFixed(Mercury))), + "iau_venus" | "IAU_VENUS" => Ok(PyFrame::IauVenus(BodyFixed(Venus))), + "iau_earth" | "IAU_EARTH" => Ok(PyFrame::IauEarth(BodyFixed(Earth))), + "iau_mars" | "IAU_MARS" => Ok(PyFrame::IauMars(BodyFixed(Mars))), + "iau_jupiter" | "IAU_JUPITER" => Ok(PyFrame::IauJupiter(BodyFixed(Jupiter))), + "iau_saturn" | "IAU_SATURN" => Ok(PyFrame::IauSaturn(BodyFixed(Saturn))), + "iau_uranus" | "IAU_URANUS" => Ok(PyFrame::IauUranus(BodyFixed(Uranus))), + "iau_neptune" | "IAU_NEPTUNE" => Ok(PyFrame::IauNeptune(BodyFixed(Neptune))), + "iau_pluto" | "IAU_PLUTO" => Ok(PyFrame::IauPluto(BodyFixed(Pluto))), + _ => Err(LoxPyError::InvalidFrame(name.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case("icrf", PyFrame::Icrf(Icrf))] + #[case("iau_mercury", PyFrame::IauMercury(BodyFixed(Mercury)))] + #[case("iau_venus", PyFrame::IauVenus(BodyFixed(Venus)))] + #[case("iau_earth", PyFrame::IauEarth(BodyFixed(Earth)))] + #[case("iau_mars", PyFrame::IauMars(BodyFixed(Mars)))] + #[case("iau_jupiter", PyFrame::IauJupiter(BodyFixed(Jupiter)))] + #[case("iau_saturn", PyFrame::IauSaturn(BodyFixed(Saturn)))] + #[case("iau_uranus", PyFrame::IauUranus(BodyFixed(Uranus)))] + #[case("iau_neptune", PyFrame::IauNeptune(BodyFixed(Neptune)))] + #[case("iau_pluto", PyFrame::IauPluto(BodyFixed(Pluto)))] + fn test_frames(#[case] name: &str, #[case] exp: PyFrame) { + let upper = name.to_uppercase(); + let act = PyFrame::from_str(name).expect("frame should be valid"); + assert_eq!(act, exp); + let act = PyFrame::from_str(&upper).expect("frame should be valid"); + assert_eq!(act, exp); + assert_eq!(format!("{}", act), upper); + } + + #[test] + fn test_invalid_frame() { + let frame = PyFrame::from_str("Flat Earth"); + assert!(frame.is_err()); + } +} diff --git a/crates/lox_py/src/lib.rs b/crates/lox_py/src/lib.rs index 4e6fa920..4fcea7e9 100644 --- a/crates/lox_py/src/lib.rs +++ b/crates/lox_py/src/lib.rs @@ -10,21 +10,24 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use thiserror::Error; -use lox_core::errors::LoxError; -use lox_core::time::dates::{Date, Time}; -use lox_core::time::epochs::Epoch; -use lox_core::time::epochs::TimeScale; - use crate::bodies::{PyBarycenter, PyMinorBody, PyPlanet, PySatellite, PySun}; +use crate::coords::{PyCartesian, PyKeplerian}; +use crate::time::{PyEpoch, PyTimeScale}; +use lox_core::errors::LoxError; mod bodies; +mod coords; +mod frames; +mod time; #[derive(Error, Debug)] pub enum LoxPyError { - #[error("invalid time scale `{0}`")] + #[error("unknown time scale `{0}`")] InvalidTimeScale(String), #[error("unknown body `{0}`")] InvalidBody(String), + #[error("unknown frame `{0}`")] + InvalidFrame(String), #[error(transparent)] LoxError(#[from] LoxError), #[error(transparent)] @@ -34,115 +37,15 @@ pub enum LoxPyError { impl From for PyErr { fn from(value: LoxPyError) -> Self { match value { - LoxPyError::InvalidTimeScale(_) => PyValueError::new_err(value.to_string()), - LoxPyError::InvalidBody(_) => PyValueError::new_err(value.to_string()), + LoxPyError::InvalidTimeScale(_) + | LoxPyError::InvalidFrame(_) + | LoxPyError::InvalidBody(_) => PyValueError::new_err(value.to_string()), LoxPyError::LoxError(value) => PyValueError::new_err(value.to_string()), LoxPyError::PyError(value) => value, } } } -#[pyclass(name = "TimeScale")] -struct PyTimeScale(TimeScale); - -#[pymethods] -impl PyTimeScale { - #[new] - fn new(name: &str) -> Result { - match name { - "TAI" => Ok(PyTimeScale(TimeScale::TAI)), - "TCB" => Ok(PyTimeScale(TimeScale::TCB)), - "TCG" => Ok(PyTimeScale(TimeScale::TCG)), - "TDB" => Ok(PyTimeScale(TimeScale::TDB)), - "TT" => Ok(PyTimeScale(TimeScale::TT)), - "UT1" => Ok(PyTimeScale(TimeScale::UT1)), - _ => Err(LoxPyError::InvalidTimeScale(name.to_string())), - } - } - - fn __repr__(&self) -> String { - format!("TimeScale(\"{}\")", self.0) - } - - fn __str__(&self) -> String { - format!("{}", self.0) - } -} - -#[pyclass(name = "Epoch")] -struct PyEpoch(Epoch); - -#[pymethods] -impl PyEpoch { - #[allow(clippy::too_many_arguments)] - #[pyo3(signature = ( - scale, - year, - month, - day, - hour = 0, - minute = 0, - second = 0, - milli = 0, - micro = 0, - nano = 0, - pico = 0, - femto = 0, - atto = 0 - ))] - #[new] - fn new( - scale: &str, - year: i64, - month: i64, - day: i64, - hour: Option, - minute: Option, - second: Option, - milli: Option, - micro: Option, - nano: Option, - pico: Option, - femto: Option, - atto: Option, - ) -> Result { - let time_scale = PyTimeScale::new(scale)?; - let date = Date::new(year, month, day)?; - - let hour = hour.unwrap_or(0); - let minute = minute.unwrap_or(0); - let second = second.unwrap_or(0); - let mut time = Time::new(hour, minute, second)?; - if let Some(milli) = milli { - time = time.milli(milli); - } - if let Some(micro) = micro { - time = time.micro(micro); - } - if let Some(nano) = nano { - time = time.nano(nano); - } - if let Some(pico) = pico { - time = time.pico(pico); - } - if let Some(femto) = femto { - time = time.femto(femto); - } - if let Some(atto) = atto { - time = time.atto(atto); - } - Ok(PyEpoch(Epoch::from_date_and_time(time_scale.0, date, time))) - } - - fn attosecond(&self) -> i64 { - self.0.attosecond() - } - - fn __str__(&self) -> String { - "foo".to_string() - } -} - /// A Python module implemented in Rust. #[pymodule] fn lox_space(_py: Python, m: &PyModule) -> PyResult<()> { @@ -153,5 +56,7 @@ fn lox_space(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs new file mode 100644 index 00000000..6d75272f --- /dev/null +++ b/crates/lox_py/src/time.rs @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use pyo3::{pyclass, pymethods}; + +use lox_core::time::dates::{Date, Time}; +use lox_core::time::epochs::{Epoch, TimeScale}; + +use crate::LoxPyError; + +#[pyclass(name = "TimeScale")] +pub struct PyTimeScale(pub TimeScale); + +#[pymethods] +impl PyTimeScale { + #[new] + fn new(name: &str) -> Result { + match name { + "TAI" => Ok(PyTimeScale(TimeScale::TAI)), + "TCB" => Ok(PyTimeScale(TimeScale::TCB)), + "TCG" => Ok(PyTimeScale(TimeScale::TCG)), + "TDB" => Ok(PyTimeScale(TimeScale::TDB)), + "TT" => Ok(PyTimeScale(TimeScale::TT)), + "UT1" => Ok(PyTimeScale(TimeScale::UT1)), + _ => Err(LoxPyError::InvalidTimeScale(name.to_string())), + } + } + + fn __repr__(&self) -> String { + format!("TimeScale(\"{}\")", self.0) + } + + fn __str__(&self) -> String { + format!("{}", self.0) + } +} + +#[pyclass(name = "Epoch")] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PyEpoch(pub Epoch); + +#[pymethods] +impl PyEpoch { + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = ( + scale, + year, + month, + day, + hour = 0, + minute = 0, + second = 0, + milli = 0, + micro = 0, + nano = 0, + pico = 0, + femto = 0, + atto = 0 + ))] + #[new] + pub fn new( + scale: &str, + year: i64, + month: i64, + day: i64, + hour: Option, + minute: Option, + second: Option, + milli: Option, + micro: Option, + nano: Option, + pico: Option, + femto: Option, + atto: Option, + ) -> Result { + let time_scale = PyTimeScale::new(scale)?; + let date = Date::new(year, month, day)?; + + let hour = hour.unwrap_or(0); + let minute = minute.unwrap_or(0); + let second = second.unwrap_or(0); + let mut time = Time::new(hour, minute, second)?; + if let Some(milli) = milli { + time = time.milli(milli); + } + if let Some(micro) = micro { + time = time.micro(micro); + } + if let Some(nano) = nano { + time = time.nano(nano); + } + if let Some(pico) = pico { + time = time.pico(pico); + } + if let Some(femto) = femto { + time = time.femto(femto); + } + if let Some(atto) = atto { + time = time.atto(atto); + } + Ok(PyEpoch(Epoch::from_date_and_time(time_scale.0, date, time))) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case("TAI", TimeScale::TAI)] + #[case("TCB", TimeScale::TCB)] + #[case("TCG", TimeScale::TCG)] + #[case("TDB", TimeScale::TDB)] + #[case("TT", TimeScale::TT)] + #[case("UT1", TimeScale::UT1)] + fn test_scale(#[case] name: &str, #[case] scale: TimeScale) { + let py_scale = PyTimeScale::new(name).expect("time scale should be valid"); + assert_eq!(py_scale.0, scale); + assert_eq!(py_scale.__str__(), name); + assert_eq!(py_scale.__repr__(), format!("TimeScale(\"{}\")", name)); + } + + #[test] + fn test_invalid_scale() { + let py_scale = PyTimeScale::new("disco time"); + assert!(py_scale.is_err()) + } + + #[test] + fn test_time() { + let time = PyEpoch::new( + "TDB", + 2024, + 1, + 1, + Some(1), + Some(1), + Some(1), + Some(123), + Some(456), + Some(789), + Some(123), + Some(456), + Some(789), + ) + .expect("time should be valid"); + assert_eq!(time.0.attosecond(), 123456789123456789); + } +} diff --git a/crates/lox_py/tests/test_coords.py b/crates/lox_py/tests/test_coords.py new file mode 100644 index 00000000..33926b0d --- /dev/null +++ b/crates/lox_py/tests/test_coords.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024. Helge Eichhorn and the LOX contributors +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. + +import lox_space as lox + + +def test_coords(): + time = lox.Epoch("TDB", 2016, 5, 30, 12) + x = 6068.27927 + y = -1692.84394 + z = -2516.61918 + vx = -0.660415582 + vy = 5.495938726 + vz = -5.303093233 + iss_cartesian = lox.Cartesian( + time, lox.Planet("Earth"), "ICRF", x, y, z, vx, vy, vz + ) + iss = iss_cartesian.to_keplerian() + assert isinstance(iss, lox.Keplerian) diff --git a/tools/lox_gen/Cargo.lock b/tools/lox_gen/Cargo.lock index c235bf80..9290fd94 100644 --- a/tools/lox_gen/Cargo.lock +++ b/tools/lox_gen/Cargo.lock @@ -8,6 +8,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + [[package]] name = "fast_polynomial" version = "0.1.0" @@ -25,9 +31,9 @@ checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" [[package]] name = "glam" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" [[package]] name = "lazy_static" @@ -39,6 +45,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" name = "lox_core" version = "0.1.0" dependencies = [ + "dyn-clone", "fast_polynomial", "float_eq", "glam",