use crate::io::{ConfigError, ConfigRepr};
#[cfg(feature = "python")]
use crate::python::pyo3utils::pyany_to_value;
use hifitime::{Duration, Epoch, TimeUnits};
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::{PyDict, PyList, PyType};
#[cfg(feature = "python")]
use pythonize::{depythonize, pythonize};
use rand::Rng;
use rand_distr::Normal;
use serde_derive::{Deserialize, Serialize};
#[cfg(feature = "python")]
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Mul;
use super::Stochastics;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "nyx_space.orbit_determination"))]
pub struct GaussMarkov {
pub tau: Duration,
pub process_noise: f64,
#[serde(skip)]
pub prev_epoch: Option<Epoch>,
#[serde(skip)]
pub init_sample: Option<f64>,
}
impl fmt::Display for GaussMarkov {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"First order Gauss-Markov process with τ = {}, σ = {}",
self.tau, self.process_noise
)
}
}
impl GaussMarkov {
pub fn new(tau: Duration, process_noise: f64) -> Result<Self, ConfigError> {
if tau <= Duration::ZERO {
return Err(ConfigError::InvalidConfig {
msg: format!("tau must be positive but got {tau}"),
});
}
Ok(Self {
tau,
process_noise,
init_sample: None,
prev_epoch: None,
})
}
pub const ZERO: Self = Self {
tau: Duration::MAX,
process_noise: 0.0,
init_sample: None,
prev_epoch: None,
};
pub fn default_range_km() -> Self {
Self {
tau: 1.minutes(),
process_noise: 60.0e-5,
init_sample: None,
prev_epoch: None,
}
}
pub fn default_doppler_km_s() -> Self {
Self {
tau: 1.minutes(),
process_noise: 0.03e-6,
init_sample: None,
prev_epoch: None,
}
}
}
impl Stochastics for GaussMarkov {
fn covariance(&self, _epoch: Epoch) -> f64 {
self.process_noise.powi(2)
}
fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64 {
let dt_s = (match self.prev_epoch {
None => Duration::ZERO,
Some(prev_epoch) => epoch - prev_epoch,
})
.to_seconds();
self.prev_epoch = Some(epoch);
if self.init_sample.is_none() {
self.init_sample = Some(rng.sample(Normal::new(0.0, self.process_noise).unwrap()));
}
let decay = (-dt_s / self.tau.to_seconds()).exp();
let anti_decay = 1.0 - decay;
let steady_noise = 0.5 * self.process_noise * self.tau.to_seconds() * anti_decay;
let ss_sample = rng.sample(Normal::new(0.0, steady_noise).unwrap());
self.init_sample.unwrap() * decay + ss_sample
}
}
impl Mul<f64> for GaussMarkov {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
tau: self.tau,
process_noise: self.process_noise * rhs,
init_sample: None,
prev_epoch: None,
}
}
}
#[cfg_attr(feature = "python", pymethods)]
impl GaussMarkov {
#[cfg(feature = "python")]
pub fn __repr__(&self) -> String {
format!("{self:?}")
}
#[cfg(feature = "python")]
pub fn __str__(&self) -> String {
format!("{self}")
}
#[cfg(feature = "python")]
#[new]
#[pyo3(text_signature = "(tau, sigma, state_state)")]
fn py_new(
tau: Option<Duration>,
sigma: Option<f64>,
steady_state: Option<f64>,
bias: Option<f64>,
epoch: Option<Epoch>,
) -> Result<Self, ConfigError> {
if tau.is_none() && sigma.is_none() && steady_state.is_none() {
return Ok(Self::ZERO);
} else if tau.is_none() || sigma.is_none() || steady_state.is_none() {
return Err(ConfigError::InvalidConfig {
msg: "tau, sigma, and steady_state must be specified".to_string(),
});
}
let tau = tau.unwrap();
let sigma = sigma.unwrap();
let steady_state = steady_state.unwrap();
if tau <= Duration::ZERO {
return Err(ConfigError::InvalidConfig {
msg: format!("tau must be positive but got {tau}"),
});
}
Ok(Self {
tau,
bias_sigma: sigma,
steady_state_sigma: steady_state,
bias,
epoch,
})
}
#[cfg(feature = "python")]
#[getter]
fn get_tau(&self) -> Duration {
self.tau
}
#[cfg(feature = "python")]
#[setter]
fn set_tau(&mut self, tau: Duration) -> PyResult<()> {
self.tau = tau;
Ok(())
}
#[cfg(feature = "python")]
#[getter]
fn get_bias(&self) -> Option<f64> {
self.bias
}
#[cfg(feature = "python")]
#[setter]
fn set_sampling(&mut self, bias: f64) -> PyResult<()> {
self.bias_sigma = bias;
Ok(())
}
#[cfg(feature = "python")]
#[classmethod]
fn default(_cls: &PyType, kind: String) -> Result<Self, NyxError> {
Self::from_default(kind)
}
#[cfg(feature = "python")]
#[classmethod]
fn load(_cls: &PyType, path: &str) -> Result<Self, ConfigError> {
<Self as ConfigRepr>::load(path)
}
#[cfg(feature = "python")]
#[classmethod]
fn load_many(_cls: &PyType, path: &str) -> Result<Vec<Self>, ConfigError> {
<Self as ConfigRepr>::load_many(path)
}
#[cfg(feature = "python")]
#[classmethod]
fn load_named(_cls: &PyType, path: &str) -> Result<BTreeMap<String, Self>, ConfigError> {
<Self as ConfigRepr>::load_named(path)
}
#[cfg(feature = "python")]
#[classmethod]
fn white(_cls: &PyType, sigma: f64) -> Result<Self, NyxError> {
Ok(Self::white_noise(sigma))
}
#[cfg(feature = "python")]
#[classmethod]
fn loads(_cls: &PyType, data: &PyAny) -> Result<Vec<Self>, ConfigError> {
use snafu::ResultExt;
use crate::io::ParseSnafu;
if let Ok(as_list) = data.downcast::<PyList>() {
let mut selves = Vec::new();
for item in as_list.iter() {
let next: Self =
serde_yml::from_value(pyany_to_value(item)?).context(ParseSnafu)?;
selves.push(next);
}
Ok(selves)
} else if let Ok(as_dict) = data.downcast::<PyDict>() {
let mut selves = Vec::new();
for item_as_list in as_dict.items() {
let v_any = item_as_list
.get_item(1)
.map_err(|_| ConfigError::InvalidConfig {
msg: "could not get key from provided dictionary item".to_string(),
})?;
match pyany_to_value(v_any) {
Ok(value) => {
match serde_yml::from_value(value) {
Ok(next) => selves.push(next),
Err(_) => {
let me: Self = depythonize(data).map_err(|e| {
ConfigError::InvalidConfig { msg: e.to_string() }
})?;
selves.clear();
selves.push(me);
return Ok(selves);
}
}
}
Err(_) => {
let me: Self = depythonize(data)
.map_err(|e| ConfigError::InvalidConfig { msg: e.to_string() })?;
selves.clear();
selves.push(me);
return Ok(selves);
}
}
}
Ok(selves)
} else {
depythonize(data).map_err(|e| ConfigError::InvalidConfig { msg: e.to_string() })
}
}
#[cfg(feature = "python")]
fn dumps(&self, py: Python) -> Result<PyObject, NyxError> {
pythonize(py, &self).map_err(|e| NyxError::CustomError { msg: e.to_string() })
}
#[cfg(feature = "python")]
fn __getstate__(&self, py: Python) -> Result<PyObject, NyxError> {
self.dumps(py)
}
#[cfg(feature = "python")]
fn __setstate__(&mut self, state: &PyAny) -> Result<(), ConfigError> {
*self =
depythonize(state).map_err(|e| ConfigError::InvalidConfig { msg: e.to_string() })?;
Ok(())
}
}
impl ConfigRepr for GaussMarkov {}
#[cfg(test)]
mod ut_gm {
use hifitime::{Duration, Epoch, TimeUnits};
use rand_pcg::Pcg64Mcg;
use rstats::{triangmat::Vecops, Stats};
use crate::{
io::ConfigRepr,
od::noise::{GaussMarkov, Stochastics},
};
#[test]
fn fogm_test() {
let mut gm = GaussMarkov::new(24.hours(), 0.1).unwrap();
let mut biases = Vec::with_capacity(1000);
let epoch = Epoch::now().unwrap();
let mut rng = Pcg64Mcg::new(0);
for seconds in 0..1000 {
biases.push(gm.sample(epoch + seconds.seconds(), &mut rng));
}
let min_max = biases.minmax();
assert_eq!(biases.amean().unwrap(), 0.09373233290645445);
assert_eq!(min_max.max, 0.24067114622652647);
assert_eq!(min_max.min, -0.045552031890295525);
}
#[test]
fn zero_noise_test() {
use rstats::{triangmat::Vecops, Stats};
let mut gm = GaussMarkov::ZERO;
let mut biases = Vec::with_capacity(1000);
let epoch = Epoch::now().unwrap();
let mut rng = Pcg64Mcg::new(0);
for seconds in 0..1000 {
biases.push(gm.sample(epoch + seconds.seconds(), &mut rng));
}
let min_max = biases.minmax();
assert_eq!(biases.amean().unwrap(), 0.0);
assert_eq!(min_max.min, 0.0);
assert_eq!(min_max.max, 0.0);
}
#[test]
fn serde_test() {
use serde_yml;
use std::env;
use std::path::PathBuf;
let gm = GaussMarkov::new(Duration::MAX, 0.1).unwrap();
let serialized = serde_yml::to_string(&gm).unwrap();
println!("{serialized}");
let gm_deser: GaussMarkov = serde_yml::from_str(&serialized).unwrap();
assert_eq!(gm_deser, gm);
let test_data: PathBuf = [
env::var("CARGO_MANIFEST_DIR").unwrap(),
"data".to_string(),
"tests".to_string(),
"config".to_string(),
"high-prec-network.yaml".to_string(),
]
.iter()
.collect();
let models = <GaussMarkov as ConfigRepr>::load_named(test_data).unwrap();
assert_eq!(models.len(), 2);
assert_eq!(
models["range_noise_model"].tau,
12.hours() + 159.milliseconds()
);
assert_eq!(models["range_noise_model"].process_noise, 5.0e-3);
assert_eq!(models["doppler_noise_model"].tau, 11.hours() + 59.minutes());
assert_eq!(models["doppler_noise_model"].process_noise, 50.0e-6);
}
}