use crate::cosmic::SPEED_OF_LIGHT_KM_S;
use crate::io::watermark::pq_writer;
use crate::io::{duration_from_str, duration_to_str, ConfigError, ConfigRepr};
#[cfg(feature = "python")]
use crate::python::pyo3utils::pyany_to_value;
use crate::NyxError;
use arrow::array::{ArrayRef, Float64Array, UInt32Array};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use hifitime::{Duration, Epoch, TimeSeries, TimeUnits};
use parquet::arrow::ArrowWriter;
#[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, SeedableRng};
use rand_distr::Normal;
use rand_pcg::Pcg64Mcg;
use serde_derive::{Deserialize, Serialize};
#[cfg(feature = "python")]
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::ops::Mul;
use std::sync::Arc;
#[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 {
#[serde(
serialize_with = "duration_to_str",
deserialize_with = "duration_from_str"
)]
pub tau: Duration,
pub bias_sigma: f64,
pub steady_state_sigma: f64,
#[serde(skip)]
pub bias: Option<f64>,
#[serde(skip)]
pub epoch: Option<Epoch>,
}
impl fmt::Display for GaussMarkov {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"First order Gauss-Markov process with τ = {}, σ_b = {}, σ_q = {}",
self.tau, self.bias_sigma, self.steady_state_sigma
)
}
}
impl GaussMarkov {
pub fn new(
tau: Duration,
bias_sigma: f64,
steady_state_sigma: f64,
) -> Result<Self, ConfigError> {
if tau <= Duration::ZERO {
return Err(ConfigError::InvalidConfig {
msg: format!("tau must be positive but got {tau}"),
});
}
Ok(Self {
tau,
bias_sigma,
steady_state_sigma,
bias: None,
epoch: None,
})
}
pub fn white_noise(sigma: f64) -> Self {
Self::new(Duration::MAX, 0.0, sigma).unwrap()
}
pub fn next_bias<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64 {
if self.is_white() {
return rng.sample(Normal::new(0.0, self.steady_state_sigma).unwrap());
}
let dt_s = (match self.epoch {
None => Duration::ZERO,
Some(prev_epoch) => epoch - prev_epoch,
})
.to_seconds();
self.epoch = Some(epoch);
if self.bias.is_none() {
self.bias = Some(rng.sample(Normal::new(0.0, self.bias_sigma).unwrap()));
}
let decay = (-dt_s / self.tau.to_seconds()).exp();
let anti_decay = 1.0 - decay;
let ss_contrib = rng.sample(Normal::new(0.0, self.steady_state_sigma).unwrap());
let bias = self.bias.unwrap() * decay + ss_contrib * anti_decay;
self.bias = Some(bias);
bias
}
pub const ZERO: Self = Self {
tau: Duration::MAX,
bias_sigma: 0.0,
steady_state_sigma: 0.0,
bias: None,
epoch: None,
};
pub fn default_range_km() -> Self {
Self {
tau: 5.minutes(),
bias_sigma: 50.0e-3, steady_state_sigma: 5e-3, bias: None,
epoch: None,
}
}
pub fn default_doppler_km_s() -> Self {
Self {
tau: 20.minutes(),
bias_sigma: 500.0e-6, steady_state_sigma: 75e-6, bias: None,
epoch: None,
}
}
pub fn high_precision_range_km() -> Self {
Self {
tau: 12.hours(),
bias_sigma: 5.0e-3, steady_state_sigma: 0.1e-3, bias: None,
epoch: None,
}
}
pub fn high_precision_doppler_km_s() -> Self {
Self {
tau: 12.hours(),
bias_sigma: 35.0e-7, steady_state_sigma: 3.5e-7, bias: None,
epoch: None,
}
}
pub fn from_pr_n0(pr_n0: f64, bandwidth_hz: f64) -> Self {
let sigma = SPEED_OF_LIGHT_KM_S / (2.0 * bandwidth_hz * (pr_n0).sqrt());
Self::white_noise(sigma)
}
pub fn from_default(kind: String) -> Result<Self, NyxError> {
match kind.as_str() {
"Range" => Ok(Self::default_range_km()),
"Doppler" => Ok(Self::default_doppler_km_s()),
"RangeHP" => Ok(Self::high_precision_range_km()),
"DopplerHP" => Ok(Self::high_precision_doppler_km_s()),
_ => Err(NyxError::CustomError {
msg: format!("No default Gauss Markov model for `{kind}`"),
}),
}
}
}
impl Mul<f64> for GaussMarkov {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
tau: self.tau,
bias_sigma: self.bias_sigma * rhs,
steady_state_sigma: self.steady_state_sigma * rhs,
bias: None,
epoch: None,
}
}
}
#[cfg_attr(feature = "python", pymethods)]
impl GaussMarkov {
pub fn simulate(
&self,
path: String,
runs: Option<u32>,
unit: Option<String>,
) -> Result<(), NyxError> {
struct GMState {
run: u32,
dt_s: f64,
bias: f64,
}
let num_runs = runs.unwrap_or(25);
let mut samples = Vec::with_capacity(num_runs as usize);
let start = Epoch::now().unwrap();
let end = start + self.tau * 5;
let step = self.tau / 100;
for run in 0..num_runs {
let mut rng = Pcg64Mcg::from_entropy();
let mut gm = *self;
for epoch in TimeSeries::inclusive(start, end, step) {
let bias = gm.next_bias(epoch, &mut rng);
samples.push(GMState {
run,
dt_s: (epoch - start).to_seconds(),
bias,
});
}
}
let bias_unit = match unit {
Some(unit) => format!("({unit})"),
None => "(unitless)".to_string(),
};
let hdrs = vec![
Field::new("Run", DataType::UInt32, false),
Field::new("Delta Time (s)", DataType::Float64, false),
Field::new(format!("Bias {bias_unit}"), DataType::Float64, false),
];
let schema = Arc::new(Schema::new(hdrs));
let record = vec![
Arc::new(UInt32Array::from(
samples.iter().map(|s| s.run).collect::<Vec<u32>>(),
)) as ArrayRef,
Arc::new(Float64Array::from(
samples.iter().map(|s| s.dt_s).collect::<Vec<f64>>(),
)) as ArrayRef,
Arc::new(Float64Array::from(
samples.iter().map(|s| s.bias).collect::<Vec<f64>>(),
)) as ArrayRef,
];
let mut metadata = HashMap::new();
metadata.insert("Purpose".to_string(), "Gauss Markov simulation".to_string());
let props = pq_writer(Some(metadata));
let file = File::create(path).map_err(|e| NyxError::CustomError { msg: e.to_string() })?;
let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap();
let batch = RecordBatch::try_new(schema, record)
.map_err(|e| NyxError::CustomError { msg: e.to_string() })?;
writer
.write(&batch)
.map_err(|e| NyxError::CustomError { msg: e.to_string() })?;
writer
.close()
.map_err(|e| NyxError::CustomError { msg: e.to_string() })?;
Ok(())
}
pub fn is_white(&self) -> bool {
self.tau > 366.days()
}
#[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_yaml::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_yaml::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 {}
#[test]
fn fogm_test() {
use hifitime::TimeUnits;
use rstats::{triangmat::Vecops, Stats};
let mut gm = GaussMarkov::new(24.hours(), 0.1, 0.0).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.next_bias(epoch + seconds.seconds(), &mut rng));
}
let min_max = biases.minmax();
assert_eq!(biases.amean().unwrap(), 0.09422989888670787);
assert_eq!(min_max.min, 0.09368618104727605);
assert_eq!(min_max.max, 0.0947757142410022);
}
#[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.next_bias(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 white_noise_test() {
let sigma = 10.0;
let mut gm = GaussMarkov::white_noise(sigma);
let mut larger_gm = gm * 10.0;
let epoch = Epoch::now().unwrap();
let mut rng = Pcg64Mcg::new(1000);
let mut cnt_above_3sigma = 0;
let mut cnt_below_3sigma = 0;
let mut larger_cnt_above_3sigma = 0;
let mut larger_cnt_below_3sigma = 0;
for seconds in 0..1000 {
let bias = gm.next_bias(epoch + seconds.seconds(), &mut rng);
if bias > 3.0 * sigma {
cnt_above_3sigma += 1;
} else if bias < -3.0 * sigma {
cnt_below_3sigma += 1;
}
let larger_bias = larger_gm.next_bias(epoch + seconds.seconds(), &mut rng);
if larger_bias > 30.0 * sigma {
larger_cnt_above_3sigma += 1;
} else if larger_bias < -30.0 * sigma {
larger_cnt_below_3sigma += 1;
}
}
assert!(dbg!(cnt_above_3sigma) <= 3);
assert!(dbg!(cnt_below_3sigma) <= 3);
assert!(dbg!(larger_cnt_above_3sigma) <= 3);
assert!(dbg!(larger_cnt_below_3sigma) <= 3);
}
#[test]
fn serde_test() {
use serde_yaml;
use std::env;
use std::path::PathBuf;
let gm = GaussMarkov::new(Duration::MAX, 0.1, 0.0).unwrap();
let serialized = serde_yaml::to_string(&gm).unwrap();
println!("{serialized}");
let gm_deser: GaussMarkov = serde_yaml::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"].bias_sigma, 5.0e-3);
assert_eq!(models["range_noise_model"].steady_state_sigma, 0.1e-3);
assert_eq!(models["doppler_noise_model"].tau, 11.hours() + 59.minutes());
assert_eq!(models["doppler_noise_model"].bias_sigma, 50.0e-6);
assert_eq!(models["doppler_noise_model"].steady_state_sigma, 1.5e-6);
}