Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions docs/source/Learn/makingModules/numbaModules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,9 @@ Allowed
``while``, ``break``, ``continue``, ``return``.
- **Math functions**: ``math.sqrt``, ``math.sin``, etc. (import ``math``
at module level, use inside the function normally).
- **Calling other** ``@nb.njit`` **functions** - factor complex logic into
helper functions decorated with ``@numba.njit``.
- **Calling compiled functions or registered Numba overloads** - use helpers
decorated with ``@numba.njit`` or compatible library functions such as
those in ``RigidBodyKinematicsNumba``.
- **``print(label, value, ...)``** - variadic print to stdout works and is
the simplest way to emit debug output.
- **``bskLogger``** methods as described in :ref:`numbaModules_logging`.
Expand All @@ -358,8 +359,10 @@ Allowed
Not Allowed
~~~~~~~~~~~

- **Python objects** of any kind: no lists, dicts, sets, tuples, classes,
generators, or iterators. Use numpy arrays for all collections.
- **Arbitrary Python objects** that require the Python interpreter. Prefer
NumPy arrays for numerical collections. Numeric tuples and native Numba
containers are supported subject to Numba's type restrictions; convert
ordinary Python lists before passing them into a compiled caller.
- **String variables** - string *literals* in ``print`` and ``bskLogger`` calls
work, but you cannot assign a string to a variable or pass one as a
function argument (other than via the ``bskLog*`` API).
Expand All @@ -368,8 +371,9 @@ Not Allowed
- **Exceptions** - ``raise``, ``try``/``except``/``finally`` are not
supported. Use conditional logic and ``bskLogger`` to handle error
conditions.
- **Calling Python functions** that are not themselves ``@nb.njit``-compiled.
Standard library functions (``os``, ``sys``, etc.) are unavailable.
- **Calling Python functions** without a compiled implementation or a
registered Numba overload. Standard library functions from ``os`` and
``sys``, for example, are unavailable.
- **Dynamic memory allocation** with arbitrary Python types. NumPy array
creation with a *constant* size (``np.zeros(3)``) is fine; allocation
whose size depends on a runtime variable may require care.
Expand Down Expand Up @@ -402,7 +406,7 @@ continues from whatever value was last written, by any means.
----------------------------

:py:mod:`Basilisk.utilities.RigidBodyKinematicsNumba` provides
``@nb.njit``-compiled versions of every function in
Python wrappers and Numba overloads for every function in
:py:mod:`Basilisk.utilities.RigidBodyKinematics`, making the full rigid-body
kinematics library callable from within compiled ``UpdateStateImpl`` kernels.

Expand All @@ -418,9 +422,51 @@ Import individual functions at module level (outside ``UpdateStateImpl``)::
attGuidOutMsgPayload.sigma_BR[:3] = sigma_BR

All kinematics functions (``MRP2C``, ``C2MRP``, ``addMRP``, ``subMRP``,
``EP2C``, ``C2EP``, ``PRV2C``, and more) are available. Because they are
decorated with ``@nb.njit(inline='always')``, calls inside ``UpdateStateImpl``
compile inline with no per-call overhead.
``EP2C``, ``C2EP``, ``PRV2C``, and more) are available. Calls inside
``UpdateStateImpl`` select the compiled implementations through Numba overloads.
The overloads and kernels use ``inline='always'`` and bypass Python dispatch.
The plain Python ``RigidBodyKinematics`` module remains unchanged.
The common :ref:`rbkNumericalLimits` also apply to these compiled functions.

Python and compiled inputs
~~~~~~~~~~~~~~~~~~~~~~~~~~

When called from Python, these functions accept numeric lists, tuples, and
NumPy arrays, including nested sequences for matrices. Wrappers convert lists
and tuples to ``float64`` arrays before entering Numba. Existing arrays are
passed through without copying, and scalar arguments such as an angle or axis
index retain their types. Positional and keyword calls use the same signatures
as ``RigidBodyKinematics``.

For compiled callers, use NumPy arrays, preferably ``float64``, or numeric
tuples. The overloads also accept native lists created inside compiled code and
``numba.typed.List`` arguments. Ordinary Python lists must be converted before
entering the **outermost** compiled function, including a user-defined
``@numba.njit`` caller. A kinematics function inside that caller cannot convert
the caller's original Python argument before Numba processes it.

For example::

import numpy as np
from numba import njit
from Basilisk.utilities.RigidBodyKinematicsNumba import MRP2C

sigma = [0.1, -0.2, 0.3] # [-] MRPs
matrix = MRP2C(sigma) # The Python wrapper converts the list.

@njit
def rotation(sigma):
return MRP2C(sigma) # Selects the compiled implementation.

matrix = rotation(np.asarray(sigma, dtype=np.float64))

This arrangement avoids Numba's deprecated Python list reflection without
suppressing its warnings. It reuses the same mathematical implementations for
Python and compiled calls. Public functions are now Python callables with
Numba overloads rather than dispatcher objects; dispatcher-specific attributes
such as ``.signatures`` and ``.py_func`` are not part of this interface. Use
``RigidBodyKinematics`` for the plain Python implementations. Compiled callers
may still use ``@numba.njit(cache=True)``.

.. note::

Expand Down
9 changes: 9 additions & 0 deletions docs/source/Support/bskKnownIssues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ Version |release|
before the GPS epoch produced invalid GPS outputs. The corrected UTC Julian date preserves the SPICE
convention of holding at the following midnight during an inserted leap second, avoiding backward jumps.

- GitHub issues 1610 and 1611: Rigid-body kinematics conversions and composition
now preserve identity, small rotations, half-turns, and Euler singularities;
PRV rate maps use analytic limits at zero and angle wrapping handles multiple
revolutions. Python and Numba retain sequence inputs and non-contiguous PRV
arrays. Numba uses wrappers and overloads to avoid deprecated list reflection;
see :ref:`numbaModules_rbkn` for compiled-call requirements and the public
callable interface. Representation singularities and extreme floating-point
magnitudes remain outside the numerical guarantees; see :ref:`rbkNumericalLimits`.

- GitHub issue 1609: :ref:`keplerianOrbit` returned incorrect eccentric and mean anomalies,
lost the sign of the flight-path angle, and returned zero rates after construction or stale
rates after ``set_a()``. These outputs are now consistent with the current elliptic orbit.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fixed rigid-body kinematics issues #1610 and #1611 across C, Python, Numba, and the corresponding Rust C bindings: conversions and composition preserve identity, small rotations, half-turns, and Euler singularities; PRV rate maps handle zero attitude and angle wrapping handles multiple revolutions. See :ref:`rbkNumericalLimits` for representation limits.
- Preserved Python and Numba list/tuple support and non-contiguous PRV array inputs. ``RigidBodyKinematicsNumba`` now exposes Python wrappers with Numba overloads instead of public dispatcher objects, avoiding deprecated list reflection; see :ref:`numbaModules_rbkn` for compiled-call requirements. Added Python/Numba ``MRPshadow()`` and extended the Rust attitude bindings.
81 changes: 81 additions & 0 deletions src/architecture/rust/bsk_utilities/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,85 @@ mod ffi_tests {
let wrapped = attitude::wrap_to_pi(4.0); // [rad]
assert_close(wrapped, 4.0 - 2.0 * PI, 1.0e-15); // [rad]
}

/// Smoke-test ordinary attitude boundaries through the expanded C bindings.
#[test]
fn attitude_boundaries_use_linked_c_functions() {
let mut sigma = [2.0, 0.0, 0.0]; // [-]
let mut quaternion = [0.0; 4];
let mut shadow = [0.0; 3];
let mut prv = [0.0; 3]; // [rad]
unsafe {
crate::raw::MRP2EP(sigma.as_mut_ptr(), quaternion.as_mut_ptr());
crate::raw::MRPswitch(sigma.as_mut_ptr(), 1.0, shadow.as_mut_ptr()); // [-] threshold
crate::raw::MRP2PRV(sigma.as_mut_ptr(), prv.as_mut_ptr());
}
assert_close(quaternion[0], -0.6, 1e-15); // [-]
assert_close(quaternion[1], 0.8, 1e-15); // [-]
assert_eq!(shadow, [-0.5, 0.0, 0.0]); // [-]
assert_close(prv[0], 4.0 * 2.0_f64.atan(), 1e-15); // [rad]
let threshold = f64::from(1.1_f32); // [-] Match Python's promoted float32 threshold.
sigma = [threshold, 0.0, 0.0]; // [-]
unsafe {
crate::raw::MRPswitch(sigma.as_mut_ptr(), threshold, shadow.as_mut_ptr());
}
assert_eq!(shadow, sigma);
for convert in [crate::raw::MRP2EP, crate::raw::Gibbs2EP] {
let mut buffer = [0.1, 0.2, 0.3, 0.0]; // [-] Space for the output quaternion.
let mut expected = [0.0; 4];
unsafe {
convert(buffer.as_mut_ptr(), expected.as_mut_ptr());
let pointer = buffer.as_mut_ptr();
convert(pointer, pointer);
}
assert_eq!(buffer, expected);
}
assert_eq!(
attitude::add_mrp([1.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
[0.0; 3]
);

let mut zero = [0.0; 3]; // [rad]
let mut omega = [0.01, -0.02, 0.03]; // [rad/s]
let mut derivative = [0.0; 3]; // [rad/s]
unsafe {
crate::raw::dPRV(
zero.as_mut_ptr(),
omega.as_mut_ptr(),
derivative.as_mut_ptr(),
);
}
assert_eq!(derivative, omega);
}

/// Verify the Rust wrapper retains the C angle-wrapping convention.
#[test]
fn angle_wrapping_handles_multiple_revolutions() {
let offsets = [-2.9, -0.2, 0.0, 0.2, 2.9]; // [rad]
for turns in [
-1_000_000, -100, -10, -3, -2, -1, 0, 1, 2, 3, 10, 100, 1_000_000,
] {
for offset in offsets {
let angle = f64::from(turns) * (2.0 * PI) + offset; // [rad]
let wrapped = attitude::wrap_to_pi(angle);
let tolerance = 4.0 * f64::EPSILON * angle.abs().max(1.0); // [rad]
assert!((-PI..=PI).contains(&wrapped));
assert_close(wrapped, offset, tolerance);
assert_eq!(attitude::wrap_to_pi(wrapped), wrapped);
}
}
for sign in [-1.0, 1.0] {
for multiple in [1.0, 3.0, 5.0] {
assert_eq!(attitude::wrap_to_pi(sign * multiple * PI), sign * PI);
}
for multiple in [0.0, 2.0, 4.0, 8.0] {
let wrapped = attitude::wrap_to_pi(sign * multiple * PI);
assert_eq!(wrapped, 0.0);
assert_eq!(wrapped.is_sign_negative(), sign < 0.0);
}
}
for angle in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(attitude::wrap_to_pi(angle).is_nan());
}
}
}
14 changes: 14 additions & 0 deletions src/architecture/rust/bsk_utilities/src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,21 +211,35 @@ unsafe extern "C" {
advec: *mut f64,
);
pub fn solarRad(A: f64, m: f64, sunvec: *mut f64, arvec: *mut f64);
pub fn addGibbs(q1: *mut f64, q2: *mut f64, result: *mut f64);
pub fn addMRP(q1: *mut f64, q2: *mut f64, result: *mut f64);
pub fn BinvGibbs(q: *mut f64, B: *mut [f64; 3usize]);
pub fn BinvMRP(q: *mut f64, B: *mut [f64; 3usize]);
pub fn BinvPRV(q: *mut f64, B: *mut [f64; 3usize]);
pub fn BmatMRP(q: *mut f64, B: *mut [f64; 3usize]);
pub fn BdotmatMRP(q: *mut f64, dq: *mut f64, B: *mut [f64; 3usize]);
pub fn BmatPRV(q: *mut f64, B: *mut [f64; 3usize]);
pub fn C2MRP(C: *mut [f64; 3usize], q: *mut f64);
pub fn dGibbs(q: *mut f64, w: *mut f64, dq: *mut f64);
pub fn dMRP(q: *mut f64, w: *mut f64, dq: *mut f64);
pub fn dMRP2Omega(q: *mut f64, dq: *mut f64, w: *mut f64);
pub fn ddMRP(q: *mut f64, dq: *mut f64, w: *mut f64, dw: *mut f64, ddq: *mut f64);
pub fn ddMRP2dOmega(q: *mut f64, dq: *mut f64, ddq: *mut f64, dw: *mut f64);
pub fn dPRV(q: *mut f64, w: *mut f64, dq: *mut f64);
pub fn EP2MRP(q1: *mut f64, q: *mut f64);
pub fn EP2PRV(q1: *mut f64, q: *mut f64);
pub fn Gibbs2C(q: *mut f64, C: *mut [f64; 3usize]);
pub fn Gibbs2EP(q1: *mut f64, q: *mut f64);
pub fn Gibbs2MRP(q1: *mut f64, q: *mut f64);
pub fn Gibbs2PRV(q1: *mut f64, q: *mut f64);
pub fn MRP2C(q: *mut f64, C: *mut [f64; 3usize]);
pub fn MRP2EP(q1: *mut f64, q: *mut f64);
pub fn MRP2Gibbs(q1: *mut f64, q: *mut f64);
pub fn MRP2PRV(q1: *mut f64, q: *mut f64);
pub fn MRPswitch(q: *mut f64, s2: f64, s: *mut f64);
pub fn MRPshadow(qIn: *mut f64, qOut: *mut f64);
pub fn wrapToPi(x: f64) -> f64;
pub fn subGibbs(q1: *mut f64, q2: *mut f64, q: *mut f64);
pub fn subMRP(q1: *mut f64, q2: *mut f64, q: *mut f64);
pub fn Mi(angle: f64, axis: ::core::ffi::c_int, C: *mut [f64; 3usize]);
pub fn tilde(v: *mut f64, mat: *mut [f64; 3usize]);
Expand Down
5 changes: 3 additions & 2 deletions src/architecture/rust/gen_rust_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ def run_bindgen(wrapper_path: Path, include_root: Path, basilisk_dir: Path) -> s
"elem2rv|rv2elem|clMeanOscMap|clElem2eqElem|"
"hillFrame|hill2rv|rv2hill|"
"atmosphericDensity|debyeLength|atmosphericDrag|solarRad|"
"addMRP|subMRP|MRP2C|C2MRP|MRP2EP|EP2MRP|"
"addMRP|subMRP|MRP2C|C2MRP|MRP2EP|EP2MRP|MRP2PRV|EP2PRV|MRP2Gibbs|"
"Gibbs2C|Gibbs2EP|Gibbs2MRP|Gibbs2PRV|addGibbs|subGibbs|dGibbs|"
"MRPshadow|MRPswitch|dMRP|dMRP2Omega|ddMRP|ddMRP2dOmega|"
"BmatMRP|BdotmatMRP|BinvMRP|wrapToPi|tilde|Mi"
"BmatMRP|BdotmatMRP|BinvMRP|BinvGibbs|BmatPRV|BinvPRV|dPRV|wrapToPi|tilde|Mi"
)
allow_constants = (
"G_UNIVERSIAL|AU|AU2M|SPEED_LIGHT|SOLAR_FLUX_EARTH|"
Expand Down
Loading
Loading