Skip to content

Commit 1d8338e

Browse files
Merge pull request #454 from nyx-space/feat/python-parallel-exec
Enable Rust multithreading speeds for Python: search for the `many` keyword in the functions
2 parents 61831d6 + 345c046 commit 1d8338e

File tree

7 files changed

+245
-39
lines changed

7 files changed

+245
-39
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pyo3 = { version = "0.25", features = ["multiple-pymethods"] }
4444
pyo3-log = "0.12"
4545
numpy = "0.25"
4646
ndarray = ">= 0.15, < 0.17"
47+
rayon = "1.10.0"
4748

4849
anise = { version = "0.6.1", path = "anise", default-features = false }
4950

anise-py/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ANISE (Attitude, Navigation, Instrument, Spacecraft, Ephemeris)
22

3-
ANISE is a rewrite of the core functionalities of the NAIF SPICE toolkit with enhanced performance, and ease of use, while leveraging Rust's safety and speed.
3+
ANISE is a modern rewrite of the core functionalities of the NAIF SPICE toolkit with enhanced features, and ease of use, while leveraging Rust's safety and speed.
44

55
[**Please fill out our user survey**](https://7ug5imdtt8v.typeform.com/to/qYDB14Hj)
66

@@ -10,6 +10,7 @@ In the realm of space exploration, navigation, and astrophysics, precise and eff
1010

1111
+ Loading SPK, BPC, PCK, FK, and TPC files.
1212
+ High-precision translations, rotations, and their combination (rigid body transformations).
13+
+ Querying SPICE files in parallel at incredible speeds (~ 125,000 queries per second), search for the functions with the `many` keyword
1314
+ Comprehensive time system conversions using the hifitime library (including TT, TAI, ET, TDB, UTC, GPS time, and more).
1415

1516
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).

anise-py/anise.pyi

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ The obstructing body _should_ be a tri-axial ellipsoid body, e.g. IAU_MOON_FRAME
9292
6. Compute the elevation, and ensure it is between +/- 180 degrees.
9393
7. Compute the azimuth with a quadrant check, and ensure it is between 0 and 360 degrees."""
9494

95+
def azimuth_elevation_range_sez_many(self, rx_tx_states: List[Orbit], obstructing_body: Frame=None, ab_corr: Aberration=None) -> List[AzElRange]:
96+
"""Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
97+
receiver states (first item in tuple) seen from the transmitter state (second item in states tuple), once converted into the SEZ frame of the transmitter.
98+
99+
Note: if any computation fails, the error will be printed to the stderr.
100+
Note: the output AER will be chronologically sorted, regardless of transmitter.
101+
102+
Refer to [azimuth_elevation_range_sez] for details."""
103+
95104
def beta_angle_deg(self, state: Orbit, ab_corr: Aberration=None) -> float:
96105
"""Computes the Beta angle (β) for a given orbital state, in degrees. A Beta angle of 0° indicates that the orbit plane is edge-on to the Sun, leading to maximum eclipse time. Conversely, a Beta angle of +90° or -90° means the orbit plane is face-on to the Sun, resulting in continuous sunlight exposure and no eclipses.
97106
@@ -190,6 +199,14 @@ This function performs a recursion of no more than twice the MAX_TREE_DEPTH."""
190199
This function calls `occultation` where the back object is the Sun in the J2000 frame, and the front object
191200
is the provided eclipsing frame."""
192201

202+
def solar_eclipsing_many(self, eclipsing_frame: Frame, observers: List[Orbit], ab_corr: Aberration=None) -> List[Occultation]:
203+
"""Computes the solar eclipsing of all the observers due to the eclipsing_frame, computed in parallel under the hood.
204+
205+
Note: if any computation fails, the error will be printed to the stderr.
206+
Note: the output AER will be chronologically sorted, regardless of transmitter.
207+
208+
Refer to [solar_eclipsing] for details."""
209+
193210
def spk_domain(self, id: int) -> typing.Tuple:
194211
"""Returns the applicable domain of the request id, i.e. start and end epoch that the provided id has loaded data."""
195212

@@ -269,10 +286,21 @@ will return exactly the same data as the spkerz SPICE call.
269286
# Note
270287
The units will be those of the underlying ephemeris data (typically km and km/s)"""
271288

272-
def transform_to(self, state: Orbit, observer_frame: Frame, ab_corr: Aberration=None) -> Orbit:
273-
"""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
289+
def transform_many(self, target_frame: Orbit, observer_frame: Frame, time_series: TimeSeries, ab_corr: Aberration=None) -> List[Orbit]:
290+
"""Returns a chronologically sorted list of the Cartesian states that transform the `from_frame` to the `to_frame` for each epoch of the time series, computed in parallel under the hood.
291+
Note: if any transformation fails, the error will be printed to the stderr.
292+
293+
Refer to [transform] for details."""
274294

275-
**WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_state_to` function instead to include rotations."""
295+
def transform_many_to(self, states: List[Orbit], observer_frame: Frame, ab_corr: Aberration=None) -> List[Orbit]:
296+
"""Returns a chronologically sorted list of the provided states as seen from the observer frame, given the aberration.
297+
Note: if any transformation fails, the error will be printed to the stderr.
298+
Note: the input ordering is lost: the output states will not be in the same order as the input states if these are not chronologically sorted!
299+
300+
Refer to [transform_to] for details."""
301+
302+
def transform_to(self, state: Orbit, observer_frame: Frame, ab_corr: Aberration=None) -> Orbit:
303+
"""Returns the provided state as seen from the observer frame, given the aberration."""
276304

277305
def translate(self, target_frame: Orbit, observer_frame: Frame, epoch: Epoch, ab_corr: Aberration=None) -> Orbit:
278306
"""Returns the Cartesian state of the target frame as seen from the observer frame at the provided epoch, and optionally given the aberration correction.
@@ -2409,26 +2437,26 @@ class rotation:
24092437
def transpose(self) -> DCM:
24102438
"""Returns the transpose of this DCM"""
24112439

2412-
def __eq__(self, value: typing.Any) -> bool:
2413-
"""Return self==value."""
2440+
def __eq__(self, value: typing.Any) -> bool:
2441+
"""Return self==value."""
24142442

2415-
def __ge__(self, value: typing.Any) -> bool:
2416-
"""Return self>=value."""
2443+
def __ge__(self, value: typing.Any) -> bool:
2444+
"""Return self>=value."""
24172445

2418-
def __gt__(self, value: typing.Any) -> bool:
2419-
"""Return self>value."""
2446+
def __gt__(self, value: typing.Any) -> bool:
2447+
"""Return self>value."""
24202448

2421-
def __le__(self, value: typing.Any) -> bool:
2422-
"""Return self<=value."""
2449+
def __le__(self, value: typing.Any) -> bool:
2450+
"""Return self<=value."""
24232451

2424-
def __lt__(self, value: typing.Any) -> bool:
2425-
"""Return self<value."""
2452+
def __lt__(self, value: typing.Any) -> bool:
2453+
"""Return self<value."""
24262454

2427-
def __ne__(self, value: typing.Any) -> bool:
2428-
"""Return self!=value."""
2455+
def __ne__(self, value: typing.Any) -> bool:
2456+
"""Return self!=value."""
24292457

2430-
def __repr__(self) -> str:
2431-
"""Return repr(self)."""
2458+
def __repr__(self) -> str:
2459+
"""Return repr(self)."""
24322460

2433-
def __str__(self) -> str:
2434-
"""Return str(self)."""
2461+
def __str__(self) -> str:
2462+
"""Return str(self)."""

anise-py/tests/test_almanac.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from anise.astro import *
77
from anise.astro.constants import Frames
88
from anise.rotation import DCM
9-
from anise.time import Epoch
9+
from anise.time import Duration, Epoch, TimeSeries, Unit
1010
from anise.utils import convert_tpc
1111

1212
from os import environ
@@ -27,7 +27,7 @@ def test_state_transformation():
2727
print(meta)
2828
# Process the files to be loaded
2929
try:
30-
ctx = meta.process()
30+
almanac = meta.process()
3131
except Exception as e:
3232
if "lfs" in str(e):
3333
# Must be some LFS error in the CI again
@@ -36,14 +36,14 @@ def test_state_transformation():
3636
else:
3737
data_path = Path(__file__).parent.joinpath("..", "..", "data")
3838
# Must ensure that the path is a string
39-
ctx = Almanac(str(data_path.joinpath("de440s.bsp")))
39+
almanac = Almanac(str(data_path.joinpath("de440s.bsp")))
4040
# Let's add another file here -- note that the Almanac will load into a NEW variable, so we must overwrite it!
4141
# This prevents memory leaks (yes, I promise)
42-
ctx = ctx.load(str(data_path.joinpath("pck08.pca"))).load(
42+
almanac = almanac.load(str(data_path.joinpath("pck08.pca"))).load(
4343
str(data_path.joinpath("earth_latest_high_prec.bpc"))
4444
)
4545

46-
eme2k = ctx.frame_info(Frames.EME2000)
46+
eme2k = almanac.frame_info(Frames.EME2000)
4747
assert eme2k.mu_km3_s2() == 398600.435436096
4848
assert eme2k.shape.polar_radius_km == 6356.75
4949
assert abs(eme2k.shape.flattening() - 0.0033536422844278) < 2e-16
@@ -94,7 +94,7 @@ def test_state_transformation():
9494
# In Python, we can set the aberration to None
9595
aberration = None
9696

97-
state_itrf93 = ctx.transform_to(orig_state, Frames.EARTH_ITRF93, aberration)
97+
state_itrf93 = almanac.transform_to(orig_state, Frames.EARTH_ITRF93, aberration)
9898

9999
print(orig_state)
100100
print(state_itrf93)
@@ -104,7 +104,7 @@ def test_state_transformation():
104104
assert abs(state_itrf93.height_km() - 1814.503598063825) < 1e-10
105105

106106
# Convert back
107-
from_state_itrf93_to_eme2k = ctx.transform_to(state_itrf93, Frames.EARTH_J2000)
107+
from_state_itrf93_to_eme2k = almanac.transform_to(state_itrf93, Frames.EARTH_J2000)
108108

109109
print(from_state_itrf93_to_eme2k)
110110

@@ -113,7 +113,7 @@ def test_state_transformation():
113113
# Demo creation of a ground station
114114
mean_earth_angular_velocity_deg_s = 0.004178079012116429
115115
# Grab the loaded frame info
116-
itrf93 = ctx.frame_info(Frames.EARTH_ITRF93)
116+
itrf93 = almanac.frame_info(Frames.EARTH_ITRF93)
117117
paris = Orbit.from_latlongalt(
118118
48.8566,
119119
2.3522,
@@ -147,7 +147,30 @@ def test_state_transformation():
147147
"line_of_sight_obstructed",
148148
"azimuth_elevation_range_sez",
149149
]:
150-
assert hasattr(ctx, fname)
150+
assert hasattr(almanac, fname)
151+
152+
# Test the parallel function calls
153+
start = Epoch("2021-10-29 12:34:56 TDB")
154+
stop = Epoch("2022-10-29 12:34:56 TDB")
155+
time_series = TimeSeries(
156+
start,
157+
stop,
158+
Duration("1 min"),
159+
False,
160+
)
161+
162+
tick = Epoch.system_now()
163+
164+
states = almanac.transform_many(
165+
Frames.EARTH_J2000,
166+
Frames.SUN_J2000,
167+
time_series,
168+
None,
169+
)
170+
171+
clock_time = Epoch.system_now().timedelta(tick)
172+
print(f"Queried {len(states)} states in {clock_time}")
173+
assert len(states) == int(stop.timedelta(start).to_unit(Unit.Minute))
151174

152175

153176
def test_convert_tpc():
@@ -213,5 +236,5 @@ def test_frame_defs():
213236
# test_meta_load()
214237
# test_exports()
215238
# test_frame_defs()
216-
test_convert_tpc()
239+
# test_convert_tpc()
217240
test_state_transformation()

anise/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ rust-embed = { version = "8.4.0", features = [
4343
"include-exclude",
4444
], optional = true }
4545
regex = { version = "1.10.5", optional = true }
46+
rayon = { workspace = true, optional = true } # Only used when building with Python
4647

4748
[dev-dependencies]
4849
rust-spice = "0.7.6"
@@ -66,7 +67,7 @@ ureq = { version = "3.0.10", default-features = false, optional = true, features
6667

6768
[features]
6869
default = ["metaload"]
69-
python = ["pyo3", "pyo3-log", "numpy", "ndarray"]
70+
python = ["pyo3", "pyo3-log", "numpy", "ndarray", "rayon"]
7071
metaload = ["url", "ureq", "platform-dirs", "regex", "serde_dhall"]
7172
embed_ephem = ["rust-embed", "ureq"]
7273
# Enabling this flag significantly increases compilation times due to Arrow and Polars.

0 commit comments

Comments
 (0)