Source code for ADCS.satellite_hardware.errors.noise

__all__ = ["Noise"]

import numpy as np
from typing import Sequence

[docs] class Noise: r""" Represents additive actuator noise with optional Gaussian randomness and bounds. Parameters ---------- noise : float, optional Mean or nominal noise offset :math:`n_0` (default 0). std_noise : float, optional Standard deviation :math:`\sigma_n` of the Gaussian perturbation (default 0). bounds : (float, float), optional Lower and upper limits :math:`[n_{\min}, n_{\max}]` applied after sampling. """ def __init__(self, noise: np.ndarray | float = np.array([0.0]), std_noise: np.ndarray | float = np.array([0.0]), bounds: Sequence[np.ndarray | float] = (-np.array([np.inf]), np.array([np.inf]))) -> None: if isinstance(noise, (float, int)): noise = np.array([noise]) else: noise = np.asarray(noise, dtype=float) if isinstance(std_noise, (float, int)): std_noise = np.array([std_noise]) else: std_noise = np.asarray(std_noise, dtype=float) lo, hi = bounds lo = np.asarray(lo, dtype=float) hi = np.asarray(hi, dtype=float) try: noise, std_noise, lo, hi = np.broadcast_arrays(noise, std_noise, lo, hi) except ValueError: raise ValueError( "noise, std_noise, 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.noise = noise self.std_noise = std_noise self.bounds = (lo, hi) def __bool__(self): r""" Return ``True`` if the noise model is active. Notes ----- The noise model is considered *inactive* (i.e., ``False``) when both :math:`n_0 = 0` and :math:`\sigma_n = 0`. """ return not (np.all(self.noise == 0.0) and np.all(self.std_noise == 0.0))
[docs] def copy(self): return Noise(noise=self.noise, std_noise=self.std_noise, bounds=self.bounds)
def _update_noise(self) -> None: r""" Draw a new Gaussian noise sample and apply bounds. The internal noise value is updated as .. math:: n \sim \mathcal{N}(n_0,\ \sigma_n^2), \qquad n \leftarrow \mathrm{clip}(n,\ n_{\min},\ n_{\max}). This method is called internally each time :math:`get_noise` is invoked. """ """Update actuator noise with a fresh Gaussian sample.""" self.noise = np.random.normal( loc=0.0, scale=self.std_noise ) self.noise = np.clip(self.noise, self.bounds[0], self.bounds[1])
[docs] def get_noise(self) -> float: r""" Return a fresh bounded Gaussian noise sample. Returns ------- float The updated noise value :math:`n` after random sampling and clipping. """ if self.noise.size == 1: return self.noise.item() else: return self.noise
[docs] def cov(self) -> np.ndarray: std2 = self.std_noise * self.std_noise if std2.size == 1: # scalar covariance return np.array([[std2.item()]]) else: # diagonal covariance matrix return np.diagflat(std2)
[docs] def srcov(self) -> float | np.ndarray: if self.std_noise.size == 1: # scalar square-root covariance return np.array([[self.std_noise.item()]]) else: # diagonal matrix of std deviations return np.diagflat(self.std_noise)