Source code for ADCS.estimators.process_model

"""Pure estimator-facing state propagation.

This module is the narrow boundary around the satellite's historical
integration API. Filters call :func:`propagate_state`; they do not need to know
how ``noiseless_rk4`` represents physical versus augmented estimator state.
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import Any

import numpy as np

from ADCS.state import EstimatorState, State


__all__ = ["propagate_state"]


[docs] def propagate_state( state: State, satellite: Any, control: Any, dt: float, orbital_state_start: Any, orbital_state_end: Any, *, midpoint_orbital_state: Any | None = None, quaternion_integrator: str = "rk4", ) -> State: r"""Return deterministic propagation without mutating ``state``. The physical state is propagated by ``satellite.noiseless_rk4``. For an :class:`EstimatorState`, wheel momentum is propagated while estimated bias and disturbance blocks, covariance, and process noise are copied unchanged. Those nominal parameter blocks are constant under this deterministic model; their uncertainty evolves separately through the process-noise model. ``quaternion_integrator`` is ``"rk4"`` for normalized component RK4 or ``"cg5"`` for the satellite's Lie-group variant. """ if not isinstance(state, State): raise TypeError(f"state must be a State, got {type(state).__name__}") control = np.array(control, dtype=float, copy=True) if control.ndim != 1: raise ValueError(f"control must be one-dimensional, got shape {control.shape}") dt = float(dt) if not np.isfinite(dt) or dt < 0.0: raise ValueError("dt must be finite and non-negative") if quaternion_integrator not in ("rk4", "cg5"): raise ValueError("quaternion_integrator must be 'rk4' or 'cg5'") expected_size = getattr(satellite, "state_len", state.full_size) physical_size = state.slice("physical", coordinates="full").stop if physical_size != expected_size: raise ValueError( f"state physical block has size {physical_size}, " f"but satellite expects {expected_size}" ) integrator_midpoint = midpoint_orbital_state if quaternion_integrator == "cg5": if isinstance(midpoint_orbital_state, Sequence): if len(midpoint_orbital_state) != 5: raise ValueError("cg5 midpoint_orbital_state must contain five stage states") elif midpoint_orbital_state is not None: integrator_midpoint = None propagated = satellite.noiseless_rk4( state, control, dt, orbital_state_start, orbital_state_end, verbose=False, mid_orbital_state=integrator_midpoint, quat_as_vec=quaternion_integrator == "rk4", give_err_est=False, ) if not isinstance(propagated, State): raise TypeError( "satellite.noiseless_rk4() must return a State when error estimation is disabled" ) if not isinstance(state, EstimatorState): return propagated result = state.copy() result.w = propagated.w result.q = propagated.q result.h = propagated.h return result