diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..2ffe8a46
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,42 @@
+
+# Summary
+
+**Summarize the proposed changes**
+
+## Architectural Changes
+
+
+
+No change
+
+## New Features
+
+
+
+No change
+
+## Improvements
+
+
+
+No change
+
+## Bug Fixes
+
+
+
+No change
+
+## Testing and validation
+
+
+
+**Detail the changes in tests, including new tests and validations**
+
+## Documentation
+
+
+
+This PR does not primarily deal with documentation changes.
+
+
\ No newline at end of file
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 72bcf185..b248f55d 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -10,8 +10,8 @@ on:
workflow_dispatch:
jobs:
- type2_chebyshev:
- name: JPL DE Benchmark
+ ephem_type2_chebyshev:
+ name: SPICE versus ANISE Benchmark
runs-on: ubuntu-latest
steps:
- name: Checkout sources
@@ -28,38 +28,17 @@ jobs:
- name: Install CSPICE
run: sh dev-env-setup.sh && cd .. # Return to root
- - name: Bench
+ - name: Bench JPL Ephemerides
run: cargo bench --bench "*_jpl_ephemerides"
-
- - name: Save benchmark artifacts
- uses: actions/upload-artifact@v3
- with:
- name: jpl-development-ephemerides-benchmark
- path: target/criterion/**/report/*
-
- type13_hermite:
- name: Hermite Benchmark
- runs-on: ubuntu-latest
- steps:
- - name: Checkout sources
- uses: actions/checkout@v3
- with:
- lfs: true
-
- - name: Install stable toolchain
- uses: actions-rs/toolchain@v1
- with:
- toolchain: stable
- override: true
-
- - name: Install CSPICE
- run: sh dev-env-setup.sh && cd .. # Return to root
-
- - name: Bench
+
+ - name: Bench Spacecraft (Hermite type 13)
run: cargo bench --bench "*_spacecraft_ephemeris"
+
+ - name: Bench Binary planetary constants
+ run: cargo bench --bench "crit_bpc_rotation"
- name: Save benchmark artifacts
uses: actions/upload-artifact@v3
with:
- name: spacecraft-ephemeris-benchmark
+ name: jpl-development-ephemerides-benchmark
path: target/criterion/**/report/*
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index da6cb08f..2cdf2efe 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -117,14 +117,18 @@ jobs:
cargo run -- inspect data/de440.bsp
- name: Rust-SPICE JPL DE validation
- run: |
- RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_jplde_de440s --features spkezr_validation --release -- --nocapture --ignored
- RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_jplde_de440_full --features spkezr_validation --release -- --nocapture --ignored
+ run: RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_jplde --features spkezr_validation --release -- --nocapture --include-ignored --test-threads 1
- name: Rust-SPICE hermite validation
- run: |
- RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_hermite_type13_from_gmat --features spkezr_validation --release -- --nocapture --ignored
- RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_hermite_type13_with_varying_segment_sizes --features spkezr_validation --release -- --nocapture --ignored
+ run: RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_hermite_type13_ --features spkezr_validation --release -- --nocapture --include-ignored --test-threads 1
+
+ - name: Rust-SPICE PCK validation
+ run: RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_iau_rotation_to_parent --release -- --nocapture --ignored
+
+ - name: Rust-SPICE BPC validation
+ run: |
+ RUST_BACKTRACE=1 RUST_LOG=debug cargo test validate_bpc_ --release -- --nocapture --include-ignored --test-threads 1
+ RUST_BACKTRACE=1 RUST_LOG=debug cargo test de440s_translation_verif_venus2emb --release -- --nocapture --include-ignored --test-threads 1
# Now analyze the results and create pretty plots
- uses: actions/setup-python@v4
@@ -172,6 +176,10 @@ jobs:
cargo llvm-cov clean --workspace
cargo llvm-cov test --no-report -- --test-threads=1
cargo llvm-cov test --no-report --tests -- compile_fail
+ cargo llvm-cov test --no-report validate_iau_rotation_to_parent -- --nocapture --ignored
+ cargo llvm-cov test --no-report validate_bpc_to_iau_rotations -- --nocapture --ignored
+ cargo llvm-cov test --no-report validate_jplde_de440s --features spkezr_validation -- --nocapture --ignored
+ cargo llvm-cov test --no-report validate_hermite_type13_from_gmat --features spkezr_validation -- --nocapture --ignored
cargo llvm-cov report --lcov > lcov.txt
env:
RUSTFLAGS: --cfg __ui_tests
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..95fa09b9
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,62 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "cargo",
+ "command": "test",
+ "args": [
+ "validate_bpc_",
+ "--",
+ "--nocapture",
+ "--include-ignored",
+ "--test-threads",
+ "1",
+ ],
+ "problemMatcher": [
+ "$rustc"
+ ],
+ "group": "none",
+ "label": "ANISE: BPC validation"
+ },
+ {
+ "type": "cargo",
+ "command": "test",
+ "args": [
+ "validate_hermite_type13_",
+ "--features",
+ "spkezr_validation",
+ "--release",
+ "--",
+ "--nocapture",
+ "--include-ignored",
+ "--test-threads",
+ "1",
+ ],
+ "problemMatcher": [
+ "$rustc"
+ ],
+ "group": "none",
+ "label": "ANISE: SPK Hermite validation"
+ },
+ {
+ "type": "cargo",
+ "command": "test",
+ "args": [
+ "validate_jplde",
+ "--features",
+ "spkezr_validation",
+ "--release",
+ "--",
+ "--nocapture",
+ "--include-ignored",
+ "--test-threads",
+ "1",
+ ],
+ "problemMatcher": [
+ "$rustc"
+ ],
+ "group": "none",
+ "label": "ANISE: SPK Chebyshev validation"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index e5749a12..74bc97af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,37 +11,45 @@ keywords = ["attitude", "navigation", "instrument", "spacecraft", "ephemeris"]
categories = ["science", "simulation"]
readme = "README.md"
license = "MPL-2.0"
-exclude = ["cspice"]
+exclude = [
+ "cspice*",
+ "data",
+ "analysis",
+ ".vscode",
+ ".github",
+ ".venv",
+ ".vscode",
+ "*.sh",
+]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hifitime = "3.8"
-memmap2 = "0.9.0"
-crc32fast = "1.3.0"
+memmap2 = "=0.9.0"
+crc32fast = "=1.3.2"
der = { version = "0.7.8", features = ["derive", "alloc", "real"] }
-clap = { version = "3.1", features = ["derive"] }
-thiserror = "1.0"
-log = "0.4"
-pretty_env_logger = "0.5"
-tabled = "0.14"
+clap = { version = "4", features = ["derive"] }
+log = "=0.4"
+pretty_env_logger = "=0.5"
+tabled = "=0.14"
const_format = "0.2"
nalgebra = "0.32"
-approx = "0.5.1"
-zerocopy = { version = "0.7.3", features = ["derive"] }
-bytes = "1.4.0"
-snafu = { version = "0.7.4", features = ["backtrace"] }
+approx = "=0.5.1"
+zerocopy = { version = "0.7.26", features = ["derive"] }
+bytes = "=1.4.0"
+snafu = { version = "0.7.5", features = ["backtrace"] }
lexical-core = "0.8.5"
heapless = "0.8.0"
rstest = "0.18.2"
[dev-dependencies]
-rust-spice = "0.7.4"
-parquet = "47.0.0"
-arrow = "47.0.0"
+rust-spice = "0.7.6"
+parquet = "49.0.0"
+arrow = "49.0.0"
criterion = "0.5"
iai = "0.1"
-polars = { version = "0.34", features = ["lazy", "parquet"] }
+polars = { version = "0.34.2", features = ["lazy", "parquet"] }
rayon = "1.7"
[features]
@@ -61,5 +69,9 @@ name = "crit_jpl_ephemerides"
harness = false
[[bench]]
-name = "iai_spacecraft_ephemeris"
+name = "crit_spacecraft_ephemeris"
+harness = false
+
+[[bench]]
+name = "crit_bpc_rotation"
harness = false
diff --git a/README.md b/README.md
index 0b05f081..16404f39 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,191 @@
-# ANISE
+# ANISE (Attitude, Navigation, Instrument, Spacecraft, Ephemeris)
-This is the main implementation of the ANISE toolkit specifications in Rust.
+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.
-# Features
+[**Please fill out our user survey**](https://7ug5imdtt8v.typeform.com/to/qYDB14Hj)
-- Thread safe computations from the SPICE toolkit
-- Convert NAIF SPK files into ANISE files with `anise convert path-to-spk`, yields a 5% space reduction (only Chebyshev Type 2 currently supported)
-- Inspect an ANISE file with `anise inspect path-to-file`
-- Perform frame translations (no rotations yet) between whichever ephemeris is in the context, or from a provided Cartesian state into another frame
+## Introduction
-Please refer to https://github.com/anise-toolkit/specs for the specifications.
+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:
-# Design
-TODO
-## Implementation choices
-As with any specification, some implementation choices, or limitations, must be made. In particular, ANISE.rs does not use any memory allocation, therefore everything is statically allocated and lives on the program stack. This is important for performance for programs on soft real-time embedded devices.
++ 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).
-### Depth of translations and rotations
-In this implementation, a translation or a rotation may not be more than 8 nodes from the root of the ANISE context.
+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).
-**Behavior:** this library can still read an ANISE file which stores data deeper than 8 nodes, however, it will not be able to perform any translations or rotations which involve it, and instead return a `MaxTreeDepth` error.
+## Features
-**Practical example:**
-The following ephemeris is valid, can be stored, and computations made with this ephemeris (from central node of the context to the further away):
++ **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.
+## Getting Started
+
+## Installation
+
+```sh
+cargo add anise
+```
+
+## Usage
+
+Please refer to the [test suite](./tests/) for comprehensive examples until I write better documentation.
+
+### Full example
+
+ANISE provides the ability to create Cartesian states (also simply called `Orbit`s), calculate orbital elements from them in an error free way (computations that may fail return a `Result` type), and transform these states into other frames via the loaded context, called `Almanac`, which stores all of the SPICE and ANISE files you need.
+
+```rust
+use anise::prelude::*;
+// ANISE provides pre-built frames, but examples below show how to build them from their NAIF IDs.
+use anise::constants::frames::{EARTH_ITRF93, EARTH_J2000};
+
+// Initialize an empty Almanac
+let ctx = Almanac::default();
+
+// Load a SPK/BSP file
+let spk = SPK::load("data/de440.bsp").unwrap();
+// Load the high precision ITRF93 kernel
+let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+// Build the planetary constants file, which includes the gravitational parameters and the IAU low fidelity rotations
+use anise::naif::kpl::parser::convert_tpc;
+// Note that the PCK variable can also be serialized to disk to avoid having to rebuild it next time.
+let pck = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
+
+// And add all of these to the Almanac context
+let almanac = ctx
+ .with_spk(spk)
+ .unwrap()
+ .with_bpc(bpc)
+ .unwrap()
+ .with_planetary_data(pck);
+
+// Let's build an orbit
+// Start by grabbing a copy of the frame.
+let eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
+
+// Define an epoch, in TDB, but you may specify UTC, TT, TAI, GPST, and more.
+let epoch = Epoch::from_str("2021-10-29 12:34:56 TDB").unwrap();
+
+// Define the orbit from its Keplerian orbital elements.
+// Note that we must specify the frame of this orbit: ANISE checks all frames are valid before any translation or rotation, even internally.
+let orig_state = Orbit::keplerian(
+ 8_191.93, 1e-6, 12.85, 306.614, 314.19, 99.887_7, epoch, eme2k,
+);
+
+// Transform that orbit into another frame.
+let state_itrf93 = almanac
+ .transform_to(orig_state, EARTH_ITRF93, Aberration::None)
+ .unwrap();
+
+// The `:x` prints this orbit's Keplerian elements
+println!("{orig_state:x}");
+// The `:X` prints the prints the range, altitude, latitude, and longitude with respect to the planetocentric frame in floating point with units if frame is celestial,
+println!("{state_itrf93:X}");
+
+// Convert back
+let from_state_itrf93_to_eme2k = almanac
+ .transform_to(state_itrf93, EARTH_J2000, Aberration::None)
+ .unwrap();
+
+println!("{from_state_itrf93_to_eme2k}");
+
+// Check that our return data matches the original one exactly
+assert_eq!(orig_state, from_state_itrf93_to_eme2k);
+```
+
+### Loading and querying a PCK/BPC file (high fidelity rotation)
+
+```rust
+use anise::prelude::*;
+use anise::constants::frames::{EARTH_ITRF93, EME2000};
+
+let pck = "data/earth_latest_high_prec.bpc";
+
+let bpc = BPC::load(pck).unwrap();
+let almanac = Almanac::from_bpc(bpc).unwrap();
+
+// Load the useful frame constants
+use anise::constants::frames::*;
+
+// Define an Epoch in the dynamical barycentric time scale
+let epoch = Epoch::from_str("2020-11-15 12:34:56.789 TDB").unwrap();
+
+// Query for the DCM
+let dcm = almanac.rotate_from_to(EARTH_ITRF93, EME2000, epoch).unwrap();
+
+println!("{dcm}");
```
-Solar System barycenter
-╰─> Earth Moon Barycenter
- ╰─> Earth
- ╰─> ISS
- ╰─> Columbus
- ╰─> Hub window #1
- ╰─> Camera mount
- ╰─> Camera lense /!\ MAX DEPTH REACHED (cannot add a deeper ephemeris) /!\
+
+### Loading and querying a text PCK/KPL file (low fidelity rotation)
+
+```rust
+use anise::prelude::*;
+// Load the TPC converter, which will create the ANISE representation too, in ASN1 format, that you may reuse.
+use anise::naif::kpl::parser::convert_tpc;
+
+// Note that the ASN1 ANISE format for planetary data also stores the gravity parameters, so we must convert both at once into a single ANISE file.
+let planetary_data = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
+
+let almanac = Almanac {
+ planetary_data,
+ ..Default::default()
+};
+
+// Load the useful frame constants
+use anise::constants::frames::*;
+
+// Define an Epoch in the dynamical barycentric time scale
+let epoch = Epoch::from_str("2020-11-15 12:34:56.789 TDB").unwrap();
+
+// Query for the DCM to the immediate parent
+let dcm = almanac.rotation_to_parent(IAU_VENUS_FRAME, epoch).unwrap();
+
+println!("{dcm}");
+```
+
+### Loading and querying an SPK/BSP file (ephemeris)
+
+```rust
+use anise::prelude::*;
+use anise::constants::frames::*;
+
+let spk = SPK::load("./data/de440s.bsp").unwrap();
+let ctx = Almanac::from_spk(spk).unwrap();
+
+// Define an Epoch in the dynamical barycentric time scale
+let epoch = Epoch::from_str("2020-11-15 12:34:56.789 TDB").unwrap();
+
+let state = ctx
+ .translate_from_to(
+ VENUS_J2000,
+ EARTH_MOON_BARYCENTER_J2000,
+ epoch,
+ Aberration::None,
+ )
+ .unwrap();
+
+println!("{state}");
```
-# Development
-## Requirements
-1. `rustc` version `1.64` or higher (required for the 2021 edition): https://rust-lang.org/ (TODO: Set a minimum compatible rust version)
-2. `git`
-1. `rust-spice` is used for exhaustive testing of the SPICE interoperability. It requires the cspice library.
\ No newline at end of file
+## Contributing
+
+Contributions to ANISE are welcome! Whether it's in the form of feature requests, bug reports, code contributions, or documentation improvements, every bit of help is greatly appreciated.
+
+## License
+
+ANISE is distributed under the Mozilla Public License 2.0 (MPL-2.0), offering a balanced approach to open-source by allowing the use of source code within both open and proprietary software. MPL-2.0 requires that modifications to the covered code be released under the same license, thus ensuring improvements remain open-source. However, it allows the combining of the covered software with proprietary parts, providing flexibility for both academic and commercial integrations.
+
+For more details, please see the [full text of the license](./LICENSE) or read [a summary by Github](https://choosealicense.com/licenses/mpl-2.0/).
+
+## Acknowledgements
+
+ANISE is heavily inspired by the NAIF SPICE toolkit and its excellent documentation
+
+
+## Contact
+
+For any inquiries, feedback, or discussions, please [open an issue here](https://github.com/nyx-space/anise/issues) or contact the maintainer at christopher.rabotin@gmail.com.
diff --git a/benches/crit_bpc_rotation.rs b/benches/crit_bpc_rotation.rs
new file mode 100644
index 00000000..a9599e50
--- /dev/null
+++ b/benches/crit_bpc_rotation.rs
@@ -0,0 +1,46 @@
+use anise::{constants::orientations::ITRF93, prelude::*};
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+
+const NUM_QUERIES_PER_PAIR: f64 = 100.0;
+
+fn benchmark_spice_single_hop_type2_cheby(time_it: TimeSeries) {
+ for epoch in time_it {
+ black_box(spice::pxform(
+ "ECLIPJ2000",
+ "ITRF93",
+ epoch.to_tdb_seconds(),
+ ));
+ }
+}
+
+fn benchmark_anise_single_hop_type2_cheby(ctx: &Almanac, time_it: TimeSeries) {
+ for epoch in time_it {
+ black_box(
+ ctx.rotation_to_parent(Frame::from_orient_ssb(ITRF93), epoch)
+ .unwrap(),
+ );
+ }
+}
+
+pub fn criterion_benchmark(c: &mut Criterion) {
+ let start_epoch = Epoch::from_gregorian_at_noon(2012, 1, 1, TimeScale::ET);
+ let end_epoch = Epoch::from_gregorian_at_noon(2021, 1, 1, TimeScale::ET);
+ let time_step = ((end_epoch - start_epoch).to_seconds() / NUM_QUERIES_PER_PAIR).seconds();
+ let time_it = TimeSeries::exclusive(start_epoch, end_epoch - time_step, time_step);
+
+ let pck = "data/earth_latest_high_prec.bpc";
+ spice::furnsh(pck);
+ let bpc = BPC::load(pck).unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ c.bench_function("ANISE DAF/BPC single hop to parent", |b| {
+ b.iter(|| benchmark_anise_single_hop_type2_cheby(&almanac, time_it.clone()))
+ });
+
+ c.bench_function("SPICE DAF/BPC single hop to parent", |b| {
+ b.iter(|| benchmark_spice_single_hop_type2_cheby(time_it.clone()))
+ });
+}
+
+criterion_group!(bpc, criterion_benchmark);
+criterion_main!(bpc);
diff --git a/benches/crit_jpl_ephemerides.rs b/benches/crit_jpl_ephemerides.rs
index 3629a6c2..7cc97398 100644
--- a/benches/crit_jpl_ephemerides.rs
+++ b/benches/crit_jpl_ephemerides.rs
@@ -52,5 +52,5 @@ pub fn criterion_benchmark(c: &mut Criterion) {
});
}
-criterion_group!(de438s, criterion_benchmark);
-criterion_main!(de438s);
+criterion_group!(de440s, criterion_benchmark);
+criterion_main!(de440s);
diff --git a/benches/crit_spacecraft_ephemeris.rs b/benches/crit_spacecraft_ephemeris.rs
index 83fa0e31..ef949cbd 100644
--- a/benches/crit_spacecraft_ephemeris.rs
+++ b/benches/crit_spacecraft_ephemeris.rs
@@ -46,7 +46,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let ctx = Almanac::from_spk(spk)
.unwrap()
- .load_spk(spacecraft)
+ .with_spk(spacecraft)
.unwrap();
// Load SPICE data
diff --git a/benches/iai_spacecraft_ephemeris.rs b/benches/iai_spacecraft_ephemeris.rs
index c6f8bbe9..1d04e3de 100644
--- a/benches/iai_spacecraft_ephemeris.rs
+++ b/benches/iai_spacecraft_ephemeris.rs
@@ -31,7 +31,7 @@ fn benchmark_anise_single_hop_type13_hermite() {
let ctx = Almanac::from_spk(spk)
.unwrap()
- .load_spk(spacecraft)
+ .with_spk(spacecraft)
.unwrap();
let my_sc_j2k = Frame::from_ephem_j2000(-10000001);
diff --git a/data/pck00011.tpc b/data/pck00011.tpc
new file mode 100644
index 00000000..c1e08afa
--- /dev/null
+++ b/data/pck00011.tpc
@@ -0,0 +1,4318 @@
+KPL/PCK
+
+P_constants (PCK) SPICE kernel file pck00011.tpc
+===========================================================================
+
+ By: Nat Bachman (NAIF) 2022 December 27
+
+
+Toolkit Compatibility Warning
+--------------------------------------------------------
+
+ This file provides Mars system rotation data usable with the
+ N0067 and newer versions of the SPICE Toolkit. Using it to compute
+ Mars, Phobos, or Deimos orientation with Toolkit versions N0066
+ or earlier may cause fatal run-time memory corruption in user
+ programs. See "Software limitations" below.
+
+ All other data are compatible with the N0066 and earlier Toolkits.
+
+ NAIF provides the PCK file pck00011_n0066.tpc for use with
+ older Toolkits. That PCK contains all the data from this file,
+ except that, in that file, one of the phase angle polynomials
+ for Phobos is truncated to first order.
+
+
+Purpose
+--------------------------------------------------------
+
+ This file makes available for use in SPICE-based application
+ software orientation and size/shape data for natural bodies. The
+ principal source of the data is a published report by the IAU
+ Working Group on Cartographic Coordinates and Rotational Elements
+ [1].
+
+ Orientation and size/shape data not provided by this file may be
+ available in mission-specific PCK files. Such PCKs may be the
+ preferred data source for mission-related applications.
+ Mission-specific PCKs can be found in PDS archives or on the NAIF
+ web site at URL:
+
+ https://naif.jpl.nasa.gov/naif/data.html
+
+
+Version Description
+--------------------------------------------------------
+
+ This file was created on December 27, 2022 by NASA's Navigation
+ and Ancillary Information Facility (NAIF) group, located at the Jet
+ Propulsion Laboratory, Pasadena, CA.
+
+ The previous version of the file was
+
+ pck00010.tpc
+
+ That file was published October 21, 2011.
+
+ This version incorporates data from sources listed under "Sources and
+ References" below. The primary sources are [1] and [2]. This file
+ contains size, shape, and orientation data for all objects covered
+ by the previous version of the file.
+
+ New objects covered by this file but not by the previous
+ version are:
+
+ Asteroid 52 Europa
+ Comet 67P/Churyumov-Gerasimenko
+ Aegaeon (Saturn LIII)
+ Comet Hartley 2
+ Asteroid Psyche
+
+ Orientation data for the following objects provided by this file differ
+ from those provided by the previous version:
+
+ Mercury
+
+ Mars
+ Deimos
+ Phobos
+
+ Neptune
+
+ Ceres
+ Steins
+ Vesta
+
+ Borrelly
+ Tempel 1
+
+ Radii for the following objects provided by this file differ
+ from those provided by the previous version:
+
+ Sun
+
+ Mercury
+
+ Anthe
+ Atlas
+ Calypso
+ Daphnis
+ Epimetheus
+ Helene
+ Janus
+ Methone
+ Pallene
+ Pan
+ Pandora
+ Prometheus
+ Telesto
+
+ Pluto
+ Charon
+
+ Itokawa
+
+
+File Organization
+--------------------------------------------------------
+
+ The contents of this file are as follows.
+
+ Introductory Information:
+
+ -- Purpose
+
+ -- Version description
+
+ -- File Organization
+
+ -- Disclaimer
+
+ -- Sources
+
+ -- Explanatory notes
+
+ -- Body numbers and names
+
+
+ PcK Data:
+
+
+ Orientation Data
+ ----------------
+
+ -- Orientation constants for the Sun, planets, and
+ Pluto. Additional items included in this section:
+
+ - North geomagnetic centered dipole value
+ for the year 2023
+
+ -- Orientation constants for satellites
+
+ -- Orientation constants for asteroids
+
+ 52 Europa
+ Davida
+ Eros
+ Gaspra
+ Hartley 2 (data shown in comments only)
+ Ida
+ Itokawa
+ Lutetia
+ Pallas
+ Steins
+ Vesta
+
+ -- Orientation constants for comets
+
+ 19P/Borrelly
+ 67P/Churyumov-Gerasimenko
+ Hartley 2 (data shown in comments only)
+ 9P/Tempel 1
+
+
+ Orientation data provided in this file are used
+ by the SPICE Toolkit to evaluate the orientation
+ of body-fixed, body-centered reference frames
+ with respect to the ICRF frame ("J2000" in
+ SPICE documentation). These body-fixed frames
+ have names of the form
+
+ IAU_
+
+ for example
+
+ IAU_JUPITER
+
+ See the PCK Required Reading file pck.req for details.
+
+
+
+ Radii of Bodies
+ ---------------
+
+ -- Radii of Sun, planets, and Pluto
+
+ -- Radii of satellites, where available
+
+ -- Radii of asteroids
+
+ 52 Europa
+ Ceres
+ Davida
+ Eros
+ Gaspra
+ Ida
+ Itokawa
+ Lutetia
+ Mathilde
+ Psyche
+ Steins
+ Toutatis
+ Vesta
+
+ -- Radii of comets
+
+ 19P/Borrelly
+ 67P/Churyumov-Gerasimenko
+ 81P/Wild 2
+ 9P/Tempel 1
+ Halley
+ Hartley 2
+
+
+Disclaimer
+--------------------------------------------------------
+
+Applicability of Data
+
+ This P_constants file (PCK) may not contain the parameter values
+ that you prefer. NAIF suggests that you inspect this file visually
+ before proceeding with any critical or extended data processing.
+
+File Modifications by Users
+
+ Note that this file may be readily modified by you to change
+ values or add/delete parameters. NAIF requests that you update the
+ "by line," date, version description section, and file name
+ if you modify this file.
+
+ A user-modified file should be thoroughly tested before
+ being published or otherwise distributed.
+
+ P_constants files must conform to the standards described
+ in the two SPICE technical reference documents:
+
+ PCK Required Reading
+ Kernel Required Reading
+
+
+Known Limitations and Caveats
+
+ Accuracy
+ --------
+
+ In general, the orientation models given here are claimed by the
+ IAU Working Group Report [1] to be accurate to 0.1 degree
+ ([1], p. 9). However, NAIF notes that orientation models for
+ natural satellites and asteroids have in some cases changed
+ substantially with the availability of new observational data, so
+ users are urged to investigate the suitability for their
+ applications of the models presented here.
+
+ Earth orientation
+ -----------------
+
+ The IAU report [1] no longer provides rotational elements for the
+ Earth. Data in this file are from [3].
+
+ NAIF strongly cautions against using the earth rotation model
+ presented here, corresponding to the SPICE reference frame name
+ IAU_EARTH, for work demanding high accuracy. This model has been
+ determined by NAIF to have an error in the prime meridian location
+ of magnitude at least 150 arcseconds, with a local minimum
+ occurring during the year 1999. Regarding availability of better
+ earth orientation data for use with the SPICE system:
+
+ Earth orientation data are available from NAIF in the form of
+ binary earth PCK files. These files provide orientation data
+ for the ITRF93 (terrestrial) reference frame relative to the
+ ICRF.
+
+ NAIF employs an automated process to create these files; each
+ time JPL's Tracking Systems and Applications Section produces a
+ new earth orientation parameter (EOP) file, a new PCK is
+ produced. These PCKs cover a roughly 23 year time span starting
+ at Jan. 1, 2000. In these PCK files, the following effects are
+ accounted for in modeling the earth's rotation:
+
+ - Precession: 1976 IAU model
+
+ - Nutation: 1980 IAU model, plus interpolated
+ EOP nutation corrections
+
+ - Polar motion: interpolated from EOP file
+
+ - True sidereal time:
+
+ UT1 - UT1R (if needed): given by analytic formula
+ + TAI - UT1 (or UT1R): interpolated from EOP file
+ + UT1 - GMST: given by analytic formula
+ + equation of equinoxes: given by analytic formula
+
+ where
+
+ TAI = International Atomic Time
+ UT1 = Greenwich hour angle of computed mean sun - 12h
+ UT1R = Regularized UT1
+ GMST = Greenwich mean sidereal time
+
+ These kernels are available from the NAIF web site
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck
+
+ At this time, these kernels have file names of the form
+
+ earth_000101_yymmdd_yymmdd.bpc
+
+ The first date in the file name, meaning 2000 January 1, is the
+ file's coverage begin time. The second and third dates are,
+ respectively, the file's coverage end time and the epoch of the
+ last datum.
+
+ These binary PCK files are very accurate (error < 0.1
+ microradian) for epochs preceding the epoch of the last datum.
+ For later epochs, the error rises to several microradians.
+
+ Binary PCK files giving accurate earth orientation from 1972 to
+ 2007 and *low accuracy* predicted earth orientation from
+ 2020 to 2099 are also available in the same location.
+
+ Characteristics and names of the binary kernels described here
+ are subject to change. See the aareadme.txt file at the URL
+ above for details.
+
+
+ Lunar orientation
+ -----------------
+
+ The IAU report [1] no longer provides rotational elements for the
+ Moon. Data in this file are from [3].
+
+ The lunar orientation formula provided by this file is a
+ trigonometric polynomial approximation yielding the orientation of
+ the lunar "Mean Earth/Polar Axis" (ME) reference frame. The
+ SPICE reference frame name corresponding to this model is
+ IAU_MOON.
+
+ A more accurate approximation can be obtained by using both the
+ latest NAIF lunar frame kernel and the latest binary lunar orientation
+ PCK file. These files provide orientation data for the both the Mean
+ Earth/Polar Axis frame, which has the SPICE name MOON_ME, and the
+ Lunar Principal Axes frame, which has the SPICE name MOON_PA.
+
+ These files are available on the NAIF web site; lunar PCKs are
+ located at the PCK URL above; lunar frame kernels are located at
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/fk/satellites
+
+ The latest lunar frame kernel has a name of the form
+
+ moon_dennn_yymmdd.tf
+
+ The latest binary lunar PCK has a name of the form
+
+ moon_pa_dennn_yymmdd.bpc
+
+ See the "aareadme.txt" files in the paths shown above for details
+ on file contents and versions. We also suggest you refer to the
+ SPICE tutorial named "lunar_earth_pck-fk," which is available from
+ the NAIF web site.
+
+
+ Geomagnetic dipole
+ ------------------
+
+ The SPICE Toolkit doesn't currently contain software to model the
+ north geomagnetic centered dipole as a function of time.
+ As a convenience for users, this dipole's location at the
+ epoch 2023.0 was selected as a representative datum, and the
+ planetocentric longitude and latitude of this location have been
+ associated with the keywords
+
+ BODY399_N_GEOMAG_CTR_DIPOLE_LON
+ BODY399_N_GEOMAG_CTR_DIPOLE_LAT
+
+ Older values for the north geomagnetic centered dipole are
+ presented in comments as a discrete time series for the time range
+ 1945-2000. For details concerning the geomagnetic field model from
+ which these values were derived, including a discussion of the
+ model's accuracy, see [9] and [11].
+
+
+ Prime meridian offsets
+ ----------------------
+
+ Prime meridian offset kernel variables, which have names
+ of the form
+
+ BODYnnn_LONG_AXIS
+
+ are not used by SPICE geometry software. These variables should be
+ considered deprecated; however, they will be retained for
+ backwards compatibility.
+
+ Users wishing to specify an offset reflecting the orientation of a
+ reference ellipsoid relative to a body-fixed reference frame
+ specified here should do so by creating a constant-offset frame
+ (also called a "TK" frame) specification. See the Frames Required
+ Reading frames.req for details.
+
+ The Mars prime meridian offset given by [6] is provided for
+ informational purposes only.
+
+
+ Software limitations
+ --------------------
+
+ SPICE Toolkits prior to version N0067 cannot make use of
+ the Mars system orientation data provided in this file. These
+ older Toolkits are unable to detect and signal a SPICE error if
+ they are used to compute orientation of Mars, Phobos, or Deimos
+ using these data: memory corruption will occur in user applications
+ linked these Toolkits if the applications attempt such computations.
+ Any results, including those of unrelated computations, may be invalid
+ after such memory corruption occurs.
+
+
+Sources and References
+--------------------------------------------------------
+
+ Sources and background references for the constants listed in this
+ file are:
+
+
+ [1] Archinal, B.A., Acton, C.H., A'Hearn, M.F., Conrad, A.,
+ Consolmagno, G.J., Duxbury, T., Hestroffer, D., Hilton,
+ J.L., Kirk, R.L., Klinoner, S.A., McCarthy, D.,
+ Meech, K., Oberst, J., Ping., J., Seidelmann, P.K., Tholen,
+ D.J., Thomas, P.C., and Williams, I.P., "Report of the IAU
+ Working Group on Cartographic Coordinates and Rotational
+ Elements: 2015," Celestial Mechanics and Dynamical Astronomy
+ 130, Article number 22 (2018).
+ DOI: https://doi.org/10.1007/s10569-017-9805-5
+
+ [2] Archinal, B.A., Acton, C.H., Conrad, A., Duxbury, T.,
+ Hestroffer, D., Hilton, J.L., Jorda, L., Kirk, R.L.,
+ Klinoner, Margot, J.-L., S.A., Meech, K., Oberst,
+ Paganelli, F., J., Ping., J., Seidelmann, P.K., Stark, A.,
+ Tholen, Wang, Y., and Williams, I.P., "Correction to:
+ Report of the IAU Working Group on Cartographic Coordinates
+ and Rotational Elements: 2015."
+
+ [3] Archinal, B.A., A'Hearn, M.F., Bowell, E., Conrad, A.,
+ Consolmagno, G.J., Courtin, R., Fukushima, T.,
+ Hestroffer, D., Hilton, J.L., Krasinsky, G.A.,
+ Neumann, G., Oberst, J., Seidelmann, P.K., Stooke, P.,
+ Tholen, D.J., Thomas, P.C., and Williams, I.P.
+ "Report of the IAU Working Group on Cartographic Coordinates
+ and Rotational Elements: 2009."
+
+ [4] Archinal, B.A., A'Hearn, M.F., Conrad, A.,
+ Consolmagno, G.J., Courtin, R., Fukushima, T.,
+ Hestroffer, D., Hilton, J.L., Krasinsky, G.A.,
+ Neumann, G., Oberst, J., Seidelmann, P.K., Stooke, P.,
+ Tholen, D.J., Thomas, P.C., and Williams, I.P.
+ "Erratum to: Reports of the IAU Working Group on
+ Cartographic Coordinates and Rotational Elements: 2006 &
+ 2009."
+
+ [5] Seidelmann, P.K., Archinal, B.A., A'Hearn, M.F.,
+ Conrad, A., Consolmagno, G.J., Hestroffer, D.,
+ Hilton, J.L., Krasinsky, G.A., Neumann, G.,
+ Oberst, J., Stooke, P., Tedesco, E.F., Tholen, D.J.,
+ and Thomas, P.C. "Report of the IAU/IAG Working Group
+ on Cartographic Coordinates and Rotational Elements: 2006."
+
+ [6] Duxbury, Thomas C. (2001). "IAU/IAG 2000 Mars Cartographic
+ Conventions," presentation to the Mars Express Data
+ Archive Working Group, Dec. 14, 2001.
+
+ [7] Russell, C.T. and Luhmann, J.G. (1990). "Earth: Magnetic
+ Field and Magnetosphere." . Originally
+ published in "Encyclopedia of Planetary Sciences," J.H.
+ Shirley and R.W. Fainbridge, eds. Chapman and Hall,
+ New York, pp 208-211.
+
+ [8] Russell, C.T. (1971). "Geophysical Coordinate
+ Transformations," Cosmic Electrodynamics 2 184-186.
+ NAIF document 181.0.
+
+ [9] ESA/ESTEC Space Environment Information System (SPENVIS)
+ (2003). Web page: "Dipole approximations of the
+ geomagnetic field." .
+
+ [10] Davies, M.E., Abalakin, V.K., Bursa, M., Hunt, G.E.,
+ and Lieske, J.H. (1989). "Report of the IAU/IAG/COSPAR
+ Working Group on Cartographic Coordinates and Rotational
+ Elements of the Planets and Satellites: 1988," Celestial
+ Mechanics and Dynamical Astronomy, v.46, no.2, pp.
+ 187-204.
+
+ [11] International Association of Geomagnetism and Aeronomy
+ Web page: "International Geomagnetic Reference Field."
+ Discussion URL:
+
+ http://www.ngdc.noaa.gov/IAGA/vmod/igrf.html
+
+ Coefficients URL:
+
+ https://www.ngdc.noaa.gov/IAGA/vmod/coeffs/igrf13coeffs.txt
+
+ [12] Email communication from Dr. Brent Archinal (IAU WGCCRE Chair,
+ USGS): "Re: Shape of comet Hartley 2." Dated December 22, 2022.
+
+ [13] Seidelmann, P.K., Archinal, B.A., A'Hearn, M.F.,
+ Cruikshank, D.P., Hilton, J.L., Keller, H.U., Oberst, J.,
+ Simon, J.L., Stooke, P., Tholen, D.J., and Thomas, P.C.
+ "Report of the IAU/IAG Working Group on Cartographic
+ Coordinates and Rotational Elements of the Planets and
+ Satellites: 2003," Unpublished.
+
+
+ Most values are from [1]. All exceptions are
+ commented where they occur in this file. The exceptions are:
+
+ -- Phobos prime meridian constants are from [2].
+
+ -- Lunar orientation data are from [3].
+
+ -- Earth orientation data are from [3].
+
+ -- North geomagnetic centered dipole values were
+ computed by Nat Bachman from the 13th generation IGRF.
+ The data source was [11].
+
+ "Old values" listed are from the SPICE PCK file pck00010.tpc
+ dated October 21, 2011. Most of these values came from the 2009
+ IAU report [3].
+
+
+
+Explanatory Notes
+--------------------------------------------------------
+
+ This file, which is logically part of the SPICE P-kernel, contains
+ constants used to model the orientation, size and shape of the
+ Sun, planets, natural satellites, and selected comets and
+ asteroids. The orientation models express the direction of the
+ pole and location of the prime meridian of a body as a function of
+ time. The size/shape models ("shape models" for short) represent
+ all bodies as ellipsoids, using two equatorial radii and a polar
+ radius. Spheroids and spheres are obtained when two or all three
+ radii are equal.
+
+ The SPICE Toolkit routines that use this file are documented in
+ the SPICE "Required Reading" file pck.req. They are also
+ documented in the "PCK" SPICE tutorial, which is available on
+ the NAIF web site.
+
+File Format
+
+ A terse description of the PCK file format is given here. See the
+ SPICE "Required Reading" files pck.req and kernel.req for a
+ detailed explanation of the SPICE text kernel file format. The
+ files pck.req and kernel.req are included in the documentation
+ provided with the SPICE Toolkit.
+
+ The file starts out with the ``ID word'' string
+
+ KPL/PCK
+
+ This string identifies the file as a text kernel containing PCK
+ data.
+
+ This file consists of a series of comment blocks and data blocks.
+ Comment blocks, which contain free-form descriptive or explanatory
+ text, are preceded by a \begintext token. Data blocks follow a
+ \begindata token. In order to be recognized, each of these tokens
+ must be placed on a line by itself.
+
+ The portion of the file preceding the first data block is treated
+ as a comment block; it doesn't require an initial \begintext
+ token.
+
+ This file identifies data using a series of
+
+ KEYWORD = VALUE
+
+ assignments. The left hand side of each assignment is a
+ "kernel variable" name; the right hand side is an associated value
+ or list of values. SPICE kernel pool access routines (see kernel.req)
+ enable other SPICE routines and user applications to retrieve the
+ set of values associated with each kernel variable name.
+
+ Kernel variable names are case-sensitive and are limited to
+ 32 characters in length.
+
+ Numeric values may be integer or floating point. String values
+ are normally limited to 80 characters in length; however, SPICE
+ provides a mechanism for identifying longer, "continued" strings.
+ See the SPICE routine STPOOL for details.
+
+ String values are single quoted.
+
+ When the right hand side of an assignment is a list of values,
+ the list items may be separated by commas or simply by blanks.
+ The list must be bracketed by parentheses. Example:
+
+ BODY399_RADII = ( 6378.1366 6378.1366 6356.7519 )
+
+ Any blanks preceding or following keyword names, values and equal
+ signs are ignored.
+
+ Assignments may be spread over multiple lines, for example:
+
+ BODY399_RADII = ( 6378.1366
+ 6378.1366
+ 6356.7519 )
+
+ This file may contain blank lines anywhere. Non-printing
+ characters including TAB should not be present in the file: the
+ presence of such characters may cause formatting errors when the
+ file is viewed.
+
+Time systems and reference frames
+
+ The 2015 IAU Working Group Report [1] states the time scale used
+ as the independent variable for the rotation formulas is
+ Barycentric Dynamical Time (TDB) and that the epoch of variable
+ quantities is J2000 TDB (2000 Jan 1 12:00:00 TDB, Julian ephemeris
+ date 2451545.0 TDB). Throughout SPICE documentation and in this
+ file, we use the names "J2000 TDB" and "J2000" for this epoch. The
+ name "J2000.0" is equivalent.
+
+ SPICE documentation refers to the time system used in this file
+ as either "ET" or "TDB." SPICE software makes no distinction
+ between TDB and the time system associated with the independent
+ variable of the JPL planetary ephemerides T_eph.
+
+ The inertial reference frame used for the rotational elements in
+ this file is identified by [1] as the ICRF (International
+ Celestial Reference Frame).
+
+ The SPICE PCK software that reads this file uses the label "J2000"
+ to refer to the ICRF; this is actually a mislabeling which has
+ been retained in the interest of backward compatibility. Using
+ data from this file, by means of calls to the SPICE frame
+ transformation routines, will actually compute orientation
+ relative to the ICRF.
+
+ The difference between the J2000 frame and the ICRF is
+ on the order of 100 milliarcseconds and is well below the
+ accuracy level of the formulas in this file.
+
+Orientation models
+
+ All of the complete orientation models use three Euler angles to
+ describe the orientation of the coordinate axes of the "Body Equator
+ and Prime Meridian" system with respect to an inertial system. By
+ default, the inertial system is the ICRF (labeled as "J2000"), but
+ other inertial frames can be specified in the file. See the PCK
+ Required Reading for details.
+
+ The first two angles, in order, are the ICRF right ascension and
+ declination (henceforth RA and DEC) of the north pole of a body as
+ a function of time. The third angle is the prime meridian location
+ (represented by "W"), which is expressed as a rotation about the
+ north pole, and is also a function of time.
+
+ For each body, the expressions for the north pole's right
+ ascension and declination, as well as prime meridian location, are
+ sums (as far as the models that appear in this file are concerned)
+ of quadratic polynomials and trigonometric polynomials, where the
+ independent variable is time.
+
+ In this file, the time arguments in expressions always refer to
+ Barycentric Dynamical Time (TDB), measured in centuries or days
+ past a reference epoch. By default, the reference epoch is the
+ J2000 epoch, which is Julian ephemeris date 2451545.0 (2000 Jan 1
+ 12:00:00 TDB), but other epochs can be specified in the file. See
+ the PCK Required Reading for details.
+
+ Orientation models for satellites and some planets (including
+ Jupiter) involve both polynomial terms and trigonometric terms.
+ The arguments of the trigonometric terms are linear or quadratic
+ polynomials. In this file, we call the arguments of these
+ trigonometric terms "nutation precession angles" or "phase angles."
+
+ Example: 2015 IAU Model for orientation of Jupiter. Note that
+ these values are used as an example only; see the data area below
+ for current values.
+
+ Right ascension
+ ---------------
+
+ alpha = 268.056595 - 0.006499 T + 0.000117 sin(Ja)
+ 0 + 0.000938 sin(Jb) + 0.001432 sin(Jc)
+ + 0.000030 sin(Jd) + 0.002150 sin(Je)
+
+ Declination
+ -----------
+
+ delta = 64.495303 + 0.002413 T + 0.000050 cos(Ja)
+ 0 + 0.000404 cos(Jb) + 0.000617 cos(Jc)
+ - 0.000013 cos(Jd) + 0.000926 cos(Je)
+
+ Prime meridian
+ --------------
+
+ W = 284.95 + 870.5366420 d
+
+
+ Here
+
+ T represents centuries past J2000 ( TDB ),
+
+ d represents days past J2000 ( TDB ).
+
+ Ja-Je are nutation precession angles.
+
+ In this file, the polynomials' coefficients above are assigned
+ to kernel variable names (left-hand-side symbols) as follows
+
+ BODY599_POLE_RA = ( 268.056595 -0.006499 0. )
+ BODY599_POLE_DEC = ( 64.495303 0.002413 0. )
+ BODY599_PM = ( 284.95 870.5360000 0. )
+
+ and the trigonometric polynomials' coefficients are assigned
+ as follows
+
+ BODY599_NUT_PREC_RA = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.000117
+ 0.000938
+ 0.001432
+ 0.000030
+ 0.002150 )
+
+ BODY599_NUT_PREC_DEC = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.000050
+ 0.000404
+ 0.000617
+ -0.000013
+ 0.000926 )
+
+ BODY599_NUT_PREC_PM = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.0
+ 0.0
+ 0.0
+ 0.0
+ 0.0 )
+
+ Note the number "599"; this is the NAIF ID code for Jupiter.
+
+ In this file, the polynomial expressions for the nutation
+ precession angles are listed along with the planet's RA, DEC, and
+ prime meridian terms. Below are the 2006 IAU nutation precession
+ angles for the Jupiter system.
+
+ J1 = 73.32 + 91472.9 T
+ J2 = 24.62 + 45137.2 T
+ J3 = 283.90 + 4850.7 T
+ J4 = 355.80 + 1191.3 T
+ J5 = 119.90 + 262.1 T
+ J6 = 229.80 + 64.3 T
+ J7 = 352.25 + 2382.6 T
+ J8 = 113.35 + 6070.0 T
+
+ J9 = 146.64 + 182945.8 T
+ J10 = 49.24 + 90274.4 T
+
+ Ja = 99.360714 + 4850.4046 T
+ Jb = 175.895369 + 1191.9605 T
+ Jc = 300.323162 + 262.5475 T
+ Jd = 114.012305 + 6070.2476 T
+ Je = 49.511251 + 64.3000 T
+
+ Here
+
+ T represents centuries past J2000 ( TDB )
+
+ J1-J10 and Ja-Je are the nutation precession angles. The angles
+ J9 and J10 are equal to 2*J1 and 2*J2, respectively.
+
+ Angles J9 and J10 are not present in [1]; they have been added
+ to fit the terms 2*J1 and 2*J2, which appear in the orientation
+ models of several satellites, into a form that can be accepted
+ by the PCK system.
+
+ The assignment of the nutation precession angles for the
+ Jupiter system is as follows:
+
+ BODY5_NUT_PREC_ANGLES = ( 73.32 91472.9
+ 24.62 45137.2
+ 283.90 4850.7
+ 355.80 1191.3
+ 119.90 262.1
+ 229.80 64.3
+ 352.25 2382.6
+ 113.35 6070.0
+ 146.64 182945.8
+ 49.24 90274.4
+ 99.360714 4850.4046
+ 175.895369 1191.9605
+ 300.323162 262.5475
+ 114.012305 6070.2476
+ 49.511251 64.3000 )
+
+ You'll see an additional symbol grouped with the ones listed
+ above; it is
+
+ BODY599_LONG_AXIS
+
+ This is a deprecated feature; see the note on "Prime meridian
+ offsets" under "Known Limitations and Caveats" above.
+
+ The pattern of the formulas for satellite orientation is similar
+ to that for Jupiter. Example: 2006 IAU values for Io. Again, these
+ values are used as an example only; see the data area below for
+ current values.
+
+ Right ascension
+ ---------------
+
+ alpha = 268.05 - 0.009 T + 0.094 sin(J3) + 0.024 sin(J4)
+ 0
+
+ Declination
+ -----------
+
+ delta = 64.50 + 0.003 T + 0.040 cos(J3) + 0.011 cos(J4)
+ 0
+
+ Prime meridian
+ --------------
+
+ W = 200.39 + 203.4889538 d - 0.085 sin(J3) - 0.022 sin(J4)
+
+
+ d represents days past J2000.
+
+ J3 and J4 are nutation precession angles.
+
+ The polynomial terms are assigned to symbols by the statements
+
+ BODY501_POLE_RA = ( 268.05 -0.009 0. )
+ BODY501_POLE_DEC = ( 64.50 0.003 0. )
+ BODY501_PM = ( 200.39 203.4889538 0. )
+
+ The coefficients of the trigonometric terms are assigned to symbols by
+ the statements
+
+ BODY501_NUT_PREC_RA = ( 0. 0. 0.094 0.024 )
+ BODY501_NUT_PREC_DEC = ( 0. 0. 0.040 0.011 )
+ BODY501_NUT_PREC_PM = ( 0. 0. -0.085 -0.022 )
+
+ 501 is the NAIF ID code for Io.
+
+ SPICE software expects the models for satellite orientation to
+ follow the form of the model shown here: the polynomial portions of the
+ RA, DEC, and W expressions are expected to be quadratic, the
+ trigonometric terms for RA and W (satellite prime meridian) are expected
+ to be linear combinations of sines of nutation precession angles, the
+ trigonometric terms for DEC are expected to be linear combinations of
+ cosines of nutation precession angles, and the polynomials for the
+ nutation precession angles themselves are expected to be linear or
+ quadratic.
+
+ Eventually, the software will handle more complex expressions, we
+ expect.
+
+
+Shape models
+
+ There is only one kind of shape model supported by the SPICE
+ Toolkit software at present: the triaxial ellipsoid. The 2015 IAU
+ report [1] does not use any other models, except in the case of
+ Mars, where separate values are given for the north and south
+ polar radii. In this file, we provide as a datum the mean Mars
+ polar radius provided by [1]. The North and South values are
+ included as comments.
+
+ For each body, three radii are listed: The first number is
+ the largest equatorial radius, the second number is the smaller
+ equatorial radius, and the third is the polar radius.
+
+ Example: Radii of the Earth.
+
+ BODY399_RADII = ( 6378.1366 6378.1366 6356.7519 )
+
+
+
+Body Numbers and Names
+--------------------------------------------------------
+
+
+ The following NAIF body ID codes and body names appear in this
+ file. See the NAIF IDs Required Reading file naif_ids.req for
+ a detailed discussion and a complete list of ID codes and names.
+
+
+ 1 Mercury barycenter
+ 2 Venus barycenter
+ 3 Earth barycenter
+ 4 Mars barycenter
+ 5 Jupiter barycenter
+ 6 Saturn barycenter
+ 7 Uranus barycenter
+ 8 Neptune barycenter
+ 9 Pluto barycenter
+ 10 Sun
+
+
+ 199 Mercury
+
+
+ 299 Venus
+
+
+ 399 Earth
+
+ 301 Moon
+
+
+ 499 Mars
+
+ 401 Phobos 402 Deimos
+
+
+ 599 Jupiter
+
+ 501 Io 502 Europa 503 Ganymede 504 Callisto
+ 505 Amalthea 506 Himalia 507 Elara 508 Pasiphae
+ 509 Sinope 510 Lysithea 511 Carme 512 Ananke
+ 513 Leda 514 Thebe 515 Adrastea 516 Metis
+
+
+ 699 Saturn
+
+ 601 Mimas 602 Enceladus 603 Tethys 604 Dione
+ 605 Rhea 606 Titan 607 Hyperion 608 Iapetus
+ 609 Phoebe 610 Janus 611 Epimetheus 612 Helene
+ 613 Telesto 614 Calypso 615 Atlas 616 Prometheus
+ 617 Pandora 618 Pan 632 Methone 633 Pallene
+ 634 Polydeuces 635 Daphnis 649 Anthe 653 Aegaeon
+
+
+ 799 Uranus
+
+ 701 Ariel 702 Umbriel 703 Titania 704 Oberon
+ 705 Miranda 706 Cordelia 707 Ophelia 708 Bianca
+ 709 Cressida 710 Desdemona 711 Juliet 712 Portia
+ 713 Rosalind 714 Belinda 715 Puck
+
+
+ 899 Neptune
+
+ 801 Triton 802 Nereid 803 Naiad 804 Thalassa
+ 805 Despina 806 Galatea 807 Larissa 808 Proteus
+
+
+ 999 Pluto
+
+ 901 Charon
+
+
+ 1000005 Comet 19P/Borrelly
+ 1000012 Comet 67P/Churyumov-Gerasimenko
+ 1000036 Comet Halley
+ 1000041 Comet Hartley 2
+ 1000093 Comet 9P/Tempel 1
+ 1000107 Comet 81P/Wild 2
+
+ 2000001 Asteroid Ceres
+ 2000002 Asteroid Pallas
+ 2000016 Asteroid Psyche
+ 2000004 Asteroid Vesta
+ 2000021 Asteroid Lutetia
+ 2000052 Asteroid 52 Europa
+ 2000216 Asteroid Kleopatra
+ 2000253 Asteroid Mathilde
+ 2000433 Asteroid Eros
+ 2000511 Asteroid Davida
+ 2002867 Asteroid Steins
+ 2004179 Asteroid Toutatis
+ 2025143 Asteroid Itokawa
+ 2431010 Asteroid Ida
+ 9511010 Asteroid Gaspra
+
+
+Orientation Constants for the Sun and Planets
+--------------------------------------------------------
+
+
+Sun
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ \begindata
+
+ BODY10_POLE_RA = ( 286.13 0. 0. )
+ BODY10_POLE_DEC = ( 63.87 0. 0. )
+ BODY10_PM = ( 84.176 14.18440 0. )
+ BODY10_LONG_AXIS = ( 0. )
+
+\begintext
+
+Mercury
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+
+ body199_pole_ra = ( 281.0097 -0.0328 0. )
+ body199_pole_dec = ( 61.4143 -0.0049 0. )
+ body199_pm = ( 329.5469 6.1385025 0. )
+
+ body199_long_axis = ( 0. )
+
+ body199_nut_prec_ra = ( 0. 0. 0. 0. 0. )
+
+ body199_nut_prec_dec = ( 0. 0. 0. 0. 0. )
+
+ body199_nut_prec_pm = ( 0.00993822
+ -0.00104581
+ -0.00010280
+ -0.00002364
+ -0.00000532 )
+
+
+ Current values:
+
+\begindata
+
+ BODY199_POLE_RA = ( 281.0103 -0.0328 0. )
+ BODY199_POLE_DEC = ( 61.4155 -0.0049 0. )
+ BODY199_PM = ( 329.5988 6.1385108 0. )
+
+ BODY199_LONG_AXIS = ( 0. )
+
+ BODY199_NUT_PREC_RA = ( 0. 0. 0. 0. 0. )
+
+ BODY199_NUT_PREC_DEC = ( 0. 0. 0. 0. 0. )
+
+ BODY199_NUT_PREC_PM = ( 0.01067257
+ -0.00112309
+ -0.00011040
+ -0.00002539
+ -0.00000571 )
+\begintext
+
+ The linear coefficients have been scaled up from degrees/day
+ to degrees/century, because the SPICELIB PCK reader expects
+ these units. The original constants were:
+
+ 174.7910857 4.092335
+ 349.5821714 8.184670
+ 164.3732571 12.277005
+ 339.1643429 16.369340
+ 153.9554286 20.461675
+
+\begindata
+
+ BODY1_NUT_PREC_ANGLES = ( 174.7910857 0.14947253587500003E+06
+ 349.5821714 0.29894507175000006E+06
+ 164.3732571 0.44841760762500006E+06
+ 339.1643429 0.59789014350000012E+06
+ 153.9554286 0.74736267937499995E+06 )
+
+\begintext
+
+
+Venus
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY299_POLE_RA = ( 272.76 0. 0. )
+ BODY299_POLE_DEC = ( 67.16 0. 0. )
+ BODY299_PM = ( 160.20 -1.4813688 0. )
+
+ BODY299_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+Earth
+
+ Old values:
+
+ The values shown below are those from the 2009 IAU report. The 2015
+ report does not provide orientation data for the Earth or Moon.
+
+\begindata
+
+ BODY399_POLE_RA = ( 0. -0.641 0. )
+ BODY399_POLE_DEC = ( 90. -0.557 0. )
+ BODY399_PM = ( 190.147 360.9856235 0. )
+ BODY399_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+ Nutation precession angles for the Earth-Moon system:
+
+ The linear coefficients have been scaled up from degrees/day
+ to degrees/century, because the SPICELIB PCK reader expects
+ these units. The original constants were:
+
+ 125.045D0 -0.0529921D0
+ 250.089D0 -0.1059842D0
+ 260.008D0 13.0120009D0
+ 176.625D0 13.3407154D0
+ 357.529D0 0.9856003D0
+ 311.589D0 26.4057084D0
+ 134.963D0 13.0649930D0
+ 276.617D0 0.3287146D0
+ 34.226D0 1.7484877D0
+ 15.134D0 -0.1589763D0
+ 119.743D0 0.0036096D0
+ 239.961D0 0.1643573D0
+ 25.053D0 12.9590088D0
+
+
+\begindata
+
+
+ BODY3_NUT_PREC_ANGLES = ( 125.045 -1935.5364525000
+ 250.089 -3871.0729050000
+ 260.008 475263.3328725000
+ 176.625 487269.6299850000
+ 357.529 35999.0509575000
+ 311.589 964468.4993100000
+ 134.963 477198.8693250000
+ 276.617 12006.3007650000
+ 34.226 63863.5132425000
+ 15.134 -5806.6093575000
+ 119.743 131.8406400000
+ 239.961 6003.1503825000
+ 25.053 473327.7964200000 )
+
+
+\begintext
+
+
+ North geomagnetic centered dipole:
+
+ The north dipole location is time-varying. The values shown
+ below, taken from [9], represent a discrete sampling of the
+ north dipole location from 1945 to 2000. The terms DGRF and
+ IGRF refer to, respectively, "Definitive Geomagnetic
+ Reference Field" and "International Geomagnetic Reference
+ Field." See references [9] and [11] for details.
+
+ Coordinates are planetocentric.
+
+ Data source Lat Lon
+ ----------- ----- ------
+ DGRF 1945 78.47 291.47
+ DGRF 1950 78.47 291.15
+ DGRF 1955 78.46 290.84
+ DGRF 1960 78.51 290.53
+ DGRF 1965 78.53 290.15
+ DGRF 1970 78.59 289.82
+ DGRF 1975 78.69 289.53
+ DGRF 1980 78.81 289.24
+ DGRF 1985 78.97 289.10
+ DGRF 1990 79.13 288.89
+ IGRF 1995 79.30 288.59
+ IGRF 2000 79.54 288.43
+
+ Original values:
+
+ Values are from [8]. Note the year of publication was 1971.
+
+ body399_mag_north_pole_lon = ( -69.761 )
+ body399_mag_north_pole_lat = ( 78.565 )
+
+ Previous values:
+
+ body399_n_geomag_ctr_dipole_lon = ( 287.62 )
+ body399_n_geomag_ctr_dipole_lat = ( 80.13 )
+
+ Current values:
+
+ Values are given for the epoch 2023.0 and were derived
+ by Nat Bachman from constants taken from IGRF-13. See [11].
+
+\begindata
+
+ BODY399_N_GEOMAG_CTR_DIPOLE_LON = ( 287.34 )
+ BODY399_N_GEOMAG_CTR_DIPOLE_LAT = ( 80.74 )
+
+\begintext
+
+
+Mars
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body499_pole_ra = ( 317.68143 -0.1061 0. )
+ body499_pole_dec = ( 52.88650 -0.0609 0. )
+ body499_pm = ( 176.630 350.89198226 0. )
+
+ body499_long_axis = ( 252. )
+
+ Below, the linear terms are scaled by 36525.0:
+
+ -0.4357640000000000 --> -15916.28010000000
+ 1128.409670000000 --> 41215163.19675000
+ -1.8151000000000000E-02 --> -662.9652750000000
+
+ We also introduce a fourth nutation precession angle, which
+ is the pi/2-complement of the third angle. This angle is used
+ in computing the prime meridian location for Deimos. See the
+ discussion of this angle below in the section containing orientation
+ constants for Deimos.
+
+ body4_nut_prec_angles = ( 169.51 -15916.2801
+ 192.93 41215163.19675
+ 53.47 -662.965275
+ 36.53 662.965275 )
+
+
+ Current values:
+
+\begindata
+
+ BODY499_POLE_RA = ( 317.269202 -0.10927547 0. )
+ BODY499_POLE_DEC = ( 54.432516 -0.05827105 0. )
+ BODY499_PM = ( 176.049863 +350.891982443297 0. )
+
+ BODY499_NUT_PREC_RA = ( 0 0 0 0 0
+ 0 0 0 0 0
+ 0.000068
+ 0.000238
+ 0.000052
+ 0.000009
+ 0.419057 )
+
+
+ BODY499_NUT_PREC_DEC = ( 0 0 0 0 0
+ 0 0 0 0 0
+ 0 0 0 0 0
+ 0.000051
+ 0.000141
+ 0.000031
+ 0.000005
+ 1.591274 )
+
+
+ BODY499_NUT_PREC_PM = ( 0 0 0 0 0
+ 0 0 0 0 0
+ 0 0 0 0 0
+ 0 0 0 0 0
+ 0.000145
+ 0.000157
+ 0.000040
+ 0.000001
+ 0.000001
+ 0.584542 )
+
+\begintext
+
+ SPICE support for quadratic phase angle polynomials
+ was introduced in the N0067 Toolkit version. Older Toolkits
+ cannot use the constants below. See the SPICE server at
+
+ https://naif.jpl.nasa.gov/naif/
+
+ for the file pck00011_n0066.tpc, which can be used with older
+ Toolkits.
+
+\begindata
+
+ BODY4_MAX_PHASE_DEGREE = 2
+
+ BODY4_NUT_PREC_ANGLES = (
+
+ 190.72646643 15917.10818695 0
+ 21.46892470 31834.27934054 0
+ 332.86082793 19139.89694742 0
+ 394.93256437 38280.79631835 0
+ 189.63271560 41215158.18420050 12.711923222
+
+ 121.46893664 660.22803474 0
+ 231.05028581 660.99123540 0
+ 251.37314025 1320.50145245 0
+ 217.98635955 38279.96125550 0
+ 196.19729402 19139.83628608 0
+
+ 198.991226 19139.4819985 0
+ 226.292679 38280.8511281 0
+ 249.663391 57420.7251593 0
+ 266.183510 76560.6367950 0
+ 79.398797 0.5042615 0
+
+ 122.433576 19139.9407476 0
+ 43.058401 38280.8753272 0
+ 57.663379 57420.7517205 0
+ 79.476401 76560.6495004 0
+ 166.325722 0.5042615 0
+
+ 129.071773 19140.0328244 0
+ 36.352167 38281.0473591 0
+ 56.668646 57420.9295360 0
+ 67.364003 76560.2552215 0
+ 104.792680 95700.4387578 0
+ 95.391654 0.5042615 0 )
+
+\begintext
+
+
+Jupiter
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ The number of nutation precession angles is 15. The ninth and
+ tenth are twice the first and second, respectively. The
+ eleventh through fifteenth correspond to angles JA-JE in
+ the 2015 IAU report.
+
+\begindata
+
+
+ BODY599_POLE_RA = ( 268.056595 -0.006499 0. )
+ BODY599_POLE_DEC = ( 64.495303 0.002413 0. )
+ BODY599_PM = ( 284.95 870.5360000 0. )
+ BODY599_LONG_AXIS = ( 0. )
+
+ BODY599_NUT_PREC_RA = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.000117
+ 0.000938
+ 0.001432
+ 0.000030
+ 0.002150 )
+
+ BODY599_NUT_PREC_DEC = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.000050
+ 0.000404
+ 0.000617
+ -0.000013
+ 0.000926 )
+
+ BODY599_NUT_PREC_PM = ( 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.0
+ 0.0
+ 0.0
+ 0.0
+ 0.0 )
+
+
+ BODY5_NUT_PREC_ANGLES = ( 73.32 91472.9
+ 24.62 45137.2
+ 283.90 4850.7
+ 355.80 1191.3
+ 119.90 262.1
+ 229.80 64.3
+ 352.25 2382.6
+ 113.35 6070.0
+ 146.64 182945.8
+ 49.24 90274.4
+ 99.360714 4850.4046
+ 175.895369 1191.9605
+ 300.323162 262.5475
+ 114.012305 6070.2476
+ 49.511251 64.3000 )
+\begintext
+
+
+Saturn
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY699_POLE_RA = ( 40.589 -0.036 0. )
+ BODY699_POLE_DEC = ( 83.537 -0.004 0. )
+ BODY699_PM = ( 38.90 810.7939024 0. )
+ BODY699_LONG_AXIS = ( 0. )
+
+\begintext
+
+ The first six angles given here are the angles S1
+ through S6 from the 2015 IAU report; the seventh and
+ eighth angles are 2*S1 and 2*S2, respectively.
+
+
+\begindata
+
+ BODY6_NUT_PREC_ANGLES = ( 353.32 75706.7
+ 28.72 75706.7
+ 177.40 -36505.5
+ 300.00 -7225.9
+ 316.45 506.2
+ 345.20 -1016.3
+ 706.64 151413.4
+ 57.44 151413.4 )
+\begintext
+
+
+Uranus
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY799_POLE_RA = ( 257.311 0. 0. )
+ BODY799_POLE_DEC = ( -15.175 0. 0. )
+ BODY799_PM = ( 203.81 -501.1600928 0. )
+ BODY799_LONG_AXIS = ( 0. )
+
+\begintext
+
+ The first 16 angles given here are the angles U1
+ through U16 from the 2000 report; the 17th and
+ 18th angles are 2*U11 and 2*U12, respectively.
+
+\begindata
+
+ BODY7_NUT_PREC_ANGLES = ( 115.75 54991.87
+ 141.69 41887.66
+ 135.03 29927.35
+ 61.77 25733.59
+ 249.32 24471.46
+ 43.86 22278.41
+ 77.66 20289.42
+ 157.36 16652.76
+ 101.81 12872.63
+ 138.64 8061.81
+ 102.23 -2024.22
+ 316.41 2863.96
+ 304.01 -51.94
+ 308.71 -93.17
+ 340.82 -75.32
+ 259.14 -504.81
+ 204.46 -4048.44
+ 632.82 5727.92 )
+
+\begintext
+
+
+
+Neptune
+
+ Old values are from the 2009 IAU report:
+
+
+ body899_pole_ra = ( 299.36 0. 0. )
+ body899_pole_dec = ( 43.46 0. 0. )
+ body899_pm = ( 253.18 536.3128492 0. )
+ body899_long_axis = ( 0. )
+
+
+ body899_nut_prec_ra = ( 0.70 0. 0. 0. 0. 0. 0. 0. )
+ body899_nut_prec_dec = ( -0.51 0. 0. 0. 0. 0. 0. 0. )
+ body899_nut_prec_pm = ( -0.48 0. 0. 0. 0. 0. 0. 0. )
+
+
+ Current values:
+
+\begindata
+
+ BODY899_POLE_RA = ( 299.36 0. 0. )
+ BODY899_POLE_DEC = ( 43.46 0. 0. )
+ BODY899_PM = ( 249.978 541.1397757 0. )
+ BODY899_LONG_AXIS = ( 0. )
+
+
+ BODY899_NUT_PREC_RA = ( 0.70 0. 0. 0. 0. 0. 0. 0. )
+ BODY899_NUT_PREC_DEC = ( -0.51 0. 0. 0. 0. 0. 0. 0. )
+ BODY899_NUT_PREC_PM = ( -0.48 0. 0. 0. 0. 0. 0. 0. )
+
+\begintext
+
+ The 2015 IAU report defines the nutation precession angles
+
+ N, N1, N2, ... , N7
+
+ and also uses the multiples of N1 and N7
+
+ 2*N1
+
+ and
+
+ 2*N7, 3*N7, ..., 9*N7
+
+ In this file, we treat the angles and their multiples as
+ separate angles. In the kernel variable
+
+ BODY8_NUT_PREC_ANGLES
+
+ the order of the angles is
+
+ N, N1, N2, ... , N7, 2*N1, 2*N7, 3*N7, ..., 9*N7
+
+ Each angle is defined by a linear polynomial, so two
+ consecutive array elements are allocated for each
+ angle. The first term of each pair is the constant term,
+ the second is the linear term.
+
+\begindata
+
+ BODY8_NUT_PREC_ANGLES = ( 357.85 52.316
+ 323.92 62606.6
+ 220.51 55064.2
+ 354.27 46564.5
+ 75.31 26109.4
+ 35.36 14325.4
+ 142.61 2824.6
+ 177.85 52.316
+ 647.840 125213.200
+ 355.700 104.632
+ 533.550 156.948
+ 711.400 209.264
+ 889.250 261.580
+ 1067.100 313.896
+ 1244.950 366.212
+ 1422.800 418.528
+ 1600.650 470.844 )
+
+\begintext
+
+
+
+
+Orientation Constants for the Dwarf Planet Pluto
+--------------------------------------------------------
+
+Pluto
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY999_POLE_RA = ( 132.993 0. 0. )
+ BODY999_POLE_DEC = ( -6.163 0. 0. )
+ BODY999_PM = ( 302.695 56.3625225 0. )
+ BODY999_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+
+Orientation constants for the satellites
+--------------------------------------------------------
+
+
+Satellites of Earth
+
+ Old values:
+
+ The values shown below are those from the 2009 IAU report. The 2015
+ report does not provide orientation data for the Earth or Moon.
+
+\begindata
+
+
+ BODY301_POLE_RA = ( 269.9949 0.0031 0. )
+ BODY301_POLE_DEC = ( 66.5392 0.0130 0. )
+ BODY301_PM = ( 38.3213 13.17635815 -1.4D-12 )
+ BODY301_LONG_AXIS = ( 0. )
+
+ BODY301_NUT_PREC_RA = ( -3.8787 -0.1204 0.0700 -0.0172
+ 0.0 0.0072 0.0 0.0
+ 0.0 -0.0052 0.0 0.0
+ 0.0043 )
+
+ BODY301_NUT_PREC_DEC = ( 1.5419 0.0239 -0.0278 0.0068
+ 0.0 -0.0029 0.0009 0.0
+ 0.0 0.0008 0.0 0.0
+ -0.0009 )
+
+ BODY301_NUT_PREC_PM = ( 3.5610 0.1208 -0.0642 0.0158
+ 0.0252 -0.0066 -0.0047 -0.0046
+ 0.0028 0.0052 0.0040 0.0019
+ -0.0044 )
+\begintext
+
+
+
+Satellites of Mars
+
+
+ Phobos
+
+ Old values are from the 2009 IAU report.
+
+
+ body401_pole_ra = ( 317.68 -0.108 0. )
+ body401_pole_dec = ( 52.90 -0.061 0. )
+ body401_pm = ( 35.06 1128.8445850 6.6443009930565219e-09 )
+
+ body401_long_axis = ( 0. )
+
+ body401_nut_prec_ra = ( 1.79 0. 0. 0. )
+ body401_nut_prec_dec = ( -1.08 0. 0. 0. )
+ body401_nut_prec_pm = ( -1.42 -0.78 0. 0. )
+
+ The quadratic prime meridian term is scaled by 1/36525**2:
+
+ 8.864000000000000 ---> 6.6443009930565219E-09
+
+
+ Current values:
+
+ Values from the 2015 IAU report [1] were corrected by [2], which
+ is used as the source for the data below.
+
+ The quadratic prime meridian term is scaled by 1/36525**2:
+
+ 12.72192797000000000000 ---> 9.536137031212154e-09
+
+\begindata
+
+ BODY401_POLE_RA = ( 317.67071657 -0.10844326 0. )
+ BODY401_POLE_DEC = ( 52.88627266 -0.06134706 0. )
+ BODY401_PM = ( 35.18774440 1128.84475928
+ 9.536137031212154e-09 )
+
+ BODY401_LONG_AXIS = ( 0. )
+
+ BODY401_NUT_PREC_RA = ( -1.78428399
+ 0.02212824
+ -0.01028251
+ -0.00475595 )
+
+
+
+ BODY401_NUT_PREC_DEC = ( -1.07516537
+ 0.00668626
+ -0.00648740
+ 0.00281576 )
+
+
+ BODY401_NUT_PREC_PM = ( 1.42421769
+ -0.02273783
+ 0.00410711
+ 0.00631964
+ -1.143 )
+
+\begintext
+
+
+ Deimos
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+
+ The Deimos prime meridian expression from that report is:
+
+
+ 2
+ W = 79.41 + 285.1618970 d - 0.520 T - 2.58 sin M
+ 3
+
+ + 0.19 cos M .
+ 3
+
+ At the present time, the PCK kernel software (the routine
+ BODEUL in particular) cannot handle the cosine term directly,
+ but we can represent it as
+
+ 0.19 sin M
+ 4
+
+ where
+
+ M = 90.D0 - M
+ 4 3
+
+ Therefore, the old nutation precession angle assignments for Phobos
+ and Deimos contain four coefficients rather than three.
+
+ The quadratic prime meridian term is scaled by 1/36525**2:
+
+ -0.5200000000000000 ---> -3.8978300049519307E-10
+
+
+ body402_pole_ra = ( 316.65 -0.108 0. )
+ body402_pole_dec = ( 53.52 -0.061 0. )
+ body402_pm = ( 79.41 285.1618970 -3.897830d-10 )
+ body402_long_axis = ( 0. )
+
+ body402_nut_prec_ra = ( 0. 0. 2.98 0. )
+ body402_nut_prec_dec = ( 0. 0. -1.78 0. )
+ body402_nut_prec_pm = ( 0. 0. -2.58 0.19 )
+
+
+ New values:
+
+\begindata
+
+ BODY402_POLE_RA = ( 316.65705808 -0.10518014 0. )
+ BODY402_POLE_DEC = ( 53.50992033 -0.05979094 0. )
+
+ BODY402_PM = ( 79.39932954 285.16188899 0. )
+ BODY402_LONG_AXIS = ( 0. )
+
+ BODY402_NUT_PREC_RA = ( 0 0 0 0 0
+ 3.09217726
+ 0.22980637
+ 0.06418655
+ 0.02533537
+ 0.00778695 )
+
+
+ BODY402_NUT_PREC_DEC = ( 0 0 0 0 0
+ 1.83936004
+ 0.14325320
+ 0.01911409
+ -0.01482590
+ 0.00192430 )
+
+
+ BODY402_NUT_PREC_PM = ( 0 0 0 0 0
+ -2.73954829
+ -0.39968606
+ -0.06563259
+ -0.02912940
+ 0.01699160 )
+
+
+\begintext
+
+
+Satellites of Jupiter
+
+
+ Io
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY501_POLE_RA = ( 268.05 -0.009 0. )
+ BODY501_POLE_DEC = ( 64.50 0.003 0. )
+ BODY501_PM = ( 200.39 203.4889538 0. )
+ BODY501_LONG_AXIS = ( 0. )
+
+ BODY501_NUT_PREC_RA = ( 0. 0. 0.094 0.024 )
+ BODY501_NUT_PREC_DEC = ( 0. 0. 0.040 0.011 )
+ BODY501_NUT_PREC_PM = ( 0. 0. -0.085 -0.022 )
+
+\begintext
+
+
+
+ Europa
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY502_POLE_RA = ( 268.08 -0.009 0. )
+ BODY502_POLE_DEC = ( 64.51 0.003 0. )
+ BODY502_PM = ( 36.022 101.3747235 0. )
+ BODY502_LONG_AXIS = ( 0. )
+
+ BODY502_NUT_PREC_RA = ( 0. 0. 0. 1.086 0.060 0.015 0.009 )
+ BODY502_NUT_PREC_DEC = ( 0. 0. 0. 0.468 0.026 0.007 0.002 )
+ BODY502_NUT_PREC_PM = ( 0. 0. 0. -0.980 -0.054 -0.014 -0.008 )
+
+\begintext
+
+
+ Ganymede
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY503_POLE_RA = ( 268.20 -0.009 0. )
+ BODY503_POLE_DEC = ( 64.57 0.003 0. )
+ BODY503_PM = ( 44.064 50.3176081 0. )
+ BODY503_LONG_AXIS = ( 0. )
+
+ BODY503_NUT_PREC_RA = ( 0. 0. 0. -0.037 0.431 0.091 )
+ BODY503_NUT_PREC_DEC = ( 0. 0. 0. -0.016 0.186 0.039 )
+ BODY503_NUT_PREC_PM = ( 0. 0. 0. 0.033 -0.389 -0.082 )
+
+\begintext
+
+
+ Callisto
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY504_POLE_RA = ( 268.72 -0.009 0. )
+ BODY504_POLE_DEC = ( 64.83 0.003 0. )
+ BODY504_PM = ( 259.51 21.5710715 0. )
+ BODY504_LONG_AXIS = ( 0. )
+
+ BODY504_NUT_PREC_RA = ( 0. 0. 0. 0. -0.068 0.590 0. 0.010 )
+ BODY504_NUT_PREC_DEC = ( 0. 0. 0. 0. -0.029 0.254 0. -0.004 )
+ BODY504_NUT_PREC_PM = ( 0. 0. 0. 0. 0.061 -0.533 0. -0.009 )
+
+\begintext
+
+
+ Amalthea
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY505_POLE_RA = ( 268.05 -0.009 0. )
+ BODY505_POLE_DEC = ( 64.49 0.003 0. )
+ BODY505_PM = ( 231.67 722.6314560 0. )
+ BODY505_LONG_AXIS = ( 0. )
+
+ BODY505_NUT_PREC_RA = ( -0.84 0. 0. 0. 0. 0. 0. 0. 0.01 0. )
+ BODY505_NUT_PREC_DEC = ( -0.36 0. 0. 0. 0. 0. 0. 0. 0. 0. )
+ BODY505_NUT_PREC_PM = ( 0.76 0. 0. 0. 0. 0. 0. 0. -0.01 0. )
+
+\begintext
+
+
+ Thebe
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY514_POLE_RA = ( 268.05 -0.009 0. )
+ BODY514_POLE_DEC = ( 64.49 0.003 0. )
+ BODY514_PM = ( 8.56 533.7004100 0. )
+ BODY514_LONG_AXIS = ( 0. )
+
+ BODY514_NUT_PREC_RA = ( 0. -2.11 0. 0. 0. 0. 0. 0. 0. 0.04 )
+ BODY514_NUT_PREC_DEC = ( 0. -0.91 0. 0. 0. 0. 0. 0. 0. 0.01 )
+ BODY514_NUT_PREC_PM = ( 0. 1.91 0. 0. 0. 0. 0. 0. 0. -0.04 )
+
+\begintext
+
+
+ Adrastea
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY515_POLE_RA = ( 268.05 -0.009 0. )
+ BODY515_POLE_DEC = ( 64.49 0.003 0. )
+ BODY515_PM = ( 33.29 1206.9986602 0. )
+ BODY515_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+ Metis
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY516_POLE_RA = ( 268.05 -0.009 0. )
+ BODY516_POLE_DEC = ( 64.49 0.003 0. )
+ BODY516_PM = ( 346.09 1221.2547301 0. )
+ BODY516_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Satellites of Saturn
+
+
+ Mimas
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY601_POLE_RA = ( 40.66 -0.036 0. )
+ BODY601_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY601_PM = ( 333.46 381.9945550 0. )
+ BODY601_LONG_AXIS = ( 0. )
+
+ BODY601_NUT_PREC_RA = ( 0. 0. 13.56 0. 0. 0. 0. 0. )
+ BODY601_NUT_PREC_DEC = ( 0. 0. -1.53 0. 0. 0. 0. 0. )
+ BODY601_NUT_PREC_PM = ( 0. 0. -13.48 0. -44.85 0. 0. 0. )
+
+\begintext
+
+
+ Enceladus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY602_POLE_RA = ( 40.66 -0.036 0. )
+ BODY602_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY602_PM = ( 6.32 262.7318996 0. )
+ BODY602_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Tethys
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY603_POLE_RA = ( 40.66 -0.036 0. )
+ BODY603_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY603_PM = ( 8.95 190.6979085 0. )
+ BODY603_LONG_AXIS = ( 0. )
+
+ BODY603_NUT_PREC_RA = ( 0. 0. 0. 9.66 0. 0. 0. 0. )
+ BODY603_NUT_PREC_DEC = ( 0. 0. 0. -1.09 0. 0. 0. 0. )
+ BODY603_NUT_PREC_PM = ( 0. 0. 0. -9.60 2.23 0. 0. 0. )
+
+\begintext
+
+
+ Dione
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY604_POLE_RA = ( 40.66 -0.036 0. )
+ BODY604_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY604_PM = ( 357.6 131.5349316 0. )
+ BODY604_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Rhea
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY605_POLE_RA = ( 40.38 -0.036 0. )
+ BODY605_POLE_DEC = ( 83.55 -0.004 0. )
+ BODY605_PM = ( 235.16 79.6900478 0. )
+ BODY605_LONG_AXIS = ( 0. )
+
+ BODY605_NUT_PREC_RA = ( 0. 0. 0. 0. 0. 3.10 0. 0. )
+ BODY605_NUT_PREC_DEC = ( 0. 0. 0. 0. 0. -0.35 0. 0. )
+ BODY605_NUT_PREC_PM = ( 0. 0. 0. 0. 0. -3.08 0. 0. )
+
+\begintext
+
+
+
+ Titan
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ Note removal of dependence on the nutation precession
+ angles.
+
+\begindata
+
+ BODY606_POLE_RA = ( 39.4827 0. 0. )
+ BODY606_POLE_DEC = ( 83.4279 0. 0. )
+ BODY606_PM = ( 186.5855 22.5769768 0. )
+ BODY606_LONG_AXIS = ( 0. )
+
+ BODY606_NUT_PREC_RA = ( 0. 0. 0. 0. 0. 0. 0. 0 )
+ BODY606_NUT_PREC_DEC = ( 0. 0. 0. 0. 0. 0. 0. 0 )
+ BODY606_NUT_PREC_PM = ( 0. 0. 0. 0. 0. 0. 0. 0 )
+
+\begintext
+
+
+
+ Hyperion
+
+ The IAU report does not give an orientation model for Hyperion.
+ Hyperion's rotation is in chaotic and is not predictable for
+ long periods.
+
+
+ Iapetus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY608_POLE_RA = ( 318.16 -3.949 0. )
+ BODY608_POLE_DEC = ( 75.03 -1.143 0. )
+ BODY608_PM = ( 355.2 4.5379572 0. )
+ BODY608_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Phoebe
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY609_POLE_RA = ( 356.90 0. 0. )
+ BODY609_POLE_DEC = ( 77.80 0. 0. )
+ BODY609_PM = ( 178.58 931.639 0. )
+ BODY609_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+ Janus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY610_POLE_RA = ( 40.58 -0.036 0. )
+ BODY610_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY610_PM = ( 58.83 518.2359876 0. )
+ BODY610_LONG_AXIS = ( 0. )
+
+ BODY610_NUT_PREC_RA = ( 0. -1.623 0. 0. 0. 0. 0. 0.023 )
+ BODY610_NUT_PREC_DEC = ( 0. -0.183 0. 0. 0. 0. 0. 0.001 )
+ BODY610_NUT_PREC_PM = ( 0. 1.613 0. 0. 0. 0. 0. -0.023 )
+
+\begintext
+
+
+
+ Epimetheus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY611_POLE_RA = ( 40.58 -0.036 0. )
+ BODY611_POLE_DEC = ( 83.52 -0.004 0. )
+ BODY611_PM = ( 293.87 518.4907239 0. )
+ BODY611_LONG_AXIS = ( 0. )
+
+ BODY611_NUT_PREC_RA = ( -3.153 0. 0. 0. 0. 0. 0.086 0. )
+ BODY611_NUT_PREC_DEC = ( -0.356 0. 0. 0. 0. 0. 0.005 0. )
+ BODY611_NUT_PREC_PM = ( 3.133 0. 0. 0. 0. 0. -0.086 0. )
+
+\begintext
+
+
+
+ Helene
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY612_POLE_RA = ( 40.85 -0.036 0. )
+ BODY612_POLE_DEC = ( 83.34 -0.004 0. )
+ BODY612_PM = ( 245.12 131.6174056 0. )
+ BODY612_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Telesto
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY613_POLE_RA = ( 50.51 -0.036 0. )
+ BODY613_POLE_DEC = ( 84.06 -0.004 0. )
+ BODY613_PM = ( 56.88 190.6979332 0. )
+ BODY613_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Calypso
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY614_POLE_RA = ( 36.41 -0.036 0. )
+ BODY614_POLE_DEC = ( 85.04 -0.004 0. )
+ BODY614_PM = ( 153.51 190.6742373 0. )
+ BODY614_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Atlas
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY615_POLE_RA = ( 40.58 -0.036 0. )
+ BODY615_POLE_DEC = ( 83.53 -0.004 0. )
+ BODY615_PM = ( 137.88 598.3060000 0. )
+ BODY615_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Prometheus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY616_POLE_RA = ( 40.58 -0.036 )
+ BODY616_POLE_DEC = ( 83.53 -0.004 )
+ BODY616_PM = ( 296.14 587.289000 )
+ BODY616_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Pandora
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY617_POLE_RA = ( 40.58 -0.036 0. )
+ BODY617_POLE_DEC = ( 83.53 -0.004 0. )
+ BODY617_PM = ( 162.92 572.7891000 0. )
+ BODY617_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+ Pan
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY618_POLE_RA = ( 40.6 -0.036 0. )
+ BODY618_POLE_DEC = ( 83.5 -0.004 0. )
+ BODY618_PM = ( 48.8 626.0440000 0. )
+ BODY618_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+
+
+Satellites of Uranus
+
+
+
+ Ariel
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY701_POLE_RA = ( 257.43 0. 0. )
+ BODY701_POLE_DEC = ( -15.10 0. 0. )
+ BODY701_PM = ( 156.22 -142.8356681 0. )
+ BODY701_LONG_AXIS = ( 0. )
+
+ BODY701_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0.29 )
+
+ BODY701_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0.28 )
+
+ BODY701_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0.05 0.08 )
+\begintext
+
+
+
+ Umbriel
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+\begindata
+
+ BODY702_POLE_RA = ( 257.43 0. 0. )
+ BODY702_POLE_DEC = ( -15.10 0. 0. )
+ BODY702_PM = ( 108.05 -86.8688923 0. )
+ BODY702_LONG_AXIS = ( 0. )
+
+ BODY702_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0. 0.21 )
+
+ BODY702_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0. 0.20 )
+
+ BODY702_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. -0.09 0. 0.06 )
+
+\begintext
+
+
+
+ Titania
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY703_POLE_RA = ( 257.43 0. 0. )
+ BODY703_POLE_DEC = ( -15.10 0. 0. )
+ BODY703_PM = ( 77.74 -41.3514316 0. )
+ BODY703_LONG_AXIS = ( 0. )
+
+ BODY703_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.29 )
+
+ BODY703_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.28 )
+
+ BODY703_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.08 )
+\begintext
+
+
+
+ Oberon
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY704_POLE_RA = ( 257.43 0. 0. )
+ BODY704_POLE_DEC = ( -15.10 0. 0. )
+ BODY704_PM = ( 6.77 -26.7394932 0. )
+ BODY704_LONG_AXIS = ( 0. )
+
+
+ BODY704_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0.16 )
+
+ BODY704_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0.16 )
+
+ BODY704_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0. 0.04 )
+\begintext
+
+
+
+ Miranda
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY705_POLE_RA = ( 257.43 0. 0. )
+ BODY705_POLE_DEC = ( -15.08 0. 0. )
+ BODY705_PM = ( 30.70 -254.6906892 0. )
+ BODY705_LONG_AXIS = ( 0. )
+
+ BODY705_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 4.41 0. 0. 0. 0.
+ 0. -0.04 0. )
+
+ BODY705_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 4.25 0. 0. 0. 0.
+ 0. -0.02 0. )
+
+ BODY705_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 1.15 -1.27 0. 0. 0.
+ 0. -0.09 0.15 )
+\begintext
+
+
+
+ Cordelia
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY706_POLE_RA = ( 257.31 0. 0. )
+ BODY706_POLE_DEC = ( -15.18 0. 0. )
+ BODY706_PM = ( 127.69 -1074.5205730 0. )
+ BODY706_LONG_AXIS = ( 0. )
+
+ BODY706_NUT_PREC_RA = ( -0.15 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY706_NUT_PREC_DEC = ( 0.14 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY706_NUT_PREC_PM = ( -0.04 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Ophelia
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY707_POLE_RA = ( 257.31 0. 0. )
+ BODY707_POLE_DEC = ( -15.18 0. 0. )
+ BODY707_PM = ( 130.35 -956.4068150 0. )
+ BODY707_LONG_AXIS = ( 0. )
+
+ BODY707_NUT_PREC_RA = ( 0. -0.09 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY707_NUT_PREC_DEC = ( 0. 0.09 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY707_NUT_PREC_PM = ( 0. -0.03 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Bianca
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY708_POLE_RA = ( 257.31 0. 0. )
+ BODY708_POLE_DEC = ( -15.18 0. 0. )
+ BODY708_PM = ( 105.46 -828.3914760 0. )
+ BODY708_LONG_AXIS = ( 0. )
+
+ BODY708_NUT_PREC_RA = ( 0. 0. -0.16 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY708_NUT_PREC_DEC = ( 0. 0. 0.16 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY708_NUT_PREC_PM = ( 0. 0. -0.04 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Cressida
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+
+ BODY709_POLE_RA = ( 257.31 0. 0. )
+ BODY709_POLE_DEC = ( -15.18 0. 0. )
+ BODY709_PM = ( 59.16 -776.5816320 0. )
+ BODY709_LONG_AXIS = ( 0. )
+
+
+ BODY709_NUT_PREC_RA = ( 0. 0. 0. -0.04 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+
+ BODY709_NUT_PREC_DEC = ( 0. 0. 0. 0.04 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+
+ BODY709_NUT_PREC_PM = ( 0. 0. 0. -0.01 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+
+\begintext
+
+
+
+ Desdemona
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY710_POLE_RA = ( 257.31 0. 0. )
+ BODY710_POLE_DEC = ( -15.18 0. 0. )
+ BODY710_PM = ( 95.08 -760.0531690 0. )
+ BODY710_LONG_AXIS = ( 0. )
+
+ BODY710_NUT_PREC_RA = ( 0. 0. 0. 0. -0.17
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY710_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.16
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY710_NUT_PREC_PM = ( 0. 0. 0. 0. -0.04
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Juliet
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY711_POLE_RA = ( 257.31 0. 0. )
+ BODY711_POLE_DEC = ( -15.18 0. 0. )
+ BODY711_PM = ( 302.56 -730.1253660 0. )
+ BODY711_LONG_AXIS = ( 0. )
+
+ BODY711_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ -0.06 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY711_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0.06 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY711_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ -0.02 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Portia
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY712_POLE_RA = ( 257.31 0. 0. )
+ BODY712_POLE_DEC = ( -15.18 0. 0. )
+ BODY712_PM = ( 25.03 -701.4865870 0. )
+ BODY712_LONG_AXIS = ( 0. )
+
+ BODY712_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. -0.09 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY712_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0.09 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY712_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. -0.02 0. 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Rosalind
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY713_POLE_RA = ( 257.31 0. 0. )
+ BODY713_POLE_DEC = ( -15.18 0. 0. )
+ BODY713_PM = ( 314.90 -644.6311260 0. )
+ BODY713_LONG_AXIS = ( 0. )
+
+ BODY713_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. -0.29 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY713_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0.28 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY713_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. -0.08 0. 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+ Belinda
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY714_POLE_RA = ( 257.31 0. 0. )
+ BODY714_POLE_DEC = ( -15.18 0. 0. )
+ BODY714_PM = ( 297.46 -577.3628170 0. )
+ BODY714_LONG_AXIS = ( 0. )
+
+ BODY714_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. -0.03 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY714_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0.03 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY714_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. -0.01 0.
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+\begintext
+
+
+
+ Puck
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY715_POLE_RA = ( 257.31 0. 0. )
+ BODY715_POLE_DEC = ( -15.18 0. 0. )
+ BODY715_PM = ( 91.24 -472.5450690 0. )
+ BODY715_LONG_AXIS = ( 0. )
+
+ BODY715_NUT_PREC_RA = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. -0.33
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY715_NUT_PREC_DEC = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. 0.31
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+ BODY715_NUT_PREC_PM = ( 0. 0. 0. 0. 0.
+ 0. 0. 0. 0. -0.09
+ 0. 0. 0. 0. 0.
+ 0. 0. 0. )
+
+\begintext
+
+
+
+
+Satellites of Neptune
+
+
+ Triton
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY801_POLE_RA = ( 299.36 0. 0. )
+ BODY801_POLE_DEC = ( 41.17 0. 0. )
+ BODY801_PM = ( 296.53 -61.2572637 0. )
+ BODY801_LONG_AXIS = ( 0. )
+
+
+ BODY801_NUT_PREC_RA = ( 0. 0. 0. 0.
+ 0. 0. 0. -32.35
+ 0. -6.28 -2.08 -0.74
+ -0.28 -0.11 -0.07 -0.02
+ -0.01 )
+
+
+ BODY801_NUT_PREC_DEC = ( 0. 0. 0. 0.
+ 0. 0. 0. 22.55
+ 0. 2.10 0.55 0.16
+ 0.05 0.02 0.01 0.
+ 0. )
+
+
+ BODY801_NUT_PREC_PM = ( 0. 0. 0. 0.
+ 0. 0. 0. 22.25
+ 0. 6.73 2.05 0.74
+ 0.28 0.11 0.05 0.02
+ 0.01 )
+
+\begintext
+
+
+
+
+ Nereid
+
+ Old values:
+
+ The 2009 IAU report [3] states that values for Nereid are not
+ given because Nereid is not in synchronous rotation with Neptune
+ (notes following table 2).
+
+ Current values:
+
+ The 2015 IAU report does not provide values for Nereid.
+
+ Naiad
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY803_POLE_RA = ( 299.36 0. 0. )
+ BODY803_POLE_DEC = ( 43.36 0. 0. )
+ BODY803_PM = ( 254.06 +1222.8441209 0. )
+ BODY803_LONG_AXIS = ( 0. )
+
+
+ BODY803_NUT_PREC_RA = ( 0.70 -6.49 0. 0.
+ 0. 0. 0. 0.
+ 0.25 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY803_NUT_PREC_DEC = ( -0.51 -4.75 0. 0.
+ 0. 0. 0. 0.
+ 0.09 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY803_NUT_PREC_PM = ( -0.48 4.40 0. 0.
+ 0. 0. 0. 0.
+ -0.27 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+\begintext
+
+
+ Thalassa
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY804_POLE_RA = ( 299.36 0. 0. )
+ BODY804_POLE_DEC = ( 43.45 0. 0. )
+ BODY804_PM = ( 102.06 1155.7555612 0. )
+ BODY804_LONG_AXIS = ( 0. )
+
+
+ BODY804_NUT_PREC_RA = ( 0.70 0. -0.28 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+
+ BODY804_NUT_PREC_DEC = ( -0.51 0. -0.21 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY804_NUT_PREC_PM = ( -0.48 0. 0.19 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+\begintext
+
+
+ Despina
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY805_POLE_RA = ( 299.36 0. 0. )
+ BODY805_POLE_DEC = ( 43.45 0. 0. )
+ BODY805_PM = ( 306.51 +1075.7341562 0. )
+ BODY805_LONG_AXIS = ( 0. )
+
+
+ BODY805_NUT_PREC_RA = ( 0.70 0. 0. -0.09
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY805_NUT_PREC_DEC = ( -0.51 0. 0. -0.07
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY805_NUT_PREC_PM = ( -0.49 0. 0. 0.06
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+\begintext
+
+
+
+ Galatea
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY806_POLE_RA = ( 299.36 0. 0. )
+ BODY806_POLE_DEC = ( 43.43 0. 0. )
+ BODY806_PM = ( 258.09 839.6597686 0. )
+ BODY806_LONG_AXIS = ( 0. )
+
+
+ BODY806_NUT_PREC_RA = ( 0.70 0. 0. 0.
+ -0.07 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY806_NUT_PREC_DEC = ( -0.51 0. 0. 0.
+ -0.05 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY806_NUT_PREC_PM = ( -0.48 0. 0. 0.
+ 0.05 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+\begintext
+
+
+ Larissa
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY807_POLE_RA = ( 299.36 0. 0. )
+ BODY807_POLE_DEC = ( 43.41 0. 0. )
+ BODY807_PM = ( 179.41 +649.0534470 0. )
+ BODY807_LONG_AXIS = ( 0. )
+
+
+ BODY807_NUT_PREC_RA = ( 0.70 0. 0. 0.
+ 0. -0.27 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY807_NUT_PREC_DEC = ( -0.51 0. 0. 0.
+ 0. -0.20 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY807_NUT_PREC_PM = ( -0.48 0. 0. 0.
+ 0. 0.19 0. 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+\begintext
+
+
+
+ Proteus
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY808_POLE_RA = ( 299.27 0. 0. )
+ BODY808_POLE_DEC = ( 42.91 0. 0. )
+ BODY808_PM = ( 93.38 +320.7654228 0. )
+ BODY808_LONG_AXIS = ( 0. )
+
+
+ BODY808_NUT_PREC_RA = ( 0.70 0. 0. 0.
+ 0. 0. -0.05 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY808_NUT_PREC_DEC = ( -0.51 0. 0. 0.
+ 0. 0. -0.04 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+ BODY808_NUT_PREC_PM = ( -0.48 0. 0. 0.
+ 0. 0. 0.04 0.
+ 0. 0. 0. 0.
+ 0. 0. 0. 0.
+ 0. )
+
+\begintext
+
+
+
+
+
+Satellites of Pluto
+
+ Charon
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY901_POLE_RA = ( 132.993 0. 0. )
+ BODY901_POLE_DEC = ( -6.163 0. 0. )
+ BODY901_PM = ( 122.695 56.3625225 0. )
+ BODY901_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Orientation constants for Selected Comets and Asteroids
+--------------------------------------------------------
+
+
+
+Ceres
+
+ Old values are from the 2009 IAU report.
+
+ body2000001_pole_ra = ( 291. 0. 0. )
+ body2000001_pole_dec = ( 59. 0. 0. )
+ body2000001_pm = ( 170.90 952.1532 0. )
+ body2000001_long_axis = ( 0. )
+
+
+ Current values:
+
+\begindata
+
+ BODY2000001_POLE_RA = ( 291.418 0. 0. )
+ BODY2000001_POLE_DEC = ( 66.764 0. 0. )
+ BODY2000001_PM = ( 170.650 952.1532 0. )
+ BODY2000001_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Pallas
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY2000002_POLE_RA = ( 33. 0. 0. )
+ BODY2000002_POLE_DEC = ( -3. 0. 0. )
+ BODY2000002_PM = ( 38. 1105.8036 0. )
+ BODY2000002_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Vesta
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body2000004_pole_ra = ( 305.8 0. 0. )
+ body2000004_pole_dec = ( 41.4 0. 0. )
+ body2000004_pm = ( 292. 1617.332776 0. )
+ body2000004_long_axis = ( 0. )
+
+ Current values:
+
+\begindata
+
+ BODY2000004_POLE_RA = ( 309.031 0. 0. )
+ BODY2000004_POLE_DEC = ( 42.235 0. 0. )
+ BODY2000004_PM = ( 285.39 1617.3329428 0. )
+ BODY2000004_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+52 Europa (asteroid)
+
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+\begindata
+
+ BODY2000052_POLE_RA = ( 257.0 0. 0. )
+ BODY2000052_POLE_DEC = ( 12.0 0. 0. )
+ BODY2000052_PM = ( 55.0 1534.6472187 0. )
+ BODY2000052_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Lutetia
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY2000021_POLE_RA = ( 52. 0. 0. )
+ BODY2000021_POLE_DEC = ( 12. 0. 0. )
+ BODY2000021_PM = ( 94. 1057.7515 0. )
+ BODY2000021_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Ida
+
+ Old values are from the 2009 IAU report.
+
+ body2431010_pole_ra = ( 168.76 0. 0. )
+ body2431010_pole_dec = ( -2.88 0. 0. )
+ body2431010_pm = ( 274.05 +1864.6280070 0. )
+ body2431010_long_axis = ( 0. )
+
+ The PM constant W0 is from [4].
+
+
+ Current values:
+
+
+\begindata
+
+ BODY2431010_POLE_RA = ( 168.76 0. 0. )
+ BODY2431010_POLE_DEC = ( -87.12 0. 0. )
+ BODY2431010_PM = ( 274.05 +1864.6280070 0. )
+ BODY2431010_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Eros
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY2000433_POLE_RA = ( 11.35 0. 0. )
+ BODY2000433_POLE_DEC = ( 17.22 0. 0. )
+ BODY2000433_PM = ( 326.07 1639.38864745 0. )
+ BODY2000433_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Davida
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY2000511_POLE_RA = ( 297. 0. 0. )
+ BODY2000511_POLE_DEC = ( 5. 0. 0. )
+ BODY2000511_PM = ( 268.1 1684.4193549 0. )
+ BODY2000511_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Gaspra
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY9511010_POLE_RA = ( 9.47 0. 0. )
+ BODY9511010_POLE_DEC = ( 26.70 0. 0. )
+ BODY9511010_PM = ( 83.67 1226.9114850 0. )
+ BODY9511010_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Steins
+
+ Old values are from the 2009 IAU report.
+
+ body2002867_pole_ra = ( 90. 0. 0. )
+ body2002867_pole_dec = ( -62. 0. 0. )
+ body2002867_pm = ( 93.94 1428.852332 0. )
+ body2002867_long_axis = ( 0. )
+
+ Current values:
+
+\begindata
+
+ BODY2002867_POLE_RA = ( 91. 0. 0. )
+ BODY2002867_POLE_DEC = ( -62. 0. 0. )
+ BODY2002867_PM = ( 321.76 1428.09917 0. )
+ BODY2002867_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+Itokawa
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY2025143_POLE_RA = ( 90.53 0. 0. )
+ BODY2025143_POLE_DEC = ( -66.30 0. 0. )
+ BODY2025143_PM = ( 000.0 712.143 0. )
+ BODY2025143_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+9P/Tempel 1
+
+
+ Old values are from the 2009 IAU report.
+
+ body1000093_pole_ra = ( 294. 0. 0. )
+ body1000093_pole_dec = ( 73. 0. 0. )
+ body1000093_pm = ( 252.63 212.064 0. )
+ body1000093_long_axis = ( 0. )
+
+ Current values:
+
+ Values are from the 2015 IAU report [1].
+
+ Two sets of prime meridian values are given in [1]: one
+ for the epoch of the Deep Impact mission's impactor's
+ collision with the comet, and one for the epoch of the
+ Stardust NExT closest approach.
+
+ Deep Impact:
+
+ Epoch: 2005-07-04 05:45:38.4 TDB
+ 2453555.740027 JD TDB
+
+ . 2 2 2
+ W = 109.7, W = 211.849 deg/day, d W/dt = 0.024 deg/day
+
+ Stardust NExT:
+
+ Epoch: 2011-02-15 04:40:18.6 TDB
+ 2455607.694660 JD TDB
+
+ .
+ W = 69.2, W = 212.807 deg/day
+
+ The values of W shown above are the prime meridian angles at
+ the respective epochs.
+
+ Prime meridian data below are those associated with the epoch of the
+ Stardust NExT closest approach.
+
+ Pole direction data are the same for both epochs.
+
+\begindata
+
+ BODY1000093_POLE_RA = ( 255. 0. 0. )
+ BODY1000093_POLE_DEC = ( 64.5 0. 0. )
+ BODY1000093_PM = ( 69.2 212.807 0. )
+ BODY1000093_LONG_AXIS = ( 0. )
+
+ BODY1000093_CONSTANTS_JED_EPOCH = 2455607.694660
+
+\begintext
+
+
+
+19P/Borrelly
+
+ Old values:
+
+ body1000005_pole_ra = ( 218.5 0. 0. )
+ body1000005_pole_dec = ( -12.5 0. 0. )
+ body1000005_pm = ( 000. 390.0 0. )
+ body1000005_long_axis = ( 0. )
+
+ Current values:
+
+ The 2015 IAU report does not cite a value for W0, so a
+ complete orientation model based on that source is not
+ available. Data are provided here for backward compatibility
+ with pck00010.tpc.
+
+ The W0 value was set to zero in that file and so is
+ zero here.
+
+\begindata
+
+ BODY1000005_POLE_RA = ( 218.5 0. 0. )
+ BODY1000005_POLE_DEC = ( -12.5 0. 0. )
+ BODY1000005_PM = ( 000. 324.3 0. )
+ BODY1000005_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+67P/Churyumov-Gerasimenko
+
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+ The time range associated with the rotation model is
+
+ 2014 MAR 3 : 2014 SEP 3
+
+ The reference epoch of the rotational elements is J2000.
+
+\begindata
+
+ BODY1000012_POLE_RA = ( 69.54 0. 0. )
+ BODY1000012_POLE_DEC = ( 64.11 0. 0. )
+ BODY1000012_PM = ( 114.69 696.543884683 0. )
+ BODY1000012_LONG_AXIS = ( 0. )
+
+\begintext
+
+
+
+103P/Hartley 2
+
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+ The 2015 IAU report provides only right ascension and
+ declination values for body axes at the epoch of the EPOXI
+ closest approach. The Z-axis is the long axis. The report
+ uses the symbols alpha and delta to denote right ascension
+ and declination respectively.
+
+ Epoch: 2010-11-04 14:00:53.9 TDB
+ JD 2455505.083957 TDB
+
+
+ alpha = 285.1 deg. delta = -31.8 deg.
+ X X
+
+ alpha = 350.4 deg. delta = 34.4 deg.
+ Y Y
+
+ alpha = 226.1 deg. delta = 39.4 deg.
+ Z Z
+
+
+
+
+Radii of Sun and Planets
+--------------------------------------------------------
+
+
+Sun
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body10_radii = ( 696000. 696000. 696000. )
+
+
+\begindata
+
+ BODY10_RADII = ( 695700. 695700. 695700. )
+
+\begintext
+
+
+Mercury
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body199_radii = ( 2439.7 2439.7 2439.7 )
+
+
+ Current values:
+
+\begindata
+
+ BODY199_RADII = ( 2440.53 2440.53 2438.26 )
+
+\begintext
+
+
+Venus
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY299_RADII = ( 6051.8 6051.8 6051.8 )
+
+\begintext
+
+
+Earth
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY399_RADII = ( 6378.1366 6378.1366 6356.7519 )
+
+\begintext
+
+
+Mars
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ The 2015 IAU report gives separate values for the north and
+ south polar radii:
+
+ north: 3373.19
+ south: 3379.21
+
+ The report provides the average of these values as well,
+ which we use as the polar radius for the triaxial model.
+
+\begindata
+
+ BODY499_RADII = ( 3396.19 3396.19 3376.20 )
+
+\begintext
+
+
+
+Jupiter
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY599_RADII = ( 71492 71492 66854 )
+
+\begintext
+
+
+
+Saturn
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY699_RADII = ( 60268 60268 54364 )
+
+\begintext
+
+
+
+Uranus
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY799_RADII = ( 25559 25559 24973 )
+
+\begintext
+
+
+
+Neptune
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ (Values are for the 1 bar pressure level.)
+
+\begindata
+
+ BODY899_RADII = ( 24764 24764 24341 )
+
+\begintext
+
+
+
+Radii of the Dwarf Planet Pluto
+--------------------------------------------------------
+
+
+Pluto
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body999_radii = ( 1195 1195 1195 )
+
+ Current values:
+
+\begindata
+
+ BODY999_RADII = ( 1188.3 1188.3 1188.3 )
+
+\begintext
+
+
+
+
+Radii of Satellites
+--------------------------------------------------------
+
+
+Moon
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY301_RADII = ( 1737.4 1737.4 1737.4 )
+
+\begintext
+
+
+
+Satellites of Mars
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY401_RADII = ( 13.0 11.4 9.1 )
+ BODY402_RADII = ( 7.8 6.0 5.1 )
+
+\begintext
+
+
+
+Satellites of Jupiter
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ Note that for Ganymede and Callisto only mean radii
+ are provided.
+
+\begindata
+
+ BODY501_RADII = ( 1829.4 1819.4 1815.7 )
+ BODY502_RADII = ( 1562.6 1560.3 1559.5 )
+ BODY503_RADII = ( 2631.2 2631.2 2631.2 )
+ BODY504_RADII = ( 2410.3 2410.3 2410.3 )
+ BODY505_RADII = ( 125 73 64 )
+
+\begintext
+
+ Only mean radii are available in the 2015 IAU report for bodies
+ 506-513.
+
+\begindata
+
+ BODY506_RADII = ( 85 85 85 )
+ BODY507_RADII = ( 40 40 40 )
+ BODY508_RADII = ( 18 18 18 )
+ BODY509_RADII = ( 14 14 14 )
+ BODY510_RADII = ( 12 12 12 )
+ BODY511_RADII = ( 15 15 15 )
+ BODY512_RADII = ( 10 10 10 )
+ BODY513_RADII = ( 5 5 5 )
+ BODY514_RADII = ( 58 49 42 )
+ BODY515_RADII = ( 10 8 7 )
+ BODY516_RADII = ( 30 20 17 )
+
+\begintext
+
+
+
+Satellites of Saturn
+
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+
+ body601_radii = ( 207.8 196.7 190.6 )
+ body602_radii = ( 256.6 251.4 248.3 )
+ body603_radii = ( 538.4 528.3 526.3 )
+ body604_radii = ( 563.4 561.3 559.6 )
+ body605_radii = ( 765.0 763.1 762.4 )
+ body606_radii = ( 2575.15 2574.78 2574.47 )
+ body607_radii = ( 180.1 133.0 102.7 )
+ body608_radii = ( 745.7 745.7 712.1 )
+ body609_radii = ( 109.4 108.5 101.8 )
+ body610_radii = ( 101.5 92.5 76.3 )
+ body611_radii = ( 64.9 57.0 53.1 )
+ body612_radii = ( 21.7 19.1 13.0 )
+ body613_radii = ( 16.3 11.8 10.0 )
+ body614_radii = ( 15.1 11.5 7.0 )
+ body615_radii = ( 20.4 17.7 9.4 )
+ body616_radii = ( 67.8 39.7 29.7 )
+ body617_radii = ( 52.0 40.5 32.0 )
+ body618_radii = ( 17.2 15.7 10.4 )
+ body632_radii = ( 1.6 1.6 1.6 )
+ body633_radii = ( 2.9 2.8 2.0 )
+ body634_radii = ( 1.5 1.2 1.0 )
+ body635_radii = ( 4.3 4.1 3.2 )
+ body649_radii = ( 1 1 1 )
+
+
+ Current values:
+
+\begindata
+
+ BODY601_RADII = ( 207.8 196.7 190.6 )
+ BODY602_RADII = ( 256.6 251.4 248.3 )
+ BODY603_RADII = ( 538.4 528.3 526.3 )
+ BODY604_RADII = ( 563.4 561.3 559.6 )
+ BODY605_RADII = ( 765.0 763.1 762.4 )
+ BODY606_RADII = ( 2575.15 2574.78 2574.47 )
+ BODY607_RADII = ( 180.1 133.0 102.7 )
+ BODY608_RADII = ( 745.7 745.7 712.1 )
+ BODY609_RADII = ( 109.4 108.5 101.8 )
+ BODY610_RADII = ( 101.7 93.0 76.3 )
+ BODY611_RADII = ( 64.9 57.3 53.0 )
+ BODY612_RADII = ( 22.5 19.6 13.3 )
+ BODY613_RADII = ( 16.3 11.8 9.8 )
+ BODY614_RADII = ( 15.3 9.3 6.3 )
+ BODY615_RADII = ( 20.5 17.8 9.4 )
+ BODY616_RADII = ( 68.2 41.6 28.2 )
+ BODY617_RADII = ( 52.2 40.8 31.5 )
+ BODY618_RADII = ( 17.2 15.4 10.4 )
+ BODY632_RADII = ( 1.94 1.29 1.21 )
+ BODY633_RADII = ( 2.88 2.08 1.8 )
+ BODY634_RADII = ( 1.5 1.2 1.0 )
+ BODY635_RADII = ( 4.6 4.5 2.8 )
+ BODY649_RADII = ( 0.5 0.5 0.5 )
+ BODY653_RADII = ( 0.7 0.25 0.2 )
+
+\begintext
+
+
+
+Satellites of Uranus
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY701_RADII = ( 581.1 577.9 577.7 )
+ BODY702_RADII = ( 584.7 584.7 584.7 )
+ BODY703_RADII = ( 788.9 788.9 788.9 )
+ BODY704_RADII = ( 761.4 761.4 761.4 )
+ BODY705_RADII = ( 240.4 234.2 232.9 )
+
+\begintext
+
+ The 2015 IAU report gives only mean radii for satellites 706--715.
+
+\begindata
+
+ BODY706_RADII = ( 13 13 13 )
+ BODY707_RADII = ( 15 15 15 )
+ BODY708_RADII = ( 21 21 21 )
+ BODY709_RADII = ( 31 31 31 )
+ BODY710_RADII = ( 27 27 27 )
+ BODY711_RADII = ( 42 42 42 )
+ BODY712_RADII = ( 54 54 54 )
+ BODY713_RADII = ( 27 27 27 )
+ BODY714_RADII = ( 33 33 33 )
+ BODY715_RADII = ( 77 77 77 )
+
+\begintext
+
+
+
+
+Satellites of Neptune
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ The 2015 IAU report gives mean radii only for bodies 801-806.
+
+\begindata
+
+ BODY801_RADII = ( 1352.6 1352.6 1352.6 )
+ BODY802_RADII = ( 170 170 170 )
+ BODY803_RADII = ( 29 29 29 )
+ BODY804_RADII = ( 40 40 40 )
+ BODY805_RADII = ( 74 74 74 )
+ BODY806_RADII = ( 79 79 79 )
+
+\begintext
+
+ The second equatorial radius for Larissa is not given in the 2009
+ report. The available values are:
+
+ BODY807_RADII = ( 104 --- 89 )
+
+ For use within the SPICE system, we use only the mean radius.
+
+\begindata
+
+ BODY807_RADII = ( 96 96 96 )
+ BODY808_RADII = ( 218 208 201 )
+
+\begintext
+
+
+
+
+Satellites of Pluto
+
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ BODY901_RADII = ( 605 605 605 )
+
+ Current values:
+
+\begindata
+
+ BODY901_RADII = ( 606 606 606 )
+
+\begintext
+
+
+
+Radii for Selected Comets and Asteroids
+--------------------------------------------------------
+
+
+Ceres
+
+ Old values:
+
+ Values are from the 2009 IAU report.
+
+ body2000001_radii = ( 487.3 487.3 454.7 )
+
+ Current values:
+
+
+\begindata
+
+ BODY2000001_RADII = ( 487.3 487.3 446. )
+
+\begintext
+
+
+
+Vesta
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2000004_RADII = ( 289. 280. 229. )
+
+\begintext
+
+
+
+Psyche
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+
+\begindata
+
+ BODY2000016_RADII = ( 139.5 116. 94.5 )
+
+\begintext
+
+
+
+Lutetia
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2000021_RADII = ( 62.0 50.5 46.5 )
+
+\begintext
+
+
+
+52 Europa
+
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+
+\begindata
+
+ BODY2000052_RADII = ( 189.5 165. 124.5 )
+
+\begintext
+
+Ida
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2431010_RADII = ( 26.8 12.0 7.6 )
+
+\begintext
+
+
+
+Mathilde
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2000253_RADII = ( 33. 24. 23. )
+
+\begintext
+
+
+
+Eros
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2000433_RADII = ( 17.0 5.5 5.5 )
+
+\begintext
+
+
+
+Davida
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2000511_RADII = ( 180. 147. 127. )
+
+\begintext
+
+
+
+Gaspra
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY9511010_RADII = ( 9.1 5.2 4.4 )
+
+\begintext
+
+
+
+Steins
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2002867_RADII = ( 3.24 2.73 2.04 )
+
+\begintext
+
+
+
+Toutatis
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY2004179_RADII = ( 2.13 1.015 0.85 )
+
+\begintext
+
+
+
+Itokawa
+
+
+ Old values:
+
+ Values are from the 2009 IAU report. Note that the
+ diameters rather than radii were given.
+
+ body2025143_radii = ( 0.535 0.294 0.209 )
+
+ Current values:
+
+
+\begindata
+
+ BODY2025143_RADII = ( 0.268 0.147 0.104 )
+
+\begintext
+
+
+Kleopatra
+
+
+ Old values:
+
+ Values are from the 2003 report [13].
+
+ A shape model was not provided in later reports because,
+ according to [5], the shape had been "modeled from
+ low resolution radar data, and cannot be mapped from those
+ data."
+
+ body2000216_radii = ( 108.5 47 40.5 )
+
+
+ Current values:
+
+
+ No values are provided in the 2015 IAU report.
+
+
+
+Halley
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+\begindata
+
+ BODY1000036_RADII = ( 8.0 4.0 4.0 )
+
+\begintext
+
+
+
+9P/Tempel 1
+
+
+ Old values:
+
+ The effective radius is unchanged in the 2009 IAU report.
+
+ Current values:
+
+ According to [1]:
+
+ The maximum and minimum radii are not properly
+ the values of the principal semi-axes, they
+ are half the maximum and minimum values of the
+ diameter. Due to the large deviations from a
+ simple ellipsoid, they may not correspond with
+ measurements along the principal axes, or be
+ orthogonal to each other.
+
+ The radii along the first and second principal axes
+ are given as
+
+ 3.7 km
+ 2.5 km
+
+ The value in the data assignment below is the mean radius.
+
+\begindata
+
+ BODY1000093_RADII = ( 3.0 3.0 3.0 )
+
+\begintext
+
+
+19P/Borrelly
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+ The first principal axis length is
+
+ 3.5 km
+
+ The lengths of the other semi-axes are not provided
+ by [1].
+
+ The value in the data assignment below is the mean radius.
+
+\begindata
+
+ BODY1000005_RADII = ( 4.22 4.22 4.22 )
+
+\begintext
+
+
+
+81P/Wild 2
+
+
+ Old values:
+
+ Values are unchanged in the 2015 IAU report.
+
+ Current values:
+
+
+\begindata
+
+ BODY1000107_RADII = ( 2.7 1.9 1.5 )
+
+\begintext
+
+
+
+67P/Churyumov-Gerasimenko
+
+ Current values:
+
+ Values are provided for the first time in the 2015 IAU report.
+
+\begindata
+
+ BODY1000012_RADII = ( 2.40 1.55 1.20 )
+
+\begintext
+
+
+
+103P/Hartley 2
+
+ The most recent "Report of the IAU Working Group on Cartographic
+ Coordinates and Rotational Elements: 2015" appears to incorrectly list
+ the radii for Comet 103P/Hartley 2 in Table 6. The following text by
+ Brent Archinal, Chair of the Working Group, examines this problem and
+ what radii should likely be used.
+
+ However, note that this explanation has not yet been officially
+ considered and the proposed changes recommended by the Working Group.
+ Such a process will begin shortly, and a consensus document about any
+ changes may be published on the Working Group website.
+
+ Clearly from the many figures in various papers (e.g. A'Hearn et al,
+ 2011, Science, 332, 1396; Belton, et al., 2012 Icarus, 222, 595;
+ Thomas, et al. 2013, Icarus, 222, 550), Comet 103P/Hartley 2 has an
+ elongated nucleus, with the long axis being the (reference) spin axis.
+ This is stated/shown e.g., in Figure 3 of the A'Hearn et al. paper.
+ The size is stated in a few places as having a diameter of 0.69 to
+ 2.33 km and a mean radius of 0.58 +/-0.02 km (A'Hearn, et al.,
+ Table 1; Thomas et al., Table 1 (but now with a mean radius uncertainty
+ of 0.018 km)). This of course translates to a mean radius of 0.58 km,
+ a minimum radius of 0.345 km (or rounding, 0.34 km), and a maximum
+ radius of 1.165 km (rounding, 1.16 km). I don't really see a clear
+ statement in these papers that these last two numbers can be used for
+ the semi axes, just that they are minimum and maximum values.
+ Apparently though that's been assumed (as it has been for some of the
+ other comets listed in the WG report). There is also no clear statement
+ of what if any difference there is between the first and second semi
+ axis, but only that they are "similar". E.g., Thomas et al. says
+ (Section 3) "This bi-lobed object has near rotational symmetry".
+ That does correspond to what's said in footnote (g) of the WG report.
+
+ So, I think it is correct that:
+
+ mean radius = 0.58 km
+ a semi-axis = 0.34 km
+ b semi-axis = 0.34 km (or "~0.34 km")
+ c semi-axis (polar axis) = 1.16 km
+
+ Due to their uncertain status, these data are included in this file
+ only as comments. To enable SPICE software to access the data,
+ move this assignment
+
+ BODY1000041_RADII = ( 0.34 0.34 1.16 )
+
+ below the \begindata marker below.
+
+\begindata
+
+
+\begintext
+
+===========================================================================
+End of file pck00011.tpc
+===========================================================================
diff --git a/src/almanac/bpc.rs b/src/almanac/bpc.rs
index e6d8b09f..22b9b34e 100644
--- a/src/almanac/bpc.rs
+++ b/src/almanac/bpc.rs
@@ -14,13 +14,17 @@ use crate::naif::daf::DAFError;
use crate::naif::pck::BPCSummaryRecord;
use crate::naif::BPC;
use crate::orientations::OrientationError;
-use log::error;
use super::{Almanac, MAX_LOADED_BPCS};
-impl<'a: 'b, 'b> Almanac<'a> {
+impl Almanac {
+ pub fn from_bpc(bpc: BPC) -> Result {
+ let me = Self::default();
+ me.with_bpc(bpc)
+ }
+
/// Loads a Binary Planetary Constants kernel.
- pub fn load_bpc(&self, bpc: BPC) -> Result, OrientationError> {
+ pub fn with_bpc(&self, bpc: BPC) -> Result {
// This is just a bunch of pointers so it doesn't use much memory.
let mut me = self.clone();
let mut data_idx = MAX_LOADED_BPCS;
@@ -72,7 +76,6 @@ impl<'a: 'b, 'b> Almanac<'a> {
}
// If we're reached this point, there is no relevant summary at this epoch.
- error!("Almanac: No summary {name} valid at epoch {epoch}");
Err(OrientationError::BPC {
action: "searching for BPC summary",
source: DAFError::SummaryNameAtEpochError {
@@ -103,7 +106,6 @@ impl<'a: 'b, 'b> Almanac<'a> {
}
}
- error!("Almanac: No summary {id} valid at epoch {epoch}");
// If we're reached this point, there is no relevant summary at this epoch.
Err(OrientationError::BPC {
action: "searching for BPC summary",
@@ -134,7 +136,6 @@ impl<'a: 'b, 'b> Almanac<'a> {
}
// If we're reached this point, there is no relevant summary at this epoch.
- error!("Almanac: No summary {name} valid");
Err(OrientationError::BPC {
action: "searching for BPC summary",
source: DAFError::SummaryNameError {
@@ -163,7 +164,6 @@ impl<'a: 'b, 'b> Almanac<'a> {
}
}
- error!("Almanac: No summary {id} valid");
// If we're reached this point, there is no relevant summary
Err(OrientationError::BPC {
action: "searching for BPC summary",
diff --git a/src/almanac/mod.rs b/src/almanac/mod.rs
index 18503a2e..681ca893 100644
--- a/src/almanac/mod.rs
+++ b/src/almanac/mod.rs
@@ -9,7 +9,7 @@
*/
use crate::naif::{BPC, SPK};
-use crate::structure::{PlanetaryDataSet, SpacecraftDataSet};
+use crate::structure::{EulerParameterDataSet, PlanetaryDataSet, SpacecraftDataSet};
use core::fmt;
// TODO: Switch these to build constants so that it's configurable when building the library.
@@ -21,28 +21,31 @@ pub const MAX_PLANETARY_DATA: usize = 64;
pub mod bpc;
pub mod planetary;
pub mod spk;
+pub mod transform;
/// An Almanac contains all of the loaded SPICE and ANISE data.
///
/// # Limitations
/// The stack space required depends on the maximum number of each type that can be loaded.
#[derive(Clone, Default)]
-pub struct Almanac<'a> {
+pub struct Almanac {
/// NAIF SPK is kept unchanged
pub spk_data: [Option; MAX_LOADED_SPKS],
/// NAIF BPC is kept unchanged
pub bpc_data: [Option; MAX_LOADED_BPCS],
/// Dataset of planetary data
- pub planetary_data: PlanetaryDataSet<'a>,
+ pub planetary_data: PlanetaryDataSet,
/// Dataset of spacecraft data
- pub spacecraft_data: SpacecraftDataSet<'a>,
+ pub spacecraft_data: SpacecraftDataSet,
+ /// Dataset of euler parameters
+ pub euler_param_data: EulerParameterDataSet,
}
-impl<'a> fmt::Display for Almanac<'a> {
+impl fmt::Display for Almanac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
- "Context: #SPK = {}\t#BPC = {}",
+ "Almanac: #SPK = {}\t#BPC = {}",
self.num_loaded_spk(),
self.num_loaded_bpc()
)?;
diff --git a/src/almanac/planetary.rs b/src/almanac/planetary.rs
index 39f75953..df38e57d 100644
--- a/src/almanac/planetary.rs
+++ b/src/almanac/planetary.rs
@@ -1,6 +1,6 @@
use crate::{
prelude::{Frame, FrameUid},
- structure::dataset::DataSetError,
+ structure::{dataset::DataSetError, PlanetaryDataSet},
};
/*
@@ -25,7 +25,7 @@ pub enum PlanetaryDataError {
},
}
-impl<'a: 'b, 'b> Almanac<'a> {
+impl Almanac {
/// Given the frame UID (or something that can be transformed into it), attempt to retrieve the full frame information, if that frame is loaded
pub fn frame_from_uid>(&self, uid: U) -> Result {
let uid = uid.into();
@@ -37,4 +37,11 @@ impl<'a: 'b, 'b> Almanac<'a> {
})?
.to_frame(uid))
}
+
+ /// Loads the provided planetary data into a clone of this original Almanac.
+ pub fn with_planetary_data(&self, planetary_data: PlanetaryDataSet) -> Self {
+ let mut me = self.clone();
+ me.planetary_data = planetary_data;
+ me
+ }
}
diff --git a/src/almanac/spk.rs b/src/almanac/spk.rs
index 09e54e08..ea9edc5f 100644
--- a/src/almanac/spk.rs
+++ b/src/almanac/spk.rs
@@ -18,15 +18,15 @@ use log::error;
use super::{Almanac, MAX_LOADED_SPKS};
-impl<'a: 'b, 'b> Almanac<'a> {
- pub fn from_spk(spk: SPK) -> Result, EphemerisError> {
+impl Almanac {
+ pub fn from_spk(spk: SPK) -> Result {
let me = Self::default();
- me.load_spk(spk)
+ me.with_spk(spk)
}
/// Loads a new SPK file into a new context.
/// This new context is needed to satisfy the unloading of files. In fact, to unload a file, simply let the newly loaded context drop out of scope and Rust will clean it up.
- pub fn load_spk(&self, spk: SPK) -> Result, EphemerisError> {
+ pub fn with_spk(&self, spk: SPK) -> Result {
// This is just a bunch of pointers so it doesn't use much memory.
let mut me = self.clone();
// Parse as SPK and place into the SPK list if there is room
@@ -218,7 +218,7 @@ mod ut_almanac_spk {
let e = Epoch::now().unwrap();
assert!(
- almanac.try_find_context_center().is_err(),
+ almanac.try_find_ephemeris_root().is_err(),
"empty Almanac should report an error"
);
diff --git a/src/almanac/transform.rs b/src/almanac/transform.rs
new file mode 100644
index 00000000..a1da7f68
--- /dev/null
+++ b/src/almanac/transform.rs
@@ -0,0 +1,143 @@
+/*
+ * 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 hifitime::{Epoch, Unit as TimeUnit};
+use snafu::ResultExt;
+
+use crate::{
+ errors::{AlmanacError, EphemerisSnafu, OrientationSnafu},
+ math::{cartesian::CartesianState, units::LengthUnit, Vector3},
+ orientations::OrientationPhysicsSnafu,
+ prelude::{Aberration, Frame},
+ NaifId,
+};
+
+use super::Almanac;
+
+impl Almanac {
+ /// Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`.
+ ///
+ /// # Note
+ /// The units will be those of the underlying ephemeris data (typically km and km/s)
+ pub fn transform_from_to(
+ &self,
+ from_frame: Frame,
+ to_frame: Frame,
+ epoch: Epoch,
+ ab_corr: Aberration,
+ ) -> Result {
+ // Translate
+ let state = self
+ .translate_from_to(from_frame, to_frame, epoch, ab_corr)
+ .with_context(|_| EphemerisSnafu {
+ action: "transform from/to",
+ })?;
+ // Rotate
+ let dcm = self
+ .rotate_from_to(from_frame, to_frame, epoch)
+ .with_context(|_| OrientationSnafu {
+ action: "transform from/to",
+ })?;
+
+ (dcm * state)
+ .with_context(|_| OrientationPhysicsSnafu {})
+ .with_context(|_| OrientationSnafu {
+ action: "transform from/to",
+ })
+ }
+
+ /// Translates a state with its origin (`to_frame`) and given its units (distance_unit, time_unit), returns that state with respect to the requested frame
+ ///
+ /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_state_to` function instead to include rotations.
+ #[allow(clippy::too_many_arguments)]
+ pub fn transform_to(
+ &self,
+ state: CartesianState,
+ to_frame: Frame,
+ ab_corr: Aberration,
+ ) -> Result {
+ let state = self
+ .translate_to(state, to_frame, ab_corr)
+ .with_context(|_| EphemerisSnafu {
+ action: "transform state",
+ })?;
+
+ // Compute the frame rotation
+ let dcm = self
+ .rotate_from_to(state.frame, to_frame, state.epoch)
+ .with_context(|_| OrientationSnafu {
+ action: "transform state dcm",
+ })?;
+
+ (dcm * state)
+ .with_context(|_| OrientationPhysicsSnafu {})
+ .with_context(|_| OrientationSnafu {
+ action: "transform state",
+ })
+ }
+
+ /// Translates a state with its origin (`to_frame`) and given its units (distance_unit, time_unit), returns that state with respect to the requested frame
+ ///
+ /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_state_to` function instead to include rotations.
+ #[allow(clippy::too_many_arguments)]
+ pub fn transform_state_to(
+ &self,
+ position: Vector3,
+ velocity: Vector3,
+ from_frame: Frame,
+ to_frame: Frame,
+ epoch: Epoch,
+ ab_corr: Aberration,
+ distance_unit: LengthUnit,
+ time_unit: TimeUnit,
+ ) -> Result {
+ let state = self
+ .translate_state_to(
+ position,
+ velocity,
+ from_frame,
+ to_frame,
+ epoch,
+ ab_corr,
+ distance_unit,
+ time_unit,
+ )
+ .with_context(|_| EphemerisSnafu {
+ action: "transform provided state",
+ })?;
+
+ // Compute the frame rotation
+ let dcm = self
+ .rotate_from_to(from_frame, to_frame, epoch)
+ .with_context(|_| OrientationSnafu {
+ action: "transform provided state dcm",
+ })?;
+
+ (dcm * state)
+ .with_context(|_| OrientationPhysicsSnafu {})
+ .with_context(|_| OrientationSnafu {
+ action: "transform provided state",
+ })
+ }
+
+ /// Returns the Cartesian state of the object as seen from the provided observer frame (essentially `spkezr`).
+ ///
+ /// # Note
+ /// The units will be those of the underlying ephemeris data (typically km and km/s)
+ pub fn state_of(
+ &self,
+ object: NaifId,
+ observer: Frame,
+ epoch: Epoch,
+ ab_corr: Aberration,
+ ) -> Result {
+ self.transform_from_to(Frame::from_ephem_j2000(object), observer, epoch, ab_corr)
+ }
+}
diff --git a/src/astro/orbit.rs b/src/astro/orbit.rs
index 1501782c..f83d1d8a 100644
--- a/src/astro/orbit.rs
+++ b/src/astro/orbit.rs
@@ -755,13 +755,15 @@ impl CartesianState {
#[allow(clippy::format_in_format_args)]
impl fmt::LowerHex for Orbit {
- // Prints the Keplerian orbital elements in floating point with units
+ /// Prints the Keplerian orbital elements in floating point with units if frame is celestial,
+ /// If frame is geodetic, prints the range, altitude, latitude, and longitude with respect to the planetocentric frame
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.frame.is_celestial() {
error!("you must update the frame from the Almanac before printing this state's orbital parameters");
Err(fmt::Error)
} else {
let decimals = f.precision().unwrap_or(6);
+
write!(
f,
"[{:x}] {}\tsma = {} km\tecc = {}\tinc = {} deg\traan = {} deg\taop = {} deg\tta = {} deg",
@@ -795,3 +797,40 @@ impl fmt::LowerHex for Orbit {
}
}
}
+
+#[allow(clippy::format_in_format_args)]
+impl fmt::UpperHex for Orbit {
+ /// Prints the prints the range, altitude, latitude, and longitude with respect to the planetocentric frame in floating point with units if frame is celestial,
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if !self.frame.is_geodetic() {
+ error!("you must update the frame from the Almanac before printing this state's planetocentric parameters");
+ Err(fmt::Error)
+ } else {
+ let decimals = f.precision().unwrap_or(3);
+ write!(
+ f,
+ "[{:x}] {}\trange = {} km\talt. = {} km\tlatitude = {} deg\tlongitude = {} deg",
+ self.frame,
+ self.epoch,
+ format!("{:.*}", decimals, self.rmag_km()),
+ format!(
+ "{:.*}",
+ decimals,
+ self.geodetic_height().map_err(|err| {
+ error!("{err}");
+ fmt::Error
+ })?
+ ),
+ format!(
+ "{:.*}",
+ decimals,
+ self.geodetic_latitude().map_err(|err| {
+ error!("{err}");
+ fmt::Error
+ })?
+ ),
+ format!("{:.*}", decimals, self.geodetic_longitude()),
+ )
+ }
+ }
+}
diff --git a/src/bin/anise/main.rs b/src/bin/anise/main.rs
index 910d3b4f..24ba6c23 100644
--- a/src/bin/anise/main.rs
+++ b/src/bin/anise/main.rs
@@ -1,6 +1,7 @@
extern crate pretty_env_logger;
use std::env::{set_var, var};
+use anise::structure::{EulerParameterDataSet, PlanetaryDataSet, SpacecraftDataSet};
use snafu::prelude::*;
use anise::cli::args::{Actions, Args};
@@ -8,12 +9,10 @@ use anise::cli::inspect::{BpcRow, SpkRow};
use anise::cli::{AniseSnafu, CliDAFSnafu, CliDataSetSnafu, CliErrors, CliFileRecordSnafu};
use anise::file2heap;
use anise::naif::daf::{FileRecord, NAIFRecord, NAIFSummaryRecord};
-use anise::naif::kpl::parser::convert_tpc;
+use anise::naif::kpl::parser::{convert_fk, convert_tpc};
use anise::prelude::*;
-use anise::structure::dataset::{DataSet, DataSetType};
+use anise::structure::dataset::DataSetType;
use anise::structure::metadata::Metadata;
-use anise::structure::planetocentric::PlanetaryData;
-use anise::structure::spacecraft::SpacecraftData;
use clap::Parser;
use log::info;
use tabled::{settings::Style, Table};
@@ -45,14 +44,21 @@ fn main() -> Result<(), CliErrors> {
DataSetType::NotApplicable => unreachable!("no such ANISE data yet"),
DataSetType::SpacecraftData => {
// Decode as spacecraft data
- let dataset = DataSet::::try_from_bytes(&bytes)
+ let dataset = SpacecraftDataSet::try_from_bytes(bytes)
.with_context(|_| CliDataSetSnafu)?;
println!("{dataset}");
Ok(())
}
DataSetType::PlanetaryData => {
// Decode as planetary data
- let dataset = DataSet::::try_from_bytes(&bytes)
+ let dataset = PlanetaryDataSet::try_from_bytes(bytes)
+ .with_context(|_| CliDataSetSnafu)?;
+ println!("{dataset}");
+ Ok(())
+ }
+ DataSetType::EulerParameterData => {
+ // Decode as euler paramater data
+ let dataset = EulerParameterDataSet::try_from_bytes(bytes)
.with_context(|_| CliDataSetSnafu)?;
println!("{dataset}");
Ok(())
@@ -181,7 +187,16 @@ fn main() -> Result<(), CliErrors> {
let dataset = convert_tpc(pckfile, gmfile).with_context(|_| CliDataSetSnafu)?;
dataset
- .save_as(outfile, false)
+ .save_as(&outfile, false)
+ .with_context(|_| CliDataSetSnafu)?;
+
+ Ok(())
+ }
+ Actions::ConvertFk { fkfile, outfile } => {
+ let dataset = convert_fk(fkfile, false).unwrap();
+
+ dataset
+ .save_as(&outfile, false)
.with_context(|_| CliDataSetSnafu)?;
Ok(())
diff --git a/src/cli/args.rs b/src/cli/args.rs
index 3c362d91..06b0433e 100644
--- a/src/cli/args.rs
+++ b/src/cli/args.rs
@@ -14,7 +14,6 @@ pub enum Actions {
/// Checks the integrity of the file
Check {
/// Path to ANISE file
- #[clap(parse(from_os_str))]
file: PathBuf,
/// CRC32 checksum
crc32_checksum: u32,
@@ -22,19 +21,22 @@ pub enum Actions {
/// Inspects what's in an ANISE file (and also checks the integrity)
Inspect {
/// Path to ANISE or NAIF file
- #[clap(parse(from_os_str))]
file: PathBuf,
},
/// Convert the provided KPL files into ANISE datasets
ConvertTpc {
/// Path to the KPL PCK/TPC file (e.g. pck00008.tpc)
- #[clap(parse(from_os_str))]
pckfile: PathBuf,
/// Path to the KPL gravity data TPC file (e.g. gm_de431.tpc)
- #[clap(parse(from_os_str))]
gmfile: PathBuf,
/// Output ANISE binary file
- #[clap(parse(from_os_str))]
+ outfile: PathBuf,
+ },
+ /// Convert the provided Frame Kernel into an ANISE dataset
+ ConvertFk {
+ /// Path to the FK (e.g. moon_080317.fk)
+ fkfile: PathBuf,
+ /// Output ANISE binary file
outfile: PathBuf,
},
}
diff --git a/src/constants.rs b/src/constants.rs
index 767af4be..f6cd2725 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -24,6 +24,11 @@ pub mod celestial_objects {
pub const SUN: NaifId = 10;
pub const LUNA: NaifId = 301;
pub const EARTH: NaifId = 399;
+ pub const MARS: NaifId = 499;
+ pub const JUPITER: NaifId = 599;
+ pub const SATURN: NaifId = 699;
+ pub const URANUS: NaifId = 799;
+ pub const NEPTUNE: NaifId = 899;
pub const fn celestial_name_from_id(id: NaifId) -> Option<&'static str> {
match id {
@@ -177,10 +182,24 @@ pub mod orientations {
/// The DE-403 frame is treated as equivalent to the J2000 frame.
pub const DE143: NaifId = 21;
+ /// Body fixed IAU rotation
+ pub const IAU_MERCURY: NaifId = 199;
+ pub const IAU_VENUS: NaifId = 299;
+ pub const IAU_EARTH: NaifId = 399;
+ pub const ITRF93: NaifId = 3000;
+ pub const IAU_MARS: NaifId = 499;
+ pub const IAU_JUPITER: NaifId = 599;
+ pub const IAU_SATURN: NaifId = 699;
+ pub const IAU_NEPTUNE: NaifId = 799;
+ pub const IAU_URANUS: NaifId = 899;
+
+ /// Angle between J2000 to solar system ecliptic J2000 ([ECLIPJ2000]), in radians (about 23.43929 degrees). Apply this rotation about the X axis ([r1])
+ pub const J2000_TO_ECLIPJ2000_ANGLE_RAD: f64 = 0.40909280422232897;
+
/// Given the frame ID, try to return a human name
/// Source: // https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/frames.html#Appendix.%20%60%60Built%20in''%20Inertial%20Reference%20Frames
- pub const fn orientation_name_from_id(hash: NaifId) -> Option<&'static str> {
- match hash {
+ pub const fn orientation_name_from_id(id: NaifId) -> Option<&'static str> {
+ match id {
J2000 => Some("J2000"),
B1950 => Some("B1950"),
FK4 => Some("FK4"),
@@ -188,6 +207,15 @@ pub mod orientations {
MARSIAU => Some("Mars IAU"),
ECLIPJ2000 => Some("ECLIPJ2000"),
ECLIPB1950 => Some("ECLIPB1950"),
+ IAU_MERCURY => Some("IAU_MERCURY"),
+ IAU_VENUS => Some("IAU_VENUS"),
+ IAU_EARTH => Some("IAU_EARTH"),
+ ITRF93 => Some("ITRF93"),
+ IAU_MARS => Some("IAU_MARS"),
+ IAU_JUPITER => Some("IAU_JUPITER"),
+ IAU_SATURN => Some("IAU_SATURN"),
+ IAU_NEPTUNE => Some("IAU_NEPTUNE"),
+ IAU_URANUS => Some("IAU_URANUS"),
_ => None,
}
}
@@ -196,7 +224,7 @@ pub mod orientations {
pub mod frames {
use crate::prelude::Frame;
- use super::{celestial_objects::*, orientations::J2000};
+ use super::{celestial_objects::*, orientations::*};
pub const SSB_J2000: Frame = Frame::from_ephem_orient(SOLAR_SYSTEM_BARYCENTER, J2000);
pub const MERCURY_J2000: Frame = Frame::from_ephem_orient(MERCURY, J2000);
@@ -213,6 +241,20 @@ pub mod frames {
pub const LUNA_J2000: Frame = Frame::from_ephem_orient(LUNA, J2000);
pub const EARTH_J2000: Frame = Frame::from_ephem_orient(EARTH, J2000);
pub const EME2000: Frame = Frame::from_ephem_orient(EARTH, J2000);
+ pub const EARTH_ECLIPJ2000: Frame = Frame::from_ephem_orient(EARTH, ECLIPJ2000);
+
+ /// Body fixed IAU rotation
+ pub const IAU_MERCURY_FRAME: Frame = Frame::from_ephem_orient(MERCURY, IAU_MERCURY);
+ pub const IAU_VENUS_FRAME: Frame = Frame::from_ephem_orient(VENUS, IAU_VENUS);
+ pub const IAU_EARTH_FRAME: Frame = Frame::from_ephem_orient(EARTH, IAU_EARTH);
+ pub const IAU_MARS_FRAME: Frame = Frame::from_ephem_orient(MARS, IAU_MARS);
+ pub const IAU_JUPITER_FRAME: Frame = Frame::from_ephem_orient(JUPITER, IAU_JUPITER);
+ pub const IAU_SATURN_FRAME: Frame = Frame::from_ephem_orient(SATURN, IAU_SATURN);
+ pub const IAU_NEPTUNE_FRAME: Frame = Frame::from_ephem_orient(NEPTUNE, IAU_NEPTUNE);
+ pub const IAU_URANUS_FRAME: Frame = Frame::from_ephem_orient(URANUS, IAU_URANUS);
+
+ /// Common high precision frame
+ pub const EARTH_ITRF93: Frame = Frame::from_ephem_orient(EARTH, ITRF93);
}
#[cfg(test)]
diff --git a/src/ephemerides/mod.rs b/src/ephemerides/mod.rs
index 55a870b6..b1dda0ef 100644
--- a/src/ephemerides/mod.rs
+++ b/src/ephemerides/mod.rs
@@ -46,7 +46,7 @@ pub enum EphemerisError {
source: DAFError,
},
#[snafu(display("during an ephemeris operation: {source}"))]
- UnderlyingPhysics {
+ EphemerisPhysics {
#[snafu(backtrace)]
source: PhysicsError,
},
diff --git a/src/ephemerides/paths.rs b/src/ephemerides/paths.rs
index 12d7b13b..35a33acb 100644
--- a/src/ephemerides/paths.rs
+++ b/src/ephemerides/paths.rs
@@ -20,14 +20,14 @@ use crate::NaifId;
/// **Limitation:** no translation or rotation may have more than 8 nodes.
pub const MAX_TREE_DEPTH: usize = 8;
-impl<'a> Almanac<'a> {
- /// Returns the center of all of the loaded ephemerides, typically this should be the Solar System Barycenter.
+impl Almanac {
+ /// Returns the root of all of the loaded ephemerides, typically this should be the Solar System Barycenter.
///
/// # Algorithm
///
/// 1. For each loaded SPK, iterated in reverse order (to mimic SPICE behavior)
/// 2. For each summary record in each SPK, follow the ephemeris branch all the way up until the end of this SPK or until the SSB.
- pub fn try_find_context_center(&self) -> Result {
+ pub fn try_find_ephemeris_root(&self) -> Result {
ensure!(self.num_loaded_spk() > 0, NoEphemerisLoadedSnafu);
// The common center is the absolute minimum of all centers due to the NAIF numbering.
@@ -58,7 +58,7 @@ impl<'a> Almanac<'a> {
source: Frame,
epoch: Epoch,
) -> Result<(usize, [Option; MAX_TREE_DEPTH]), EphemerisError> {
- let common_center = self.try_find_context_center()?;
+ let common_center = self.try_find_ephemeris_root()?;
// Build a tree, set a fixed depth to avoid allocations
let mut of_path = [None; MAX_TREE_DEPTH];
let mut of_path_len = 0;
diff --git a/src/ephemerides/translate_to_parent.rs b/src/ephemerides/translate_to_parent.rs
index b981fef0..b4260d71 100644
--- a/src/ephemerides/translate_to_parent.rs
+++ b/src/ephemerides/translate_to_parent.rs
@@ -18,22 +18,19 @@ use crate::ephemerides::EphemInterpolationSnafu;
use crate::hifitime::Epoch;
use crate::math::cartesian::CartesianState;
use crate::math::Vector3;
-use crate::naif::daf::NAIFDataSet;
-use crate::naif::spk::datatypes::{HermiteSetType13, LagrangeSetType9, Type2ChebyshevSet};
+use crate::naif::daf::datatypes::{HermiteSetType13, LagrangeSetType9, Type2ChebyshevSet};
+use crate::naif::daf::{DAFError, DafDataType, NAIFDataSet};
use crate::prelude::Frame;
-impl<'a> Almanac<'a> {
+impl Almanac {
/// Returns the position vector and velocity vector of the `source` with respect to its parent in the ephemeris at the provided epoch,
- /// and in the provided distance and time units.
- ///
- /// # Example
- /// If the ephemeris stores position interpolation coefficients in kilometer but this function is called with millimeters as a distance unit,
- /// the output vectors will be in mm, mm/s, mm/s^2 respectively.
+ /// Units are those used in the SPK, typically distances are in kilometers and velocities in kilometers per second.
///
/// # Errors
/// + As of now, some interpolation types are not supported, and if that were to happen, this would return an error.
///
- /// **WARNING:** This function only performs the translation and no rotation whatsoever. Use the `transform_to_parent_from` function instead to include rotations.
+ /// # Warning
+ /// This function only performs the translation and no rotation whatsoever. Use the `transform_to_parent_from` function instead to include rotations.
pub(crate) fn translation_parts_to_parent(
&self,
source: Frame,
@@ -46,7 +43,7 @@ impl<'a> Almanac<'a> {
let new_frame = source.with_ephem(summary.center_id);
- trace!("query {source} wrt to {new_frame} @ {epoch:E}");
+ trace!("translate {source} wrt to {new_frame} @ {epoch:E}");
// This should not fail because we've fetched the spk_no from above with the spk_summary_at_epoch call.
let spk_data = self.spk_data[spk_no]
@@ -54,10 +51,8 @@ impl<'a> Almanac<'a> {
.ok_or(EphemerisError::Unreachable)?;
// Now let's simply evaluate the data
- let (pos_km, vel_km_s) = match summary.data_type_i {
- // TODO : match against enumeration instead of direct integer match for clarity ?
- 2 => {
- // Type 2 Chebyshev
+ let (pos_km, vel_km_s) = match summary.data_type()? {
+ DafDataType::Type2ChebyshevTriplet => {
let data = spk_data
.nth_data::(idx_in_spk)
.with_context(|_| SPKSnafu {
@@ -66,8 +61,7 @@ impl<'a> Almanac<'a> {
data.evaluate(epoch, summary)
.with_context(|_| EphemInterpolationSnafu)?
}
- 9 => {
- // Type 9: Lagrange Interpolation --- Unequal Time Steps
+ DafDataType::Type9LagrangeUnequalStep => {
let data = spk_data
.nth_data::(idx_in_spk)
.with_context(|_| SPKSnafu {
@@ -76,8 +70,7 @@ impl<'a> Almanac<'a> {
data.evaluate(epoch, summary)
.with_context(|_| EphemInterpolationSnafu)?
}
- 13 => {
- // Type 13: Hermite Interpolation --- Unequal Time Steps
+ DafDataType::Type13HermiteUnequalStep => {
let data = spk_data
.nth_data::(idx_in_spk)
.with_context(|_| SPKSnafu {
@@ -86,7 +79,15 @@ impl<'a> Almanac<'a> {
data.evaluate(epoch, summary)
.with_context(|_| EphemInterpolationSnafu)?
}
- _ => todo!("{} is not yet supported", summary.data_type_i),
+ dtype => {
+ return Err(EphemerisError::SPK {
+ action: "translation to parent",
+ source: DAFError::UnsupportedDatatype {
+ dtype,
+ kind: "SPK computations",
+ },
+ })
+ }
};
Ok((pos_km, vel_km_s, new_frame))
diff --git a/src/ephemerides/translations.rs b/src/ephemerides/translations.rs
index abde0d5e..038ca80f 100644
--- a/src/ephemerides/translations.rs
+++ b/src/ephemerides/translations.rs
@@ -11,7 +11,7 @@
use snafu::ResultExt;
use super::EphemerisError;
-use super::UnderlyingPhysicsSnafu;
+use super::EphemerisPhysicsSnafu;
use crate::almanac::Almanac;
use crate::astro::Aberration;
use crate::hifitime::Epoch;
@@ -23,12 +23,14 @@ use crate::prelude::Frame;
/// **Limitation:** no translation or rotation may have more than 8 nodes.
pub const MAX_TREE_DEPTH: usize = 8;
-impl<'a> Almanac<'a> {
- /// Returns the Cartesian state needed to translate the `from_frame` to the `to_frame`.
+impl Almanac {
+ /// Returns the Cartesian state needed to translate the `from_frame` to the `to_frame`.
///
- /// **WARNING:** This function only performs the translation and no rotation whatsoever. Use the `transform_from_to` function instead to include rotations.
+ /// # Warning
+ /// This function only performs the translation and no rotation whatsoever. Use the `transform_from_to` function instead to include rotations.
///
- /// Note: this function performs a recursion of no more than twice the [MAX_TREE_DEPTH].
+ /// # Note
+ /// This function performs a recursion of no more than twice the [MAX_TREE_DEPTH].
pub fn translate_from_to(
&self,
from_frame: Frame,
@@ -68,7 +70,7 @@ impl<'a> Almanac<'a> {
self.translation_parts_to_parent(to_frame, epoch, ab_corr)?
};
- for cur_node_hash in path.iter().take(node_count) {
+ for cur_node_id in path.iter().take(node_count) {
if !frame_fwrd.ephem_origin_id_match(common_node) {
let (cur_pos_fwrd, cur_vel_fwrd, cur_frame_fwrd) =
self.translation_parts_to_parent(frame_fwrd, epoch, ab_corr)?;
@@ -88,7 +90,7 @@ impl<'a> Almanac<'a> {
}
// We know this exist, so we can safely unwrap it
- if cur_node_hash.unwrap() == common_node {
+ if cur_node_id.unwrap() == common_node {
break;
}
}
@@ -97,7 +99,7 @@ impl<'a> Almanac<'a> {
radius_km: pos_fwrd - pos_bwrd,
velocity_km_s: vel_fwrd - vel_bwrd,
epoch,
- frame: to_frame,
+ frame: to_frame.with_orient(from_frame.orientation_id),
})
}
@@ -111,9 +113,24 @@ impl<'a> Almanac<'a> {
self.translate_from_to(from_frame, to_frame, epoch, Aberration::None)
}
+ /// Translates the provided Cartesian state into the requested frame
+ ///
+ /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the [transform_to] function instead to include rotations.
+ #[allow(clippy::too_many_arguments)]
+ pub fn translate_to(
+ &self,
+ state: CartesianState,
+ to_frame: Frame,
+ ab_corr: Aberration,
+ ) -> Result {
+ let frame_state = self.translate_from_to(state.frame, to_frame, state.epoch, ab_corr)?;
+
+ Ok(state.add_unchecked(frame_state))
+ }
+
/// Translates a state with its origin (`to_frame`) and given its units (distance_unit, time_unit), returns that state with respect to the requested frame
///
- /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_state_to` function instead to include rotations.
+ /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the [transform_state_to] function instead to include rotations.
#[allow(clippy::too_many_arguments)]
pub fn translate_state_to(
&self,
@@ -139,6 +156,6 @@ impl<'a> Almanac<'a> {
frame: from_frame,
};
- (input_state + frame_state).with_context(|_| UnderlyingPhysicsSnafu {})
+ (input_state + frame_state).with_context(|_| EphemerisPhysicsSnafu {})
}
}
diff --git a/src/errors.rs b/src/errors.rs
index 5401854f..953e4b5a 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -11,6 +11,8 @@
use hifitime::Epoch;
use snafu::prelude::*;
+use crate::ephemerides::EphemerisError;
+use crate::orientations::OrientationError;
use crate::prelude::FrameUid;
use crate::structure::semver::Semver;
use crate::NaifId;
@@ -18,6 +20,21 @@ use core::convert::From;
use der::Error as DerError;
use std::io::ErrorKind as IOErrorKind;
+#[derive(Debug, Snafu)]
+#[snafu(visibility(pub))]
+pub enum AlmanacError {
+ #[snafu(display("{action} encountered an error with ephemeris computation {source}"))]
+ Ephemeris {
+ action: &'static str,
+ source: EphemerisError,
+ },
+ #[snafu(display("{action} encountered an error with orientation computation {source}"))]
+ Orientation {
+ action: &'static str,
+ source: OrientationError,
+ },
+}
+
#[derive(Debug, Snafu)]
pub enum InputOutputError {
/// Raised for an error in reading or writing the file(s)
@@ -116,11 +133,21 @@ pub enum PhysicsError {
frame1: FrameUid,
frame2: FrameUid,
},
- #[snafu(display("origins {from1} and {from2} differ while {action}"))]
- OriginMismatch {
+ #[snafu(display(
+ "cannot {action} because rotations {from1}->{to1} and {from2}->{to2} are incompatible"
+ ))]
+ InvalidRotation {
action: &'static str,
from1: NaifId,
+ to1: NaifId,
from2: NaifId,
+ to2: NaifId,
+ },
+ #[snafu(display("cannot rotate state in frame {state_frame} with rotation {from}->{to}"))]
+ InvalidStateRotation {
+ from: NaifId,
+ to: NaifId,
+ state_frame: FrameUid,
},
#[snafu(display("{action} requires the time derivative of the DCM but it is not set"))]
DCMMissingDerivative { action: &'static str },
diff --git a/src/frames/frame.rs b/src/frames/frame.rs
index c34c02a0..39b3179a 100644
--- a/src/frames/frame.rs
+++ b/src/frames/frame.rs
@@ -11,7 +11,7 @@
use core::fmt;
use core::fmt::Debug;
-use crate::constants::celestial_objects::celestial_name_from_id;
+use crate::constants::celestial_objects::{celestial_name_from_id, SOLAR_SYSTEM_BARYCENTER};
use crate::constants::orientations::{orientation_name_from_id, J2000};
use crate::errors::PhysicsError;
use crate::prelude::FrameUid;
@@ -44,6 +44,10 @@ impl Frame {
Self::from_ephem_orient(ephemeris_id, J2000)
}
+ pub const fn from_orient_ssb(orientation_id: NaifId) -> Self {
+ Self::from_ephem_orient(SOLAR_SYSTEM_BARYCENTER, orientation_id)
+ }
+
/// Returns a copy of this Frame whose ephemeris ID is set to the provided ID
pub const fn with_ephem(&self, new_ephem_id: NaifId) -> Self {
let mut me = *self;
@@ -69,19 +73,19 @@ impl Frame {
}
/// Returns true if the ephemeris origin is equal to the provided ID
- pub fn ephem_origin_id_match(&self, other_id: NaifId) -> bool {
+ pub const fn ephem_origin_id_match(&self, other_id: NaifId) -> bool {
self.ephemeris_id == other_id
}
/// Returns true if the orientation origin is equal to the provided ID
- pub fn orient_origin_id_match(&self, other_id: NaifId) -> bool {
+ pub const fn orient_origin_id_match(&self, other_id: NaifId) -> bool {
self.orientation_id == other_id
}
/// Returns true if the ephemeris origin is equal to the provided frame
- pub fn ephem_origin_match(&self, other: Self) -> bool {
+ pub const fn ephem_origin_match(&self, other: Self) -> bool {
self.ephem_origin_id_match(other.ephemeris_id)
}
/// Returns true if the orientation origin is equal to the provided frame
- pub fn orient_origin_match(&self, other: Self) -> bool {
+ pub const fn orient_origin_match(&self, other: Self) -> bool {
self.orient_origin_id_match(other.orientation_id)
}
@@ -160,23 +164,20 @@ impl fmt::Display for Frame {
impl fmt::LowerExp for Frame {
/// Only prints the ephemeris name
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
- let body_name = match celestial_name_from_id(self.ephemeris_id) {
- Some(name) => name.to_string(),
- None => format!("{}", self.ephemeris_id),
- };
- write!(f, "{body_name}")
+ match celestial_name_from_id(self.ephemeris_id) {
+ Some(name) => write!(f, "{name}"),
+ None => write!(f, "{}", self.ephemeris_id),
+ }
}
}
impl fmt::Octal for Frame {
/// Only prints the orientation name
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
- let orientation_name = match orientation_name_from_id(self.orientation_id) {
- Some(name) => name.to_string(),
- None => format!("orientation {}", self.orientation_id),
- };
-
- write!(f, "{orientation_name}")
+ match orientation_name_from_id(self.orientation_id) {
+ Some(name) => write!(f, "{name}"),
+ None => write!(f, "orientation {}", self.orientation_id),
+ }
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 2675c7d9..76b608de 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+#![doc = include_str!("../README.md")]
/*
* ANISE Toolkit
* Copyright (C) 2021-2023 Christopher Rabotin et al. (cf. AUTHORS.md)
@@ -26,12 +27,13 @@ pub mod structure;
/// Re-export of hifitime
pub mod time {
+ pub use core::str::FromStr;
pub use hifitime::*;
}
pub mod prelude {
pub use crate::almanac::Almanac;
- pub use crate::astro::Aberration;
+ pub use crate::astro::{orbit::Orbit, Aberration};
pub use crate::errors::InputOutputError;
pub use crate::frames::*;
pub use crate::math::units::*;
diff --git a/src/math/cartesian.rs b/src/math/cartesian.rs
index d37f56ac..222c3e60 100644
--- a/src/math/cartesian.rs
+++ b/src/math/cartesian.rs
@@ -15,7 +15,7 @@ use crate::{
prelude::Frame,
};
use core::fmt;
-use core::ops::Add;
+use core::ops::{Add, Neg, Sub};
use hifitime::Epoch;
use nalgebra::Vector6;
use snafu::ensure;
@@ -174,6 +174,26 @@ impl CartesianState {
&& (self.velocity_km_s.z - other.velocity_km_s.z).abs() < velocity_tol_km_s
&& self.frame == other.frame
}
+
+ /// Adds the other state to this state WITHOUT checking if the frames match.
+ pub(crate) fn add_unchecked(&self, other: Self) -> Self {
+ Self {
+ radius_km: self.radius_km + other.radius_km,
+ velocity_km_s: self.velocity_km_s + other.velocity_km_s,
+ epoch: self.epoch,
+ frame: self.frame,
+ }
+ }
+
+ /// Subs the other state to this state WITHOUT checking if the frames match.
+ pub(crate) fn sub_unchecked(&self, other: Self) -> Self {
+ Self {
+ radius_km: self.radius_km - other.radius_km,
+ velocity_km_s: self.velocity_km_s - other.velocity_km_s,
+ epoch: self.epoch,
+ frame: self.frame,
+ }
+ }
}
impl Add for CartesianState {
@@ -191,7 +211,7 @@ impl Add for CartesianState {
);
ensure!(
- self.frame == other.frame,
+ self.frame.ephemeris_id == other.frame.ephemeris_id,
FrameMismatchSnafu {
action: "translating states",
frame1: self.frame,
@@ -199,12 +219,7 @@ impl Add for CartesianState {
}
);
- Ok(CartesianState {
- radius_km: self.radius_km + other.radius_km,
- velocity_km_s: self.velocity_km_s + other.velocity_km_s,
- epoch: self.epoch,
- frame: self.frame,
- })
+ Ok(self.add_unchecked(other))
}
}
@@ -217,6 +232,44 @@ impl PartialEq for CartesianState {
}
}
+impl Sub for CartesianState {
+ type Output = Result;
+
+ /// Adds one state to another. This will return an error if the epochs or frames are different.
+ fn sub(self, other: CartesianState) -> Self::Output {
+ ensure!(
+ self.epoch == other.epoch,
+ EpochMismatchSnafu {
+ action: "translating states",
+ epoch1: self.epoch,
+ epoch2: other.epoch
+ }
+ );
+
+ ensure!(
+ self.frame.ephemeris_id == other.frame.ephemeris_id,
+ FrameMismatchSnafu {
+ action: "translating states",
+ frame1: self.frame,
+ frame2: other.frame
+ }
+ );
+
+ Ok(self.sub_unchecked(other))
+ }
+}
+
+impl Neg for CartesianState {
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ let mut me = self;
+ me.radius_km = -me.radius_km;
+ me.velocity_km_s = -me.velocity_km_s;
+ me
+ }
+}
+
#[allow(clippy::format_in_format_args)]
impl fmt::Display for CartesianState {
// Prints as Cartesian in floating point with units
diff --git a/src/math/rotation/dcm.rs b/src/math/rotation/dcm.rs
index 0486fd1f..9cac9321 100644
--- a/src/math/rotation/dcm.rs
+++ b/src/math/rotation/dcm.rs
@@ -8,16 +8,19 @@
* Documentation: https://nyxspace.com/
*/
use crate::{
- errors::PhysicsError,
- math::{Matrix3, Matrix6, Vector3, Vector6},
+ astro::PhysicsResult,
+ errors::{InvalidRotationSnafu, InvalidStateRotationSnafu, PhysicsError},
+ math::{cartesian::CartesianState, Matrix3, Matrix6, Vector3, Vector6},
+ prelude::Frame,
NaifId,
};
use nalgebra::Vector4;
+use snafu::ensure;
+use super::{r1, r2, r3, Quaternion, Rotation};
+use core::fmt;
use core::ops::Mul;
-use super::{Quaternion, Rotation};
-
#[derive(Copy, Clone, Debug, Default)]
pub struct DCM {
/// The rotation matrix itself
@@ -41,10 +44,8 @@ impl DCM {
/// * `angle_rad` - The angle of rotation in radians.
///
pub fn r1(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
- let (s, c) = angle_rad.sin_cos();
- let rot_mat = Matrix3::new(1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c);
Self {
- rot_mat,
+ rot_mat: r1(angle_rad),
from,
to,
rot_mat_dt: None,
@@ -59,10 +60,8 @@ impl DCM {
/// * `angle` - The angle of rotation in radians.
///
pub fn r2(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
- let (s, c) = angle_rad.sin_cos();
- let rot_mat = Matrix3::new(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c);
Self {
- rot_mat,
+ rot_mat: r2(angle_rad),
from,
to,
rot_mat_dt: None,
@@ -77,10 +76,8 @@ impl DCM {
/// * `angle_rad` - The angle of rotation in radians.
///
pub fn r3(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
- let (s, c) = angle_rad.sin_cos();
- let rot_mat = Matrix3::new(c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0);
Self {
- rot_mat,
+ rot_mat: r3(angle_rad),
from,
to,
rot_mat_dt: None,
@@ -88,28 +85,25 @@ impl DCM {
}
/// Returns the 6x6 DCM to rotate a state, if the time derivative of this DCM exists.
- pub fn state_dcm(&self) -> Result {
- match self.rot_mat_dt {
- Some(mat_dt) => {
- let mut full_dcm = Matrix6::zeros();
- for i in 0..6 {
- for j in 0..6 {
- if (i < 3 && j < 3) || (i >= 3 && j >= 3) {
- full_dcm[(i, j)] = self.rot_mat[(i % 3, j % 3)];
- } else if i >= 3 && j < 3 {
- full_dcm[(i, j)] = mat_dt[(i - 3, j)];
- }
- }
+ pub fn state_dcm(&self) -> Matrix6 {
+ let mut full_dcm = Matrix6::zeros();
+ for i in 0..6 {
+ for j in 0..6 {
+ if (i < 3 && j < 3) || (i >= 3 && j >= 3) {
+ full_dcm[(i, j)] = self.rot_mat[(i % 3, j % 3)];
+ } else if i >= 3 && j < 3 {
+ full_dcm[(i, j)] = self
+ .rot_mat_dt
+ .map(|dcm_dt| dcm_dt[(i - 3, j)])
+ .unwrap_or(0.0);
}
-
- Ok(full_dcm)
}
- None => Err(PhysicsError::DCMMissingDerivative {
- action: "building the 6x6 DCM matrix",
- }),
}
+
+ full_dcm
}
+ /// Builds an identity rotation
pub fn identity(from: i32, to: i32) -> Self {
let rot_mat = Matrix3::identity();
@@ -120,6 +114,85 @@ impl DCM {
rot_mat_dt: None,
}
}
+
+ /// Returns whether this rotation is identity, checking first the frames and then the rotation matrix (but ignores its time derivative)
+ pub fn is_identity(&self) -> bool {
+ self.to == self.from || (self.rot_mat - Matrix3::identity()).norm() < 1e-8
+ }
+
+ /// Returns whether the `rot_mat` of this DCM is a valid rotation matrix.
+ /// The criteria for validity are:
+ /// -- The columns of the matrix are unit vectors, within a specified tolerance.
+ /// -- The determinant of the matrix formed by unitizing the columns of the input matrix is 1, within a specified tolerance. This criterion ensures that the columns of the matrix are nearly orthogonal, and that they form a right-handed basis.
+ /// [Source: SPICE's rotation.req](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/rotation.html#Validating%20a%20rotation%20matrix)
+ pub fn is_valid(&self, unit_tol: f64, det_tol: f64) -> bool {
+ for col in self.rot_mat.column_iter() {
+ if (col.norm() - 1.0).abs() > unit_tol {
+ return false;
+ }
+ }
+ (self.rot_mat.determinant() - 1.0).abs() < det_tol
+ }
+
+ /// Multiplies this DCM with another one WITHOUT checking if the frames match.
+ pub(crate) fn mul_unchecked(&self, other: Self) -> Self {
+ let mut rslt = *self;
+ rslt.rot_mat *= other.rot_mat;
+ rslt.from = other.from;
+ // Make sure to apply the transport theorem.
+ if let Some(other_rot_mat_dt) = other.rot_mat_dt {
+ if let Some(rot_mat_dt) = self.rot_mat_dt {
+ rslt.rot_mat_dt =
+ Some(rot_mat_dt * other.rot_mat + self.rot_mat * other_rot_mat_dt);
+ } else {
+ rslt.rot_mat_dt = Some(self.rot_mat * other_rot_mat_dt);
+ }
+ } else if let Some(rot_mat_dt) = self.rot_mat_dt {
+ rslt.rot_mat_dt = Some(rot_mat_dt * other.rot_mat);
+ }
+ rslt
+ }
+
+ pub fn transpose(&self) -> Self {
+ Self {
+ rot_mat: self.rot_mat.transpose(),
+ rot_mat_dt: self.rot_mat_dt.map(|rot_mat_dt| rot_mat_dt.transpose()),
+ to: self.from,
+ from: self.to,
+ }
+ }
+}
+
+impl Mul for DCM {
+ type Output = Result;
+
+ fn mul(self, rhs: Self) -> Self::Output {
+ if self.is_identity() {
+ let mut rslt = rhs;
+ rslt.from = rhs.from;
+ rslt.to = self.to;
+ Ok(rslt)
+ } else if rhs.is_identity() {
+ let mut rslt = self;
+ rslt.from = rhs.from;
+ rslt.to = self.to;
+ Ok(rslt)
+ // Ok(self)
+ } else {
+ ensure!(
+ self.from == rhs.to,
+ InvalidRotationSnafu {
+ action: "multiply DCMs",
+ from1: self.from,
+ to1: self.to,
+ from2: rhs.from,
+ to2: rhs.to
+ }
+ );
+
+ Ok(self.mul_unchecked(rhs))
+ }
+ }
}
impl Mul for DCM {
@@ -156,11 +229,34 @@ impl Mul for DCM {
}
impl Mul for DCM {
- type Output = Result;
+ type Output = Vector6;
/// Applying the matrix to a vector yields the vector's representation in the new coordinate system.
fn mul(self, rhs: Vector6) -> Self::Output {
- Ok(self.state_dcm()? * rhs)
+ self.state_dcm() * rhs
+ }
+}
+
+impl Mul for DCM {
+ type Output = PhysicsResult;
+
+ fn mul(self, rhs: CartesianState) -> Self::Output {
+ ensure!(
+ self.from == rhs.frame.orientation_id,
+ InvalidStateRotationSnafu {
+ from: self.from,
+ to: self.to,
+ state_frame: rhs.frame
+ }
+ );
+ let new_state = self.state_dcm() * rhs.to_cartesian_pos_vel();
+
+ let mut rslt = rhs;
+ rslt.radius_km = new_state.fixed_rows::<3>(0).to_owned().into();
+ rslt.velocity_km_s = new_state.fixed_rows::<3>(3).to_owned().into();
+ rslt.frame.orientation_id = self.to;
+
+ Ok(rslt)
}
}
@@ -283,6 +379,23 @@ impl PartialEq for DCM {
}
}
+impl fmt::Display for DCM {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "Rotation {:o} -> {:o} (transport theorem = {}){}Derivative: {}",
+ Frame::from_orient_ssb(self.from),
+ Frame::from_orient_ssb(self.to),
+ self.rot_mat_dt.is_some(),
+ self.rot_mat,
+ match self.rot_mat_dt {
+ None => "None".to_string(),
+ Some(dcm_dt) => format!("{dcm_dt}"),
+ }
+ )
+ }
+}
+
#[cfg(test)]
mod ut_dcm {
use crate::math::Matrix3;
@@ -333,6 +446,8 @@ mod ut_dcm {
fn test_r3() {
let r3 = DCM::r3(FRAC_PI_2, 0, 1);
+ assert!(r3.is_valid(1e-12, 1e-12));
+
// Rotation of the Z vector about Z, yields Z
assert_eq!(r3 * Vector3::z(), Vector3::z());
// Rotation of the X vector about Z by -half pi, yields -Y
diff --git a/src/math/rotation/mod.rs b/src/math/rotation/mod.rs
index b666e30f..e1714cf1 100644
--- a/src/math/rotation/mod.rs
+++ b/src/math/rotation/mod.rs
@@ -22,6 +22,42 @@ pub use quaternion::Quaternion;
pub trait Rotation: TryInto {}
+/// Build a 3x3 rotation matrix around the X axis
+pub fn r1(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c)
+}
+
+/// Build the derivative of the 3x3 rotation matrix around the X axis
+pub fn r1_dot(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(0.0, 0.0, 0.0, 0.0, -s, c, 0.0, -c, -s)
+}
+
+/// Build a 3x3 rotation matrix around the Y axis
+pub fn r2(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c)
+}
+
+/// Build the derivative of the 3x3 rotation matrix around the Y axis
+pub fn r2_dot(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(-s, 0.0, -c, 0.0, 0.0, 0.0, c, 0.0, -s)
+}
+
+/// Build a 3x3 rotation matrix around the Z axis
+pub fn r3(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0)
+}
+
+/// Build the derivative of the 3x3 rotation matrix around the Z axis
+pub fn r3_dot(angle_rad: f64) -> Matrix3 {
+ let (s, c) = angle_rad.sin_cos();
+ Matrix3::new(-s, c, 0.0, -c, -s, 0.0, 0.0, 0.0, 0.0)
+}
+
/// Generates the angles for the test
#[cfg(test)]
pub(crate) fn generate_angles() -> Vec {
@@ -38,6 +74,7 @@ pub(crate) fn generate_angles() -> Vec {
angles
}
+use super::Matrix3;
#[cfg(test)]
use super::Vector3;
/// Returns whether two vectors can be considered equal after a rotation
diff --git a/src/math/rotation/mrp.rs b/src/math/rotation/mrp.rs
index 11f3478b..48b9b25f 100644
--- a/src/math/rotation/mrp.rs
+++ b/src/math/rotation/mrp.rs
@@ -11,7 +11,7 @@
use snafu::ensure;
use crate::{
- errors::{DivisionByZeroSnafu, MathError, OriginMismatchSnafu, PhysicsError},
+ errors::{DivisionByZeroSnafu, InvalidRotationSnafu, MathError, PhysicsError},
math::{Matrix3, Vector3},
NaifId,
};
@@ -167,10 +167,12 @@ impl MRP {
pub fn relative_to(&self, rhs: &Self) -> Result {
ensure!(
self.from == rhs.from,
- OriginMismatchSnafu {
- action: "computing relative MRP",
+ InvalidRotationSnafu {
+ action: "compute relative MRP",
from1: self.from,
- from2: rhs.from
+ to1: self.to,
+ from2: rhs.from,
+ to2: rhs.to
}
);
@@ -206,10 +208,12 @@ impl Mul for MRP {
fn mul(self, rhs: Self) -> Self::Output {
ensure!(
self.to == rhs.from,
- OriginMismatchSnafu {
- action: "composing MRPs",
+ InvalidRotationSnafu {
+ action: "compose MRPs",
from1: self.from,
- from2: rhs.from
+ to1: self.to,
+ from2: rhs.from,
+ to2: rhs.to
}
);
diff --git a/src/math/rotation/quaternion.rs b/src/math/rotation/quaternion.rs
index fa9781b5..63883387 100644
--- a/src/math/rotation/quaternion.rs
+++ b/src/math/rotation/quaternion.rs
@@ -8,11 +8,13 @@
* Documentation: https://nyxspace.com/
*/
-use crate::errors::{OriginMismatchSnafu, PhysicsError};
+use crate::errors::{InvalidRotationSnafu, PhysicsError};
use crate::math::rotation::EPSILON;
+use crate::structure::dataset::DataSetT;
use crate::{math::Vector3, math::Vector4, NaifId};
use core::fmt;
use core::ops::Mul;
+use der::{Decode, Encode, Reader, Writer};
use nalgebra::Matrix4x3;
use snafu::ensure;
@@ -233,15 +235,17 @@ impl EulerParameter {
}
impl Mul for Quaternion {
- type Output = Result;
+ type Output = Result;
- fn mul(self, rhs: Quaternion) -> Result {
+ fn mul(self, rhs: Quaternion) -> Self::Output {
ensure!(
self.to == rhs.from,
- OriginMismatchSnafu {
- action: "multiplying quaternions",
+ InvalidRotationSnafu {
+ action: "multiply quaternions",
from1: self.from,
- from2: rhs.from
+ to1: self.to,
+ from2: rhs.from,
+ to2: rhs.to
}
);
@@ -318,6 +322,56 @@ impl fmt::Display for EulerParameter {
}
}
+impl Default for EulerParameter {
+ fn default() -> Self {
+ Self::identity(0, 0)
+ }
+}
+
+impl<'a> Decode<'a> for EulerParameter {
+ fn decode>(decoder: &mut R) -> der::Result {
+ let from = decoder.decode()?;
+ let to = decoder.decode()?;
+ let w = decoder.decode()?;
+ let x = decoder.decode()?;
+ let y = decoder.decode()?;
+ let z = decoder.decode()?;
+
+ Ok(Self {
+ w,
+ x,
+ y,
+ z,
+ from,
+ to,
+ })
+ }
+}
+
+impl Encode for EulerParameter {
+ fn encoded_len(&self) -> der::Result {
+ self.from.encoded_len()?
+ + self.to.encoded_len()?
+ + self.w.encoded_len()?
+ + self.x.encoded_len()?
+ + self.y.encoded_len()?
+ + self.z.encoded_len()?
+ }
+
+ fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
+ self.from.encode(encoder)?;
+ self.to.encode(encoder)?;
+ self.w.encode(encoder)?;
+ self.x.encode(encoder)?;
+ self.y.encode(encoder)?;
+ self.z.encode(encoder)
+ }
+}
+
+impl DataSetT for EulerParameter {
+ const NAME: &'static str = "euler parameter";
+}
+
#[cfg(test)]
mod ut_quaternion {
use crate::math::{
@@ -494,4 +548,27 @@ mod ut_quaternion {
}
// TODO: Add useful tests
+
+ use der::{Decode, Encode};
+
+ #[test]
+ fn ep_encdec_min_repr() {
+ // A minimal representation of a planetary constant.
+ let repr = EulerParameter {
+ from: -123,
+ to: 345,
+ w: 0.1,
+ x: 0.2,
+ y: 0.2,
+ z: 0.2,
+ }
+ .normalize();
+
+ let mut buf = vec![];
+ repr.encode_to_vec(&mut buf).unwrap();
+
+ let repr_dec = EulerParameter::from_der(&buf).unwrap();
+
+ assert_eq!(repr, repr_dec);
+ }
}
diff --git a/src/naif/daf/daf.rs b/src/naif/daf/daf.rs
index 9b8dc205..cb12f1df 100644
--- a/src/naif/daf/daf.rs
+++ b/src/naif/daf/daf.rs
@@ -10,11 +10,12 @@
use super::file_record::FileRecordError;
use super::{
- DAFError, DecodingNameSnafu, DecodingSummarySnafu, FileRecordSnafu, NAIFDataSet, NAIFRecord,
- NAIFSummaryRecord,
+ DAFError, DecodingNameSnafu, DecodingSummarySnafu, FileRecordSnafu, IOSnafu, NAIFDataSet,
+ NAIFRecord, NAIFSummaryRecord,
};
pub use super::{FileRecord, NameRecord, SummaryRecord};
-use crate::errors::DecodingError;
+use crate::errors::{DecodingError, InputOutputError};
+use crate::file2heap;
use crate::naif::daf::DecodingDataSnafu;
use crate::{errors::IntegrityError, DBL_SIZE};
use bytes::Bytes;
@@ -84,26 +85,12 @@ impl DAF {
Self::parse(bytes)
}
- pub fn load + Debug>(path: P) -> Result {
- match File::open(&path) {
- Err(source) => Err(DAFError::IO {
- action: format!("loading {path:?}"),
- source,
- }),
- Ok(file) => unsafe {
- use memmap2::MmapOptions;
- match MmapOptions::new().map(&file) {
- Err(source) => Err(DAFError::IO {
- action: format!("mmap of {path:?}"),
- source,
- }),
- Ok(mmap) => {
- let bytes = Bytes::copy_from_slice(&mmap);
- Self::parse(bytes)
- }
- }
- },
- }
+ pub fn load(path: &str) -> Result {
+ let bytes = file2heap!(path).with_context(|_| IOSnafu {
+ action: format!("loading {path:?}"),
+ })?;
+
+ Self::parse(bytes)
}
/// Parse the provided static byte array as a SPICE Double Array File
@@ -411,7 +398,8 @@ mod daf_ut {
use crate::{
errors::{InputOutputError, IntegrityError},
file2heap,
- naif::{daf::DAFError, spk::datatypes::HermiteSetType13, SPK},
+ naif::daf::{datatypes::HermiteSetType13, DAFError},
+ prelude::SPK,
};
use std::fs::File;
diff --git a/src/naif/daf/data_types.rs b/src/naif/daf/data_types.rs
new file mode 100644
index 00000000..2eced934
--- /dev/null
+++ b/src/naif/daf/data_types.rs
@@ -0,0 +1,159 @@
+/*
+ * 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 snafu::ensure;
+
+use crate::naif::daf::DatatypeSnafu;
+
+use super::DAFError;
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[repr(u8)]
+pub enum DataType {
+ Type1ModifiedDifferenceArray = 1,
+ Type2ChebyshevTriplet = 2,
+ Type3ChebyshevSextuplet = 3,
+ Type5DiscreteStates = 5,
+ Type8LagrangeEqualStep = 8,
+ Type9LagrangeUnequalStep = 9,
+ Type10SpaceCommandTLE = 10,
+ Type12HermiteEqualStep = 12,
+ Type13HermiteUnequalStep = 13,
+ Type14ChebyshevUnequalStep = 14,
+ Type15PrecessingConics = 15,
+ Type17Equinoctial = 17,
+ Type18ESOCHermiteLagrange = 18,
+ Type19ESOCPiecewise = 19,
+ Type20ChebyshevDerivative = 20,
+ Type21ExtendedModifiedDifferenceArray = 21,
+}
+
+impl TryFrom for DataType {
+ type Error = DAFError;
+
+ fn try_from(id: i32) -> Result {
+ ensure!(
+ (1..=21).contains(&id),
+ DatatypeSnafu {
+ kind: "unknown data type",
+ id
+ }
+ );
+ match id {
+ 1 => Ok(DataType::Type1ModifiedDifferenceArray),
+ 2 => Ok(DataType::Type2ChebyshevTriplet),
+ 3 => Ok(DataType::Type3ChebyshevSextuplet),
+ 5 => Ok(DataType::Type5DiscreteStates),
+ 8 => Ok(DataType::Type8LagrangeEqualStep),
+ 9 => Ok(DataType::Type9LagrangeUnequalStep),
+ 10 => Ok(DataType::Type10SpaceCommandTLE),
+ 12 => Ok(DataType::Type12HermiteEqualStep),
+ 13 => Ok(DataType::Type13HermiteUnequalStep),
+ 14 => Ok(DataType::Type14ChebyshevUnequalStep),
+ 15 => Ok(DataType::Type15PrecessingConics),
+ 17 => Ok(DataType::Type17Equinoctial),
+ 18 => Ok(DataType::Type18ESOCHermiteLagrange),
+ 19 => Ok(DataType::Type19ESOCPiecewise),
+ 20 => Ok(DataType::Type20ChebyshevDerivative),
+ 21 => Ok(DataType::Type21ExtendedModifiedDifferenceArray),
+ _ => Err(DAFError::Datatype {
+ id,
+ kind: "unknown data type",
+ }),
+ }
+ }
+}
+
+#[cfg(test)]
+mod ut_datatype {
+ use super::*;
+
+ #[test]
+ fn test_try_from_valid_values() {
+ assert_eq!(
+ DataType::try_from(1).unwrap(),
+ DataType::Type1ModifiedDifferenceArray
+ );
+ assert_eq!(
+ DataType::try_from(2).unwrap(),
+ DataType::Type2ChebyshevTriplet
+ );
+ assert_eq!(
+ DataType::try_from(3).unwrap(),
+ DataType::Type3ChebyshevSextuplet
+ );
+ assert_eq!(
+ DataType::try_from(5).unwrap(),
+ DataType::Type5DiscreteStates
+ );
+ assert_eq!(
+ DataType::try_from(8).unwrap(),
+ DataType::Type8LagrangeEqualStep
+ );
+ assert_eq!(
+ DataType::try_from(9).unwrap(),
+ DataType::Type9LagrangeUnequalStep
+ );
+ assert_eq!(
+ DataType::try_from(10).unwrap(),
+ DataType::Type10SpaceCommandTLE
+ );
+ assert_eq!(
+ DataType::try_from(12).unwrap(),
+ DataType::Type12HermiteEqualStep
+ );
+ assert_eq!(
+ DataType::try_from(13).unwrap(),
+ DataType::Type13HermiteUnequalStep
+ );
+ assert_eq!(
+ DataType::try_from(14).unwrap(),
+ DataType::Type14ChebyshevUnequalStep
+ );
+ assert_eq!(
+ DataType::try_from(15).unwrap(),
+ DataType::Type15PrecessingConics
+ );
+ assert_eq!(DataType::try_from(17).unwrap(), DataType::Type17Equinoctial);
+ assert_eq!(
+ DataType::try_from(18).unwrap(),
+ DataType::Type18ESOCHermiteLagrange
+ );
+ assert_eq!(
+ DataType::try_from(19).unwrap(),
+ DataType::Type19ESOCPiecewise
+ );
+ assert_eq!(
+ DataType::try_from(20).unwrap(),
+ DataType::Type20ChebyshevDerivative
+ );
+ assert_eq!(
+ DataType::try_from(21).unwrap(),
+ DataType::Type21ExtendedModifiedDifferenceArray
+ );
+ }
+
+ #[test]
+ fn test_try_from_invalid_values() {
+ let invalid_values = [0, 4, 6, 7, 11, 16, 22, 23, 100, -1, -5];
+ for &value in &invalid_values {
+ match DataType::try_from(value) {
+ Ok(_) => panic!("Expected error for value {}", value),
+ Err(e) => match e {
+ DAFError::Datatype { id, kind } => {
+ assert_eq!(id, value);
+ assert_eq!(kind, "unknown data type");
+ }
+ _ => panic!("Unexpected error variant"),
+ },
+ }
+ }
+ }
+}
diff --git a/src/naif/spk/datatypes/chebyshev.rs b/src/naif/daf/datatypes/chebyshev.rs
similarity index 96%
rename from src/naif/spk/datatypes/chebyshev.rs
rename to src/naif/daf/datatypes/chebyshev.rs
index 3ef79e22..c152b1a7 100644
--- a/src/naif/spk/datatypes/chebyshev.rs
+++ b/src/naif/daf/datatypes/chebyshev.rs
@@ -18,10 +18,7 @@ use crate::{
interpolation::{chebyshev_eval, InterpDecodingSnafu, InterpolationError},
Vector3,
},
- naif::{
- daf::{NAIFDataRecord, NAIFDataSet, NAIFSummaryRecord},
- spk::summary::SPKSummaryRecord,
- },
+ naif::daf::{NAIFDataRecord, NAIFDataSet, NAIFSummaryRecord},
};
#[derive(PartialEq)]
@@ -54,7 +51,6 @@ impl<'a> fmt::Display for Type2ChebyshevSet<'a> {
}
impl<'a> NAIFDataSet<'a> for Type2ChebyshevSet<'a> {
- type SummaryKind = SPKSummaryRecord;
type StateKind = (Vector3, Vector3);
type RecordKind = Type2ChebyshevRecord<'a>;
const DATASET_NAME: &'static str = "Chebyshev Type 2";
@@ -125,10 +121,10 @@ impl<'a> NAIFDataSet<'a> for Type2ChebyshevSet<'a> {
))
}
- fn evaluate(
+ fn evaluate(
&self,
epoch: Epoch,
- summary: &Self::SummaryKind,
+ summary: &S,
) -> Result<(Vector3, Vector3), InterpolationError> {
if epoch < summary.start_epoch() || epoch > summary.end_epoch() {
// No need to go any further.
@@ -142,7 +138,7 @@ impl<'a> NAIFDataSet<'a> for Type2ChebyshevSet<'a> {
let window_duration_s = self.interval_length.to_seconds();
let radius_s = window_duration_s / 2.0;
- let ephem_start_delta_s = epoch.to_et_seconds() - summary.start_epoch_et_s;
+ let ephem_start_delta_s = epoch.to_et_seconds() - summary.start_epoch_et_s();
/*
CSPICE CODE
@@ -178,8 +174,8 @@ impl<'a> NAIFDataSet<'a> for Type2ChebyshevSet<'a> {
let normalized_time = (epoch.to_et_seconds() - record.midpoint_et_s) / radius_s;
- let mut pos = Vector3::zeros();
- let mut vel = Vector3::zeros();
+ let mut state = Vector3::zeros();
+ let mut rate = Vector3::zeros();
for (cno, coeffs) in [record.x_coeffs, record.y_coeffs, record.z_coeffs]
.iter()
@@ -187,11 +183,11 @@ impl<'a> NAIFDataSet<'a> for Type2ChebyshevSet<'a> {
{
let (val, deriv) =
chebyshev_eval(normalized_time, coeffs, radius_s, epoch, self.degree())?;
- pos[cno] = val;
- vel[cno] = deriv;
+ state[cno] = val;
+ rate[cno] = deriv;
}
- Ok((pos, vel))
+ Ok((state, rate))
}
fn check_integrity(&self) -> Result<(), IntegrityError> {
diff --git a/src/naif/spk/datatypes/hermite.rs b/src/naif/daf/datatypes/hermite.rs
similarity index 97%
rename from src/naif/spk/datatypes/hermite.rs
rename to src/naif/daf/datatypes/hermite.rs
index 5c418da4..6e086edf 100644
--- a/src/naif/spk/datatypes/hermite.rs
+++ b/src/naif/daf/datatypes/hermite.rs
@@ -16,7 +16,7 @@ use crate::errors::{DecodingError, IntegrityError, TooFewDoublesSnafu};
use crate::math::interpolation::{
hermite_eval, InterpDecodingSnafu, InterpolationError, MAX_SAMPLES,
};
-use crate::naif::spk::summary::SPKSummaryRecord;
+use crate::naif::daf::NAIFSummaryRecord;
use crate::{
math::{cartesian::CartesianState, Vector3},
naif::daf::{NAIFDataRecord, NAIFDataSet, NAIFRecord},
@@ -49,7 +49,6 @@ impl<'a> fmt::Display for HermiteSetType12<'a> {
}
impl<'a> NAIFDataSet<'a> for HermiteSetType12<'a> {
- type SummaryKind = SPKSummaryRecord;
type StateKind = CartesianState;
type RecordKind = PositionVelocityRecord;
const DATASET_NAME: &'static str = "Hermite Type 12";
@@ -111,10 +110,10 @@ impl<'a> NAIFDataSet<'a> for HermiteSetType12<'a> {
))
}
- fn evaluate(
+ fn evaluate(
&self,
_epoch: Epoch,
- _: &Self::SummaryKind,
+ _: &S,
) -> Result {
todo!("https://github.com/anise-toolkit/anise.rs/issues/14")
}
@@ -168,7 +167,6 @@ impl<'a> fmt::Display for HermiteSetType13<'a> {
}
impl<'a> NAIFDataSet<'a> for HermiteSetType13<'a> {
- type SummaryKind = SPKSummaryRecord;
type StateKind = (Vector3, Vector3);
type RecordKind = PositionVelocityRecord;
const DATASET_NAME: &'static str = "Hermite Type 13";
@@ -260,10 +258,10 @@ impl<'a> NAIFDataSet<'a> for HermiteSetType13<'a> {
))
}
- fn evaluate(
+ fn evaluate(
&self,
epoch: Epoch,
- _: &Self::SummaryKind,
+ _: &S,
) -> Result {
// Start by doing a binary search on the epoch registry to limit the search space in the total number of epochs.
// TODO: use the epoch registry to reduce the search space
@@ -416,7 +414,7 @@ mod hermite_ut {
// Two metadata, one state, one epoch
let zeros = [0.0_f64; 2 * 7 + 2];
- let mut invalid_num_records = zeros.clone();
+ let mut invalid_num_records = zeros;
invalid_num_records[zeros.len() - 1] = f64::INFINITY;
match HermiteSetType13::from_slice_f64(&invalid_num_records) {
Ok(_) => panic!("test failed on invalid num records"),
@@ -435,7 +433,7 @@ mod hermite_ut {
}
}
- let mut invalid_num_samples = zeros.clone();
+ let mut invalid_num_samples = zeros;
invalid_num_samples[zeros.len() - 2] = f64::INFINITY;
match HermiteSetType13::from_slice_f64(&invalid_num_samples) {
Ok(_) => panic!("test failed on invalid num samples"),
@@ -454,7 +452,7 @@ mod hermite_ut {
}
}
- let mut invalid_epoch = zeros.clone();
+ let mut invalid_epoch = zeros;
invalid_epoch[zeros.len() - 3] = f64::INFINITY;
let dataset = HermiteSetType13::from_slice_f64(&invalid_epoch).unwrap();
@@ -471,7 +469,7 @@ mod hermite_ut {
}
}
- let mut invalid_record = zeros.clone();
+ let mut invalid_record = zeros;
invalid_record[0] = f64::INFINITY;
// Force the number of records to be one, otherwise everything is considered the epoch registry
invalid_record[zeros.len() - 1] = 1.0;
diff --git a/src/naif/spk/datatypes/lagrange.rs b/src/naif/daf/datatypes/lagrange.rs
similarity index 96%
rename from src/naif/spk/datatypes/lagrange.rs
rename to src/naif/daf/datatypes/lagrange.rs
index 2190bd20..5f3af40d 100644
--- a/src/naif/spk/datatypes/lagrange.rs
+++ b/src/naif/daf/datatypes/lagrange.rs
@@ -15,10 +15,7 @@ use snafu::ensure;
use crate::{
errors::{DecodingError, IntegrityError, TooFewDoublesSnafu},
math::{cartesian::CartesianState, interpolation::InterpolationError, Vector3},
- naif::{
- daf::{NAIFDataRecord, NAIFDataSet, NAIFRecord},
- spk::summary::SPKSummaryRecord,
- },
+ naif::daf::{NAIFDataRecord, NAIFDataSet, NAIFRecord, NAIFSummaryRecord},
DBL_SIZE,
};
@@ -48,7 +45,6 @@ impl<'a> fmt::Display for LagrangeSetType8<'a> {
}
impl<'a> NAIFDataSet<'a> for LagrangeSetType8<'a> {
- type SummaryKind = SPKSummaryRecord;
type StateKind = CartesianState;
type RecordKind = PositionVelocityRecord;
const DATASET_NAME: &'static str = "Lagrange Type 8";
@@ -111,10 +107,10 @@ impl<'a> NAIFDataSet<'a> for LagrangeSetType8<'a> {
))
}
- fn evaluate(
+ fn evaluate(
&self,
_epoch: Epoch,
- _: &Self::SummaryKind,
+ _: &S,
) -> Result {
todo!("https://github.com/anise-toolkit/anise.rs/issues/12")
}
@@ -157,7 +153,6 @@ impl<'a> fmt::Display for LagrangeSetType9<'a> {
}
impl<'a> NAIFDataSet<'a> for LagrangeSetType9<'a> {
- type SummaryKind = SPKSummaryRecord;
type StateKind = (Vector3, Vector3);
type RecordKind = PositionVelocityRecord;
const DATASET_NAME: &'static str = "Lagrange Type 9";
@@ -205,10 +200,10 @@ impl<'a> NAIFDataSet<'a> for LagrangeSetType9<'a> {
))
}
- fn evaluate(
+ fn evaluate(
&self,
_epoch: Epoch,
- _: &Self::SummaryKind,
+ _: &S,
) -> Result {
todo!("https://github.com/anise-toolkit/anise.rs/issues/13")
}
diff --git a/src/naif/spk/datatypes/mod.rs b/src/naif/daf/datatypes/mod.rs
similarity index 100%
rename from src/naif/spk/datatypes/mod.rs
rename to src/naif/daf/datatypes/mod.rs
diff --git a/src/naif/spk/datatypes/posvel.rs b/src/naif/daf/datatypes/posvel.rs
similarity index 100%
rename from src/naif/spk/datatypes/posvel.rs
rename to src/naif/daf/datatypes/posvel.rs
diff --git a/src/naif/daf/mod.rs b/src/naif/daf/mod.rs
index e60cc89d..8004555f 100644
--- a/src/naif/daf/mod.rs
+++ b/src/naif/daf/mod.rs
@@ -8,19 +8,25 @@
* Documentation: https://nyxspace.com/
*/
-use crate::{errors::IntegrityError, math::interpolation::InterpolationError, NaifId};
+use crate::{
+ errors::IntegrityError, math::interpolation::InterpolationError, prelude::InputOutputError,
+ NaifId,
+};
use core::fmt::Display;
use hifitime::Epoch;
use snafu::prelude::*;
-use std::io::Error as IOError;
use zerocopy::{AsBytes, FromBytes};
pub(crate) const RCRD_LEN: usize = 1024;
#[allow(clippy::module_inception)]
pub mod daf;
+mod data_types;
+pub use data_types::DataType as DafDataType;
pub mod file_record;
pub mod name_record;
pub mod summary_record;
+// Defines the supported data types
+pub mod datatypes;
pub use daf::DAF;
@@ -60,9 +66,6 @@ pub trait NAIFDataSet<'a>: Sized + Display + PartialEq {
/// The underlying record representation
type RecordKind: NAIFDataRecord<'a>;
- /// The summary record supported by this data set
- type SummaryKind: NAIFSummaryRecord;
-
/// The state that is returned from an evaluation of this data set
type StateKind;
@@ -74,10 +77,10 @@ pub trait NAIFDataSet<'a>: Sized + Display + PartialEq {
fn nth_record(&self, n: usize) -> Result;
- fn evaluate(
+ fn evaluate(
&self,
epoch: Epoch,
- summary: &Self::SummaryKind,
+ summary: &S,
) -> Result;
/// Checks the integrity of this data set, returns an error if the data has issues.
@@ -172,7 +175,17 @@ pub enum DAFError {
source: IntegrityError,
},
#[snafu(display("while {action} encountered input/output error {source}"))]
- IO { action: String, source: IOError },
+ IO {
+ action: String,
+ source: InputOutputError,
+ },
+ #[snafu(display("data type {id}: {kind} (corrupted data?)"))]
+ Datatype { id: i32, kind: &'static str },
+ #[snafu(display("{dtype:?} not supported for {kind}"))]
+ UnsupportedDatatype {
+ dtype: DafDataType,
+ kind: &'static str,
+ },
}
// Manual implementation of PartialEq because IOError does not derive it, sadly.
diff --git a/src/naif/kpl/fk.rs b/src/naif/kpl/fk.rs
index f4bc903d..49c285cf 100644
--- a/src/naif/kpl/fk.rs
+++ b/src/naif/kpl/fk.rs
@@ -77,6 +77,8 @@ impl KPLItem for FKItem {
#[cfg(test)]
mod fk_ut {
+ use crate::naif::kpl::parser::convert_fk;
+
use super::{FKItem, KPLValue, Parameter};
#[test]
@@ -188,4 +190,23 @@ mod fk_ut {
);
assert_eq!(assignments[&31007].data.len(), 7);
}
+
+ #[test]
+ fn test_convert_fk() {
+ use crate::math::rotation::{r1, r2, r3, DCM};
+ let dataset = convert_fk("data/moon_080317.txt", false).unwrap();
+
+ assert_eq!(dataset.len(), 3, "expected three items");
+
+ // Check that we've correctly set the names.
+ let moon_me = dataset.get_by_name("MOON_ME_DE421").unwrap();
+ // From the file:
+ // TKFRAME_31007_ANGLES = (67.92 78.56 0.30 )
+ // TKFRAME_31007_AXES = (3, 2, 1 )
+ // These angles are in arcseconds.
+ let expected = r3((67.92 / 3600.0_f64).to_radians())
+ * r2((78.56 / 3600.0_f64).to_radians())
+ * r1((0.30 / 3600.0_f64).to_radians());
+ assert!((DCM::from(moon_me).rot_mat - expected).norm() < 1e-10);
+ }
}
diff --git a/src/naif/kpl/mod.rs b/src/naif/kpl/mod.rs
index 850315e5..14fb8c7a 100644
--- a/src/naif/kpl/mod.rs
+++ b/src/naif/kpl/mod.rs
@@ -41,7 +41,14 @@ impl KPLValue {
pub fn to_vec_f64(&self) -> Result, Whatever> {
match self {
KPLValue::Matrix(data) => Ok(data.clone()),
- _ => whatever!("can only convert matrices to vec of f64"),
+ _ => whatever!("can only convert matrices to vec of f64 but this is {self:?}"),
+ }
+ }
+
+ pub fn to_i32(&self) -> Result {
+ match self {
+ KPLValue::Integer(data) => Ok(*data),
+ _ => whatever!("can only convert Integer to i32 but this is {self:?}"),
}
}
}
@@ -64,6 +71,17 @@ impl From for KPLValue {
}
}
+impl TryFrom<&KPLValue> for f64 {
+ type Error = Whatever;
+
+ fn try_from(value: &KPLValue) -> Result {
+ match value {
+ KPLValue::Float(data) => Ok(*data),
+ _ => whatever!("can only convert float to f64 but this is {value:?}"),
+ }
+ }
+}
+
/// Known KPL parameters
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Parameter {
@@ -71,6 +89,7 @@ pub enum Parameter {
NutPrecDec,
NutPrecPm,
NutPrecAngles,
+ MaxPhaseDegree,
LongAxis,
PoleRa,
PoleDec,
@@ -114,6 +133,7 @@ impl FromStr for Parameter {
"MATRIX" => Ok(Self::Matrix),
"UNITS" => Ok(Self::Units),
"AXES" => Ok(Self::Axes),
+ "MAX_PHASE_DEGREE" => Ok(Self::MaxPhaseDegree),
"GMLIST" | "NAME" | "SPEC" => {
whatever!("unsupported parameter `{s}`")
}
diff --git a/src/naif/kpl/parser.rs b/src/naif/kpl/parser.rs
index aef4079e..02a90e64 100644
--- a/src/naif/kpl/parser.rs
+++ b/src/naif/kpl/parser.rs
@@ -17,14 +17,18 @@ use std::path::Path;
use log::{error, info, warn};
-use crate::almanac::MAX_PLANETARY_DATA;
+use crate::constants::orientations::J2000;
+use crate::math::rotation::{r1, r2, r3, DCM};
+use crate::math::Matrix3;
+use crate::naif::kpl::fk::FKItem;
use crate::naif::kpl::tpc::TPCItem;
use crate::naif::kpl::Parameter;
-use crate::structure::dataset::{DataSet, DataSetBuilder, DataSetError, DataSetType};
+use crate::structure::dataset::{DataSetBuilder, DataSetError, DataSetType};
use crate::structure::metadata::Metadata;
use crate::structure::planetocentric::ellipsoid::Ellipsoid;
use crate::structure::planetocentric::phaseangle::PhaseAngle;
-use crate::structure::planetocentric::PlanetaryData;
+use crate::structure::planetocentric::{PlanetaryData, MAX_NUT_PREC_ANGLES};
+use crate::structure::{EulerParameterDataSet, PlanetaryDataSet};
use super::{KPLItem, KPLValue};
@@ -132,10 +136,7 @@ pub fn parse_file, I: KPLItem>(
Ok(map)
}
-pub fn convert_tpc<'a, P: AsRef>(
- pck: P,
- gm: P,
-) -> Result, DataSetError> {
+pub fn convert_tpc>(pck: P, gm: P) -> Result {
let mut buf = vec![];
let mut dataset_builder = DataSetBuilder::default();
@@ -150,7 +151,7 @@ pub fn convert_tpc<'a, P: AsRef>(
}
}
- // Now that planetary_data has everything, we'll create a vector of the planetary data in the ANISE ASN1 format.
+ // Now that planetary_data has everything, we'll create the planetary dataset in the ANISE ASN1 format.
for (object_id, planetary_data) in planetary_data {
match planetary_data.data.get(&Parameter::GravitationalParameter) {
@@ -172,34 +173,71 @@ pub fn convert_tpc<'a, P: AsRef>(
}),
_ => unreachable!(),
},
- _ => todo!(),
+ _ => panic!("radii_km should be float or matrix, got {radii_km:?}"),
},
None => None,
};
- let constant = match planetary_data.data.get(&Parameter::PoleRa) {
+ let mut constant = match planetary_data.data.get(&Parameter::PoleRa) {
Some(data) => match data {
KPLValue::Matrix(pole_ra_data) => {
- let pola_ra = PhaseAngle::maybe_new(pole_ra_data);
- let pola_dec_data: Vec = planetary_data.data
+ let mut pole_ra_data = pole_ra_data.clone();
+ if let Some(coeffs) =
+ planetary_data.data.get(&Parameter::NutPrecRa)
+ {
+ pole_ra_data.extend(coeffs.to_vec_f64().unwrap());
+ }
+ let pola_ra = PhaseAngle::maybe_new(&pole_ra_data);
+
+ let mut pola_dec_data: Vec = planetary_data.data
[&Parameter::PoleDec]
.to_vec_f64()
.unwrap();
+ if let Some(coeffs) =
+ planetary_data.data.get(&Parameter::NutPrecDec)
+ {
+ pola_dec_data.extend(coeffs.to_vec_f64().unwrap());
+ }
let pola_dec = PhaseAngle::maybe_new(&pola_dec_data);
- let prime_mer_data: Vec = planetary_data.data
- [&Parameter::PoleDec]
+ let mut prime_mer_data: Vec = planetary_data.data
+ [&Parameter::PrimeMeridian]
.to_vec_f64()
.unwrap();
+ if let Some(coeffs) =
+ planetary_data.data.get(&Parameter::NutPrecPm)
+ {
+ prime_mer_data.extend(coeffs.to_vec_f64().unwrap());
+ }
let prime_mer = PhaseAngle::maybe_new(&prime_mer_data);
+ let long_axis =
+ match planetary_data.data.get(&Parameter::LongAxis) {
+ Some(val) => match val {
+ KPLValue::Float(data) => Some(*data),
+ KPLValue::Matrix(data) => Some(data[0]),
+ _ => panic!(
+ "long axis must be float or matrix, got {val:?}"
+ ),
+ },
+ None => None,
+ };
+
PlanetaryData {
object_id,
+ parent_id: if [199, 299].contains(&object_id) {
+ J2000
+ } else if object_id > 100 {
+ object_id / 100
+ } else {
+ J2000
+ },
mu_km3_s2: *mu_km3_s2,
shape: ellipsoid,
pole_right_ascension: pola_ra,
pole_declination: pola_dec,
prime_meridian: prime_mer,
+ long_axis,
..Default::default()
}
}
@@ -211,12 +249,39 @@ pub fn convert_tpc<'a, P: AsRef>(
object_id,
mu_km3_s2: *mu_km3_s2,
shape: ellipsoid,
+ parent_id: J2000,
..Default::default()
}
}
};
- dataset_builder.push_into(&mut buf, constant, Some(object_id), None)?;
+ // Add the nutation precession angles, which are defined for the system
+ if let Some(nut_prec_val) =
+ planetary_data.data.get(&Parameter::NutPrecAngles)
+ {
+ let phase_deg =
+ match planetary_data.data.get(&Parameter::MaxPhaseDegree) {
+ Some(val) => (val.to_i32().unwrap() + 1) as usize,
+ None => 2,
+ };
+ let nut_prec_data = nut_prec_val.to_vec_f64().unwrap();
+ let mut coeffs = [PhaseAngle::<0>::default(); MAX_NUT_PREC_ANGLES];
+ let mut num = 0;
+ for (i, nut_prec) in nut_prec_data.chunks(phase_deg).enumerate() {
+ coeffs[i] = PhaseAngle::<0> {
+ offset_deg: nut_prec[0],
+ rate_deg: nut_prec[1],
+ ..Default::default()
+ };
+ num += 1;
+ }
+
+ constant.num_nut_prec_angles = num;
+ constant.nut_prec_angles = coeffs;
+ };
+
+ // Skip the DER serialization in full.
+ dataset_builder.push_into(&mut buf, &constant, Some(object_id), None)?;
info!("Added {object_id}");
}
_ => error!(
@@ -238,3 +303,94 @@ pub fn convert_tpc<'a, P: AsRef>(
Ok(dataset)
}
+
+pub fn convert_fk>(
+ fk_file_path: P,
+ show_comments: bool,
+) -> Result {
+ let mut buf = vec![];
+ let mut dataset_builder = DataSetBuilder::default();
+
+ let assignments = parse_file::<_, FKItem>(fk_file_path, show_comments)?;
+
+ // Add all of the data into the data set
+ for (id, item) in assignments {
+ if !item.data.contains_key(&Parameter::Angles)
+ && !item.data.contains_key(&Parameter::Matrix)
+ {
+ warn!("{id} contains neither angles nor matrix, cannot convert to Euler Parameter");
+ continue;
+ } else if let Some(angles) = item.data.get(&Parameter::Angles) {
+ let unit = item
+ .data
+ .get(&Parameter::Units)
+ .ok_or(DataSetError::Conversion {
+ action: format!("no unit data for FK ID {id}"),
+ })?;
+ let mut angle_data = angles.to_vec_f64().unwrap();
+ if unit == &KPLValue::String("ARCSECONDS".to_string()) {
+ // Convert the angles data into degrees
+ for item in &mut angle_data {
+ *item /= 3600.0;
+ }
+ }
+ // Build the quaternion from the Euler matrices
+ let from = id;
+ let to = item.data[&Parameter::Center].to_i32().unwrap();
+
+ let mut dcm = Matrix3::identity();
+
+ for (i, rot) in item.data[&Parameter::Axes]
+ .to_vec_f64()
+ .unwrap()
+ .iter()
+ .enumerate()
+ {
+ let this_dcm = if rot == &1.0 {
+ r1(angle_data[i].to_radians())
+ } else if rot == &2.0 {
+ r2(angle_data[i].to_radians())
+ } else {
+ r3(angle_data[i].to_radians())
+ };
+ dcm *= this_dcm;
+ }
+ // Convert to quaternion
+ let q = DCM {
+ rot_mat: dcm,
+ to,
+ from,
+ rot_mat_dt: None,
+ }
+ .into();
+
+ dataset_builder.push_into(&mut buf, &q, Some(id), item.name.as_deref())?;
+ } else if let Some(matrix) = item.data.get(&Parameter::Matrix) {
+ let mat_data = matrix.to_vec_f64().unwrap();
+ let rot_mat = Matrix3::new(
+ mat_data[0],
+ mat_data[1],
+ mat_data[2],
+ mat_data[3],
+ mat_data[4],
+ mat_data[5],
+ mat_data[6],
+ mat_data[7],
+ mat_data[8],
+ );
+ let dcm = DCM {
+ from: id,
+ to: item.data[&Parameter::Center].to_i32().unwrap(),
+ rot_mat,
+ rot_mat_dt: None,
+ };
+ dataset_builder.push_into(&mut buf, &dcm.into(), Some(id), item.name.as_deref())?;
+ }
+ }
+
+ let mut dataset: EulerParameterDataSet = dataset_builder.finalize(buf)?;
+ dataset.metadata = Metadata::default();
+ dataset.metadata.dataset_type = DataSetType::EulerParameterData;
+
+ Ok(dataset)
+}
diff --git a/src/naif/kpl/tpc.rs b/src/naif/kpl/tpc.rs
index 60be287d..64bab50f 100644
--- a/src/naif/kpl/tpc.rs
+++ b/src/naif/kpl/tpc.rs
@@ -115,6 +115,13 @@ fn test_parse_pck() {
assignments[&5].data[&Parameter::NutPrecAngles],
KPLValue::Matrix(expt_nutprec.into())
);
+
+ // Check for Neptune which has the NUT_PREC_PM
+ let expt_nut_prec_pm = [-0.48, 0., 0., 0., 0., 0., 0., 0.];
+ assert_eq!(
+ assignments[&899].data[&Parameter::NutPrecPm],
+ KPLValue::Matrix(expt_nut_prec_pm.into())
+ );
}
#[test]
@@ -140,30 +147,32 @@ fn test_anise_conversion() {
use crate::naif::kpl::parser::convert_tpc;
use crate::{file2heap, file_mmap, structure::dataset::DataSet};
use std::fs::File;
+ use std::path::PathBuf;
let dataset = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
- assert_eq!(dataset.lut.by_id.len(), 47);
+ assert!(!dataset.is_empty(), "should not be empty");
+ assert_eq!(dataset.lut.by_id.len(), 49);
let path = "target/gm_pck_08.anise";
// Test saving
- dataset.save_as(path.into(), true).unwrap();
+ dataset.save_as(&PathBuf::from(path), true).unwrap();
// Test reloading
let bytes = file2heap!(path).unwrap();
- let reloaded = DataSet::from_bytes(&bytes);
+ let reloaded = DataSet::from_bytes(bytes);
assert_eq!(reloaded, dataset);
// Test loading from file loaded on heap
use std::fs;
let data = fs::read(path).unwrap();
- let reloaded = DataSet::from_bytes(&data);
+ let reloaded = DataSet::from_bytes(data);
assert_eq!(reloaded, dataset);
// Test reloading with real mmap
let mmap = file_mmap!(path).unwrap();
- let reloaded = DataSet::from_bytes(&mmap);
+ let reloaded = DataSet::from_bytes(mmap);
assert_eq!(reloaded, dataset);
}
diff --git a/src/naif/pck/mod.rs b/src/naif/pck/mod.rs
index 1994e6ff..6c13b14f 100644
--- a/src/naif/pck/mod.rs
+++ b/src/naif/pck/mod.rs
@@ -8,10 +8,15 @@
* Documentation: https://nyxspace.com/
*/
-use crate::naif::daf::{NAIFRecord, NAIFSummaryRecord};
+use crate::{
+ naif::daf::{NAIFRecord, NAIFSummaryRecord},
+ orientations::OrientationError,
+};
use hifitime::Epoch;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
+use super::daf::DafDataType;
+
#[derive(Clone, Copy, Debug, Default, AsBytes, FromZeroes, FromBytes)]
#[repr(C)]
pub struct BPCSummaryRecord {
@@ -25,6 +30,15 @@ pub struct BPCSummaryRecord {
pub unused: i32,
}
+impl BPCSummaryRecord {
+ pub fn data_type(&self) -> Result {
+ DafDataType::try_from(self.data_type_i).map_err(|source| OrientationError::BPC {
+ action: "converting data type from i32",
+ source,
+ })
+ }
+}
+
impl NAIFRecord for BPCSummaryRecord {}
impl NAIFSummaryRecord for BPCSummaryRecord {
diff --git a/src/naif/spk/mod.rs b/src/naif/spk/mod.rs
index 48117ed2..c24eda8f 100644
--- a/src/naif/spk/mod.rs
+++ b/src/naif/spk/mod.rs
@@ -8,7 +8,5 @@
* Documentation: https://nyxspace.com/
*/
-// Defines the supported data types
-pub mod datatypes;
// Defines how to read an SPK
pub mod summary;
diff --git a/src/naif/spk/summary.rs b/src/naif/spk/summary.rs
index 2d19ec0d..e8840c6d 100644
--- a/src/naif/spk/summary.rs
+++ b/src/naif/spk/summary.rs
@@ -14,7 +14,7 @@ use zerocopy::{AsBytes, FromBytes, FromZeroes};
use crate::{
ephemerides::EphemerisError,
- naif::daf::{NAIFRecord, NAIFSummaryRecord},
+ naif::daf::{DafDataType, NAIFRecord, NAIFSummaryRecord},
prelude::{Frame, FrameUid},
};
@@ -32,6 +32,13 @@ pub struct SPKSummaryRecord {
}
impl<'a> SPKSummaryRecord {
+ pub fn data_type(&self) -> Result {
+ DafDataType::try_from(self.data_type_i).map_err(|source| EphemerisError::SPK {
+ action: "converting data type from i32",
+ source,
+ })
+ }
+
/// Returns the target frame UID of this summary
pub fn target_frame_uid(&self) -> FrameUid {
FrameUid {
diff --git a/src/orientations/mod.rs b/src/orientations/mod.rs
index ff1cae07..5e7ac663 100644
--- a/src/orientations/mod.rs
+++ b/src/orientations/mod.rs
@@ -13,9 +13,13 @@ use snafu::prelude::*;
use crate::{
errors::PhysicsError, math::interpolation::InterpolationError, naif::daf::DAFError,
- prelude::FrameUid,
+ prelude::FrameUid, structure::dataset::DataSetError,
};
+mod paths;
+mod rotate_to_parent;
+mod rotations;
+
#[derive(Debug, Snafu, PartialEq)]
#[snafu(visibility(pub(crate)))]
pub enum OrientationError {
@@ -33,7 +37,7 @@ pub enum OrientationError {
to: FrameUid,
epoch: Epoch,
},
- #[snafu(display("no oreitnation data loaded (must call load_bpc or DataSet::from_bytes)"))]
+ #[snafu(display("no orientation data loaded (must call load_bpc or DataSet::from_bytes)"))]
NoOrientationsLoaded,
#[snafu(display("when {action} caused {source}"))]
BPC {
@@ -51,4 +55,9 @@ pub enum OrientationError {
#[snafu(backtrace)]
source: InterpolationError,
},
+ #[snafu(display("during an orientation query {source}"))]
+ OrientationDataSet {
+ #[snafu(backtrace)]
+ source: DataSetError,
+ },
}
diff --git a/src/orientations/paths.rs b/src/orientations/paths.rs
new file mode 100644
index 00000000..64fd7f66
--- /dev/null
+++ b/src/orientations/paths.rs
@@ -0,0 +1,200 @@
+/*
+ * 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 hifitime::Epoch;
+use snafu::{ensure, ResultExt};
+
+use super::{BPCSnafu, NoOrientationsLoadedSnafu, OrientationDataSetSnafu, OrientationError};
+use crate::almanac::Almanac;
+use crate::constants::orientations::{ECLIPJ2000, J2000};
+use crate::frames::Frame;
+use crate::naif::daf::{DAFError, NAIFSummaryRecord};
+use crate::NaifId;
+
+/// **Limitation:** no translation or rotation may have more than 8 nodes.
+pub const MAX_TREE_DEPTH: usize = 8;
+
+impl Almanac {
+ /// Returns the root of all of the loaded orientations (BPC or planetary), typically this should be J2000.
+ ///
+ /// # Algorithm
+ ///
+ /// 1. For each loaded BPC, iterated in reverse order (to mimic SPICE behavior)
+ /// 2. For each summary record in each BPC, follow the orientation branch all the way up until the end of this BPC or until the J2000.
+ pub fn try_find_orientation_root(&self) -> Result {
+ ensure!(
+ self.num_loaded_bpc() > 0 || !self.planetary_data.is_empty(),
+ NoOrientationsLoadedSnafu
+ );
+
+ // The common center is the absolute minimum of all centers due to the NAIF numbering.
+ let mut common_center = i32::MAX;
+
+ for maybe_bpc in self.bpc_data.iter().take(self.num_loaded_bpc()).rev() {
+ let bpc = maybe_bpc.as_ref().unwrap();
+
+ for summary in bpc.data_summaries().with_context(|_| BPCSnafu {
+ action: "finding orientation root",
+ })? {
+ // This summary exists, so we need to follow the branch of centers up the tree.
+ if !summary.is_empty() && summary.inertial_frame_id.abs() < common_center.abs() {
+ common_center = summary.inertial_frame_id;
+ if common_center == J2000 {
+ // there is nothing higher up
+ return Ok(common_center);
+ }
+ }
+ }
+ }
+
+ // If we reached this point, it means that we didn't find J2000 in the loaded BPCs, so let's iterate through the planetary data
+ if !self.planetary_data.is_empty() {
+ for id in self.planetary_data.lut.by_id.keys() {
+ if let Ok(pc) = self.planetary_data.get_by_id(*id) {
+ if pc.parent_id < common_center {
+ common_center = pc.parent_id;
+ if common_center == J2000 {
+ // there is nothing higher up
+ return Ok(common_center);
+ }
+ }
+ }
+ }
+ }
+
+ if common_center == ECLIPJ2000 {
+ // Rotation from ecliptic J2000 to J2000 is embedded.
+ common_center = J2000;
+ }
+
+ Ok(common_center)
+ }
+
+ /// Try to construct the path from the source frame all the way to the root orientation of this context.
+ pub fn orientation_path_to_root(
+ &self,
+ source: Frame,
+ epoch: Epoch,
+ ) -> Result<(usize, [Option; MAX_TREE_DEPTH]), OrientationError> {
+ let common_center = self.try_find_orientation_root()?;
+ // Build a tree, set a fixed depth to avoid allocations
+ let mut of_path = [None; MAX_TREE_DEPTH];
+ let mut of_path_len = 0;
+
+ if common_center == source.orientation_id {
+ // We're querying the source, no need to check that this summary even exists.
+ return Ok((of_path_len, of_path));
+ }
+
+ // Grab the summary data, which we use to find the paths
+ // Let's see if this orientation is defined in the loaded BPC files
+ let mut inertial_frame_id = match self.bpc_summary_at_epoch(source.orientation_id, epoch) {
+ Ok((summary, _, _)) => summary.inertial_frame_id,
+ Err(_) => {
+ // Not available as a BPC, so let's see if there's planetary data for it.
+ let planetary_data = self
+ .planetary_data
+ .get_by_id(source.orientation_id)
+ .with_context(|_| OrientationDataSetSnafu)?;
+ planetary_data.parent_id
+ }
+ };
+
+ of_path[of_path_len] = Some(inertial_frame_id);
+ of_path_len += 1;
+
+ if inertial_frame_id == ECLIPJ2000 {
+ // Add the hop to J2000
+ inertial_frame_id = J2000;
+ of_path[of_path_len] = Some(inertial_frame_id);
+ of_path_len += 1;
+ }
+
+ if inertial_frame_id == common_center {
+ // Well that was quick!
+ return Ok((of_path_len, of_path));
+ }
+
+ for _ in 0..MAX_TREE_DEPTH - 1 {
+ inertial_frame_id = match self.bpc_summary_at_epoch(inertial_frame_id, epoch) {
+ Ok((summary, _, _)) => summary.inertial_frame_id,
+ Err(_) => {
+ // Not available as a BPC, so let's see if there's planetary data for it.
+ let planetary_data = self
+ .planetary_data
+ .get_by_id(inertial_frame_id)
+ .with_context(|_| OrientationDataSetSnafu)?;
+ planetary_data.parent_id
+ }
+ };
+
+ // let summary = self.bpc_summary_at_epoch(inertial_frame_id, epoch)?.0;
+ // inertial_frame_id = summary.inertial_frame_id;
+ of_path[of_path_len] = Some(inertial_frame_id);
+ of_path_len += 1;
+ if inertial_frame_id == common_center {
+ // We're found the path!
+ return Ok((of_path_len, of_path));
+ }
+ }
+
+ Err(OrientationError::BPC {
+ action: "computing path to common node",
+ source: DAFError::MaxRecursionDepth,
+ })
+ }
+
+ /// Returns the orientation path between two frames and the common node. This may return a `DisjointRoots` error if the frames do not share a common root, which is considered a file integrity error.
+ pub fn common_orientation_path(
+ &self,
+ from_frame: Frame,
+ to_frame: Frame,
+ epoch: Epoch,
+ ) -> Result<(usize, [Option; MAX_TREE_DEPTH], NaifId), OrientationError> {
+ if from_frame == to_frame {
+ // Both frames match, return this frame's hash (i.e. no need to go higher up).
+ return Ok((0, [None; MAX_TREE_DEPTH], from_frame.orientation_id));
+ }
+
+ // Grab the paths
+ let (from_len, from_path) = self.orientation_path_to_root(from_frame, epoch)?;
+ let (to_len, to_path) = self.orientation_path_to_root(to_frame, epoch)?;
+
+ // Now that we have the paths, we can find the matching origin.
+
+ // If either path is of zero length, that means one of them is at the root of this ANISE file, so the common
+ // path is which brings the non zero-length path back to the file root.
+ if from_len == 0 && to_len == 0 {
+ Err(OrientationError::RotationOrigin {
+ from: from_frame.into(),
+ to: to_frame.into(),
+ epoch,
+ })
+ } else if from_len != 0 && to_len == 0 {
+ // One has an empty path but not the other, so the root is at the empty path
+ Ok((from_len, from_path, to_frame.orientation_id))
+ } else if to_len != 0 && from_len == 0 {
+ // One has an empty path but not the other, so the root is at the empty path
+ Ok((to_len, to_path, from_frame.orientation_id))
+ } else {
+ // Either are at the orientation root, so we'll step through the paths until we find the common root.
+ let mut common_path = to_path;
+ let mut items: usize = to_len;
+ let common_node = to_path[to_len - 1].unwrap();
+
+ for from_obj in from_path.iter().take(from_len).rev().skip(1) {
+ common_path[items] = Some(from_obj.unwrap());
+ items += 1;
+ }
+
+ Ok((items, common_path, common_node))
+ }
+ }
+}
diff --git a/src/orientations/rotate_to_parent.rs b/src/orientations/rotate_to_parent.rs
new file mode 100644
index 00000000..c9a86976
--- /dev/null
+++ b/src/orientations/rotate_to_parent.rs
@@ -0,0 +1,125 @@
+/*
+ * 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 log::trace;
+use snafu::ResultExt;
+
+use super::{OrientationError, OrientationPhysicsSnafu};
+use crate::almanac::Almanac;
+use crate::constants::orientations::{ECLIPJ2000, J2000, J2000_TO_ECLIPJ2000_ANGLE_RAD};
+use crate::hifitime::Epoch;
+use crate::math::rotation::{r1, r1_dot, r3, r3_dot, DCM};
+use crate::naif::daf::datatypes::Type2ChebyshevSet;
+use crate::naif::daf::{DAFError, DafDataType, NAIFDataSet};
+use crate::orientations::{BPCSnafu, OrientationDataSetSnafu, OrientationInterpolationSnafu};
+use crate::prelude::Frame;
+
+impl Almanac {
+ /// Returns the direct cosine matrix (DCM) to rotate from the `source` to its parent in the orientation hierarchy at the provided epoch,
+ ///
+ /// # Example
+ /// If the ephemeris stores position interpolation coefficients in kilometer but this function is called with millimeters as a distance unit,
+ /// the output vectors will be in mm, mm/s, mm/s^2 respectively.
+ ///
+ /// # Errors
+ /// + As of now, some interpolation types are not supported, and if that were to happen, this would return an error.
+ ///
+ /// **WARNING:** This function only performs the rotation and no translation whatsoever. Use the `transform_to_parent_from` function instead to include rotations.
+ pub fn rotation_to_parent(&self, source: Frame, epoch: Epoch) -> Result {
+ if source.orient_origin_id_match(J2000) {
+ // The parent of Earth ecliptic J2000 is the J2000 inertial frame.
+ return Ok(DCM::identity(J2000, J2000));
+ } else if source.orient_origin_id_match(ECLIPJ2000) {
+ // The parent of Earth ecliptic J2000 is the J2000 inertial frame.
+ return Ok(DCM {
+ rot_mat: r1(J2000_TO_ECLIPJ2000_ANGLE_RAD),
+ rot_mat_dt: None,
+ from: J2000,
+ to: ECLIPJ2000,
+ });
+ }
+ // Let's see if this orientation is defined in the loaded BPC files
+ match self.bpc_summary_at_epoch(source.orientation_id, epoch) {
+ Ok((summary, bpc_no, idx_in_bpc)) => {
+ let new_frame = source.with_orient(summary.inertial_frame_id);
+
+ trace!("rotate {source} wrt to {new_frame} @ {epoch:E}");
+
+ // This should not fail because we've fetched the spk_no from above with the spk_summary_at_epoch call.
+ let bpc_data = self.bpc_data[bpc_no]
+ .as_ref()
+ .ok_or(OrientationError::Unreachable)?;
+
+ // Compute the angles and their rates
+ let (ra_dec_w, d_ra_dec_w) = match summary.data_type()? {
+ DafDataType::Type2ChebyshevTriplet => {
+ let data = bpc_data
+ .nth_data::(idx_in_bpc)
+ .with_context(|_| BPCSnafu {
+ action: "fetching data for interpolation",
+ })?;
+ data.evaluate(epoch, summary)
+ .with_context(|_| OrientationInterpolationSnafu)?
+ }
+ dtype => {
+ return Err(OrientationError::BPC {
+ action: "rotation to parent",
+ source: DAFError::UnsupportedDatatype {
+ dtype,
+ kind: "BPC computations",
+ },
+ })
+ }
+ };
+
+ // And build the DCM
+ let twist_rad = ra_dec_w[2];
+ let dec_rad = ra_dec_w[1];
+ let ra_rad = ra_dec_w[0];
+
+ let twist_dot_rad = d_ra_dec_w[2];
+ let dec_dot_rad = d_ra_dec_w[1];
+ let ra_dot_rad = d_ra_dec_w[0];
+
+ let rot_mat = r3(twist_rad) * r1(dec_rad) * r3(ra_rad);
+ let rot_mat_dt = Some(
+ twist_dot_rad * r3_dot(twist_rad) * r1(dec_rad) * r3(ra_rad)
+ + dec_dot_rad * r3(twist_rad) * r1_dot(dec_rad) * r3(ra_rad)
+ + ra_dot_rad * r3(twist_rad) * r1(dec_rad) * r3_dot(ra_rad),
+ );
+
+ Ok(DCM {
+ rot_mat,
+ rot_mat_dt,
+ from: summary.inertial_frame_id,
+ to: source.orientation_id,
+ })
+ }
+ Err(_) => {
+ trace!("query {source} wrt to its parent @ {epoch:E} using planetary data");
+ // Not available as a BPC, so let's see if there's planetary data for it.
+ let planetary_data = self
+ .planetary_data
+ .get_by_id(source.orientation_id)
+ .with_context(|_| OrientationDataSetSnafu)?;
+
+ // Fetch the parent info
+ let system_data = match self.planetary_data.get_by_id(planetary_data.parent_id) {
+ Ok(parent) => parent,
+ Err(_) => planetary_data,
+ };
+
+ planetary_data
+ .rotation_to_parent(epoch, &system_data)
+ .with_context(|_| OrientationPhysicsSnafu)
+ }
+ }
+ }
+}
diff --git a/src/orientations/rotations.rs b/src/orientations/rotations.rs
new file mode 100644
index 00000000..2e2e915a
--- /dev/null
+++ b/src/orientations/rotations.rs
@@ -0,0 +1,145 @@
+/*
+ * 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 snafu::ResultExt;
+
+use super::OrientationError;
+use super::OrientationPhysicsSnafu;
+use crate::almanac::Almanac;
+use crate::constants::orientations::J2000;
+use crate::hifitime::Epoch;
+use crate::math::cartesian::CartesianState;
+use crate::math::rotation::DCM;
+use crate::math::units::*;
+use crate::math::Vector3;
+use crate::prelude::Frame;
+
+impl Almanac {
+ /// Returns the 6x6 DCM needed to rotation the `from_frame` to the `to_frame`.
+ ///
+ /// # Warning
+ /// This function only performs the rotation and no translation whatsoever. Use the `transform_from_to` function instead to include rotations.
+ ///
+ /// # Note
+ /// This function performs a recursion of no more than twice the [MAX_TREE_DEPTH].
+ pub fn rotate_from_to(
+ &self,
+ from_frame: Frame,
+ to_frame: Frame,
+ epoch: Epoch,
+ ) -> Result {
+ let mut to_frame: Frame = to_frame;
+
+ // If there is no frame info, the user hasn't loaded this frame, but might still want to compute a translation.
+ if let Ok(to_frame_info) = self.frame_from_uid(to_frame) {
+ // User has loaded the planetary data for this frame, so let's use that as the to_frame.
+ to_frame = to_frame_info;
+ }
+
+ if from_frame.orient_origin_match(to_frame) {
+ // Both frames match, return this frame's hash (i.e. no need to go higher up).
+ return Ok(DCM::identity(
+ from_frame.orientation_id,
+ to_frame.orientation_id,
+ ));
+ }
+
+ let (node_count, path, common_node) =
+ self.common_orientation_path(from_frame, to_frame, epoch)?;
+
+ // The fwrd variables are the states from the `from frame` to the common node
+ let mut dcm_fwrd = if from_frame.orient_origin_id_match(common_node) {
+ DCM::identity(common_node, common_node)
+ } else {
+ self.rotation_to_parent(from_frame, epoch)?
+ };
+
+ // The bwrd variables are the states from the `to frame` back to the common node
+ let mut dcm_bwrd = if to_frame.orient_origin_id_match(common_node) {
+ DCM::identity(common_node, common_node)
+ } else {
+ self.rotation_to_parent(to_frame, epoch)?.transpose()
+ };
+
+ for cur_node_id in path.iter().take(node_count) {
+ let next_parent = cur_node_id.unwrap();
+ if next_parent == J2000 {
+ // The parent rotation of J2000 is itself, so we can skip this.
+ continue;
+ }
+
+ let cur_dcm = self.rotation_to_parent(Frame::from_orient_ssb(next_parent), epoch)?;
+
+ if dcm_fwrd.from == cur_dcm.from {
+ dcm_fwrd =
+ (cur_dcm * dcm_fwrd.transpose()).with_context(|_| OrientationPhysicsSnafu)?;
+ } else if dcm_fwrd.from == cur_dcm.to {
+ dcm_fwrd = (dcm_fwrd * cur_dcm)
+ .with_context(|_| OrientationPhysicsSnafu)?
+ .transpose();
+ } else if dcm_bwrd.to == cur_dcm.from {
+ dcm_bwrd = (cur_dcm * dcm_bwrd).with_context(|_| OrientationPhysicsSnafu)?;
+ } else if dcm_bwrd.to == cur_dcm.to {
+ dcm_bwrd =
+ (dcm_bwrd.transpose() * cur_dcm).with_context(|_| OrientationPhysicsSnafu)?;
+ } else {
+ return Err(OrientationError::Unreachable);
+ }
+
+ if next_parent == common_node {
+ break;
+ }
+ }
+
+ if dcm_fwrd.from == dcm_bwrd.from {
+ (dcm_bwrd * dcm_fwrd.transpose()).with_context(|_| OrientationPhysicsSnafu)
+ } else if dcm_fwrd.from == dcm_bwrd.to {
+ Ok((dcm_fwrd * dcm_bwrd)
+ .with_context(|_| OrientationPhysicsSnafu)?
+ .transpose())
+ } else if dcm_fwrd.to == dcm_bwrd.to {
+ Ok((dcm_fwrd.transpose() * dcm_bwrd)
+ .with_context(|_| OrientationPhysicsSnafu)?
+ .transpose())
+ } else {
+ (dcm_bwrd * dcm_fwrd).with_context(|_| OrientationPhysicsSnafu)
+ }
+ }
+
+ /// Translates a state with its origin (`to_frame`) and given its units (distance_unit, time_unit), returns that state with respect to the requested frame
+ ///
+ /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_state_to` function instead to include rotations.
+ #[allow(clippy::too_many_arguments)]
+ pub fn rotate_state_to(
+ &self,
+ position: Vector3,
+ velocity: Vector3,
+ from_frame: Frame,
+ to_frame: Frame,
+ epoch: Epoch,
+ distance_unit: LengthUnit,
+ time_unit: TimeUnit,
+ ) -> Result {
+ // Compute the frame translation
+ let dcm = self.rotate_from_to(from_frame, to_frame, epoch)?;
+
+ let dist_unit_factor = LengthUnit::Kilometer.from_meters() * distance_unit.to_meters();
+ let time_unit_factor = time_unit.in_seconds();
+
+ let input_state = CartesianState {
+ radius_km: position * dist_unit_factor,
+ velocity_km_s: velocity * dist_unit_factor / time_unit_factor,
+ epoch,
+ frame: from_frame,
+ };
+
+ (dcm * input_state).with_context(|_| OrientationPhysicsSnafu {})
+ }
+}
diff --git a/src/structure/dataset/builder.rs b/src/structure/dataset/builder.rs
new file mode 100644
index 00000000..92597af1
--- /dev/null
+++ b/src/structure/dataset/builder.rs
@@ -0,0 +1,111 @@
+/*
+ * 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 crate::{
+ structure::lookuptable::{Entry, LutError},
+ NaifId,
+};
+use bytes::Bytes;
+use snafu::prelude::*;
+
+use super::{
+ error::{DataSetError, DataSetLutSnafu},
+ DataSet, DataSetT,
+};
+
+/// Dataset builder allows building a dataset. It requires allocations.
+#[derive(Clone, Default, Debug)]
+pub struct DataSetBuilder {
+ pub dataset: DataSet,
+}
+
+impl<'a, T: DataSetT, const ENTRIES: usize> DataSetBuilder {
+ pub fn push_into(
+ &mut self,
+ buf: &mut Vec,
+ data: &T,
+ id: Option,
+ name: Option<&'a str>,
+ ) -> Result<(), DataSetError> {
+ let mut this_buf = vec![];
+ data.encode_to_vec(&mut this_buf).unwrap();
+ // Build this entry data.
+ let entry = Entry {
+ start_idx: buf.len() as u32,
+ end_idx: (buf.len() + this_buf.len()) as u32,
+ };
+
+ match id {
+ Some(id) => {
+ match name {
+ Some(name) => {
+ // Both an ID and a name
+ self.dataset.lut.append(id, name, entry).with_context(|_| {
+ DataSetLutSnafu {
+ action: "pushing data with ID and name",
+ }
+ })?;
+ // If the ID is the body of a system with a single object, also insert it for the system ID.
+ if [199, 299].contains(&id) {
+ self.dataset
+ .lut
+ .append(id / 100, name, entry)
+ .with_context(|_| DataSetLutSnafu {
+ action: "pushing data with ID and name",
+ })?;
+ }
+ }
+ None => {
+ // Only an ID and no name
+ self.dataset.lut.append_id(id, entry).with_context(|_| {
+ DataSetLutSnafu {
+ action: "pushing data with ID only",
+ }
+ })?;
+ // If the ID is the body of a system with a single object, also insert it for the system ID.
+ if [199, 299].contains(&id) {
+ self.dataset
+ .lut
+ .append_id(id / 100, entry)
+ .with_context(|_| DataSetLutSnafu {
+ action: "pushing data with ID and name",
+ })?;
+ }
+ }
+ }
+ }
+ None => {
+ if name.is_some() {
+ // Only a name
+ self.dataset
+ .lut
+ .append_name(name.unwrap(), entry)
+ .with_context(|_| DataSetLutSnafu {
+ action: "pushing data with name only",
+ })?;
+ } else {
+ return Err(DataSetError::DataSetLut {
+ action: "pushing data",
+ source: LutError::NoKeyProvided,
+ });
+ }
+ }
+ }
+
+ buf.extend_from_slice(&this_buf);
+
+ Ok(())
+ }
+
+ pub fn finalize(mut self, buf: Vec) -> Result, DataSetError> {
+ self.dataset.bytes = Bytes::copy_from_slice(&buf);
+ self.dataset.set_crc32();
+ Ok(self.dataset)
+ }
+}
diff --git a/src/structure/dataset/datatype.rs b/src/structure/dataset/datatype.rs
new file mode 100644
index 00000000..54eb7fee
--- /dev/null
+++ b/src/structure/dataset/datatype.rs
@@ -0,0 +1,56 @@
+/*
+ * 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 der::{Decode, Encode, Reader, Writer};
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+#[repr(u8)]
+pub enum DataSetType {
+ /// Used only if not encoding a dataset but some other structure
+ NotApplicable,
+ SpacecraftData,
+ PlanetaryData,
+ EulerParameterData,
+}
+
+impl From for DataSetType {
+ fn from(val: u8) -> Self {
+ match val {
+ 0 => DataSetType::NotApplicable,
+ 1 => DataSetType::SpacecraftData,
+ 2 => DataSetType::PlanetaryData,
+ 3 => DataSetType::EulerParameterData,
+ _ => panic!("Invalid value for DataSetType {val}"),
+ }
+ }
+}
+
+impl From for u8 {
+ fn from(val: DataSetType) -> Self {
+ val as u8
+ }
+}
+
+impl Encode for DataSetType {
+ fn encoded_len(&self) -> der::Result {
+ (*self as u8).encoded_len()
+ }
+
+ fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
+ (*self as u8).encode(encoder)
+ }
+}
+
+impl<'a> Decode<'a> for DataSetType {
+ fn decode>(decoder: &mut R) -> der::Result {
+ let asu8: u8 = decoder.decode()?;
+ Ok(Self::from(asu8))
+ }
+}
diff --git a/src/structure/dataset/error.rs b/src/structure/dataset/error.rs
new file mode 100644
index 00000000..c6e32ac3
--- /dev/null
+++ b/src/structure/dataset/error.rs
@@ -0,0 +1,82 @@
+use snafu::prelude::*;
+
+use crate::{
+ errors::{DecodingError, IntegrityError},
+ structure::lookuptable::LutError,
+};
+use std::io::Error as IOError;
+
+#[derive(Debug, Snafu)]
+#[snafu(visibility(pub(crate)))]
+pub enum DataSetError {
+ #[snafu(display("when {action} {source}"))]
+ DataSetLut {
+ action: &'static str,
+ source: LutError,
+ },
+ #[snafu(display("when {action} {source}"))]
+ DataSetIntegrity {
+ action: &'static str,
+ source: IntegrityError,
+ },
+ #[snafu(display("when {action} {source}"))]
+ DataDecoding {
+ action: &'static str,
+ source: DecodingError,
+ },
+ #[snafu(display("input/output error while {action}"))]
+ IO {
+ action: &'static str,
+ source: IOError,
+ },
+ #[snafu(display("data set conversion error: {action}"))]
+ Conversion { action: String },
+}
+
+impl PartialEq for DataSetError {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (
+ Self::DataSetLut {
+ action: l_action,
+ source: l_source,
+ },
+ Self::DataSetLut {
+ action: r_action,
+ source: r_source,
+ },
+ ) => l_action == r_action && l_source == r_source,
+ (
+ Self::DataSetIntegrity {
+ action: l_action,
+ source: l_source,
+ },
+ Self::DataSetIntegrity {
+ action: r_action,
+ source: r_source,
+ },
+ ) => l_action == r_action && l_source == r_source,
+ (
+ Self::DataDecoding {
+ action: l_action,
+ source: l_source,
+ },
+ Self::DataDecoding {
+ action: r_action,
+ source: r_source,
+ },
+ ) => l_action == r_action && l_source == r_source,
+ (
+ Self::IO {
+ action: l_action,
+ source: _l_source,
+ },
+ Self::IO {
+ action: r_action,
+ source: _r_source,
+ },
+ ) => l_action == r_action,
+ _ => false,
+ }
+ }
+}
diff --git a/src/structure/dataset.rs b/src/structure/dataset/mod.rs
similarity index 67%
rename from src/structure/dataset.rs
rename to src/structure/dataset/mod.rs
index c9c0992c..cb4bf577 100644
--- a/src/structure/dataset.rs
+++ b/src/structure/dataset/mod.rs
@@ -7,14 +7,16 @@
*
* Documentation: https://nyxspace.com/
*/
+use self::error::DataDecodingSnafu;
use super::{
- lookuptable::{Entry, LookUpTable, LutError},
+ lookuptable::{LookUpTable, LutError},
metadata::Metadata,
semver::Semver,
ANISE_VERSION,
};
use crate::{
errors::{DecodingError, IntegrityError},
+ structure::dataset::error::DataSetIntegritySnafu,
NaifId,
};
use bytes::Bytes;
@@ -36,204 +38,35 @@ macro_rules! io_imports {
io_imports!();
-#[derive(Debug, Snafu)]
-#[snafu(visibility(pub(crate)))]
-pub enum DataSetError {
- #[snafu(display("when {action} {source}"))]
- DataSetLut {
- action: &'static str,
- source: LutError,
- },
- #[snafu(display("when {action} {source}"))]
- DataSetIntegrity {
- action: &'static str,
- source: IntegrityError,
- },
- #[snafu(display("when {action} {source}"))]
- DataDecoding {
- action: &'static str,
- source: DecodingError,
- },
- #[snafu(display("input/output error while {action}"))]
- IO {
- action: &'static str,
- source: IOError,
- },
-}
-
-impl PartialEq for DataSetError {
- fn eq(&self, other: &Self) -> bool {
- match (self, other) {
- (
- Self::DataSetLut {
- action: l_action,
- source: l_source,
- },
- Self::DataSetLut {
- action: r_action,
- source: r_source,
- },
- ) => l_action == r_action && l_source == r_source,
- (
- Self::DataSetIntegrity {
- action: l_action,
- source: l_source,
- },
- Self::DataSetIntegrity {
- action: r_action,
- source: r_source,
- },
- ) => l_action == r_action && l_source == r_source,
- (
- Self::DataDecoding {
- action: l_action,
- source: l_source,
- },
- Self::DataDecoding {
- action: r_action,
- source: r_source,
- },
- ) => l_action == r_action && l_source == r_source,
- (
- Self::IO {
- action: l_action,
- source: _l_source,
- },
- Self::IO {
- action: r_action,
- source: _r_source,
- },
- ) => l_action == r_action,
- _ => false,
- }
- }
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[repr(u8)]
-pub enum DataSetType {
- /// Used only if not encoding a dataset but some other structure
- NotApplicable,
- SpacecraftData,
- PlanetaryData,
-}
-
-impl From for DataSetType {
- fn from(val: u8) -> Self {
- match val {
- 0 => DataSetType::NotApplicable,
- 1 => DataSetType::SpacecraftData,
- 2 => DataSetType::PlanetaryData,
- _ => panic!("Invalid value for DataSetType {val}"),
- }
- }
-}
-
-impl From for u8 {
- fn from(val: DataSetType) -> Self {
- val as u8
- }
-}
-
-impl Encode for DataSetType {
- fn encoded_len(&self) -> der::Result {
- (*self as u8).encoded_len()
- }
+mod builder;
+mod datatype;
+mod error;
- fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
- (*self as u8).encode(encoder)
- }
-}
-
-impl<'a> Decode<'a> for DataSetType {
- fn decode>(decoder: &mut R) -> der::Result {
- let asu8: u8 = decoder.decode()?;
- Ok(Self::from(asu8))
- }
-}
+pub use builder::DataSetBuilder;
+pub use datatype::DataSetType;
+pub use error::DataSetError;
/// The kind of data that can be encoded in a dataset
-pub trait DataSetT<'a>: Encode + Decode<'a> {
+pub trait DataSetT: Encode + for<'a> Decode<'a> {
const NAME: &'static str;
}
/// A DataSet is the core structure shared by all ANISE binary data.
#[derive(Clone, Default, PartialEq, Eq, Debug)]
-pub struct DataSet<'a, T: DataSetT<'a>, const ENTRIES: usize> {
- pub metadata: Metadata<'a>,
+pub struct DataSet {
+ pub metadata: Metadata,
/// All datasets have LookUpTable (LUT) that stores the mapping between a key and its index in the ephemeris list.
- pub lut: LookUpTable<'a, ENTRIES>,
+ pub lut: LookUpTable,
pub data_checksum: u32,
/// The actual data from the dataset
pub bytes: Bytes,
_daf_type: PhantomData,
}
-/// Dataset builder allows building a dataset. It requires allocations.
-#[derive(Clone, Default, Debug)]
-pub struct DataSetBuilder<'a, T: DataSetT<'a>, const ENTRIES: usize> {
- pub dataset: DataSet<'a, T, ENTRIES>,
-}
-
-impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSetBuilder<'a, T, ENTRIES> {
- pub fn push_into(
- &mut self,
- buf: &mut Vec,
- data: T,
- id: Option,
- name: Option<&'a str>,
- ) -> Result<(), DataSetError> {
- let mut this_buf = vec![];
- data.encode_to_vec(&mut this_buf).unwrap();
- // Build this entry data.
- let entry = Entry {
- start_idx: buf.len() as u32,
- end_idx: (buf.len() + this_buf.len()) as u32,
- };
-
- if id.is_some() && name.is_some() {
- self.dataset
- .lut
- .append(id.unwrap(), name.unwrap(), entry)
- .with_context(|_| DataSetLutSnafu {
- action: "pushing data with ID and name",
- })?;
- } else if id.is_some() {
- self.dataset
- .lut
- .append_id(id.unwrap(), entry)
- .with_context(|_| DataSetLutSnafu {
- action: "pushing data with ID only",
- })?;
- } else if name.is_some() {
- self.dataset
- .lut
- .append_name(name.unwrap(), entry)
- .with_context(|_| DataSetLutSnafu {
- action: "pushing data with name only",
- })?;
- } else {
- return Err(DataSetError::DataSetLut {
- action: "pushing data",
- source: LutError::NoKeyProvided,
- });
- }
- buf.extend_from_slice(&this_buf);
-
- Ok(())
- }
-
- pub fn finalize(mut self, buf: Vec) -> Result, DataSetError> {
- self.dataset.bytes = Bytes::copy_from_slice(&buf);
- self.dataset.set_crc32();
- Ok(self.dataset)
- }
-}
-
-impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
+impl DataSet {
/// Try to load an Anise file from a pointer of bytes
- pub fn try_from_bytes>(bytes: &'a B) -> Result {
- match Self::from_der(bytes) {
+ pub fn try_from_bytes>(bytes: B) -> Result {
+ match Self::from_der(&bytes) {
Ok(ctx) => {
trace!("[try_from_bytes] loaded context successfully");
// Check the full integrity on load of the file.
@@ -286,7 +119,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
/// Forces to load an Anise file from a pointer of bytes.
/// **Panics** if the bytes cannot be interpreted as an Anise file.
- pub fn from_bytes>(buf: &'a B) -> Self {
+ pub fn from_bytes>(buf: B) -> Self {
Self::try_from_bytes(buf).unwrap()
}
@@ -330,7 +163,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
}
}
- pub fn get_by_id(&'a self, id: NaifId) -> Result {
+ pub fn get_by_id(&self, id: NaifId) -> Result {
if let Some(entry) = self.lut.by_id.get(&id) {
// Found the ID
let bytes = self
@@ -353,8 +186,8 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
}
}
- pub fn get_by_name(&'a self, name: &str) -> Result {
- if let Some(entry) = self.lut.by_name.get(&name) {
+ pub fn get_by_name(&self, name: &str) -> Result {
+ if let Some(entry) = self.lut.by_name.get(&name.try_into().unwrap()) {
// Found the name
let bytes = self
.bytes
@@ -372,7 +205,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
Err(DataSetError::DataSetLut {
action: "fetching by ID",
source: LutError::UnknownName {
- name: name.to_string(),
+ name: name.try_into().unwrap(),
},
})
}
@@ -381,7 +214,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
/// Saves this dataset to the provided file
/// If overwrite is set to false, and the filename already exists, this function will return an error.
- pub fn save_as(&self, filename: PathBuf, overwrite: bool) -> Result<(), DataSetError> {
+ pub fn save_as(&self, filename: &PathBuf, overwrite: bool) -> Result<(), DataSetError> {
use log::{info, warn};
if Path::new(&filename).exists() {
@@ -400,7 +233,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
let mut buf = vec![];
- match File::create(&filename) {
+ match File::create(filename) {
Ok(mut file) => {
if let Err(err) = self.encode_to_vec(&mut buf) {
return Err(DataSetError::DataDecoding {
@@ -424,9 +257,23 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> DataSet<'a, T, ENTRIES> {
}),
}
}
+
+ /// Returns the length of the LONGEST of the two look up tables
+ pub fn len(&self) -> usize {
+ if self.lut.by_id.len() > self.lut.by_name.len() {
+ self.lut.by_id.len()
+ } else {
+ self.lut.by_name.len()
+ }
+ }
+
+ /// Returns whether this dataset is empty
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
}
-impl<'a, T: DataSetT<'a>, const ENTRIES: usize> Encode for DataSet<'a, T, ENTRIES> {
+impl Encode for DataSet {
fn encoded_len(&self) -> der::Result {
let as_byte_ref = OctetStringRef::new(&self.bytes)?;
self.metadata.encoded_len()?
@@ -444,7 +291,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> Encode for DataSet<'a, T, ENTRIE
}
}
-impl<'a, T: DataSetT<'a>, const ENTRIES: usize> Decode<'a> for DataSet<'a, T, ENTRIES> {
+impl<'a, T: DataSetT, const ENTRIES: usize> Decode<'a> for DataSet {
fn decode>(decoder: &mut D) -> der::Result {
let metadata = decoder.decode()?;
let lut = decoder.decode()?;
@@ -460,7 +307,7 @@ impl<'a, T: DataSetT<'a>, const ENTRIES: usize> Decode<'a> for DataSet<'a, T, EN
}
}
-impl<'a, T: DataSetT<'a>, const ENTRIES: usize> fmt::Display for DataSet<'a, T, ENTRIES> {
+impl fmt::Display for DataSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
@@ -491,23 +338,22 @@ mod dataset_ut {
let mut buf = vec![];
repr.encode_to_vec(&mut buf).unwrap();
- dbg!(buf.len());
+ assert_eq!(buf.len(), 58);
let repr_dec = DataSet::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
dbg!(repr);
- dbg!(core::mem::size_of::>());
- dbg!(core::mem::size_of::>());
+ assert_eq!(core::mem::size_of::>(), 288);
+ assert_eq!(core::mem::size_of::>(), 10368);
}
#[test]
fn spacecraft_constants_lookup() {
// Build some data first.
let full_sc = SpacecraftData {
- name: "full spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "full spacecraft".try_into().unwrap(),
srp_data: Some(SRPData {
area_m2: 2.0,
coeff_reflectivity: 1.8,
@@ -525,8 +371,7 @@ mod dataset_ut {
drag_data: Some(DragData::default()),
};
let srp_sc = SpacecraftData {
- name: "SRP only spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "SRP only spacecraft".try_into().unwrap(),
srp_data: Some(SRPData::default()),
..Default::default()
};
@@ -601,8 +446,7 @@ mod dataset_ut {
fn spacecraft_constants_lookup_builder() {
// Build some data first.
let full_sc = SpacecraftData {
- name: "full spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "full spacecraft".try_into().unwrap(),
srp_data: Some(SRPData {
area_m2: 2.0,
coeff_reflectivity: 1.8,
@@ -620,8 +464,7 @@ mod dataset_ut {
drag_data: Some(DragData::default()),
};
let srp_sc = SpacecraftData {
- name: "SRP only spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "SRP only spacecraft".try_into().unwrap(),
srp_data: Some(SRPData::default()),
..Default::default()
};
@@ -630,21 +473,21 @@ mod dataset_ut {
let mut buf = vec![];
let mut builder = DataSetBuilder::default();
builder
- .push_into(&mut buf, srp_sc, Some(-20), Some("SRP spacecraft"))
+ .push_into(&mut buf, &srp_sc, Some(-20), Some("SRP spacecraft"))
.unwrap();
builder
- .push_into(&mut buf, full_sc, Some(-50), Some("Full spacecraft"))
+ .push_into(&mut buf, &full_sc, Some(-50), Some("Full spacecraft"))
.unwrap();
// Pushing without name as ID -51
builder
- .push_into(&mut buf, full_sc, Some(-51), None)
+ .push_into(&mut buf, &full_sc, Some(-51), None)
.unwrap();
// Pushing without ID
builder
- .push_into(&mut buf, srp_sc, None, Some("ID less SRP spacecraft"))
+ .push_into(&mut buf, &srp_sc, None, Some("ID less SRP spacecraft"))
.unwrap();
let dataset = builder.finalize(buf).unwrap();
@@ -654,9 +497,9 @@ mod dataset_ut {
let mut ebuf = vec![];
dataset.encode_to_vec(&mut ebuf).unwrap();
- dbg!(ebuf.len());
+ assert_eq!(ebuf.len(), 530);
- let repr_dec = SpacecraftDataSet::from_bytes(&ebuf);
+ let repr_dec = SpacecraftDataSet::from_bytes(ebuf);
assert_eq!(dataset, repr_dec);
diff --git a/src/structure/lookuptable.rs b/src/structure/lookuptable.rs
index 24d22657..b3a2c8e2 100644
--- a/src/structure/lookuptable.rs
+++ b/src/structure/lookuptable.rs
@@ -11,12 +11,15 @@ use der::{
asn1::{OctetStringRef, SequenceOf},
Decode, Encode, Reader, Writer,
};
-use heapless::FnvIndexMap;
+use heapless::{FnvIndexMap, String};
use log::warn;
use snafu::prelude::*;
use crate::{errors::DecodingError, NaifId};
+/// Maximum length of a look up table name string
+pub const KEY_NAME_LEN: usize = 32;
+
#[derive(Debug, Snafu, PartialEq)]
#[snafu(visibility(pub(crate)))]
pub enum LutError {
@@ -33,7 +36,7 @@ pub enum LutError {
#[snafu(display("ID {id} not in look up table"))]
UnknownId { id: NaifId },
#[snafu(display("name {name} not in look up table"))]
- UnknownName { name: String },
+ UnknownName { name: String },
}
/// A lookup table entry contains the start and end indexes in the data array of the data that is sought after.
@@ -86,20 +89,20 @@ impl<'a> Decode<'a> for Entry {
/// # Note
/// _Both_ the IDs and the name MUST be unique in the look up table.
#[derive(Clone, Default, Debug, PartialEq, Eq)]
-pub struct LookUpTable<'a, const ENTRIES: usize> {
+pub struct LookUpTable {
/// Unique IDs of each item in the
pub by_id: FnvIndexMap,
/// Corresponding index for each hash
- pub by_name: FnvIndexMap<&'a str, Entry, ENTRIES>,
+ pub by_name: FnvIndexMap, Entry, ENTRIES>,
}
-impl<'a, const ENTRIES: usize> LookUpTable<'a, ENTRIES> {
- pub fn append(&mut self, id: i32, name: &'a str, entry: Entry) -> Result<(), LutError> {
+impl LookUpTable {
+ pub fn append(&mut self, id: i32, name: &str, entry: Entry) -> Result<(), LutError> {
self.by_id
.insert(id, entry)
.map_err(|_| LutError::IdLutFull { max_slots: ENTRIES })?;
self.by_name
- .insert(name, entry)
+ .insert(name.try_into().unwrap(), entry)
.map_err(|_| LutError::NameLutFull { max_slots: ENTRIES })?;
Ok(())
}
@@ -111,9 +114,9 @@ impl<'a, const ENTRIES: usize> LookUpTable<'a, ENTRIES> {
Ok(())
}
- pub fn append_name(&mut self, name: &'a str, entry: Entry) -> Result<(), LutError> {
+ pub fn append_name(&mut self, name: &str, entry: Entry) -> Result<(), LutError> {
self.by_name
- .insert(name, entry)
+ .insert(name.try_into().unwrap(), entry)
.map_err(|_| LutError::NameLutFull { max_slots: ENTRIES })?;
Ok(())
}
@@ -174,7 +177,7 @@ impl<'a, const ENTRIES: usize> LookUpTable<'a, ENTRIES> {
}
}
-impl<'a, const ENTRIES: usize> Encode for LookUpTable<'a, ENTRIES> {
+impl Encode for LookUpTable {
fn encoded_len(&self) -> der::Result {
let (ids, names, id_entries, name_entries) = self.der_encoding();
ids.encoded_len()?
@@ -192,7 +195,7 @@ impl<'a, const ENTRIES: usize> Encode for LookUpTable<'a, ENTRIES> {
}
}
-impl<'a, const ENTRIES: usize> Decode<'a> for LookUpTable<'a, ENTRIES> {
+impl<'a, const ENTRIES: usize> Decode<'a> for LookUpTable {
fn decode>(decoder: &mut R) -> der::Result {
// Decode as sequences and use that to build the look up table.
let mut lut = Self::default();
@@ -206,8 +209,12 @@ impl<'a, const ENTRIES: usize> Decode<'a> for LookUpTable<'a, ENTRIES> {
}
for (name, entry) in names.iter().zip(name_entries.iter()) {
+ let key = core::str::from_utf8(name.as_bytes()).unwrap();
lut.by_name
- .insert(core::str::from_utf8(name.as_bytes()).unwrap(), *entry)
+ .insert(
+ key[..KEY_NAME_LEN.min(key.len())].try_into().unwrap(),
+ *entry,
+ )
.unwrap();
}
@@ -237,7 +244,7 @@ mod lut_ut {
assert_eq!(repr, repr_dec);
dbg!(repr);
- dbg!(core::mem::size_of::>());
+ assert_eq!(core::mem::size_of::>(), 5136);
}
#[test]
diff --git a/src/structure/metadata.rs b/src/structure/metadata.rs
index a65bca48..d7727282 100644
--- a/src/structure/metadata.rs
+++ b/src/structure/metadata.rs
@@ -7,17 +7,21 @@
*
* Documentation: https://nyxspace.com/
*/
+use crate::errors::DecodingError;
+use bytes::Bytes;
use core::fmt;
use core::str::FromStr;
use der::{asn1::Utf8StringRef, Decode, Encode, Reader, Writer};
+use heapless::String;
use hifitime::Epoch;
-use crate::errors::DecodingError;
+/// Default maximum length of the Metadata originator length string
+pub const MAX_ORIGINATOR_LEN: usize = 32;
use super::{dataset::DataSetType, semver::Semver, ANISE_VERSION};
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub struct Metadata<'a> {
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Metadata {
/// The ANISE version number. Can be used for partial decoding to determine whether a file is compatible with a library.
pub anise_version: Semver,
/// The type of dataset encoded in the rest of the structure
@@ -25,12 +29,10 @@ pub struct Metadata<'a> {
/// Date time of the creation of this file.
pub creation_date: Epoch,
/// Originator of the file, either an organization, a person, a tool, or a combination thereof
- pub originator: &'a str,
- /// Unique resource identifier to the metadata of this file. This is for FAIR compliance.
- pub metadata_uri: &'a str,
+ pub originator: String,
}
-impl<'a> Metadata<'a> {
+impl Metadata {
/// Only decode the anise version and dataset type
pub fn decode_header(bytes: &[u8]) -> Result {
let anise_version =
@@ -55,52 +57,59 @@ impl<'a> Metadata<'a> {
};
Ok(me)
}
+
+ pub fn from_bytes(buf: Bytes) -> Self {
+ Self::from_der(&buf).unwrap()
+ }
}
-impl Default for Metadata<'_> {
+impl Default for Metadata {
fn default() -> Self {
Self {
anise_version: ANISE_VERSION,
dataset_type: DataSetType::NotApplicable,
creation_date: Epoch::now().unwrap(),
originator: Default::default(),
- metadata_uri: Default::default(),
}
}
}
-impl<'a> Encode for Metadata<'a> {
+impl Encode for Metadata {
fn encoded_len(&self) -> der::Result {
self.anise_version.encoded_len()?
+ self.dataset_type.encoded_len()?
+ Utf8StringRef::new(&format!("{}", self.creation_date))?.encoded_len()?
- + Utf8StringRef::new(self.originator)?.encoded_len()?
- + Utf8StringRef::new(self.metadata_uri)?.encoded_len()?
+ + Utf8StringRef::new(&self.originator)?.encoded_len()?
}
fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
self.anise_version.encode(encoder)?;
self.dataset_type.encode(encoder)?;
Utf8StringRef::new(&format!("{}", self.creation_date))?.encode(encoder)?;
- Utf8StringRef::new(self.originator)?.encode(encoder)?;
- Utf8StringRef::new(self.metadata_uri)?.encode(encoder)
+ Utf8StringRef::new(&self.originator)?.encode(encoder)
}
}
-impl<'a> Decode<'a> for Metadata<'a> {
+impl<'a> Decode<'a> for Metadata {
fn decode>(decoder: &mut R) -> der::Result {
+ let anise_version = decoder.decode()?;
+ let dataset_type = decoder.decode()?;
+ let creation_date =
+ Epoch::from_str(decoder.decode::>()?.as_str()).unwrap();
+ let orig_str = decoder.decode::>()?.as_str();
+ let originator = orig_str[..MAX_ORIGINATOR_LEN.min(orig_str.len())]
+ .try_into()
+ .unwrap();
Ok(Self {
- anise_version: decoder.decode()?,
- dataset_type: decoder.decode()?,
- creation_date: Epoch::from_str(decoder.decode::>()?.as_str())
- .unwrap(),
- originator: decoder.decode::>()?.as_str(),
- metadata_uri: decoder.decode::>()?.as_str(),
+ anise_version,
+ dataset_type,
+ creation_date,
+ originator,
})
}
}
-impl<'a> fmt::Display for Metadata<'a> {
+impl fmt::Display for Metadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ANISE version {}", self.anise_version)?;
writeln!(
@@ -109,24 +118,16 @@ impl<'a> fmt::Display for Metadata<'a> {
if self.originator.is_empty() {
"(not set)"
} else {
- self.originator
+ &self.originator
}
)?;
- writeln!(f, "Creation date: {}", self.creation_date)?;
- writeln!(
- f,
- "Metadata URI: {}",
- if self.metadata_uri.is_empty() {
- "(not set)"
- } else {
- self.metadata_uri
- }
- )
+ writeln!(f, "Creation date: {}", self.creation_date)
}
}
#[cfg(test)]
mod metadata_ut {
+
use super::Metadata;
use der::{Decode, Encode};
@@ -148,7 +149,6 @@ mod metadata_ut {
r#"ANISE version ANISE version 0.0.1
Originator: (not set)
Creation date: {}
-Metadata URI: (not set)
"#,
repr_dec.creation_date
)
@@ -174,4 +174,19 @@ Metadata URI: (not set)
"should not have enough for version"
);
}
+
+ #[test]
+ fn meta_with_orig() {
+ let repr = Metadata {
+ originator: "Nyx Space Origin".try_into().unwrap(),
+ ..Default::default()
+ };
+
+ let mut buf = vec![];
+ repr.encode_to_vec(&mut buf).unwrap();
+
+ let repr_dec = Metadata::from_der(&buf).unwrap();
+
+ assert_eq!(repr, repr_dec);
+ }
}
diff --git a/src/structure/mod.rs b/src/structure/mod.rs
index 7519a5bd..a94510af 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -22,7 +22,10 @@ pub mod spacecraft;
use self::{
dataset::DataSet, planetocentric::PlanetaryData, semver::Semver, spacecraft::SpacecraftData,
};
-use crate::almanac::{MAX_PLANETARY_DATA, MAX_SPACECRAFT_DATA};
+use crate::{
+ almanac::{MAX_PLANETARY_DATA, MAX_SPACECRAFT_DATA},
+ math::rotation::Quaternion,
+};
/// The current version of ANISE
pub const ANISE_VERSION: Semver = Semver {
@@ -31,5 +34,9 @@ pub const ANISE_VERSION: Semver = Semver {
patch: 1,
};
-pub type SpacecraftDataSet<'a> = DataSet<'a, SpacecraftData<'a>, MAX_SPACECRAFT_DATA>;
-pub type PlanetaryDataSet<'a> = DataSet<'a, PlanetaryData, MAX_PLANETARY_DATA>;
+/// Spacecraft Data Set allow mapping an ID and/or name to spacecraft data, optionally including mass, drag, SRP, an inertia information
+pub type SpacecraftDataSet = DataSet;
+/// Planetary Data Set allow mapping an ID and/or name to planetary data, optionally including shape information and rotation information
+pub type PlanetaryDataSet = DataSet;
+/// Euler Parameter Data Set allow mapping an ID and/or name to a time invariant Quaternion
+pub type EulerParameterDataSet = DataSet;
diff --git a/src/structure/planetocentric/mod.rs b/src/structure/planetocentric/mod.rs
index 322fc7d9..b38c1959 100644
--- a/src/structure/planetocentric/mod.rs
+++ b/src/structure/planetocentric/mod.rs
@@ -9,22 +9,61 @@
*/
use crate::{
+ astro::PhysicsResult,
+ constants::orientations::orientation_name_from_id,
+ math::{
+ rotation::{r1, r3, DCM},
+ Matrix3,
+ },
prelude::{Frame, FrameUid},
NaifId,
};
+use core::f64::consts::FRAC_PI_2;
+use core::fmt;
pub mod ellipsoid;
-pub mod nutprec;
pub mod phaseangle;
use der::{Decode, Encode, Reader, Writer};
use ellipsoid::Ellipsoid;
-use nutprec::NutationPrecessionAngle;
+use hifitime::{Epoch, TimeUnits, Unit};
use phaseangle::PhaseAngle;
use super::dataset::DataSetT;
-pub const MAX_NUT_PREC_ANGLES: usize = 16;
+pub const MAX_NUT_PREC_ANGLES: usize = 32;
/// ANISE supports two different kinds of orientation data. High precision, with spline based interpolations, and constants right ascension, declination, and prime meridian, typically used for planetary constant data.
+///
+/// # Documentation of rotation angles
+/// Source: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/pck.html#Models%20for%20the%20Sun,%20Planets,%20and%20some%20Minor%20Bodies%20in%20Text%20PCK%20Kernels
+/// The angles RA, DEC, and W are defined as follows:
+///
+/// ```text
+/// 2
+/// RA2*t
+/// RA = RA0 + RA1*t/T + ------ + [optional trig polynomials]
+/// 2
+/// T
+///
+/// 2
+/// DEC2*t
+/// DEC = DEC0 + DEC1*t/T + ------- + [optional trig polynomials]
+/// 2
+/// T
+///
+/// 2
+/// W2*t
+/// W = W0 + W1*t/d + ----- + [optional trig polynomials]
+/// 2
+/// d
+/// ```
+///
+/// where
+///
+/// d = seconds/day
+/// T = seconds/Julian century
+/// t = ephemeris time, expressed as seconds past the reference epoch
+/// for this body or planetary system
+///
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct PlanetaryData {
/// The NAIF ID of this object
@@ -35,22 +74,22 @@ pub struct PlanetaryData {
pub mu_km3_s2: f64,
/// The shape is always a tri axial ellipsoid
pub shape: Option,
- pub pole_right_ascension: Option,
- pub pole_declination: Option,
- pub prime_meridian: Option,
+ pub pole_right_ascension: Option>,
+ pub pole_declination: Option>,
+ pub prime_meridian: Option>,
pub long_axis: Option,
/// These are the nutation precession angles as a list of tuples to rebuild them.
/// E.g. For `E1 = 125.045 - 0.052992 d`, this would be stored as a single entry `(125.045, -0.052992)`.
pub num_nut_prec_angles: u8,
- pub nut_prec_angles: [NutationPrecessionAngle; MAX_NUT_PREC_ANGLES],
+ pub nut_prec_angles: [PhaseAngle<0>; MAX_NUT_PREC_ANGLES],
}
-impl<'a> DataSetT<'a> for PlanetaryData {
+impl DataSetT for PlanetaryData {
const NAME: &'static str = "planetary data";
}
impl PlanetaryData {
- /// Converts this planetary data into a Frame
+ /// Converts this planetary data into a Frame, unsetting any shape data for non-body-fixed frames (ID < 100).
pub fn to_frame(&self, uid: FrameUid) -> Frame {
Frame {
ephemeris_id: uid.ephemeris_id,
@@ -66,6 +105,7 @@ impl PlanetaryData {
/// + Bit 1 is set if `pole_right_ascension` is available
/// + Bit 2 is set if `pole_declination` is available
/// + Bit 3 is set if `prime_meridian` is available
+ /// + Bit 4 is set if `long_axis` is available
fn available_data(&self) -> u8 {
let mut bits: u8 = 0;
@@ -87,6 +127,137 @@ impl PlanetaryData {
bits
}
+
+ fn uses_trig_polynomial(&self) -> bool {
+ if let Some(phase) = self.pole_right_ascension {
+ if phase.coeffs_count > 0 {
+ return true;
+ }
+ }
+
+ if let Some(phase) = self.pole_declination {
+ if phase.coeffs_count > 0 {
+ return true;
+ }
+ }
+
+ if let Some(phase) = self.prime_meridian {
+ if phase.coeffs_count > 0 {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ /// Computes the rotation to the parent frame, returning only the rotation matrix
+ fn dcm_to_parent(&self, epoch: Epoch, system: &Self) -> PhysicsResult {
+ if self.pole_declination.is_none()
+ && self.prime_meridian.is_none()
+ && self.pole_right_ascension.is_none()
+ {
+ Ok(Matrix3::identity())
+ } else {
+ let mut variable_angles_deg = [0.0_f64; MAX_NUT_PREC_ANGLES];
+ // Skip the computation of the nutation and precession angles of the system if we won't be using them.
+ if self.uses_trig_polynomial() {
+ for (ii, nut_prec_angle) in system
+ .nut_prec_angles
+ .iter()
+ .enumerate()
+ .take(system.num_nut_prec_angles.into())
+ {
+ variable_angles_deg[ii] = nut_prec_angle.evaluate_deg(epoch, Unit::Century);
+ }
+ }
+
+ let right_asc_rad = match self.pole_right_ascension {
+ Some(right_asc_deg) => {
+ let mut angle_rad = right_asc_deg
+ .evaluate_deg(epoch, Unit::Century)
+ .to_radians();
+ // Add the nutation and precession angles for this phase angle
+ for (ii, coeff) in right_asc_deg
+ .coeffs
+ .iter()
+ .enumerate()
+ .take(right_asc_deg.coeffs_count as usize)
+ {
+ angle_rad += coeff * variable_angles_deg[ii].to_radians().sin();
+ }
+ angle_rad + FRAC_PI_2
+ }
+ None => 0.0,
+ };
+
+ let dec_rad = match self.pole_declination {
+ Some(decl_deg) => {
+ let mut angle_rad = decl_deg.evaluate_deg(epoch, Unit::Century).to_radians();
+ // Add the nutation and precession angles for this phase angle
+ for (ii, coeff) in decl_deg
+ .coeffs
+ .iter()
+ .enumerate()
+ .take(decl_deg.coeffs_count as usize)
+ {
+ angle_rad += coeff * variable_angles_deg[ii].to_radians().cos();
+ }
+ FRAC_PI_2 - angle_rad
+ }
+ None => 0.0,
+ };
+
+ let twist_rad = match self.prime_meridian {
+ Some(twist_deg) => {
+ let mut angle_rad = twist_deg.evaluate_deg(epoch, Unit::Day).to_radians();
+ // Add the nutation and precession angles for this phase angle
+ for (ii, coeff) in twist_deg
+ .coeffs
+ .iter()
+ .enumerate()
+ .take(twist_deg.coeffs_count as usize)
+ {
+ angle_rad += coeff * variable_angles_deg[ii].to_radians().sin();
+ }
+ angle_rad
+ }
+ None => 0.0,
+ };
+
+ let ra_dcm = r3(right_asc_rad);
+ let dec_dcm = r1(dec_rad);
+ let w_dcm = r3(twist_rad);
+ // Perform a multiplication of the DCMs, regardless of frames.
+ Ok(w_dcm * dec_dcm * ra_dcm)
+ }
+ }
+
+ /// Computes the rotation to the parent frame, including its time derivative.
+ ///
+ /// Source: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/rotation.html#Working%20with%20RA,%20Dec%20and%20Twist
+ pub fn rotation_to_parent(&self, epoch: Epoch, system: &Self) -> PhysicsResult {
+ if self.pole_declination.is_none()
+ && self.prime_meridian.is_none()
+ && self.pole_right_ascension.is_none()
+ {
+ Ok(DCM::identity(self.object_id, self.parent_id))
+ } else {
+ // For planetary constants data, we perform a finite differencing to compute the time derivative.
+ let mut dcm = DCM {
+ rot_mat: self.dcm_to_parent(epoch, system)?,
+ from: self.parent_id,
+ to: self.object_id,
+ rot_mat_dt: None,
+ };
+ // Compute rotation matrix one second before
+ let pre_rot_dcm = self.dcm_to_parent(epoch - 1.seconds(), system)?;
+ let post_rot_dcm = self.dcm_to_parent(epoch + 1.seconds(), system)?;
+
+ dcm.rot_mat_dt = Some((post_rot_dcm - pre_rot_dcm) / 2.0);
+
+ Ok(dcm)
+ }
+ }
}
impl Encode for PlanetaryData {
@@ -100,6 +271,7 @@ impl Encode for PlanetaryData {
+ self.pole_right_ascension.encoded_len()?
+ self.pole_declination.encoded_len()?
+ self.prime_meridian.encoded_len()?
+ + self.long_axis.encoded_len()?
+ self.num_nut_prec_angles.encoded_len()?
+ self.nut_prec_angles.encoded_len()?
}
@@ -113,6 +285,7 @@ impl Encode for PlanetaryData {
self.pole_right_ascension.encode(encoder)?;
self.pole_declination.encode(encoder)?;
self.prime_meridian.encode(encoder)?;
+ self.long_axis.encode(encoder)?;
self.num_nut_prec_angles.encode(encoder)?;
self.nut_prec_angles.encode(encoder)
}
@@ -171,6 +344,41 @@ impl<'a> Decode<'a> for PlanetaryData {
}
}
+impl fmt::Display for PlanetaryData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ // Initialize new frame UIDs with arbitrary ephemeris centers, and we don't print those.
+ let orientation_name = match orientation_name_from_id(self.object_id) {
+ Some(name) => name.to_string(),
+ None => format!("planetary data {}", self.object_id),
+ };
+
+ write!(f, "{orientation_name}")?;
+ match self.shape {
+ Some(shape) => {
+ write!(f, " (μ = {} km3/s, {})", self.mu_km3_s2, shape)?;
+ }
+ None => {
+ write!(f, " (μ = {} km3/s)", self.mu_km3_s2)?;
+ }
+ }
+
+ if let Some(ra) = self.pole_right_ascension {
+ write!(f, " RA = {}", ra)?;
+ }
+ if let Some(dec) = self.pole_declination {
+ write!(f, " Dec = {}", dec)?;
+ }
+ if let Some(pm) = self.prime_meridian {
+ write!(f, " PM = {}", pm)?;
+ }
+ if self.num_nut_prec_angles > 0 {
+ write!(f, " + {} nut/prec angles", self.num_nut_prec_angles)?;
+ }
+
+ Ok(())
+ }
+}
+
#[cfg(test)]
mod planetary_constants_ut {
use super::{Ellipsoid, PhaseAngle, PlanetaryData};
@@ -191,6 +399,10 @@ mod planetary_constants_ut {
let repr_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
+ assert_eq!(
+ format!("{repr}"),
+ "planetary data 1234 (μ = 12345.6789 km3/s)"
+ );
}
#[test]
@@ -209,6 +421,10 @@ mod planetary_constants_ut {
let repr_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
+ assert_eq!(
+ format!("{repr}"),
+ "planetary data 1234 (μ = 12345.6789 km3/s, eq. radius = 6378.1366 km, polar radius = 6356.7519 km, f = 0.0033528131084554717)"
+ );
}
#[test]
@@ -231,6 +447,10 @@ mod planetary_constants_ut {
let repr_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
+ assert_eq!(
+ format!("{repr}"),
+ "planetary data 1234 (μ = 12345.6789 km3/s) RA = 270 + 0.003 x"
+ );
}
#[test]
@@ -253,6 +473,10 @@ mod planetary_constants_ut {
let repr_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
+ assert_eq!(
+ format!("{repr}"),
+ "planetary data 1234 (μ = 12345.6789 km3/s) Dec = 66.541 + 0.013 x"
+ );
}
#[test]
@@ -262,7 +486,7 @@ mod planetary_constants_ut {
rate_deg: 13.1763582,
..Default::default()
};
- let min_repr = PlanetaryData {
+ let repr = PlanetaryData {
object_id: 1234,
mu_km3_s2: 12345.6789,
prime_meridian: Some(earth_data),
@@ -270,11 +494,15 @@ mod planetary_constants_ut {
};
let mut buf = vec![];
- min_repr.encode_to_vec(&mut buf).unwrap();
+ repr.encode_to_vec(&mut buf).unwrap();
let min_repr_dec = PlanetaryData::from_der(&buf).unwrap();
- assert_eq!(min_repr, min_repr_dec);
+ assert_eq!(repr, min_repr_dec);
+ assert_eq!(
+ format!("{repr}"),
+ "planetary data 1234 (μ = 12345.6789 km3/s) PM = 38.317 + 13.1763582 x"
+ );
}
#[test]
@@ -289,7 +517,7 @@ mod planetary_constants_ut {
rate_deg: 13.1763582,
..Default::default()
};
- let min_repr = PlanetaryData {
+ let repr = PlanetaryData {
object_id: 1234,
mu_km3_s2: 12345.6789,
pole_declination: Some(earth_data_dec),
@@ -297,15 +525,34 @@ mod planetary_constants_ut {
..Default::default()
};
+ let mut buf = vec![];
+ repr.encode_to_vec(&mut buf).unwrap();
+ assert_eq!(buf.len(), 566);
+
+ let min_repr_dec = PlanetaryData::from_der(&buf).unwrap();
+
+ assert_eq!(repr, min_repr_dec);
+
+ assert_eq!(core::mem::size_of::(), 1984);
+
+ assert_eq!(format!("{repr}"), "planetary data 1234 (μ = 12345.6789 km3/s) Dec = 66.541 + 0.013 x PM = 38.317 + 13.1763582 x");
+ }
+
+ #[test]
+ fn pc_encdec_with_long_axis_only() {
+ let min_repr = PlanetaryData {
+ object_id: 1234,
+ mu_km3_s2: 12345.6789,
+ long_axis: Some(1789.4),
+ ..Default::default()
+ };
+
let mut buf = vec![];
min_repr.encode_to_vec(&mut buf).unwrap();
- dbg!(buf.len());
let min_repr_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(min_repr, min_repr_dec);
-
- dbg!(core::mem::size_of::());
}
#[test]
@@ -360,10 +607,12 @@ mod planetary_constants_ut {
// Encode
let mut buf = vec![];
moon.encode_to_vec(&mut buf).unwrap();
- dbg!(buf.len());
+ assert_eq!(buf.len(), 946);
let moon_dec = PlanetaryData::from_der(&buf).unwrap();
assert_eq!(moon, moon_dec);
+
+ assert_eq!(format!("{moon}"), "planetary data 301 (μ = 4902.800066163796 km3/s) RA = 269.9949 + 0.0031 x Dec = 66.5392 + 0.013 x PM = 38.3213 + 13.17635815 x + -0.0000000000014 x^2");
}
}
diff --git a/src/structure/planetocentric/nutprec.rs b/src/structure/planetocentric/nutprec.rs
deleted file mode 100644
index cae93745..00000000
--- a/src/structure/planetocentric/nutprec.rs
+++ /dev/null
@@ -1,86 +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 der::{Decode, Encode, Reader, Writer};
-use hifitime::{Epoch, Unit};
-use zerocopy::{AsBytes, FromBytes, FromZeroes};
-
-/// This structure is only used to store the nutation and precession angle data.
-#[derive(Copy, Clone, Debug, Default, PartialEq, AsBytes, FromZeroes, FromBytes)]
-#[repr(C)]
-pub struct NutationPrecessionAngle {
- offset_deg: f64,
- rate_deg: f64,
-}
-
-impl NutationPrecessionAngle {
- /// Evaluates this nutation precession angle at the given epoch
- pub fn evaluate_deg(&self, epoch: Epoch) -> f64 {
- // SPICE actually uses ET not TDB, so we use that too.
- let d = epoch.to_tdb_duration().to_unit(Unit::Century);
- self.offset_deg + self.rate_deg * d
- }
-}
-
-impl Encode for NutationPrecessionAngle {
- fn encoded_len(&self) -> der::Result {
- self.offset_deg.encoded_len()? + self.rate_deg.encoded_len()?
- }
-
- fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
- self.offset_deg.encode(encoder)?;
- self.rate_deg.encode(encoder)
- }
-}
-
-impl<'a> Decode<'a> for NutationPrecessionAngle {
- fn decode>(decoder: &mut R) -> der::Result {
- Ok(Self {
- offset_deg: decoder.decode()?,
- rate_deg: decoder.decode()?,
- })
- }
-}
-
-#[cfg(test)]
-mod nut_prec_ut {
- use super::{Decode, Encode, Epoch, NutationPrecessionAngle};
- #[test]
- fn zero_repr() {
- let repr = NutationPrecessionAngle {
- ..Default::default()
- };
-
- let mut buf = vec![];
- repr.encode_to_vec(&mut buf).unwrap();
-
- let repr_dec = NutationPrecessionAngle::from_der(&buf).unwrap();
-
- assert_eq!(repr, repr_dec);
- }
-
- #[test]
- fn example_repr() {
- let repr = NutationPrecessionAngle {
- offset_deg: 125.045,
- rate_deg: -0.052992,
- };
-
- let mut buf = vec![];
- repr.encode_to_vec(&mut buf).unwrap();
-
- let repr_dec = NutationPrecessionAngle::from_der(&buf).unwrap();
-
- assert_eq!(repr, repr_dec);
-
- // Ensure that at zero, we have only an offset.
- assert_eq!(repr.evaluate_deg(Epoch::from_tdb_seconds(0.0)), 125.045);
- }
-}
diff --git a/src/structure/planetocentric/phaseangle.rs b/src/structure/planetocentric/phaseangle.rs
index c26391da..39e490ce 100644
--- a/src/structure/planetocentric/phaseangle.rs
+++ b/src/structure/planetocentric/phaseangle.rs
@@ -7,49 +7,55 @@
*
* Documentation: https://nyxspace.com/
*/
+use core::fmt;
use der::{Decode, Encode, Reader, Writer};
-use zerocopy::{AsBytes, FromBytes, FromZeroes};
-
-use super::MAX_NUT_PREC_ANGLES;
+use hifitime::{Epoch, Unit};
/// Angle data is represented as a polynomial of an angle, exactly like in SPICE PCK.
/// In fact, the following documentation is basically copied from [the required PCK reading](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/pck.html).
-#[derive(Copy, Clone, Debug, Default, PartialEq, AsBytes, FromZeroes, FromBytes)]
+#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(C)]
-pub struct PhaseAngle {
+pub struct PhaseAngle {
/// The fixed offset of the angular data
pub offset_deg: f64,
/// The rate of change of this angle per T, where T represents then number of centuries since J2000 TDB for right ascension and declination, and days since J2000 TDB for the axis twist.
pub rate_deg: f64,
/// The acceleration of this angle per T (same definition as above).
pub accel_deg: f64,
- pub coeffs_count: u64,
- pub coeffs: [f64; MAX_NUT_PREC_ANGLES],
+ /// Number of nutation / precession angle coefficients
+ pub coeffs_count: u8,
+ pub coeffs: [f64; N],
}
-impl PhaseAngle {
+impl PhaseAngle {
pub fn maybe_new(data: &[f64]) -> Option {
- if data.len() < 3 {
+ if data.is_empty() {
None
} else {
- let mut coeffs = [0.0; MAX_NUT_PREC_ANGLES];
+ let mut coeffs = [0.0; N];
for (i, coeff) in data.iter().skip(3).enumerate() {
coeffs[i] = *coeff;
}
Some(Self {
offset_deg: data[0],
- rate_deg: data[1],
- accel_deg: data[2],
- coeffs_count: data.len().saturating_sub(3) as u64,
+ rate_deg: *data.get(1).unwrap_or(&0.0),
+ accel_deg: *data.get(2).unwrap_or(&0.0),
+ coeffs_count: (data.len() as u8).saturating_sub(3),
coeffs,
})
}
}
+
+ /// Evaluates this phase angle in degrees provided the epoch
+ pub fn evaluate_deg(&self, epoch: Epoch, rate_unit: Unit) -> f64 {
+ let factor = epoch.to_tdb_duration().to_unit(rate_unit);
+
+ self.offset_deg + self.rate_deg * factor + self.accel_deg * factor.powi(2)
+ }
}
-impl Encode for PhaseAngle {
+impl Encode for PhaseAngle {
fn encoded_len(&self) -> der::Result {
- // TODO: Consider encoding this as a DataArray?
self.offset_deg.encoded_len()?
+ self.rate_deg.encoded_len()?
+ self.accel_deg.encoded_len()?
@@ -66,7 +72,7 @@ impl Encode for PhaseAngle {
}
}
-impl<'a> Decode<'a> for PhaseAngle {
+impl<'a, const N: usize> Decode<'a> for PhaseAngle {
fn decode>(decoder: &mut R) -> der::Result {
Ok(Self {
offset_deg: decoder.decode()?,
@@ -77,3 +83,77 @@ impl<'a> Decode<'a> for PhaseAngle {
})
}
}
+
+impl fmt::Display for PhaseAngle {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.accel_deg.abs() > 0.0 {
+ write!(
+ f,
+ "{} + {} x + {} x^2",
+ self.offset_deg, self.rate_deg, self.accel_deg
+ )
+ } else {
+ write!(f, "{} + {} x", self.offset_deg, self.rate_deg)
+ }
+ }
+}
+
+impl Default for PhaseAngle {
+ fn default() -> Self {
+ Self {
+ offset_deg: Default::default(),
+ rate_deg: Default::default(),
+ accel_deg: Default::default(),
+ coeffs_count: Default::default(),
+ coeffs: [0.0; N],
+ }
+ }
+}
+
+#[cfg(test)]
+mod phase_angle_ut {
+ use super::{Decode, Encode, Epoch, PhaseAngle};
+ use hifitime::{TimeUnits, Unit};
+ #[test]
+ fn zero_repr() {
+ let repr = PhaseAngle::<32> {
+ ..Default::default()
+ };
+
+ let mut buf = vec![];
+ repr.encode_to_vec(&mut buf).unwrap();
+
+ let repr_dec = PhaseAngle::from_der(&buf).unwrap();
+
+ assert_eq!(repr, repr_dec);
+ }
+
+ #[test]
+ fn example_repr() {
+ // From the start example of the pck00008 file
+ let repr = PhaseAngle::<0> {
+ offset_deg: 125.045,
+ rate_deg: -0.052992,
+ ..Default::default()
+ };
+
+ let mut buf = vec![];
+ repr.encode_to_vec(&mut buf).unwrap();
+
+ let repr_dec = PhaseAngle::from_der(&buf).unwrap();
+
+ assert_eq!(repr, repr_dec);
+
+ // Ensure that at zero, we have only an offset.
+ assert_eq!(
+ repr.evaluate_deg(Epoch::from_tdb_seconds(0.0), Unit::Century),
+ 125.045
+ );
+ // Ensure that we correctly evaluate this variable.
+ // E1 = 125.045 - 0.052992 d, d represents days past J2000 ( TDB )
+ assert_eq!(
+ repr.evaluate_deg(Epoch::from_tdb_duration(1.days()), Unit::Century),
+ 125.04499854915811
+ );
+ }
+}
diff --git a/src/structure/spacecraft/mod.rs b/src/structure/spacecraft/mod.rs
index 897b53fa..1e88b883 100644
--- a/src/structure/spacecraft/mod.rs
+++ b/src/structure/spacecraft/mod.rs
@@ -8,26 +8,24 @@
* Documentation: https://nyxspace.com/
*/
use der::{asn1::Utf8StringRef, Decode, Encode, Reader, Writer};
+use heapless::String;
mod drag;
mod inertia;
mod mass;
mod srp;
+use super::dataset::DataSetT;
pub use drag::DragData;
pub use inertia::Inertia;
pub use mass::Mass;
pub use srp::SRPData;
-use super::dataset::DataSetT;
-
/// Spacecraft constants can store the same spacecraft constant data as the CCSDS Orbit Parameter Message (OPM) and CCSDS Attitude Parameter Messages (APM)
-#[derive(Copy, Clone, Default, Debug, PartialEq)]
-pub struct SpacecraftData<'a> {
+#[derive(Clone, Default, Debug, PartialEq)]
+pub struct SpacecraftData {
/// Name is used as the input for the hashing function
- pub name: &'a str,
- /// Generic comments field
- pub comments: &'a str,
+ pub name: String<32>,
/// Mass of the spacecraft in kg
pub mass_kg: Option,
/// Solar radiation pressure data
@@ -38,11 +36,11 @@ pub struct SpacecraftData<'a> {
pub inertia: Option,
}
-impl<'a> DataSetT<'a> for SpacecraftData<'a> {
+impl DataSetT for SpacecraftData {
const NAME: &'static str = "spacecraft data";
}
-impl<'a> SpacecraftData<'a> {
+impl SpacecraftData {
/// Specifies what data is available in this structure.
///
/// Returns:
@@ -70,11 +68,10 @@ impl<'a> SpacecraftData<'a> {
}
}
-impl<'a> Encode for SpacecraftData<'a> {
+impl Encode for SpacecraftData {
fn encoded_len(&self) -> der::Result {
let available_flags = self.available_data();
- Utf8StringRef::new(self.name)?.encoded_len()?
- + Utf8StringRef::new(self.comments)?.encoded_len()?
+ Utf8StringRef::new(&self.name)?.encoded_len()?
+ available_flags.encoded_len()?
+ self.mass_kg.encoded_len()?
+ self.srp_data.encoded_len()?
@@ -83,8 +80,7 @@ impl<'a> Encode for SpacecraftData<'a> {
}
fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
- Utf8StringRef::new(self.name)?.encode(encoder)?;
- Utf8StringRef::new(self.comments)?.encode(encoder)?;
+ Utf8StringRef::new(&self.name)?.encode(encoder)?;
self.available_data().encode(encoder)?;
self.mass_kg.encode(encoder)?;
self.srp_data.encode(encoder)?;
@@ -93,10 +89,9 @@ impl<'a> Encode for SpacecraftData<'a> {
}
}
-impl<'a> Decode<'a> for SpacecraftData<'a> {
+impl<'a> Decode<'a> for SpacecraftData {
fn decode>(decoder: &mut R) -> der::Result {
- let name: Utf8StringRef = decoder.decode()?;
- let comments: Utf8StringRef = decoder.decode()?;
+ let name = decoder.decode::()?.as_str();
let data_flags: u8 = decoder.decode()?;
@@ -125,8 +120,7 @@ impl<'a> Decode<'a> for SpacecraftData<'a> {
};
Ok(Self {
- name: name.as_str(),
- comments: comments.as_str(),
+ name: name[..name.len().min(32)].try_into().unwrap(),
mass_kg,
srp_data,
drag_data,
@@ -142,8 +136,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_min_repr() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
..Default::default()
};
@@ -158,8 +151,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_with_srp_only() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
srp_data: Some(SRPData::default()),
..Default::default()
};
@@ -175,8 +167,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_with_drag_only() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
drag_data: Some(DragData::default()),
..Default::default()
};
@@ -192,8 +183,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_with_mass_only() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
mass_kg: Some(Mass::default()),
..Default::default()
};
@@ -209,8 +199,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_with_inertial_only() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
inertia: Some(Inertia::default()),
..Default::default()
};
@@ -226,8 +215,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_with_srp_mass_inertia() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
srp_data: Some(SRPData {
area_m2: 2.0,
coeff_reflectivity: 1.8,
@@ -256,8 +244,7 @@ mod spacecraft_constants_ut {
#[test]
fn sc_full() {
let repr = SpacecraftData {
- name: "demo spacecraft",
- comments: "this is an example of encoding spacecraft data",
+ name: "demo spacecraft".try_into().unwrap(),
srp_data: Some(SRPData {
area_m2: 2.0,
coeff_reflectivity: 1.8,
diff --git a/tests/almanac/mod.rs b/tests/almanac/mod.rs
new file mode 100644
index 00000000..757f23d7
--- /dev/null
+++ b/tests/almanac/mod.rs
@@ -0,0 +1,73 @@
+// Start by creating the ANISE planetary data
+use anise::{
+ constants::frames::{EARTH_ITRF93, EARTH_J2000},
+ naif::kpl::parser::convert_tpc,
+ prelude::{Aberration, Almanac, Orbit, BPC, SPK},
+};
+use core::str::FromStr;
+use hifitime::Epoch;
+
+#[test]
+fn test_load_ctx() {
+ dbg!(core::mem::size_of::());
+
+ let dataset = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
+
+ // Load BSP and BPC
+ let ctx = Almanac::default();
+
+ let spk = SPK::load("data/de440.bsp").unwrap();
+ let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+
+ let mut loaded_ctx = ctx.with_spk(spk).unwrap().with_bpc(bpc).unwrap();
+
+ loaded_ctx.planetary_data = dataset;
+
+ println!("{loaded_ctx}");
+
+ dbg!(core::mem::size_of::());
+}
+
+#[test]
+fn test_state_transformation() {
+ // Load BSP and BPC
+ let ctx = Almanac::default();
+
+ let spk = SPK::load("data/de440.bsp").unwrap();
+ let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+ let pck = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
+
+ let almanac = ctx
+ .with_spk(spk)
+ .unwrap()
+ .with_bpc(bpc)
+ .unwrap()
+ .with_planetary_data(pck);
+
+ // Let's build an orbit
+ // Start by grabbing a copy of the frame.
+ let eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
+ // Define an epoch
+ let epoch = Epoch::from_str("2021-10-29 12:34:56 TDB").unwrap();
+
+ let orig_state = Orbit::keplerian(
+ 8_191.93, 1e-6, 12.85, 306.614, 314.19, 99.887_7, epoch, eme2k,
+ );
+
+ // Transform that into another frame.
+ let state_itrf93 = almanac
+ .transform_to(orig_state, EARTH_ITRF93, Aberration::None)
+ .unwrap();
+
+ println!("{orig_state:x}");
+ println!("{state_itrf93:X}");
+
+ // Convert back
+ let from_state_itrf93_to_eme2k = almanac
+ .transform_to(state_itrf93, EARTH_J2000, Aberration::None)
+ .unwrap();
+
+ println!("{from_state_itrf93_to_eme2k}");
+
+ assert_eq!(orig_state, from_state_itrf93_to_eme2k);
+}
diff --git a/tests/astro/orbit.rs b/tests/astro/orbit.rs
index 7438ef27..8c42f1fc 100644
--- a/tests/astro/orbit.rs
+++ b/tests/astro/orbit.rs
@@ -10,11 +10,11 @@ use anise::time::{Epoch, Unit};
use rstest::*;
#[fixture]
-fn almanac<'a>() -> Almanac<'a> {
- let mut ctx = Almanac::default();
-
- ctx.planetary_data = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
- ctx
+fn almanac() -> Almanac {
+ Almanac {
+ planetary_data: convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap(),
+ ..Default::default()
+ }
}
macro_rules! f64_eq {
@@ -34,7 +34,7 @@ macro_rules! f64_eq {
fn val_state_def_circ_inc(almanac: Almanac) {
let mut eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
// Set the GM value from the GMAT data since we're validating the calculations against GMAT.
- eme2k.mu_km3_s2 = Some(398_600.4415);
+ eme2k.mu_km3_s2 = Some(398_600.441_5);
let epoch = Epoch::from_mjd_tai(21_545.0);
let cart = Orbit::new(
@@ -153,7 +153,7 @@ fn val_state_def_circ_inc(almanac: Almanac) {
fn val_state_def_elliptical(almanac: Almanac) {
let mut eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
// Set the GM value from the GMAT data since we're validating the calculations against GMAT.
- eme2k.mu_km3_s2 = Some(398_600.4415);
+ eme2k.mu_km3_s2 = Some(398_600.441_5);
let epoch = Epoch::from_mjd_tai(21_545.0);
let cart = Orbit::new(
@@ -253,7 +253,7 @@ fn val_state_def_elliptical(almanac: Almanac) {
fn val_state_def_circ_eq(almanac: Almanac) {
let mut eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
// Set the GM value from the GMAT data since we're validating the calculations against GMAT.
- eme2k.mu_km3_s2 = Some(398_600.4415);
+ eme2k.mu_km3_s2 = Some(398_600.441_5);
let epoch = Epoch::from_mjd_tai(21_545.0);
let cart = Orbit::new(
@@ -351,7 +351,7 @@ fn val_state_def_circ_eq(almanac: Almanac) {
fn val_state_def_equatorial(almanac: Almanac) {
let mut eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
// Set the GM value from the GMAT data since we're validating the calculations against GMAT.
- eme2k.mu_km3_s2 = Some(398_600.4415);
+ eme2k.mu_km3_s2 = Some(398_600.441_5);
let epoch = Epoch::from_mjd_tai(21_545.0);
let cart = Orbit::new(
@@ -370,14 +370,14 @@ fn val_state_def_equatorial(almanac: Almanac) {
f64_eq!(cart.inc_deg().unwrap(), 0.005000000478594339, "inc");
f64_eq!(cart.raan_deg().unwrap(), 360.0, "raan");
f64_eq!(cart.aop_deg().unwrap(), 177.9999736473912, "aop");
- f64_eq!(cart.ta_deg().unwrap(), 2.650826247094554e-05, "ta");
+ f64_eq!(cart.ta_deg().unwrap(), 2.650_826_247_094_554e-5, "ta");
}
#[rstest]
fn val_state_def_reciprocity(almanac: Almanac) {
let mut eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
// Set the GM value from the GMAT data since we're validating the calculations against GMAT.
- eme2k.mu_km3_s2 = Some(398_600.4415);
+ eme2k.mu_km3_s2 = Some(398_600.441_5);
let epoch = Epoch::from_mjd_tai(21_545.0);
@@ -483,7 +483,7 @@ fn verif_geodetic_vallado(almanac: Almanac) {
let long = 345.5975;
let height = 56.0e-3;
let height_val = 0.056_000_000_000_494_765;
- let ri = 6_119.4032_332_711_09;
+ let ri = 6_119.403_233_271_109;
let rj = -1_571.480_316_600_378_3;
let rk = -871.560_226_712_024_7;
let r = Orbit::from_altlatlong(
diff --git a/tests/context/mod.rs b/tests/context/mod.rs
deleted file mode 100644
index abc95859..00000000
--- a/tests/context/mod.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-#[test]
-fn test_load_ctx() {
- // Start by creating the ANISE planetary data
- use anise::{
- naif::kpl::parser::convert_tpc,
- prelude::{Almanac, BPC, SPK},
- };
-
- dbg!(core::mem::size_of::());
-
- let dataset = convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap();
-
- // Load BSP and BPC
- let ctx = Almanac::default();
-
- let spk = SPK::load("data/de440.bsp").unwrap();
- let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
-
- let mut loaded_ctx = ctx.load_spk(spk).unwrap().load_bpc(bpc).unwrap();
-
- loaded_ctx.planetary_data = dataset;
-
- println!("{loaded_ctx}");
-
- dbg!(core::mem::size_of::());
-}
diff --git a/tests/ephemerides/mod.rs b/tests/ephemerides/mod.rs
index 264cdfbb..9756c0c0 100644
--- a/tests/ephemerides/mod.rs
+++ b/tests/ephemerides/mod.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -10,6 +10,7 @@
mod parent_translation_verif;
mod paths;
+mod transform;
mod translation;
#[cfg(feature = "spkezr_validation")]
mod validation;
diff --git a/tests/ephemerides/parent_translation_verif.rs b/tests/ephemerides/parent_translation_verif.rs
index f74579ef..a9a4994e 100644
--- a/tests/ephemerides/parent_translation_verif.rs
+++ b/tests/ephemerides/parent_translation_verif.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/ephemerides/paths.rs b/tests/ephemerides/paths.rs
index 4b852e59..c8bc95ab 100644
--- a/tests/ephemerides/paths.rs
+++ b/tests/ephemerides/paths.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -34,7 +34,7 @@ fn common_root_verif() {
// The root of all these files should be the SSB
assert_eq!(
- ctx.try_find_context_center().unwrap(),
+ ctx.try_find_ephemeris_root().unwrap(),
SOLAR_SYSTEM_BARYCENTER
);
diff --git a/tests/ephemerides/transform.rs b/tests/ephemerides/transform.rs
new file mode 100644
index 00000000..c40bbb39
--- /dev/null
+++ b/tests/ephemerides/transform.rs
@@ -0,0 +1,144 @@
+/*
+ * 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::constants::frames::{EARTH_ITRF93, VENUS_J2000};
+use anise::math::Vector3;
+use anise::prelude::*;
+
+// Corresponds to an error of 2e-2 meters, or 20 millimeters
+const POSITION_EPSILON_KM: f64 = 2e-5;
+// Corresponds to an error of 5e-7 meters per second, or 0.5 micrometers per second
+const VELOCITY_EPSILON_KM_S: f64 = 5e-10;
+
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn de440s_transform_verif_venus2emb() {
+ if pretty_env_logger::try_init().is_err() {
+ println!("could not init env_logger");
+ }
+
+ let spk_path = "./data/de440s.bsp";
+ let bpc_path = "./data/earth_latest_high_prec.bpc";
+
+ // Load into ANISE
+ let spk = SPK::load(spk_path).unwrap();
+ let bpc = BPC::load(bpc_path).unwrap();
+
+ // Load into SPICE
+ spice::furnsh(spk_path);
+ spice::furnsh(bpc_path);
+
+ let almanac = Almanac::default()
+ .with_spk(spk)
+ .unwrap()
+ .with_bpc(bpc)
+ .unwrap();
+
+ let epoch = Epoch::from_gregorian_utc_at_midnight(2020, 2, 7);
+
+ let state = almanac
+ .transform_from_to(VENUS_J2000, EARTH_ITRF93, epoch, Aberration::None)
+ .unwrap();
+
+ let (spice_state, _) = spice::spkezr("VENUS", epoch.to_et_seconds(), "ITRF93", "NONE", "EARTH");
+
+ let pos_expct_km = Vector3::new(spice_state[0], spice_state[1], spice_state[2]);
+ let vel_expct_km_s = Vector3::new(spice_state[3], spice_state[4], spice_state[5]);
+
+ dbg!(pos_expct_km - state.radius_km);
+ dbg!(vel_expct_km_s - state.velocity_km_s);
+
+ assert!(
+ relative_eq!(state.radius_km, pos_expct_km, epsilon = POSITION_EPSILON_KM),
+ "pos = {}\nexp = {pos_expct_km}\nerr = {:e}",
+ state.radius_km,
+ pos_expct_km - state.radius_km
+ );
+
+ assert!(
+ relative_eq!(
+ state.velocity_km_s,
+ vel_expct_km_s,
+ epsilon = VELOCITY_EPSILON_KM_S
+ ),
+ "vel = {}\nexp = {vel_expct_km_s}\nerr = {:e}",
+ state.velocity_km_s,
+ vel_expct_km_s - state.velocity_km_s
+ );
+
+ // TODO https://github.com/nyx-space/anise/issues/130
+ // Test the opposite translation
+ // let state_rtn = almanac
+ // .transform_from_to(EARTH_ITRF93, VENUS_J2000, epoch, Aberration::None)
+ // .unwrap();
+
+ // println!("state = {state}");
+ // println!("state_rtn = {state_rtn}");
+
+ // let (spice_state, _) = spice::spkezr("EARTH", epoch.to_et_seconds(), "ITRF93", "NONE", "VENUS");
+
+ // let pos_rtn_expct_km = Vector3::new(spice_state[0], spice_state[1], spice_state[2]);
+ // let vel_rtn_expct_km_s = Vector3::new(spice_state[3], spice_state[4], spice_state[5]);
+
+ // dbg!(pos_expct_km + pos_rtn_expct_km);
+ // dbg!(vel_expct_km_s + vel_rtn_expct_km_s);
+
+ // dbg!(pos_expct_km + state_rtn.radius_km);
+ // dbg!(pos_rtn_expct_km - state_rtn.radius_km);
+ // dbg!(vel_expct_km_s + state_rtn.velocity_km_s);
+ // dbg!(vel_rtn_expct_km_s - state_rtn.velocity_km_s);
+
+ // assert!(
+ // relative_eq!(
+ // state_rtn.radius_km,
+ // -pos_expct_km,
+ // epsilon = POSITION_EPSILON_KM
+ // ),
+ // "pos = {}\nexp = {pos_expct_km}\nerr = {:e}",
+ // state_rtn.radius_km,
+ // pos_expct_km + state_rtn.radius_km
+ // );
+
+ // assert!(
+ // relative_eq!(
+ // state.velocity_km_s,
+ // -vel_expct_km_s,
+ // epsilon = VELOCITY_EPSILON_KM_S
+ // ),
+ // "vel = {}\nexp = {vel_expct_km_s}\nerr = {:e}",
+ // state.velocity_km_s,
+ // vel_expct_km_s + state.velocity_km_s
+ // );
+
+ // // Check that the return state is exactly opposite to the forward state
+ // assert!(
+ // relative_eq!(state_rtn.radius_km, -state.radius_km, epsilon = EPSILON),
+ // "pos = {}\nexp = {}\nerr = {:e}",
+ // state_rtn.radius_km,
+ // -state.radius_km,
+ // state_rtn.radius_km - state.radius_km
+ // );
+
+ // assert!(
+ // relative_eq!(
+ // state_rtn.velocity_km_s,
+ // -state.velocity_km_s,
+ // epsilon = EPSILON
+ // ),
+ // "vel = {}\nexp = {}\nerr = {:e}",
+ // state.velocity_km_s,
+ // -state_rtn.velocity_km_s,
+ // state.velocity_km_s - state_rtn.velocity_km_s,
+ // );
+
+ // Unload spice
+ spice::unload(bpc_path);
+ spice::unload(spk_path);
+}
diff --git a/tests/ephemerides/translation.rs b/tests/ephemerides/translation.rs
index 2acb374a..c410dda9 100644
--- a/tests/ephemerides/translation.rs
+++ b/tests/ephemerides/translation.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -28,8 +28,7 @@ fn de440s_translation_verif_venus2emb() {
// "Load" the file via a memory map (avoids allocations)
let path = "./data/de440s.bsp";
- let buf = file2heap!(path).unwrap();
- let spk = SPK::parse(buf).unwrap();
+ let spk = SPK::load(path).unwrap();
let ctx = Almanac::from_spk(spk).unwrap();
let epoch = Epoch::from_gregorian_utc_at_midnight(2002, 2, 7);
@@ -318,7 +317,7 @@ fn spk_hermite_type13_verif() {
let ctx = Almanac::from_spk(spk)
.unwrap()
- .load_spk(spacecraft)
+ .with_spk(spacecraft)
.unwrap();
let epoch = Epoch::from_gregorian_hms(2000, 1, 1, 14, 0, 0, TimeScale::UTC);
diff --git a/tests/ephemerides/validation/compare.rs b/tests/ephemerides/validation/compare.rs
index baea02d9..627f83b6 100644
--- a/tests/ephemerides/validation/compare.rs
+++ b/tests/ephemerides/validation/compare.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -209,7 +209,7 @@ impl CompareEphem {
}
for spk in spks {
- ctx = ctx.load_spk(spk).unwrap();
+ ctx = ctx.with_spk(spk).unwrap();
}
info!("Pairs in comparator: {:?}", &pairs);
diff --git a/tests/ephemerides/validation/mod.rs b/tests/ephemerides/validation/mod.rs
index d0ea1b56..2c4e7b20 100644
--- a/tests/ephemerides/validation/mod.rs
+++ b/tests/ephemerides/validation/mod.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/ephemerides/validation/type02_chebyshev_jpl_de.rs b/tests/ephemerides/validation/type02_chebyshev_jpl_de.rs
index 2b3c6540..47c2560f 100644
--- a/tests/ephemerides/validation/type02_chebyshev_jpl_de.rs
+++ b/tests/ephemerides/validation/type02_chebyshev_jpl_de.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -12,7 +12,7 @@ use super::{compare::*, validate::Validation};
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
-fn validate_de440_full() {
+fn validate_jplde_de440_full() {
let file_name = "spk-type2-validation-de440".to_string();
let comparator =
CompareEphem::new(vec!["data/de440.bsp".to_string()], file_name.clone(), 1_000);
@@ -31,7 +31,7 @@ fn validate_de440_full() {
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
-fn validate_de440s() {
+fn validate_jplde_de440s() {
let output_file_name = "spk-type2-validation-de440s".to_string();
let comparator = CompareEphem::new(
vec!["data/de440s.bsp".to_string()],
diff --git a/tests/ephemerides/validation/type13_hermite.rs b/tests/ephemerides/validation/type13_hermite.rs
index 0a8a0af9..c5ebaad4 100644
--- a/tests/ephemerides/validation/type13_hermite.rs
+++ b/tests/ephemerides/validation/type13_hermite.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/ephemerides/validation/validate.rs b/tests/ephemerides/validation/validate.rs
index 68a3a4dc..abdc92f5 100644
--- a/tests/ephemerides/validation/validate.rs
+++ b/tests/ephemerides/validation/validate.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/frames/format.rs b/tests/frames/format.rs
index f9e795ea..68109eaa 100644
--- a/tests/frames/format.rs
+++ b/tests/frames/format.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/frames/mod.rs b/tests/frames/mod.rs
index 0aa7a195..8a682ceb 100644
--- a/tests/frames/mod.rs
+++ b/tests/frames/mod.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
diff --git a/tests/lib.rs b/tests/lib.rs
index 2a5f8354..f28c3e77 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -10,7 +10,8 @@
#[macro_use]
extern crate approx;
+mod almanac;
mod astro;
-mod context;
mod ephemerides;
mod frames;
+mod orientations;
diff --git a/tests/naif.rs b/tests/naif.rs
index de36774b..d7e2b04e 100644
--- a/tests/naif.rs
+++ b/tests/naif.rs
@@ -1,6 +1,6 @@
/*
* ANISE Toolkit
- * Copyright (C) 2021-2022 Christopher Rabotin et al. (cf. AUTHORS.md)
+ * 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/.
@@ -13,9 +13,9 @@ use std::mem::size_of_val;
use anise::{
file2heap,
naif::{
- daf::DAF,
+ daf::{datatypes::Type2ChebyshevSet, DAF},
pck::BPCSummaryRecord,
- spk::{datatypes::Type2ChebyshevSet, summary::SPKSummaryRecord},
+ spk::summary::SPKSummaryRecord,
Endian,
},
prelude::*,
@@ -137,7 +137,7 @@ fn test_spk_load_bytes() {
// Put this in a context
let default_almanac = Almanac::default();
- let spice = default_almanac.load_spk(de421).unwrap();
+ let spice = default_almanac.with_spk(de421).unwrap();
assert_eq!(spice.num_loaded_spk(), 1);
assert_eq!(default_almanac.num_loaded_spk(), 0);
@@ -146,12 +146,12 @@ fn test_spk_load_bytes() {
{
let bytes = file2heap!("data/de440.bsp").unwrap();
let de440 = DAF::::parse(bytes).unwrap();
- let spice = spice.load_spk(de440).unwrap();
+ let spice = spice.with_spk(de440).unwrap();
// And another
let bytes = file2heap!("data/de440s.bsp").unwrap();
let de440 = DAF::::parse(bytes).unwrap();
- let spice = spice.load_spk(de440).unwrap();
+ let spice = spice.with_spk(de440).unwrap();
// NOTE: Because everything is a pointer, the size on the stack remains constant at 521 bytes.
println!("{}", size_of_val(&spice));
diff --git a/tests/orientations/mod.rs b/tests/orientations/mod.rs
new file mode 100644
index 00000000..d3ed7ed2
--- /dev/null
+++ b/tests/orientations/mod.rs
@@ -0,0 +1,206 @@
+use anise::constants::frames::{EARTH_ITRF93, EME2000};
+use anise::constants::orientations::{ECLIPJ2000, ITRF93, J2000};
+use anise::math::rotation::DCM;
+use anise::math::Matrix3;
+use anise::naif::kpl::parser::convert_tpc;
+
+use anise::prelude::*;
+
+mod validation;
+
+#[test]
+fn test_find_root() {
+ // try_find_orientation_root
+ let almanac = Almanac {
+ planetary_data: convert_tpc("data/pck00008.tpc", "data/gm_de431.tpc").unwrap(),
+ ..Default::default()
+ };
+
+ assert_eq!(almanac.try_find_orientation_root(), Ok(J2000));
+}
+
+#[test]
+fn test_single_bpc() {
+ use core::str::FromStr;
+ let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ let epoch = Epoch::from_str("2019-03-01T04:02:51.0 ET").unwrap();
+
+ let dcm = almanac.rotation_to_parent(EARTH_ITRF93, epoch).unwrap();
+
+ assert_eq!(dcm.from, ECLIPJ2000);
+ assert_eq!(dcm.to, ITRF93);
+
+ let spice_dcm = DCM {
+ from: ITRF93,
+ to: ECLIPJ2000,
+ rot_mat: Matrix3::new(
+ -0.7787074378266214,
+ -0.5750522285696024,
+ 0.25085784956949614,
+ 0.62738454047427242,
+ -0.7149156903669622,
+ 0.30868137948540997,
+ 0.0018342975179237739,
+ 0.3977568228003932,
+ 0.9174890436775538,
+ ),
+ rot_mat_dt: Some(Matrix3::new(
+ 0.000045749603091397784,
+ -0.00005213242562826116,
+ 0.00002250951555355774,
+ 0.00005678424274353827,
+ 0.00004193347475880688,
+ -0.000018292833187960967,
+ 0.00000000008998156330541006,
+ 0.00000000007924039406561106,
+ -0.000000000034532794214329133,
+ )),
+ };
+
+ assert!(
+ (dcm.rot_mat - spice_dcm.rot_mat).norm() < 1e-9,
+ "dcm error! got: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ spice_dcm.rot_mat,
+ (dcm.rot_mat - spice_dcm.rot_mat).norm(),
+ dcm.rot_mat - spice_dcm.rot_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
+ "derivative error! got: {}want:{}derivative err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+}
+
+#[test]
+fn test_itrf93_to_j2k() {
+ use core::str::FromStr;
+ let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ let epoch = Epoch::from_str("2019-03-01T04:02:51.0 ET").unwrap();
+
+ let dcm = almanac
+ .rotate_from_to(EARTH_ITRF93, EME2000, epoch)
+ .unwrap();
+
+ let spice_dcm = DCM {
+ from: ITRF93,
+ to: J2000,
+ rot_mat: Matrix3::new(
+ -0.7787074378266214,
+ 0.6273845404742724,
+ 0.0018342975179237739,
+ -0.6273856264104672,
+ -0.7787087230243394,
+ -0.000021432407757815408,
+ 0.0014149371165367297,
+ -0.0011675014726372779,
+ 0.9999983174452183,
+ ),
+ rot_mat_dt: Some(Matrix3::new(
+ 0.000045749603091397784,
+ 0.00005678424274353827,
+ 0.00000000008998156330541006,
+ -0.000056784336444384685,
+ 0.00004574968205088016,
+ 0.00000000008643799681544929,
+ -0.0000000850112519852614,
+ -0.00000010316798647710046,
+ -0.00000000000016320065843054112,
+ )),
+ };
+
+ assert_eq!(dcm.from, ITRF93);
+ assert_eq!(dcm.to, J2000);
+
+ assert!(
+ (dcm.rot_mat - spice_dcm.rot_mat).norm() < 1e-9,
+ "dcm error! got: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ spice_dcm.rot_mat,
+ (dcm.rot_mat - spice_dcm.rot_mat).norm(),
+ dcm.rot_mat - spice_dcm.rot_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
+ "derivative error! got: {}want:{}derivative err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+}
+
+#[test]
+fn test_j2k_to_itrf93() {
+ use core::str::FromStr;
+ let bpc = BPC::load("data/earth_latest_high_prec.bpc").unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ let epoch = Epoch::from_str("2019-03-01T04:02:51.0 ET").unwrap();
+
+ let dcm = almanac
+ .rotate_from_to(EME2000, EARTH_ITRF93, epoch)
+ .unwrap();
+
+ let spice_dcm_t = DCM {
+ from: ITRF93,
+ to: J2000,
+ rot_mat: Matrix3::new(
+ -0.7787074378266214,
+ 0.6273845404742724,
+ 0.0018342975179237739,
+ -0.6273856264104672,
+ -0.7787087230243394,
+ -0.000021432407757815408,
+ 0.0014149371165367297,
+ -0.0011675014726372779,
+ 0.9999983174452183,
+ ),
+ rot_mat_dt: Some(Matrix3::new(
+ 0.000045749603091397784,
+ 0.00005678424274353827,
+ 0.00000000008998156330541006,
+ -0.000056784336444384685,
+ 0.00004574968205088016,
+ 0.00000000008643799681544929,
+ -0.0000000850112519852614,
+ -0.00000010316798647710046,
+ -0.00000000000016320065843054112,
+ )),
+ };
+
+ let spice_dcm = spice_dcm_t.transpose();
+
+ assert_eq!(dcm.to, ITRF93);
+ assert_eq!(dcm.from, J2000);
+
+ assert!(
+ (dcm.rot_mat - spice_dcm.rot_mat).norm() < 1e-9,
+ "dcm error! got: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ spice_dcm.rot_mat,
+ (dcm.rot_mat - spice_dcm.rot_mat).norm(),
+ dcm.rot_mat - spice_dcm.rot_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
+ "derivative error! got: {}want:{}derivative err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+}
diff --git a/tests/orientations/validation.rs b/tests/orientations/validation.rs
new file mode 100644
index 00000000..74bcc76d
--- /dev/null
+++ b/tests/orientations/validation.rs
@@ -0,0 +1,755 @@
+/*
+ * 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::f64::EPSILON;
+
+use anise::{
+ constants::{
+ celestial_objects::EARTH,
+ frames::*,
+ orientations::{ECLIPJ2000, FK4, ITRF93, J2000},
+ },
+ math::{
+ cartesian::CartesianState,
+ rotation::{Quaternion, DCM},
+ Matrix3, Vector3,
+ },
+ naif::kpl::parser::convert_tpc,
+ prelude::{Almanac, Frame, BPC},
+};
+use hifitime::{Duration, Epoch, TimeSeries, TimeUnits};
+use spice::cstr;
+
+// Allow up to two arcsecond of error (or 0.12 microradians), but check test results for actualized error
+const MAX_ERR_DEG: f64 = 7.2e-6;
+const DCM_EPSILON: f64 = 1e-9;
+// Absolute error tolerance between ANISE and SPICE for the same state rotation.
+const POSITION_ERR_TOL_KM: f64 = 2e-5;
+const VELOCITY_ERR_TOL_KM_S: f64 = 5e-7;
+// Return absolute tolerance, i.e. perform the same rotation from A to B and B to A, and check that the norm error is less than that.
+const RTN_POSITION_EPSILON_KM: f64 = 1e-10;
+const RTN_VELOCITY_EPSILON_KM_S: f64 = 1e-10;
+
+/// This test converts the PCK file into its ANISE equivalent format, loads it into an Almanac, and compares the rotations computed by the Almanac and by SPICE
+/// It only check the IAU rotations to its J2000 parent, and accounts for nutation and precession coefficients where applicable.
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn validate_iau_rotation_to_parent() {
+ // Known bug with nutation and precession angles: https://github.com/nyx-space/anise/issues/122
+ let pck = "data/pck00008.tpc";
+ spice::furnsh(pck);
+ let planetary_data = convert_tpc(pck, "data/gm_de431.tpc").unwrap();
+
+ let almanac = Almanac {
+ planetary_data,
+ ..Default::default()
+ };
+
+ for frame in [
+ IAU_MERCURY_FRAME,
+ IAU_VENUS_FRAME,
+ IAU_EARTH_FRAME,
+ IAU_MARS_FRAME,
+ IAU_JUPITER_FRAME,
+ IAU_SATURN_FRAME,
+ // IAU_NEPTUNE_FRAME, // Bug: https://github.com/nyx-space/anise/issues/122
+ // IAU_URANUS_FRAME,
+ ] {
+ for (num, epoch) in TimeSeries::inclusive(
+ Epoch::from_tdb_duration(Duration::ZERO),
+ Epoch::from_tdb_duration(0.2.centuries()),
+ 1.days(),
+ )
+ .enumerate()
+ {
+ let dcm = almanac.rotation_to_parent(frame, epoch).unwrap();
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ unsafe {
+ spice::c::sxform_c(
+ cstr!("J2000"),
+ cstr!(format!("{frame:o}")),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ // Parent rotation of Earth IAU frame is 3 not J2000, etc.
+ assert!(
+ [J2000, FK4, 4, 5, 6].contains(&dcm.from),
+ "unexpected DCM from frame {}",
+ dcm.from
+ );
+ assert_eq!(dcm.to, frame.orientation_id);
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let spice_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Some(Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ ));
+
+ let spice_dcm = DCM {
+ rot_mat: spice_mat,
+ from: dcm.from,
+ to: dcm.to,
+ rot_mat_dt,
+ };
+
+ if num == 0 {
+ println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
+ println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
+
+ println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
+
+ println!(
+ "derivative error\n{:e}",
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+
+ // Compute the different in PRV and rotation angle
+ let q_anise = Quaternion::from(dcm);
+ let q_spice = Quaternion::from(spice_dcm);
+
+ let (anise_uvec, anise_angle) = q_anise.uvec_angle();
+ let (spice_uvec, spice_angle) = q_spice.uvec_angle();
+
+ let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
+ let deg_err = (anise_angle - spice_angle).to_degrees();
+
+ // In some cases, the arc cos of the angle between the unit vectors is NaN (because the dot product is rounded just past -1 or +1)
+ // so we allow NaN.
+ // However, we also check the rotation about that unit vector AND we check that the DCMs match too.
+ assert!(
+ uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
+ "#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
+ );
+ assert!(
+ deg_err.abs() < MAX_ERR_DEG,
+ "#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
+ );
+
+ assert!(
+ (dcm.rot_mat - spice_mat).norm() < DCM_EPSILON,
+ "#{num} {epoch}\ngot: {}want:{spice_mat}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ (dcm.rot_mat - spice_mat).norm(),
+ dcm.rot_mat - spice_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < DCM_EPSILON,
+ "#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+ }
+}
+
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn validate_bpc_rotation_to_parent() {
+ let pck = "data/earth_latest_high_prec.bpc";
+ spice::furnsh(pck);
+
+ let bpc = BPC::load(pck).unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ // This BPC file start in 2011 and ends in 2022.
+ for (num, epoch) in TimeSeries::inclusive(
+ Epoch::from_tdb_duration(0.11.centuries()),
+ Epoch::from_tdb_duration(0.2.centuries()),
+ 1.days(),
+ )
+ .enumerate()
+ {
+ let dcm = almanac.rotation_to_parent(EARTH_ITRF93, epoch).unwrap();
+
+ if num == 0 {
+ println!("{dcm}");
+ }
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ unsafe {
+ spice::c::sxform_c(
+ cstr!("ECLIPJ2000"),
+ cstr!("ITRF93"),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let rot_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Some(Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ ));
+
+ let spice_dcm = DCM {
+ rot_mat,
+ from: dcm.from,
+ to: dcm.to,
+ rot_mat_dt,
+ };
+
+ if num == 0 {
+ println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
+ println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
+
+ println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
+
+ println!(
+ "derivative error\n{:e}",
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+
+ // Compute the different in PRV and rotation angle
+ let q_anise = Quaternion::from(dcm);
+ let q_spice = Quaternion::from(spice_dcm);
+
+ let (anise_uvec, anise_angle) = q_anise.uvec_angle();
+ let (spice_uvec, spice_angle) = q_spice.uvec_angle();
+
+ let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
+ let deg_err = (anise_angle - spice_angle).to_degrees();
+
+ // In some cases, the arc cos of the angle between the unit vectors is NaN (because the dot product is rounded just past -1 or +1)
+ // so we allow NaN.
+ // However, we also check the rotation about that unit vector AND we check that the DCMs match too.
+ assert!(
+ uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
+ "#{num} @ {epoch} unit vector angle error: {uvec_angle_deg_err:e} deg"
+ );
+ assert!(
+ deg_err.abs() < MAX_ERR_DEG,
+ "#{num} @ {epoch} rotation error: {deg_err:e} deg"
+ );
+
+ assert!(
+ (dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
+ "#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ (dcm.rot_mat - rot_mat).norm(),
+ dcm.rot_mat - rot_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
+ "#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+
+ // Check the frames
+ assert_eq!(dcm.from, ECLIPJ2000);
+ assert_eq!(dcm.to, ITRF93);
+ }
+}
+
+/// Ensure that our rotation for [ECLIPJ2000] to [J2000] matches the one from SPICE.
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn validate_j2000_ecliptic() {
+ // The eclipj2000 to j2000 rotation is embedded, so we don't need to load anything.
+ let almanac = Almanac::default();
+
+ for (num, epoch) in TimeSeries::inclusive(
+ Epoch::from_tdb_duration(0.11.centuries()),
+ Epoch::from_tdb_duration(0.2.centuries()),
+ 100.days(),
+ )
+ .enumerate()
+ {
+ let dcm = almanac.rotation_to_parent(EARTH_ECLIPJ2000, epoch).unwrap();
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ unsafe {
+ spice::c::sxform_c(
+ cstr!("J2000"),
+ cstr!("ECLIPJ2000"),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ assert_eq!(dcm.from, J2000);
+ assert_eq!(dcm.to, ECLIPJ2000);
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let rot_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ );
+
+ let spice_dcm = DCM {
+ rot_mat,
+ from: dcm.from,
+ to: dcm.to,
+ rot_mat_dt: if rot_mat_dt.norm() == 0.0 {
+ // I know this will be the case.
+ None
+ } else {
+ Some(rot_mat_dt)
+ },
+ };
+
+ assert!(
+ (dcm.rot_mat - rot_mat).norm() < EPSILON,
+ "#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ (dcm.rot_mat - rot_mat).norm(),
+ dcm.rot_mat - rot_mat
+ );
+
+ // Check the derivative
+ assert_eq!(
+ dcm.rot_mat_dt, spice_dcm.rot_mat_dt,
+ "expected both derivatives to be unuset"
+ );
+ }
+}
+
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn validate_bpc_rotations() {
+ let pck = "data/earth_latest_high_prec.bpc";
+ spice::furnsh(pck);
+
+ let bpc = BPC::load(pck).unwrap();
+ let almanac = Almanac::from_bpc(bpc).unwrap();
+
+ let frame = Frame::from_ephem_orient(EARTH, ITRF93);
+
+ let mut actual_max_uvec_err_deg = 0.0;
+ let mut actual_max_err_deg = 0.0;
+
+ // This BPC file start in 2011 and ends in 2022.
+ for (num, epoch) in TimeSeries::inclusive(
+ Epoch::from_tdb_duration(0.11.centuries()),
+ Epoch::from_tdb_duration(0.2.centuries()),
+ 1.days(),
+ )
+ .enumerate()
+ {
+ let dcm = almanac
+ .rotate_from_to(EARTH_ITRF93, EME2000, epoch)
+ .unwrap();
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ unsafe {
+ spice::c::sxform_c(
+ cstr!("ITRF93"),
+ cstr!("J2000"),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let rot_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Some(Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ ));
+
+ let spice_dcm = DCM {
+ rot_mat,
+ from: ITRF93,
+ to: J2000,
+ rot_mat_dt,
+ };
+
+ if num == 0 {
+ println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
+ println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
+
+ println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
+
+ println!(
+ "derivative error\n{:e}",
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+
+ // Compute the different in PRV and rotation angle
+ let q_anise = Quaternion::from(dcm);
+ let q_spice = Quaternion::from(spice_dcm);
+
+ let (anise_uvec, anise_angle) = q_anise.uvec_angle();
+ let (spice_uvec, spice_angle) = q_spice.uvec_angle();
+
+ let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
+ let deg_err = (anise_angle - spice_angle).to_degrees();
+
+ // In some cases, the arc cos of the angle between the unit vectors is NaN (because the dot product is rounded just past -1 or +1)
+ // so we allow NaN.
+ // However, we also check the rotation about that unit vector AND we check that the DCMs match too.
+ assert!(
+ uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
+ "#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
+ );
+
+ if uvec_angle_deg_err.abs() > actual_max_uvec_err_deg {
+ actual_max_uvec_err_deg = uvec_angle_deg_err.abs();
+ }
+
+ assert!(
+ deg_err.abs() < MAX_ERR_DEG,
+ "#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
+ );
+
+ if deg_err.abs() > actual_max_err_deg {
+ actual_max_err_deg = deg_err.abs();
+ }
+
+ assert!(
+ (dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
+ "#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ (dcm.rot_mat - rot_mat).norm(),
+ dcm.rot_mat - rot_mat
+ );
+
+ // Check the derivative
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
+ "#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+ println!("actualized max error in rotation angle = {actual_max_err_deg:.3e} deg");
+ println!("actualized max error in rotation direction = {actual_max_uvec_err_deg:.3e} deg");
+}
+
+#[ignore = "Requires Rust SPICE -- must be executed serially"]
+#[test]
+fn validate_bpc_to_iau_rotations() {
+ let pck = "data/pck00008.tpc";
+ let bpc = "data/earth_latest_high_prec.bpc";
+ spice::furnsh(bpc);
+ spice::furnsh(pck);
+ let planetary_data = convert_tpc(pck, "data/gm_de431.tpc").unwrap();
+
+ let almanac = Almanac {
+ planetary_data,
+ ..Default::default()
+ };
+ let almanac = almanac.with_bpc(BPC::load(bpc).unwrap()).unwrap();
+
+ println!("{almanac}");
+
+ let mut actual_max_uvec_err_deg = 0.0;
+ let mut actual_max_err_deg = 0.0;
+ let mut actual_pos_err_km = 0.0;
+ let mut actual_vel_err_km_s = 0.0;
+
+ let start = Epoch::from_tdb_duration(0.11.centuries());
+ let end = Epoch::from_tdb_duration(0.20.centuries());
+
+ for frame in [
+ IAU_MERCURY_FRAME,
+ IAU_VENUS_FRAME,
+ IAU_EARTH_FRAME,
+ IAU_MARS_FRAME,
+ IAU_JUPITER_FRAME,
+ IAU_SATURN_FRAME,
+ ] {
+ for (num, epoch) in TimeSeries::inclusive(start, end, 27.days()).enumerate() {
+ let dcm = almanac.rotate_from_to(EARTH_ITRF93, frame, epoch).unwrap();
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ let spice_name = format!("{frame:o}");
+ unsafe {
+ spice::c::sxform_c(
+ cstr!("ITRF93"),
+ cstr!(spice_name),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let rot_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Some(Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ ));
+
+ let spice_dcm = DCM {
+ rot_mat,
+ from: ITRF93,
+ to: frame.orientation_id,
+ rot_mat_dt,
+ };
+
+ if num == 0 {
+ println!("ANISE: {dcm}");
+ println!("SPICE: {spice_dcm}");
+
+ println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
+
+ println!(
+ "derivative error\n{:e}",
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+ }
+
+ assert_eq!(dcm.from, EARTH_ITRF93.orientation_id);
+ assert_eq!(dcm.to, frame.orientation_id);
+
+ // Compute the different in PRV and rotation angle
+ let q_anise = Quaternion::from(dcm);
+ let q_spice = Quaternion::from(spice_dcm);
+
+ let (anise_uvec, anise_angle) = q_anise.uvec_angle();
+ let (spice_uvec, spice_angle) = q_spice.uvec_angle();
+
+ let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
+ let deg_err = (anise_angle - spice_angle).to_degrees();
+
+ // In some cases, the arc cos of the angle between the unit vectors is NaN (because the dot product is rounded just past -1 or +1)
+ // so we allow NaN.
+ // However, we also check the rotation about that unit vector AND we check that the DCMs match too.
+ assert!(
+ uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
+ "#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
+ );
+
+ if uvec_angle_deg_err.abs() > actual_max_uvec_err_deg {
+ actual_max_uvec_err_deg = uvec_angle_deg_err.abs();
+ }
+
+ assert!(
+ deg_err.abs() < MAX_ERR_DEG,
+ "#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
+ );
+
+ if deg_err.abs() > actual_max_err_deg {
+ actual_max_err_deg = deg_err.abs();
+ }
+
+ assert!(
+ (dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
+ "#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
+ dcm.rot_mat,
+ (dcm.rot_mat - rot_mat).norm(),
+ dcm.rot_mat - rot_mat
+ );
+
+ // Check the derivative with a slightly tighet constraint
+ assert!(
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm()
+ < DCM_EPSILON * 0.1,
+ "#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
+ dcm.rot_mat_dt.unwrap(),
+ spice_dcm.rot_mat_dt.unwrap(),
+ (dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
+ dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
+ );
+
+ // Check that we match the SXFORM documentation on the DCM * CartesianState multiplication
+ let state = CartesianState {
+ radius_km: Vector3::new(1234.0, 5678.9, 1234.0),
+ velocity_km_s: Vector3::new(1.2340, 5.6789, 1.2340),
+ epoch,
+ frame: EARTH_ITRF93,
+ };
+
+ let spice_out = (spice_dcm * state).unwrap();
+ let anise_out = (dcm * state).unwrap();
+
+ assert_eq!(spice_out.frame, anise_out.frame);
+ let pos_err_km = (spice_out.radius_km - anise_out.radius_km).norm();
+ assert!(
+ pos_err_km < POSITION_ERR_TOL_KM,
+ "#{num} {epoch}: pos error is {pos_err_km:.3e} km/s"
+ );
+ let vel_err_km_s = (spice_out.velocity_km_s - anise_out.velocity_km_s).norm();
+ assert!(
+ vel_err_km_s < VELOCITY_ERR_TOL_KM_S,
+ "#{num} {epoch}: vel error is {vel_err_km_s:.3e} km/s"
+ );
+
+ if pos_err_km > actual_pos_err_km {
+ actual_pos_err_km = pos_err_km;
+ }
+
+ if vel_err_km_s > actual_vel_err_km_s {
+ actual_vel_err_km_s = vel_err_km_s;
+ }
+
+ // Grab the transposed DCM
+ let dcm_t = almanac.rotate_from_to(frame, EARTH_ITRF93, epoch).unwrap();
+
+ let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
+ unsafe {
+ spice::c::sxform_c(
+ cstr!(format!("{frame:o}")),
+ cstr!("ITRF93"),
+ epoch.to_tdb_seconds(),
+ rot_data.as_mut_ptr(),
+ );
+ }
+
+ // Confirmed that the M3x3 below is the correct representation from SPICE by using the mxv spice function and compare that to the nalgebra equivalent computation.
+ let rot_mat = Matrix3::new(
+ rot_data[0][0],
+ rot_data[0][1],
+ rot_data[0][2],
+ rot_data[1][0],
+ rot_data[1][1],
+ rot_data[1][2],
+ rot_data[2][0],
+ rot_data[2][1],
+ rot_data[2][2],
+ );
+
+ let rot_mat_dt = Some(Matrix3::new(
+ rot_data[3][0],
+ rot_data[3][1],
+ rot_data[3][2],
+ rot_data[4][0],
+ rot_data[4][1],
+ rot_data[4][2],
+ rot_data[5][0],
+ rot_data[5][1],
+ rot_data[5][2],
+ ));
+
+ let spice_dcm_t = DCM {
+ rot_mat,
+ from: dcm_t.from,
+ to: dcm_t.to,
+ rot_mat_dt,
+ };
+
+ let spice_rtn = (spice_dcm_t * spice_out).unwrap();
+ let anise_rtn = (dcm_t * anise_out).unwrap();
+
+ assert_eq!(spice_rtn.frame, anise_rtn.frame);
+ assert!((spice_rtn.radius_km - state.radius_km).norm() < RTN_POSITION_EPSILON_KM);
+ assert!(
+ (spice_rtn.velocity_km_s - state.velocity_km_s).norm() < RTN_VELOCITY_EPSILON_KM_S
+ );
+ assert!((anise_rtn.radius_km - state.radius_km).norm() < RTN_POSITION_EPSILON_KM);
+ assert!(
+ (anise_rtn.velocity_km_s - state.velocity_km_s).norm() < RTN_VELOCITY_EPSILON_KM_S
+ );
+ }
+ }
+ println!("actualized max error in rotation angle = {actual_max_err_deg:.3e} deg");
+ println!("actualized max error in rotation direction = {actual_max_uvec_err_deg:.3e} deg");
+ println!("actualized max error in position = {actual_pos_err_km:.6e} km");
+ println!("actualized max error in velocity = {actual_vel_err_km_s:.6e} km/s");
+}