From 476c48204f905adf945f8fcdf04ed4cc80c19d25 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Tue, 9 Jan 2024 14:27:39 -0700 Subject: [PATCH 1/4] Move metaalmanac into the metaload module --- Cargo.toml | 4 +- README.md | 4 +- anise-py/Cargo.toml | 2 +- anise-py/README.md | 126 ++++++++++- anise-py/src/lib.rs | 2 +- anise-py/tests/test_almanac.py | 15 +- anise/Cargo.toml | 16 +- anise/src/almanac/metaload/metaalmanac.rs | 192 ++++++++++++++++ .../almanac/{meta.rs => metaload/metafile.rs} | 214 ++---------------- anise/src/almanac/metaload/mod.rs | 100 ++++++++ anise/src/almanac/mod.rs | 2 +- anise/src/errors.rs | 2 +- anise/src/frames/frame.rs | 3 +- anise/src/math/cartesian.rs | 16 +- anise/src/py_errors.rs | 2 +- .../src/structure/planetocentric/ellipsoid.rs | 3 +- 16 files changed, 475 insertions(+), 228 deletions(-) create mode 100644 anise/src/almanac/metaload/metaalmanac.rs rename anise/src/almanac/{meta.rs => metaload/metafile.rs} (58%) create mode 100644 anise/src/almanac/metaload/mod.rs diff --git a/Cargo.toml b/Cargo.toml index be830bc5..30069709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["anise", "anise-cli", "anise-gui", "anise-py"] [workspace.package] -version = "0.2.0" +version = "0.2.1" edition = "2021" authors = ["Christopher Rabotin "] description = "ANISE provides a toolkit and files for Attitude, Navigation, Instrument, Spacecraft, and Ephemeris data. It's a modern replacement of NAIF SPICE file." @@ -45,7 +45,7 @@ rstest = "0.18.2" pyo3 = { version = "0.20.0", features = ["multiple-pymethods"] } pyo3-log = "0.9.0" -anise = { version = "0.2.0", path = "anise", default-features = false } +anise = { version = "0.2.1", path = "anise", default-features = false } [profile.bench] debug = true diff --git a/README.md b/README.md index 283e607a..c9908081 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ANISE (Attitude, Navigation, Instrument, Spacecraft, Ephemeris) -ANISE, inspired by the iconic Dune universe, reimagines the functionalities of the NAIF SPICE toolkit with enhanced performance, precision, and ease of use, leveraging Rust's safety and speed. +ANISE is a rewrite of the core functionalities of the NAIF SPICE toolkit with enhanced performance, and ease of use, while leveraging Rust's safety and speed. [**Please fill out our user survey**](https://7ug5imdtt8v.typeform.com/to/qYDB14Hj) @@ -296,7 +296,7 @@ For more details, please see the [full text of the license](./LICENSE) or read [ ## Acknowledgements -ANISE is heavily inspired by the NAIF SPICE toolkit and its excellent documentation +ANISE is heavily inspired by the NAIF SPICE toolkit and its excellent documentation. ## Contact diff --git a/anise-py/Cargo.toml b/anise-py/Cargo.toml index 257ee8b4..20a713a1 100644 --- a/anise-py/Cargo.toml +++ b/anise-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "anise-py" -version = "0.1.0" +version = "0.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/anise-py/README.md b/anise-py/README.md index 2438555a..77dc5a23 100644 --- a/anise-py/README.md +++ b/anise-py/README.md @@ -1,6 +1,128 @@ -# ANISE Python +# ANISE (Attitude, Navigation, Instrument, Spacecraft, Ephemeris) -The Python interface to ANISE, a modern rewrite of NAIF SPICE. +ANISE is a rewrite of the core functionalities of the NAIF SPICE toolkit with enhanced performance, and ease of use, while leveraging Rust's safety and speed. + +[**Please fill out our user survey**](https://7ug5imdtt8v.typeform.com/to/qYDB14Hj) + +## Introduction + +In the realm of space exploration, navigation, and astrophysics, precise and efficient computation of spacecraft position, orientation, and time is critical. ANISE, standing for "Attitude, Navigation, Instrument, Spacecraft, Ephemeris," offers a Rust-native approach to these challenges. This toolkit provides a suite of functionalities including but not limited to: + ++ Loading SPK, BPC, PCK, FK, and TPC files. ++ High-precision translations, rotations, and their combination (rigid body transformations). ++ Comprehensive time system conversions using the hifitime library (including TT, TAI, ET, TDB, UTC, GPS time, and more). + +ANISE stands validated against the traditional SPICE toolkit, ensuring accuracy and reliability, with translations achieving machine precision (2e-16) and rotations presenting minimal error (less than two arcseconds in the pointing of the rotation axis and less than one arcsecond in the angle about this rotation axis). + +## Features + ++ **High Precision**: Matches SPICE to machine precision in translations and minimal errors in rotations. ++ **Time System Conversions**: Extensive support for various time systems crucial in astrodynamics. ++ **Rust Efficiency**: Harnesses the speed and safety of Rust for space computations. ++ **Multi-threaded:** Yup! Forget about mutexes and race conditions you're used to in SPICE, ANISE _guarantees_ that you won't have any race conditions. ++ **Frame safety**: ANISE checks all frames translations or rotations are physically valid before performing any computation, even internally. + +## Resources / Assets + +For convenience, Nyx Space provides a few important SPICE files on a public bucket: + ++ [de440s.bsp](http://public-data.nyxspace.com/anise/de440s.bsp): JPL's latest ephemeris dataset from 1900 until 20250 ++ [de440.bsp](http://public-data.nyxspace.com/anise/de440.bsp): JPL's latest long-term ephemeris dataset ++ [pck08.pca](http://public-data.nyxspace.com/anise/pck08.pca): planetary constants ANISE (`pca`) kernel, built from the JPL gravitational data [gm_de431.tpc](http://public-data.nyxspace.com/anise/gm_de431.tpc) and JPL's plantary constants file [pck00008.tpc](http://public-data.nyxspace.com/anise/pck00008.tpc) + +You may load any of these using the `load()` shortcut that will determine the file type upon loading, e.g. `let almanac = Almanac::default().load("pck08.pca").unwrap();`. + +## Python Usage + +In Python, start by adding anise to your project: `pip install anise`. + +```python +from anise import Almanac, Aberration +from anise.astro.constants import Frames +from anise.astro import Orbit +from anise.time import Epoch + +from pathlib import Path + + +def test_state_transformation(): + """ + This is the Python equivalent to anise/tests/almanac/mod.rs + """ + data_path = Path(__file__).parent.joinpath("..", "..", "data") + # Must ensure that the path is a string + ctx = Almanac(str(data_path.joinpath("de440s.bsp"))) + # Let's add another file here -- note that the Almanac will load into a NEW variable, so we must overwrite it! + # This prevents memory leaks (yes, I promise) + ctx = ctx.load(str(data_path.joinpath("pck08.pca"))).load( + str(data_path.joinpath("earth_latest_high_prec.bpc")) + ) + eme2k = ctx.frame_info(Frames.EME2000) + assert eme2k.mu_km3_s2() == 398600.435436096 + assert eme2k.shape.polar_radius_km == 6356.75 + assert abs(eme2k.shape.flattening() - 0.0033536422844278) < 2e-16 + + epoch = Epoch("2021-10-29 12:34:56 TDB") + + orig_state = Orbit.from_keplerian( + 8_191.93, + 1e-6, + 12.85, + 306.614, + 314.19, + 99.887_7, + epoch, + eme2k, + ) + + assert orig_state.sma_km() == 8191.93 + assert orig_state.ecc() == 1.000000000361619e-06 + assert orig_state.inc_deg() == 12.849999999999987 + assert orig_state.raan_deg() == 306.614 + assert orig_state.tlong_deg() == 0.6916999999999689 + + state_itrf93 = ctx.transform_to( + orig_state, Frames.EARTH_ITRF93, None + ) + + print(orig_state) + print(state_itrf93) + + assert state_itrf93.geodetic_latitude_deg() == 10.549246868302738 + assert state_itrf93.geodetic_longitude_deg() == 133.76889100913047 + assert state_itrf93.geodetic_height_km() == 1814.503598063825 + + # Convert back + from_state_itrf93_to_eme2k = ctx.transform_to( + state_itrf93, Frames.EARTH_J2000, None + ) + + print(from_state_itrf93_to_eme2k) + + assert orig_state == from_state_itrf93_to_eme2k + + # Demo creation of a ground station + mean_earth_angular_velocity_deg_s = 0.004178079012116429 + # Grab the loaded frame info + itrf93 = ctx.frame_info(Frames.EARTH_ITRF93) + paris = Orbit.from_latlongalt( + 48.8566, + 2.3522, + 0.4, + mean_earth_angular_velocity_deg_s, + epoch, + itrf93, + ) + + assert abs(paris.geodetic_latitude_deg() - 48.8566) < 1e-3 + assert abs(paris.geodetic_longitude_deg() - 2.3522) < 1e-3 + assert abs(paris.geodetic_height_km() - 0.4) < 1e-3 + + +if __name__ == "__main__": + test_state_transformation() + +``` ## Getting started as a developer diff --git a/anise-py/src/lib.rs b/anise-py/src/lib.rs index 062f59dd..b0f5db33 100644 --- a/anise-py/src/lib.rs +++ b/anise-py/src/lib.rs @@ -8,7 +8,7 @@ * Documentation: https://nyxspace.com/ */ -use ::anise::almanac::meta::{MetaAlmanac, MetaFile}; +use ::anise::almanac::metaload::{MetaAlmanac, MetaFile}; use ::anise::almanac::Almanac; use ::anise::astro::Aberration; use hifitime::leap_seconds::{LatestLeapSeconds, LeapSecondsFile}; diff --git a/anise-py/tests/test_almanac.py b/anise-py/tests/test_almanac.py index 1f43e49a..2e86f2d0 100644 --- a/anise-py/tests/test_almanac.py +++ b/anise-py/tests/test_almanac.py @@ -16,15 +16,12 @@ def test_state_transformation(): """ if environ.get("CI", False): - # # Load from meta kernel to not use Git LFS quota - # data_path = Path(__file__).parent.joinpath("..", "..", "data", "default_meta.dhall") - # meta = MetaAlmanac(str(data_path)) - # print(meta) - # # Process the files to be loaded - # ctx = meta.process() - print("I don't know where the files are in the Python CI") - - return + # Load from meta kernel to not use Git LFS quota + data_path = Path(__file__).parent.joinpath("..", "..", "data", "default_meta.dhall") + meta = MetaAlmanac(str(data_path)) + print(meta) + # Process the files to be loaded + ctx = meta.process() else: data_path = Path(__file__).parent.joinpath("..", "..", "data") # Must ensure that the path is a string diff --git a/anise/Cargo.toml b/anise/Cargo.toml index 7aba9e50..0266d1e1 100644 --- a/anise/Cargo.toml +++ b/anise/Cargo.toml @@ -27,9 +27,9 @@ rstest = { workspace = true } pyo3 = { workspace = true, optional = true } pyo3-log = { workspace = true, optional = true } url = { version = "2.5.0", optional = true } -serde = { version = "1", optional = true } -serde_derive = { version = "1", optional = true } -serde_dhall = { version = "0.12", optional = true } +serde = "1" +serde_derive = "1" +serde_dhall = "0.12" reqwest = { version = "0.11.23", optional = true, features = ["blocking"] } platform-dirs = { version = "0.3.0", optional = true } @@ -41,20 +41,14 @@ criterion = "0.5" iai = "0.1" polars = { version = "0.36.2", features = ["lazy", "parquet"] } rayon = "1.7" +serde_yaml = "0.9.30" [features] default = ["metaload"] # Enabling this flag significantly increases compilation times due to Arrow and Polars. spkezr_validation = [] python = ["pyo3", "pyo3-log"] -metaload = [ - "url", - "serde", - "serde_derive", - "serde_dhall", - "reqwest/blocking", - "platform-dirs", -] +metaload = ["url", "reqwest/blocking", "platform-dirs"] [[bench]] name = "iai_jpl_ephemerides" diff --git a/anise/src/almanac/metaload/metaalmanac.rs b/anise/src/almanac/metaload/metaalmanac.rs new file mode 100644 index 00000000..a828db77 --- /dev/null +++ b/anise/src/almanac/metaload/metaalmanac.rs @@ -0,0 +1,192 @@ +/* + * ANISE Toolkit + * Copyright (C) 2021-2023 Christopher Rabotin et al. (cf. AUTHORS.md) + * 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/. + * + * Documentation: https://nyxspace.com/ + */ + +use serde_derive::{Deserialize, Serialize}; +use serde_dhall::SimpleType; +use snafu::prelude::*; +use std::str::FromStr; +use url::Url; + +#[cfg(feature = "python")] +use pyo3::exceptions::PyTypeError; +#[cfg(feature = "python")] +use pyo3::prelude::*; +#[cfg(feature = "python")] +use pyo3::pyclass::CompareOp; +#[cfg(feature = "python")] +use pyo3::types::PyType; + +use crate::errors::{AlmanacError, MetaSnafu}; + +use super::{Almanac, MetaAlmanacError, MetaFile}; + +/// A structure to set up an Almanac, with automatic downloading, local storage, checksum checking, and more. +/// +/// # Behavior +/// If the URI is a local path, relative or absolute, nothing will be fetched from a remote. Relative paths are relative to the execution folder (i.e. the current working directory). +/// If the URI is a remote path, the MetaAlmanac will first check if the file exists locally. If it exists, it will check that the CRC32 checksum of this file matches that of the specs. +/// If it does not match, the file will be downloaded again. If no CRC32 is provided but the file exists, then the MetaAlmanac will fetch the remote file and overwrite the existing file. +/// The downloaded path will be stored in the "AppData" folder. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "python", pyo3(module = "anise"))] +#[cfg_attr(feature = "python", pyo3(get_all, set_all))] +pub struct MetaAlmanac { + pub files: Vec, +} + +impl MetaAlmanac { + /// Loads the provided path as a Dhall configuration file and processes each file. + pub fn new(path: String) -> Result { + match serde_dhall::from_file(&path).parse::() { + Err(e) => Err(MetaAlmanacError::ParseDhall { + path, + err: format!("{e}"), + }), + Ok(me) => Ok(me), + } + } + + /// Fetch all of the URIs and return a loaded Almanac + pub(crate) fn _process(&mut self) -> Result { + for uri in &mut self.files { + uri._process().with_context(|_| MetaSnafu)?; + } + // At this stage, all of the files are local files, so we can load them as is. + let mut ctx = Almanac::default(); + for uri in &self.files { + ctx = ctx.load(&uri.uri)?; + } + Ok(ctx) + } + + /// Fetch all of the URIs and return a loaded Almanac + #[cfg(not(feature = "python"))] + pub fn process(&mut self) -> Result { + self._process() + } +} + +impl FromStr for MetaAlmanac { + type Err = MetaAlmanacError; + + fn from_str(s: &str) -> Result { + match serde_dhall::from_str(&s).parse::() { + Err(e) => Err(MetaAlmanacError::ParseDhall { + path: s.to_string(), + err: format!("{e}"), + }), + Ok(me) => Ok(me), + } + } +} + +// Methods shared between Rust and Python +#[cfg_attr(feature = "python", pymethods)] +impl MetaAlmanac { + /// Dumps the configured Meta Almanac into a Dhall string. + pub fn dump(&self) -> Result { + // Define the Dhall type + let dhall_type: SimpleType = + serde_dhall::from_str("{ files : List { uri : Text, crc32 : Optional Natural } }") + .parse() + .unwrap(); + + serde_dhall::serialize(&self) + .type_annotation(&dhall_type) + .to_string() + .map_err(|e| MetaAlmanacError::ExportDhall { + err: format!("{e}"), + }) + } +} + +// Python only methods +#[cfg(feature = "python")] +#[cfg_attr(feature = "python", pymethods)] +impl MetaAlmanac { + /// Loads the provided path as a Dhall file. If no path is provided, creates an empty MetaAlmanac that can store MetaFiles. + #[new] + pub fn py_new(maybe_path: Option) -> Result { + match maybe_path { + Some(path) => Self::new(path), + None => Ok(Self { files: Vec::new() }), + } + } + + /// Loads the provided string as a Dhall configuration to build a MetaAlmanac + #[classmethod] + fn load(_cls: &PyType, s: String) -> Result { + Self::from_str(&s) + } + + /// Fetch all of the URIs and return a loaded Almanac + pub fn process(&mut self, py: Python) -> Result { + py.allow_threads(|| self._process()) + } + + fn __str__(&self) -> String { + format!("{self:?}") + } + + fn __repr__(&self) -> String { + format!("{self:?} (@{self:p})") + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> Result { + match op { + CompareOp::Eq => Ok(self == other), + CompareOp::Ne => Ok(self != other), + _ => Err(PyErr::new::(format!( + "{op:?} not available" + ))), + } + } +} + +/// By default, the MetaAlmanac will download the DE440s.bsp file, the PCK0008.PCA, and the latest high precision Earth kernel from JPL. +/// +/// # File list +/// - +/// - +/// - +/// +/// # Reproducibility +/// +/// Note that the `earth_latest_high_prec.bpc` file is regularily updated daily (or so). As such, +/// if queried at some future time, the Earth rotation parameters may have changed between two queries. +/// +impl Default for MetaAlmanac { + fn default() -> Self { + let nyx_cloud_stor = Url::parse("http://public-data.nyxspace.com/anise/").unwrap(); + let jpl_cloud_stor = + Url::parse("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/").unwrap(); + + Self { + files: vec![ + MetaFile { + uri: nyx_cloud_stor.join("de440s.bsp").unwrap().to_string(), + crc32: Some(0x7286750a), + }, + MetaFile { + uri: nyx_cloud_stor.join("pck08.pca").unwrap().to_string(), + crc32: Some(0x487bee78), + }, + MetaFile { + uri: jpl_cloud_stor + .join("pck/earth_latest_high_prec.bpc") + .unwrap() + .to_string(), + crc32: None, + }, + ], + } + } +} diff --git a/anise/src/almanac/meta.rs b/anise/src/almanac/metaload/metafile.rs similarity index 58% rename from anise/src/almanac/meta.rs rename to anise/src/almanac/metaload/metafile.rs index bbb79067..87fb653d 100644 --- a/anise/src/almanac/meta.rs +++ b/anise/src/almanac/metaload/metafile.rs @@ -10,10 +10,9 @@ use log::{debug, info}; use platform_dirs::AppDirs; -use reqwest::StatusCode; + use serde_derive::{Deserialize, Serialize}; -use serde_dhall::{SimpleType, StaticType}; -use snafu::prelude::*; +use serde_dhall::StaticType; use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::Path; @@ -27,165 +26,10 @@ use pyo3::prelude::*; #[cfg(feature = "python")] use pyo3::pyclass::CompareOp; -use crate::errors::{AlmanacError, MetaSnafu}; use crate::file2heap; use crate::prelude::InputOutputError; -use super::Almanac; - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub(crate)))] -pub enum MetaAlmanacError { - #[snafu(display("could not create the cache folder for ANISE, please use a relative path"))] - AppDirError, - #[snafu(display("could not find a file path in {path}"))] - MissingFilePath { path: String }, - #[snafu(display("IO error {source} when {what} with {path}"))] - MetaIO { - path: String, - what: &'static str, - source: InputOutputError, - }, - #[snafu(display("fetching {uri} returned {status}"))] - FetchError { status: StatusCode, uri: String }, - #[snafu(display("connection {uri} returned {error}"))] - CnxError { uri: String, error: String }, - #[snafu(display("error parsing {path} as Dhall config: {err}"))] - ParseDhall { path: String, err: String }, - #[snafu(display("error exporting as Dhall config: {err}"))] - ExportDhall { err: String }, -} - -/// A structure to set up an Almanac, with automatic downloading, local storage, checksum checking, and more. -/// -/// # Behavior -/// If the URI is a local path, relative or absolute, nothing will be fetched from a remote. Relative paths are relative to the execution folder (i.e. the current working directory). -/// If the URI is a remote path, the MetaAlmanac will first check if the file exists locally. If it exists, it will check that the CRC32 checksum of this file matches that of the specs. -/// If it does not match, the file will be downloaded again. If no CRC32 is provided but the file exists, then the MetaAlmanac will fetch the remote file and overwrite the existing file. -/// The downloaded path will be stored in the "AppData" folder. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[cfg_attr(feature = "python", pyclass)] -#[cfg_attr(feature = "python", pyo3(module = "anise"))] -#[cfg_attr(feature = "python", pyo3(get_all, set_all))] -pub struct MetaAlmanac { - pub files: Vec, -} - -impl MetaAlmanac { - /// Loads the provided path as a Dhall configuration file and processes each file. - pub fn new(path: String) -> Result { - match serde_dhall::from_file(&path).parse::() { - Err(e) => Err(MetaAlmanacError::ParseDhall { - path, - err: format!("{e}"), - }), - Ok(me) => Ok(me), - } - } -} - -#[cfg_attr(feature = "python", pymethods)] -impl MetaAlmanac { - /// Loads the provided path as a Dhall file. If no path is provided, creates an empty MetaAlmanac that can store MetaFiles. - #[cfg(feature = "python")] - #[new] - pub fn py_new(maybe_path: Option) -> Result { - match maybe_path { - Some(path) => Self::new(path), - None => Ok(Self { files: Vec::new() }), - } - } - - /// Fetch all of the data and return a loaded Almanac - pub fn process(&mut self) -> Result { - for uri in &mut self.files { - uri.process().with_context(|_| MetaSnafu)?; - } - // At this stage, all of the files are local files, so we can load them as is. - let mut ctx = Almanac::default(); - for uri in &self.files { - ctx = ctx.load(&uri.uri)?; - } - Ok(ctx) - } - - /// Dumps the configured Meta Almanac into a Dhall string - pub fn dump(&self) -> Result { - // Define the Dhall type - let dhall_type: SimpleType = - serde_dhall::from_str("{ files : List { uri : Text, crc32 : Optional Natural } }") - .parse() - .unwrap(); - - serde_dhall::serialize(&self) - .type_annotation(&dhall_type) - .to_string() - .map_err(|e| MetaAlmanacError::ExportDhall { - err: format!("{e}"), - }) - } - - #[cfg(feature = "python")] - fn __str__(&self) -> String { - format!("{self:?}") - } - - #[cfg(feature = "python")] - fn __repr__(&self) -> String { - format!("{self:?} (@{self:p})") - } - - #[cfg(feature = "python")] - fn __richcmp__(&self, other: &Self, op: CompareOp) -> Result { - match op { - CompareOp::Eq => Ok(self == other), - CompareOp::Ne => Ok(self != other), - _ => Err(PyErr::new::(format!( - "{op:?} not available" - ))), - } - } -} - -/// By default, the MetaAlmanac will download the DE440s.bsp file, the PCK0008.PCA, and the latest high precision Earth kernel from JPL. -/// -/// # File list -/// - -/// - -/// - -/// -/// # Reproducibility -/// -/// Note that the `earth_latest_high_prec.bpc` file is regularily updated daily (or so). As such, -/// if queried at some future time, the Earth rotation parameters may have changed between two queries. -/// -impl Default for MetaAlmanac { - fn default() -> Self { - let nyx_cloud_stor = Url::parse("http://public-data.nyxspace.com/anise/").unwrap(); - let jpl_cloud_stor = - Url::parse("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/").unwrap(); - - Self { - files: vec![ - MetaFile { - uri: nyx_cloud_stor.join("de440s.bsp").unwrap().to_string(), - crc32: Some(0x7286750a), - }, - MetaFile { - uri: nyx_cloud_stor.join("pck08.pca").unwrap().to_string(), - crc32: Some(0x487bee78), - }, - MetaFile { - uri: jpl_cloud_stor - .join("pck/earth_latest_high_prec.bpc") - .unwrap() - .to_string(), - crc32: None, - }, - ], - } - } -} +use super::MetaAlmanacError; #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "python", pyo3(module = "anise"))] @@ -231,7 +75,20 @@ impl MetaFile { /// Processes this MetaFile by downloading it if it's a URL. /// /// This function modified `self` and changes the URI to be the path to the downloaded file. - fn process(&mut self) -> Result<(), MetaAlmanacError> { + #[cfg(feature = "python")] + pub fn py_process(&mut self, py: Python) -> Result<(), MetaAlmanacError> { + py.allow_threads(|| self._process()) + } + + /// Processes this MetaFile by downloading it if it's a URL. + /// + /// This function modified `self` and changes the URI to be the path to the downloaded file. + #[cfg(not(feature = "python"))] + pub fn process(&mut self) -> Result<(), MetaAlmanacError> { + self._process() + } + + pub(crate) fn _process(&mut self) -> Result<(), MetaAlmanacError> { match Url::parse(&self.uri) { Err(e) => { debug!("parsing {} caused {e} -- assuming local path", self.uri); @@ -309,9 +166,9 @@ impl MetaFile { file.write_all(&bytes).unwrap(); info!( - "Saved {url} to {} (CRC32 = {crc32:x})", - dest_path.to_str().unwrap() - ); + "Saved {url} to {} (CRC32 = {crc32:x})", + dest_path.to_str().unwrap() + ); // Set the URI for loading self.uri = dest_path @@ -356,34 +213,3 @@ impl MetaFile { } } } - -#[cfg(test)] -mod meta_test { - use super::{MetaAlmanac, Path}; - use std::env; - - #[test] - fn test_meta_almanac() { - let _ = pretty_env_logger::try_init(); - let mut meta = MetaAlmanac::default(); - println!("{meta:?}"); - - let almanac = meta.process().unwrap(); - println!("{almanac}"); - - // Process again to confirm that the CRC check works - assert!(meta.process().is_ok()); - } - - #[test] - fn test_from_dhall() { - let default = MetaAlmanac::default(); - - println!("{}", default.dump().unwrap()); - - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../data/default_meta.dhall"); - let dhall = MetaAlmanac::new(path.to_str().unwrap().to_string()).unwrap(); - - assert_eq!(dhall, default); - } -} diff --git a/anise/src/almanac/metaload/mod.rs b/anise/src/almanac/metaload/mod.rs new file mode 100644 index 00000000..89309d01 --- /dev/null +++ b/anise/src/almanac/metaload/mod.rs @@ -0,0 +1,100 @@ +/* + * ANISE Toolkit + * Copyright (C) 2021-2023 Christopher Rabotin et al. (cf. AUTHORS.md) + * 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/. + * + * Documentation: https://nyxspace.com/ + */ + +mod metaalmanac; +mod metafile; + +pub use metaalmanac::MetaAlmanac; +pub use metafile::MetaFile; + +use super::Almanac; + +use crate::prelude::InputOutputError; +use reqwest::StatusCode; +use snafu::prelude::*; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum MetaAlmanacError { + #[snafu(display("could not create the cache folder for ANISE, please use a relative path"))] + AppDirError, + #[snafu(display("could not find a file path in {path}"))] + MissingFilePath { path: String }, + #[snafu(display("IO error {source} when {what} with {path}"))] + MetaIO { + path: String, + what: &'static str, + source: InputOutputError, + }, + #[snafu(display("fetching {uri} returned {status}"))] + FetchError { status: StatusCode, uri: String }, + #[snafu(display("connection {uri} returned {error}"))] + CnxError { uri: String, error: String }, + #[snafu(display("error parsing `{path}` as Dhall config: {err}"))] + ParseDhall { path: String, err: String }, + #[snafu(display("error exporting as Dhall config: {err}"))] + ExportDhall { err: String }, +} + +#[cfg(test)] +mod meta_test { + use super::MetaAlmanac; + use std::path::Path; + use std::{env, str::FromStr}; + + #[test] + fn test_meta_almanac() { + let _ = pretty_env_logger::try_init(); + let mut meta = MetaAlmanac::default(); + println!("{meta:?}"); + + let almanac = meta._process().unwrap(); + println!("{almanac}"); + + // Process again to confirm that the CRC check works + assert!(meta._process().is_ok()); + } + + #[test] + fn test_from_dhall() { + let default = MetaAlmanac::default(); + + println!("{}", default.dump().unwrap()); + + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../data/default_meta.dhall"); + let dhall = MetaAlmanac::new(path.to_str().unwrap().to_string()).unwrap(); + + assert_eq!(dhall, default); + + // Try FromStr + + let from_str = MetaAlmanac::from_str( + r#" + -- Default Almanac + { files = + [ { crc32 = Some 1921414410 + , uri = "http://public-data.nyxspace.com/anise/de440s.bsp" + } + , { crc32 = Some 1216081528 + , uri = "http://public-data.nyxspace.com/anise/pck08.pca" + } + , { crc32 = None Natural + , uri = + "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/earth_latest_high_prec.bpc" + } + ] + } + "#, + ) + .unwrap(); + + assert_eq!(from_str, default); + } +} diff --git a/anise/src/almanac/mod.rs b/anise/src/almanac/mod.rs index ed260319..47645489 100644 --- a/anise/src/almanac/mod.rs +++ b/anise/src/almanac/mod.rs @@ -39,7 +39,7 @@ pub mod spk; pub mod transform; #[cfg(feature = "metaload")] -pub mod meta; +pub mod metaload; #[cfg(feature = "python")] mod python; diff --git a/anise/src/errors.rs b/anise/src/errors.rs index 6c3865c7..fa27acf6 100644 --- a/anise/src/errors.rs +++ b/anise/src/errors.rs @@ -22,7 +22,7 @@ use der::Error as DerError; use std::io::ErrorKind as IOErrorKind; #[cfg(feature = "metaload")] -use crate::almanac::meta::MetaAlmanacError; +use crate::almanac::metaload::MetaAlmanacError; #[derive(Debug, Snafu)] #[snafu(visibility(pub))] diff --git a/anise/src/frames/frame.rs b/anise/src/frames/frame.rs index f27e5d1e..47026506 100644 --- a/anise/src/frames/frame.rs +++ b/anise/src/frames/frame.rs @@ -10,6 +10,7 @@ use core::fmt; use core::fmt::Debug; +use serde_derive::{Deserialize, Serialize}; use crate::astro::PhysicsResult; use crate::constants::celestial_objects::{celestial_name_from_id, SOLAR_SYSTEM_BARYCENTER}; @@ -27,7 +28,7 @@ use pyo3::prelude::*; use pyo3::pyclass::CompareOp; /// A Frame uniquely defined by its ephemeris center and orientation. Refer to FrameDetail for frames combined with parameters. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "python", pyo3(get_all, set_all))] #[cfg_attr(feature = "python", pyo3(module = "anise.astro"))] diff --git a/anise/src/math/cartesian.rs b/anise/src/math/cartesian.rs index cfd67a14..4b9b4179 100644 --- a/anise/src/math/cartesian.rs +++ b/anise/src/math/cartesian.rs @@ -15,10 +15,12 @@ use crate::{ errors::{EpochMismatchSnafu, FrameMismatchSnafu, PhysicsError}, prelude::Frame, }; + use core::fmt; use core::ops::{Add, Neg, Sub}; use hifitime::{Duration, Epoch, TimeUnits}; use nalgebra::Vector6; +use serde_derive::{Deserialize, Serialize}; use snafu::ensure; #[cfg(feature = "python")] @@ -28,7 +30,7 @@ use pyo3::prelude::*; /// Regardless of the constructor used, this struct stores all the state information in Cartesian coordinates as these are always non singular. /// /// Unless noted otherwise, algorithms are from GMAT 2016a [StateConversionUtil.cpp](https://github.com/ChristopherRabotin/GMAT/blob/37201a6290e7f7b941bc98ee973a527a5857104b/src/base/util/StateConversionUtil.cpp). -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "python", pyclass(name = "Orbit"))] #[cfg_attr(feature = "python", pyo3(module = "anise.astro"))] pub struct CartesianState { @@ -419,4 +421,16 @@ mod cartesian_state_ut { assert_eq!(s.light_time(), Duration::ZERO); } + + #[test] + fn test_serde() { + let e = Epoch::now().unwrap(); + let frame = EARTH_J2000; + let state = CartesianState::new(10.0, 20.0, 30.0, 1.0, 2.0, 2.0, e, frame); + + let serialized = serde_yaml::to_string(&state).unwrap(); + let rtn: CartesianState = serde_yaml::from_str(&serialized).unwrap(); + + assert_eq!(rtn, state); + } } diff --git a/anise/src/py_errors.rs b/anise/src/py_errors.rs index 476ba39e..d25d2ea5 100644 --- a/anise/src/py_errors.rs +++ b/anise/src/py_errors.rs @@ -8,7 +8,7 @@ * Documentation: https://nyxspace.com/ */ -use crate::almanac::meta::MetaAlmanacError; +use crate::almanac::metaload::MetaAlmanacError; use crate::almanac::planetary::PlanetaryDataError; use crate::ephemerides::EphemerisError; use crate::errors::{AlmanacError, DecodingError, InputOutputError, IntegrityError, PhysicsError}; diff --git a/anise/src/structure/planetocentric/ellipsoid.rs b/anise/src/structure/planetocentric/ellipsoid.rs index 196a1cd1..3619a08a 100644 --- a/anise/src/structure/planetocentric/ellipsoid.rs +++ b/anise/src/structure/planetocentric/ellipsoid.rs @@ -10,6 +10,7 @@ use core::f64::EPSILON; use core::fmt; use der::{Decode, Encode, Reader, Writer}; +use serde_derive::{Deserialize, Serialize}; #[cfg(feature = "python")] use pyo3::exceptions::PyTypeError; @@ -28,7 +29,7 @@ use pyo3::pyclass::CompareOp; /// Example: Radii of the Earth. /// /// BODY399_RADII = ( 6378.1366 6378.1366 6356.7519 ) -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "python", pyo3(get_all, set_all))] #[cfg_attr(feature = "python", pyo3(module = "anise.astro"))] From 87ad34dc218402c2608061eed3f9f8153688e420 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Tue, 9 Jan 2024 18:11:43 -0700 Subject: [PATCH 2/4] Export convert_tpc and convert_fk to Python --- anise-py/src/errors.rs | 12 ------- anise-py/src/lib.rs | 2 ++ anise-py/src/utils.rs | 63 ++++++++++++++++++++++++++++++++++++ anise/src/naif/kpl/parser.rs | 4 +++ anise/src/py_errors.rs | 6 ++++ 5 files changed, 75 insertions(+), 12 deletions(-) delete mode 100644 anise-py/src/errors.rs create mode 100644 anise-py/src/utils.rs diff --git a/anise-py/src/errors.rs b/anise-py/src/errors.rs deleted file mode 100644 index 7395a46a..00000000 --- a/anise-py/src/errors.rs +++ /dev/null @@ -1,12 +0,0 @@ -/* - * ANISE Toolkit - * Copyright (C) 2021-2023 Christopher Rabotin et al. (cf. AUTHORS.md) - * 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/. - * - * Documentation: https://nyxspace.com/ - */ - -use anise::errors::PhysicsError; -use pyo3::{exceptions::PyException, prelude::*}; diff --git a/anise-py/src/lib.rs b/anise-py/src/lib.rs index b0f5db33..b9f47f77 100644 --- a/anise-py/src/lib.rs +++ b/anise-py/src/lib.rs @@ -19,12 +19,14 @@ use pyo3::prelude::*; use pyo3::py_run; mod astro; +mod utils; /// A Python module implemented in Rust. #[pymodule] fn anise(py: Python, m: &PyModule) -> PyResult<()> { register_time_module(py, m)?; astro::register_astro(py, m)?; + utils::register_utils(py, m)?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/anise-py/src/utils.rs b/anise-py/src/utils.rs new file mode 100644 index 00000000..828527b9 --- /dev/null +++ b/anise-py/src/utils.rs @@ -0,0 +1,63 @@ +/* + * ANISE Toolkit + * Copyright (C) 2021-2023 Christopher Rabotin et al. (cf. AUTHORS.md) + * 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/. + * + * Documentation: https://nyxspace.com/ + */ + +use std::path::PathBuf; + +use anise::naif::kpl::parser::{convert_fk as convert_fk_rs, convert_tpc as convert_tpc_rs}; +use anise::structure::dataset::DataSetError; +use anise::structure::planetocentric::ellipsoid::Ellipsoid; +use pyo3::prelude::*; + +pub(crate) fn register_utils(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { + let sm = PyModule::new(py, "_anise.utils")?; + sm.add_class::()?; + sm.add_function(wrap_pyfunction!(convert_fk, sm)?)?; + sm.add_function(wrap_pyfunction!(convert_tpc, sm)?)?; + parent_module.add_submodule(sm)?; + Ok(()) +} + +/// Converts a KPL/FK file, that defines frame constants like fixed rotations, and frame name to ID mappings into the EulerParameterDataSet equivalent ANISE file. +/// KPL/FK files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. +#[pyfunction] +fn convert_fk( + fk_file_path: String, + anise_output_path: String, + show_comments: Option, + overwrite: Option, +) -> Result<(), DataSetError> { + let dataset = convert_fk_rs(fk_file_path, show_comments.unwrap_or(false))?; + + dataset.save_as( + &PathBuf::from(anise_output_path), + overwrite.unwrap_or(false), + )?; + + Ok(()) +} + +/// Converts two KPL/TPC files, one defining the planetary constants as text, and the other defining the gravity parameters, into the PlanetaryDataSet equivalent ANISE file. +/// KPL/TPC files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. +#[pyfunction] +fn convert_tpc( + pck_file_path: String, + gm_file_path: String, + anise_output_path: String, + overwrite: Option, +) -> Result<(), DataSetError> { + let dataset = convert_tpc_rs(pck_file_path, gm_file_path)?; + + dataset.save_as( + &PathBuf::from(anise_output_path), + overwrite.unwrap_or(false), + )?; + + Ok(()) +} diff --git a/anise/src/naif/kpl/parser.rs b/anise/src/naif/kpl/parser.rs index a1eef6fe..2c1efa4f 100644 --- a/anise/src/naif/kpl/parser.rs +++ b/anise/src/naif/kpl/parser.rs @@ -138,6 +138,8 @@ pub fn parse_file + fmt::Debug, I: KPLItem>( Ok(map) } +/// Converts two KPL/TPC files, one defining the planetary constants as text, and the other defining the gravity parameters, into the PlanetaryDataSet equivalent ANISE file. +/// KPL/TPC files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. pub fn convert_tpc + fmt::Debug>( pck: P, gm: P, @@ -309,6 +311,8 @@ pub fn convert_tpc + fmt::Debug>( Ok(dataset) } +/// Converts a KPL/FK file, that defines frame constants like fixed rotations, and frame name to ID mappings into the EulerParameterDataSet equivalent ANISE file. +/// KPL/FK files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. pub fn convert_fk + fmt::Debug>( fk_file_path: P, show_comments: bool, diff --git a/anise/src/py_errors.rs b/anise/src/py_errors.rs index d25d2ea5..05a84299 100644 --- a/anise/src/py_errors.rs +++ b/anise/src/py_errors.rs @@ -13,6 +13,7 @@ use crate::almanac::planetary::PlanetaryDataError; use crate::ephemerides::EphemerisError; use crate::errors::{AlmanacError, DecodingError, InputOutputError, IntegrityError, PhysicsError}; use crate::orientations::OrientationError; +use crate::structure::dataset::DataSetError; use core::convert::From; use pyo3::{exceptions::PyException, prelude::*}; @@ -65,3 +66,8 @@ impl From for PyErr { PyException::new_err(err.to_string()) } } +impl From for PyErr { + fn from(err: DataSetError) -> PyErr { + PyException::new_err(err.to_string()) + } +} From 1f002fea2190dd175151ce2b9683166d9f593899 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Tue, 9 Jan 2024 22:00:32 -0700 Subject: [PATCH 3/4] Add serde nalgebra --- Cargo.toml | 7 ++++++- anise/Cargo.toml | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30069709..c182d175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,9 @@ log = "=0.4" pretty_env_logger = "=0.5" tabled = "=0.15" const_format = "0.2" -nalgebra = "0.32" +nalgebra = { version = "0.32", default-features = true, features = [ + "serde-serialize", +] } approx = "=0.5.1" zerocopy = { version = "0.7.26", features = ["derive"] } bytes = "=1.5.0" @@ -44,6 +46,9 @@ heapless = "0.8.0" rstest = "0.18.2" pyo3 = { version = "0.20.0", features = ["multiple-pymethods"] } pyo3-log = "0.9.0" +serde = "1" +serde_derive = "1" +serde_dhall = "0.12" anise = { version = "0.2.1", path = "anise", default-features = false } diff --git a/anise/Cargo.toml b/anise/Cargo.toml index 0266d1e1..5350fea0 100644 --- a/anise/Cargo.toml +++ b/anise/Cargo.toml @@ -27,9 +27,9 @@ rstest = { workspace = true } pyo3 = { workspace = true, optional = true } pyo3-log = { workspace = true, optional = true } url = { version = "2.5.0", optional = true } -serde = "1" -serde_derive = "1" -serde_dhall = "0.12" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_dhall = { workspace = true, optional = true } reqwest = { version = "0.11.23", optional = true, features = ["blocking"] } platform-dirs = { version = "0.3.0", optional = true } @@ -48,7 +48,7 @@ default = ["metaload"] # Enabling this flag significantly increases compilation times due to Arrow and Polars. spkezr_validation = [] python = ["pyo3", "pyo3-log"] -metaload = ["url", "reqwest/blocking", "platform-dirs"] +metaload = ["serde_dhall", "url", "reqwest/blocking", "platform-dirs"] [[bench]] name = "iai_jpl_ephemerides" From f420ea5e441b5e0927d12b9310e261d7d8c88628 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Tue, 9 Jan 2024 22:13:17 -0700 Subject: [PATCH 4/4] Add CI Almanac config for reproducibility --- anise-py/tests/test_almanac.py | 2 +- anise/src/almanac/metaload/metaalmanac.rs | 2 +- data/ci_config.dhall | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 data/ci_config.dhall diff --git a/anise-py/tests/test_almanac.py b/anise-py/tests/test_almanac.py index 2e86f2d0..a148cc02 100644 --- a/anise-py/tests/test_almanac.py +++ b/anise-py/tests/test_almanac.py @@ -17,7 +17,7 @@ def test_state_transformation(): if environ.get("CI", False): # Load from meta kernel to not use Git LFS quota - data_path = Path(__file__).parent.joinpath("..", "..", "data", "default_meta.dhall") + data_path = Path(__file__).parent.joinpath("..", "..", "data", "ci_config.dhall") meta = MetaAlmanac(str(data_path)) print(meta) # Process the files to be loaded diff --git a/anise/src/almanac/metaload/metaalmanac.rs b/anise/src/almanac/metaload/metaalmanac.rs index a828db77..448fd5c0 100644 --- a/anise/src/almanac/metaload/metaalmanac.rs +++ b/anise/src/almanac/metaload/metaalmanac.rs @@ -78,7 +78,7 @@ impl FromStr for MetaAlmanac { type Err = MetaAlmanacError; fn from_str(s: &str) -> Result { - match serde_dhall::from_str(&s).parse::() { + match serde_dhall::from_str(s).parse::() { Err(e) => Err(MetaAlmanacError::ParseDhall { path: s.to_string(), err: format!("{e}"), diff --git a/data/ci_config.dhall b/data/ci_config.dhall new file mode 100644 index 00000000..4c0782c6 --- /dev/null +++ b/data/ci_config.dhall @@ -0,0 +1,14 @@ +-- Default Almanac +{ files = + [ { crc32 = Some 1921414410 + , uri = "http://public-data.nyxspace.com/anise/de440s.bsp" + } + , { crc32 = Some 1216081528 + , uri = "http://public-data.nyxspace.com/anise/pck08.pca" + } + , { crc32 = None Natural + , uri = + "http://public-data.nyxspace.com/anise/ci/earth_latest_high_prec-2023-09-08.bpc" + } + ] +}