Source code for ADCS.estimators.numba_kernels
"""Compiled array kernels shared by the modern attitude estimators.
The attitude filters deliberately keep their satellite, state, and measurement
objects in Python: those objects provide validation and allow different hardware
models. The covariance recursions, however, operate exclusively on homogeneous
``float64`` arrays and are executed for every EKF/UKF update. Keeping the
kernels here lets all eight modern filters use Numba without coupling their
public APIs to Numba types.
"""
from __future__ import annotations
import numpy as np
from numba import njit
[docs]
@njit(cache=True)
def linear_covariance_prediction(
transition: np.ndarray, covariance: np.ndarray, noise: np.ndarray
) -> np.ndarray:
"""Return ``F @ P @ F.T + Q``, explicitly symmetrized for roundoff."""
result = transition @ covariance @ transition.T + noise
return (result + result.T) * 0.5
[docs]
@njit(cache=True)
def linear_kalman_update(
covariance: np.ndarray,
measurement_jacobian: np.ndarray,
measurement_noise: np.ndarray,
joseph: bool,
) -> tuple[np.ndarray, np.ndarray]:
"""Return the gain and posterior covariance of a linear Kalman update."""
innovation = (
measurement_jacobian @ covariance @ measurement_jacobian.T
+ measurement_noise
)
gain = np.linalg.solve(innovation, measurement_jacobian @ covariance).T
if joseph:
residual = np.eye(covariance.shape[0]) - gain @ measurement_jacobian
posterior = (
residual @ covariance @ residual.T
+ gain @ measurement_noise @ gain.T
)
else:
posterior = (
np.eye(covariance.shape[0]) - gain @ measurement_jacobian
) @ covariance
return gain, (posterior + posterior.T) * 0.5
[docs]
@njit(cache=True)
def weighted_outer_covariance(
deviations: np.ndarray, weights: np.ndarray
) -> np.ndarray:
"""Return ``sum_i weights[i] * deviations[i] * deviations[i].T``."""
dimension = deviations.shape[1]
result = np.zeros((dimension, dimension))
for sample in range(deviations.shape[0]):
weight = weights[sample]
for row in range(dimension):
value = weight * deviations[sample, row]
for column in range(row, dimension):
result[row, column] += value * deviations[sample, column]
for row in range(dimension):
for column in range(row):
result[row, column] = result[column, row]
return result
[docs]
@njit(cache=True)
def weighted_cross_covariance(
first_deviations: np.ndarray,
second_deviations: np.ndarray,
weights: np.ndarray,
) -> np.ndarray:
"""Return ``sum_i weights[i] * first[i] * second[i].T``."""
result = np.zeros((first_deviations.shape[1], second_deviations.shape[1]))
for sample in range(first_deviations.shape[0]):
weight = weights[sample]
for row in range(first_deviations.shape[1]):
value = weight * first_deviations[sample, row]
for column in range(second_deviations.shape[1]):
result[row, column] += value * second_deviations[sample, column]
return result
[docs]
@njit(cache=True)
def weighted_row_mean(values: np.ndarray, weights: np.ndarray) -> np.ndarray:
"""Return the weighted mean of row vectors without temporary broadcasts."""
result = np.zeros(values.shape[1])
for sample in range(values.shape[0]):
for column in range(values.shape[1]):
result[column] += weights[sample] * values[sample, column]
return result