Source code for ADCS.satellite_hardware.errors.bias

__all__ = ["Bias"]

import numpy as np
from typing import Sequence
from ADCS.orbits.universal_constants import TimeConstants

[docs] class Bias: r""" Represents actuator bias modeled as a bounded random walk. Parameters ---------- bias : float, optional Initial bias value :math:`b_0` (default 0). std_bias : float, optional Standard deviation rate :math:`\sigma_b` controlling random-walk diffusion (default 0). bounds : (float, float), optional Clipping range :math:`[b_{\min}, b_{\max}]` applied after updates. """ def __init__(self, bias: np.ndarray | float = np.array([0.0]), std_bias: np.ndarray | float = np.array([0.0]), bounds: Sequence[np.ndarray | float] = (-np.array([np.inf]), np.array([np.inf]))) -> None: if isinstance(bias, (float, int)): bias = np.array([bias]) else: bias = np.asarray(bias, dtype=float) if isinstance(std_bias, (float, int)): std_bias = np.array([std_bias]) else: std_bias = np.asarray(std_bias, dtype=float) lo, hi = bounds lo = np.asarray(lo, dtype=float) hi = np.asarray(hi, dtype=float) try: bias, std_bias, lo, hi = np.broadcast_arrays(bias, std_bias, lo, hi) except ValueError: raise ValueError( "bias, std_bias, and bounds must be broadcastable to the same shape." ) if np.any(lo > hi): raise ValueError("Each lower bound must be <= the corresponding upper bound.") self.bias = bias self.std_bias = std_bias self.last_bias_time = float('nan') self.bounds = (lo, hi) def __bool__(self): r""" Return ``True`` if the bias model is active. Notes ----- The model is considered *inactive* (i.e. returns ``False``) when both :math:`b_0 = 0` and :math:`\sigma_b = 0`. """ return not (np.all(self.bias == 0.0) and np.all(self.std_bias == 0.0))
[docs] def copy(self): return Bias(bias=self.bias, std_bias=self.std_bias, bounds=self.bounds)
def _update_bias(self, j2000: float) -> None: r""" Update the bias value using a Gaussian random walk. The bias evolves as .. math:: b_{k+1} \sim \mathcal{N}\!\left(b_k,\ \sigma_b^2\,\Delta t\right), where :math:`\Delta t` is the elapsed time in seconds since the last update, converted from Julian centuries: .. math:: \Delta t = (t_{J2000}^{(k+1)} - t_{J2000}^{(k)}) \times T_\mathrm{century}. The updated value is then clipped to :math:`[b_{\min}, b_{\max}]`. Parameters ---------- j2000 : float Current time in Julian centuries since J2000. """ """Random Walk""" if not np.isfinite(self.last_bias_time): self.last_bias_time = j2000 return dt_centuries = j2000 - self.last_bias_time if dt_centuries <= 0: return dt_sec = dt_centuries * TimeConstants.cent2sec self.bias = np.random.normal(loc=self.bias, scale=self.std_bias*np.sqrt(dt_sec)) self.bias = np.clip(self.bias, self.bounds[0], self.bounds[1]) self.last_bias_time = j2000
[docs] def get_bias(self, j2000: float) -> float: r""" Return the current bias after applying a random-walk update. Parameters ---------- j2000 : float Current time in Julian centuries since J2000. Returns ------- float The updated bias value :math:`b_k`. """ if self.bias.size == 1: return self.bias.item() else: return self.bias