Skip to content

Commit

Permalink
Merge pull request #359 from nyx-space/175-expose-rotation-parameters…
Browse files Browse the repository at this point in the history
…-to-python

Expose DCM to Python
  • Loading branch information
ChristopherRabotin authored Dec 15, 2024
2 parents 0fa2507 + 2ef8969 commit 8b8bb8d
Show file tree
Hide file tree
Showing 20 changed files with 433 additions and 65 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, ppc64le]
target: [x86_64, x86, aarch64]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
set -e
pip debug --verbose
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest
pip install pytest numpy
pytest
- name: pytest
Expand All @@ -89,7 +89,7 @@ jobs:
set -e
pip debug --verbose
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest
pip install pytest numpy
pytest
- name: Notebook Regression tests
Expand All @@ -99,7 +99,7 @@ jobs:
set -e
pip debug --verbose
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest-notebook
pip install pytest-notebook numpy
pytest
windows:
Expand Down Expand Up @@ -138,7 +138,7 @@ jobs:
run: |
set -e
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest
pip install pytest numpy
pytest
macos-13: # last available x86_64 macos runner
Expand Down Expand Up @@ -173,7 +173,7 @@ jobs:
run: |
set -e
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest
pip install pytest numpy
pytest
macos-14: # last available x86_64 macos runner
Expand Down Expand Up @@ -208,7 +208,7 @@ jobs:
run: |
set -e
pip install anise --find-links anise-py/dist --force-reinstall
pip install pytest
pip install pytest numpy
pytest
sdist:
Expand Down
13 changes: 4 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["anise", "anise-cli", "anise-gui", "anise-py"]

[workspace.package]
version = "0.5.0"
version = "0.5.1"
edition = "2021"
authors = ["Christopher Rabotin <[email protected]>"]
description = "ANISE provides a toolkit and files for Attitude, Navigation, Instrument, Spacecraft, and Ephemeris data. It's a modern replacement of NAIF SPICE file."
Expand Down Expand Up @@ -33,24 +33,19 @@ der = { version = "0.7.8", features = ["derive", "alloc", "real"] }
log = "0.4"
pretty_env_logger = "0.5"
tabled = "=0.17"
const_format = "0.2"
nalgebra = { version = "0.33", default-features = true, features = [
"serde-serialize",
] }
approx = "0.5.1"
zerocopy = { version = "0.8.0", features = ["derive"] }
bytes = "1.6.0"
snafu = { version = "0.8.0", features = ["backtrace"] }
lexical-core = "1.0.1"
heapless = "0.8.0"
rstest = "0.23.0"
pyo3 = { version = "0.22", features = ["multiple-pymethods"] }
pyo3-log = "0.11"
serde = "1"
serde_derive = "1"
serde_dhall = "0.12"
numpy = "0.22"
ndarray = ">= 0.15, < 0.17"

anise = { version = "0.5.0", path = "anise", default-features = false }
anise = { version = "0.5.1", path = "anise", default-features = false }

[profile.bench]
debug = true
Expand Down
1 change: 0 additions & 1 deletion anise-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ crate-type = ["cdylib"]

[dependencies]
anise = { workspace = true, features = ["python", "metaload"] }
snafu = { workspace = true }
hifitime = { workspace = true, features = ["python"] }
pyo3 = { workspace = true, features = ["extension-module"] }
pyo3-log = { workspace = true }
3 changes: 2 additions & 1 deletion anise-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ Type hints are extremely useful for Python users. Building them is a bit of manu

1. `maturin develop` to build the latest library
1. `python generate_stubs.py anise anise.pyi` builds the top level type hints
1. Repeat for all submodules: `utils`, `time`, `astro`, `astro.constants` writing to a new file each time:
1. Repeat for all submodules: `utils`, `time`, `astro`, `astro.constants`, `rotation` writing to a new file each time:
1. `python generate_stubs.py anise.astro anise.astro.pyi`
1. `python generate_stubs.py anise.time anise.time.pyi`
1. `python generate_stubs.py anise.astro.constants anise.astro.constants.pyi`
1. `python generate_stubs.py anise.utils anise.utils.pyi`
1. `python generate_stubs.py anise.rotation anise.rotation.pyi`
1. Final, concat all of these new files back to `anise.pyi` since that's the only one used by `maturin`.
80 changes: 80 additions & 0 deletions anise-py/anise.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
import numpy

_all__: list = ["time", "astro", "utils", "Aberration", "Almanac", "MetaAlmanac", "MetaFile"]

Expand Down Expand Up @@ -2200,3 +2201,82 @@ KPL/TPC files must be converted into "PCA" (Planetary Constant ANISE) files befo
__all__: list = ...
__name__: str = ...


@typing.final
class rotation:

@typing.final
class DCM:
"""Defines a direction cosine matrix from one frame ID to another frame ID, optionally with its time derivative.
It provides a number of run-time checks that prevent invalid rotations."""
from_id: int
rot_mat: numpy.array
rot_mat_dt: numpy.array
to_id: int

def __init__(self, np_rot_mat: numpy.array, from_id: int, to_id: int, np_rot_mat_dt: numpy.array=None) -> DCM:
"""Defines a direction cosine matrix from one frame ID to another frame ID, optionally with its time derivative.
It provides a number of run-time checks that prevent invalid rotations."""

@staticmethod
def from_identity(from_id: int, to_id: int) -> DCM:
"""Builds an identity rotation."""

@staticmethod
def from_r1(angle_rad: float, from_id: int, to_id: int) -> DCM:
"""Returns a rotation matrix for a rotation about the X axis.
Source: `euler1` function from Baslisk"""

@staticmethod
def from_r2(angle_rad: float, from_id: int, to_id: int) -> DCM:
"""Returns a rotation matrix for a rotation about the Y axis.
Source: `euler2` function from Basilisk"""

@staticmethod
def from_r3(angle_rad: float, from_id: int, to_id: int) -> DCM:
"""Returns a rotation matrix for a rotation about the Z axis.
Source: `euler3` function from Basilisk"""

def get_state_dcm(self) -> numpy.array:
"""Returns the 6x6 DCM to rotate a state. If the time derivative of this DCM is defined, this 6x6 accounts for the transport theorem.
Warning: you MUST manually install numpy to call this function."""

def is_identity(self) -> bool:
"""Returns whether this rotation is identity, checking first the frames and then the rotation matrix (but ignores its time derivative)"""

def is_valid(self, unit_tol: float, det_tol: float) -> bool:
"""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 (unit_tol).
-- 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 (det_tol).
[Source: SPICE's rotation.req](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/rotation.html#Validating%20a%20rotation%20matrix)"""

def transpose(self) -> DCM:
"""Returns the transpose of this DCM"""

def __eq__(self, value: typing.Any) -> bool:
"""Return self==value."""

def __ge__(self, value: typing.Any) -> bool:
"""Return self>=value."""

def __gt__(self, value: typing.Any) -> bool:
"""Return self>value."""

def __le__(self, value: typing.Any) -> bool:
"""Return self<=value."""

def __lt__(self, value: typing.Any) -> bool:
"""Return self<value."""

def __ne__(self, value: typing.Any) -> bool:
"""Return self!=value."""

def __repr__(self) -> str:
"""Return repr(self)."""

def __str__(self) -> str:
"""Return str(self)."""
2 changes: 1 addition & 1 deletion anise-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["maturin>=1.7,<2.0"]
requires = ["maturin>=1.7,<2.0", "numpy>=1.16.0"]
build-backend = "maturin"

[project]
Expand Down
2 changes: 2 additions & 0 deletions anise-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use pyo3::py_run;

mod astro;
mod constants;
mod rotation;
mod utils;

/// A Python module implemented in Rust.
Expand All @@ -29,6 +30,7 @@ fn anise(m: &Bound<'_, PyModule>) -> PyResult<()> {
register_time_module(m)?;
astro::register_astro(m)?;
utils::register_utils(m)?;
rotation::register_rotation(m)?;
m.add_class::<Almanac>()?;
m.add_class::<Aberration>()?;
m.add_class::<MetaAlmanac>()?;
Expand Down
25 changes: 25 additions & 0 deletions anise-py/src/rotation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* ANISE Toolkit
* Copyright (C) 2021-onward Christopher Rabotin <[email protected]> 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::math::rotation::DCM;
use pyo3::prelude::*;
use pyo3::py_run;

pub(crate) fn register_rotation(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
let sm = PyModule::new_bound(parent_module.py(), "rotation")?;
sm.add_class::<DCM>()?;

Python::with_gil(|py| {
py_run!(py, sm, "import sys; sys.modules['anise.rotation'] = sm");
});

parent_module.add_submodule(&sm)?;
Ok(())
}
25 changes: 22 additions & 3 deletions anise-py/tests/test_almanac.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from anise import Almanac, MetaAlmanac
from anise.astro import *
from anise.astro.constants import Frames
from anise.rotation import DCM
from anise.time import Epoch

from os import environ
Expand Down Expand Up @@ -62,6 +63,24 @@ def test_state_transformation():
assert abs(orig_state.raan_deg() - 306.614) < 1e-10
assert abs(orig_state.tlong_deg() - 0.6916999999999689) < 1e-10

assert orig_state.cartesian_pos_vel().shape == (6,)

# Ensure we can call all of the DCM functions
for func in ["dcm_from_ric_to_inertial", "dcm_from_rcn_to_inertial", "dcm_from_vnc_to_inertial"]:
dcm = getattr(orig_state, func)()
assert dcm.get_state_dcm().shape == (6, 6)
assert dcm.rot_mat.shape == (3, 3)
assert dcm.rot_mat_dt.shape == (3, 3)
print(f"== {func} ==\n{dcm}")
# Test rebuilding the DCM from its parts
dcm_rebuilt = DCM(dcm.rot_mat, dcm.from_id, dcm.to_id, dcm.rot_mat_dt)
assert dcm_rebuilt == dcm

topo_dcm = orig_state.dcm_from_topocentric_to_body_fixed(123)
assert topo_dcm.get_state_dcm().shape == (6, 6)
assert topo_dcm.rot_mat.shape == (3, 3)
assert topo_dcm.rot_mat_dt is None

# In Python, we can set the aberration to None
aberration = None

Expand Down Expand Up @@ -139,7 +158,7 @@ def test_frame_defs():


if __name__ == "__main__":
test_meta_load()
test_exports()
test_frame_defs()
# test_meta_load()
# test_exports()
# test_frame_defs()
test_state_transformation()
24 changes: 13 additions & 11 deletions anise/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,25 @@ all-features = true
rustdoc-ars = ["--cfg", "docrs", "--generate-link-to-definition"]

[dependencies]
lexical-core = { workspace = true }
hifitime = { workspace = true }
memmap2 = { workspace = true }
crc32fast = { workspace = true }
der = { workspace = true }
log = { workspace = true }
pretty_env_logger = { workspace = true }
const_format = { workspace = true }
nalgebra = { workspace = true }
approx = { workspace = true }
zerocopy = { workspace = true }
bytes = { workspace = true }
snafu = { workspace = true }
heapless = { workspace = true }
rstest = { workspace = true }
const_format = "0.2"
heapless = "0.8.0"
# Optional dependencies follow
pyo3 = { workspace = true, optional = true }
pyo3-log = { workspace = true, optional = true }
numpy = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true }
url = { version = "2.5.0", optional = true }
serde = { workspace = true }
serde_derive = { workspace = true }
serde = "1"
serde_derive = "1"
serde_dhall = { version = "0.12", optional = true }
reqwest = { version = "0.12.0", optional = true, features = ["blocking"] }
platform-dirs = { version = "0.3.0", optional = true }
Expand All @@ -49,6 +48,9 @@ parquet = "53.0.0"
arrow = "53.0.0"
criterion = "0.5"
iai = "0.1"
pretty_env_logger = { workspace = true }
rstest = { workspace = true }
approx = "0.5.1"
polars = { version = "0.45.1", features = ["lazy", "parquet"] }
rayon = "1.7"
serde_yaml = "0.9.30"
Expand All @@ -58,11 +60,11 @@ reqwest = { version = "0.12", features = ["blocking"], optional = true }

[features]
default = ["metaload"]
# Enabling this flag significantly increases compilation times due to Arrow and Polars.
spkezr_validation = []
python = ["pyo3", "pyo3-log"]
python = ["pyo3", "pyo3-log", "numpy", "ndarray"]
metaload = ["url", "reqwest/blocking", "platform-dirs", "regex", "serde_dhall"]
embed_ephem = ["rust-embed", "reqwest/blocking"]
# Enabling this flag significantly increases compilation times due to Arrow and Polars.
spkezr_validation = []

[[bench]]
name = "iai_jpl_ephemerides"
Expand Down
10 changes: 5 additions & 5 deletions anise/src/almanac/metaload/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,26 @@ impl Almanac {
})?;
self.load(&metafile.uri)
}
}

#[cfg_attr(feature = "python", pymethods)]
impl Almanac {
/// Load from the provided MetaFile, downloading it if necessary.
/// Set autodelete to true to automatically delete lock files. Lock files are important in multi-threaded loads.
#[cfg(not(feature = "python"))]
pub fn load_from_metafile(&self, metafile: MetaFile, autodelete: bool) -> AlmanacResult<Self> {
self._load_from_metafile(metafile, autodelete)
}
}

#[cfg(feature = "python")]
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl Almanac {
/// Load from the provided MetaFile, downloading it if necessary.
/// Set autodelete to true to automatically delete lock files. Lock files are important in multi-threaded loads.
///
///
/// :type metafile: Metafile
/// :type autodelete: bool
/// :rtype: Almanac
pub fn load_from_metafile(
fn load_from_metafile(
&mut self,
py: Python,
metafile: MetaFile,
Expand Down
Loading

0 comments on commit 8b8bb8d

Please sign in to comment.