Source code for empyrean.math
"""Covariance and mixture math primitives.
Public Python wrappers for the 6-dimensional state-covariance utilities
exposed by the engine: dominant-eigenvector extraction (used wherever
you need the principal axis of an uncertainty ellipsoid) and Gaussian
mixture splitting along that axis (the building block of the AGM
uncertainty propagation method).
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
[docs]
@dataclass(frozen=True)
class MixtureComponent:
"""One weighted component of a 6D Gaussian mixture.
Returned by :func:`split_gaussian`.
Attributes
----------
weight : float
Component weight (sum across components is 1.0).
mean : numpy.ndarray
6-element mean vector.
covariance : numpy.ndarray
6 × 6 covariance matrix.
"""
weight: float
mean: np.ndarray
covariance: np.ndarray
[docs]
def eigenvector_max_6x6(matrix: np.ndarray) -> tuple[float, np.ndarray]:
"""Dominant eigenvalue / eigenvector of a 6 × 6 symmetric matrix.
Parameters
----------
matrix : numpy.ndarray
6 × 6 symmetric (covariance-like) matrix.
Returns
-------
eigenvalue : float
Largest eigenvalue.
eigenvector : numpy.ndarray
Corresponding 6-element unit eigenvector.
"""
from empyrean._empyrean_rs import _eigenvector_max_6x6
arr = np.ascontiguousarray(matrix, dtype=np.float64)
if arr.shape != (6, 6):
raise ValueError(f"matrix must be 6x6, got shape {arr.shape}")
eigenvalue, eigenvector = _eigenvector_max_6x6(arr)
return float(eigenvalue), np.asarray(eigenvector, dtype=np.float64)
[docs]
def split_gaussian(
mean: np.ndarray,
covariance: np.ndarray,
k: int,
) -> list[MixtureComponent]:
"""Split a 6D Gaussian into ``k`` weighted components along the
dominant eigenvector of the covariance.
The split direction is the principal axis of the input covariance
(matches the engine's adaptive Gaussian-mixture splitter).
Parameters
----------
mean : numpy.ndarray
6-element mean vector of the input distribution.
covariance : numpy.ndarray
6 × 6 covariance of the input distribution.
k : int
Number of mixture components. Must be ≥ 1.
Returns
-------
list[MixtureComponent]
``k`` weighted components whose marginal sums to the input
Gaussian.
"""
from empyrean._empyrean_rs import _split_gaussian
if k < 1:
raise ValueError(f"k must be >= 1, got {k}")
mean_arr = np.ascontiguousarray(mean, dtype=np.float64)
cov_arr = np.ascontiguousarray(covariance, dtype=np.float64)
if mean_arr.shape != (6,):
raise ValueError(f"mean must have shape (6,), got {mean_arr.shape}")
if cov_arr.shape != (6, 6):
raise ValueError(f"covariance must be 6x6, got shape {cov_arr.shape}")
result = _split_gaussian(mean_arr, cov_arr, k)
weights = np.asarray(result["weights"], dtype=np.float64)
means = np.asarray(result["means"], dtype=np.float64)
covs = np.asarray(result["covariances"], dtype=np.float64)
return [
MixtureComponent(
weight=float(weights[i]),
mean=means[i].copy(),
covariance=covs[i].copy(),
)
for i in range(k)
]