use crate::io::{ConfigError, ConfigRepr};
use hifitime::{Duration, Epoch, TimeUnits};
use der::{Decode, Encode, Reader};
use rand::{Rng, RngExt};
use rand_distr::Normal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::{Mul, MulAssign};
#[cfg(feature = "python")]
use pyo3::prelude::*;
use super::Stochastics;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct GaussMarkov {
pub tau: Duration,
pub process_noise: f64,
pub constant: Option<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,
constant: None,
init_sample: None,
prev_epoch: None,
})
}
pub const ZERO: Self = Self {
tau: Duration::MAX,
process_noise: 0.0,
constant: None,
init_sample: None,
prev_epoch: None,
};
pub fn default_range_km() -> Self {
Self {
tau: 1.minutes(),
process_noise: 60.0e-5,
constant: None,
init_sample: None,
prev_epoch: None,
}
}
pub fn default_doppler_km_s() -> Self {
Self {
tau: 1.minutes(),
process_noise: 0.03e-6,
constant: None,
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 + self.constant.unwrap_or(0.0)
}
}
impl Mul<f64> for GaussMarkov {
type Output = Self;
fn mul(mut self, rhs: f64) -> Self::Output {
self.process_noise *= rhs;
self.constant = None;
self.init_sample = None;
self.prev_epoch = None;
self
}
}
impl MulAssign<f64> for GaussMarkov {
fn mul_assign(&mut self, rhs: f64) {
*self = *self * rhs;
}
}
impl Encode for GaussMarkov {
fn encoded_len(&self) -> der::Result<der::Length> {
self.tau.total_nanoseconds().encoded_len()?
+ self.process_noise.encoded_len()?
+ if let Some(constant) = self.constant {
(true.encoded_len()? + constant.encoded_len()?)?
} else {
false.encoded_len()?
}
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
self.tau.total_nanoseconds().encode(encoder)?;
self.process_noise.encode(encoder)?;
if let Some(constant) = self.constant {
true.encode(encoder)?;
constant.encode(encoder)
} else {
false.encode(encoder)
}
}
}
impl<'a> Decode<'a> for GaussMarkov {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let tau = Duration::from_total_nanoseconds(decoder.decode::<i128>()?);
let process_noise = decoder.decode()?;
let constant = if decoder.decode::<bool>()? {
Some(decoder.decode()?)
} else {
None
};
Ok(Self {
tau,
process_noise,
constant,
prev_epoch: None,
init_sample: None,
})
}
}
impl ConfigRepr for GaussMarkov {}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl GaussMarkov {
#[new]
fn py_new(tau: Duration, process_noise: f64) -> Result<Self, ConfigError> {
Self::new(tau, process_noise)
}
fn __str__(&self) -> String {
format!("{self}")
}
fn __repr__(&self) -> String {
format!("{self} @ {self:p}")
}
}
#[cfg(test)]
mod ut_gm {
use hifitime::{Duration, Epoch, TimeUnits};
use rand_pcg::Pcg64Mcg;
use rstats::{Stats, triangmat::Vecops};
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::{Stats, triangmat::Vecops};
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!("CARGO_MANIFEST_DIR"),
"../data",
"03_tests",
"config",
"high-prec-network.yaml",
]
.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);
}
}