use crate::io::watermark::pq_writer;
use arrow::array::{ArrayRef, Float64Array, UInt32Array};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use der::{Decode, Encode, Reader};
use hifitime::{Epoch, TimeSeries, TimeUnits};
use parquet::arrow::ArrowWriter;
use rand::rngs::SysRng;
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt::Display;
use std::fs::File;
use std::ops::{Mul, MulAssign};
use std::path::Path;
use std::sync::Arc;
pub mod gauss_markov;
pub mod link_specific;
pub mod white;
#[cfg(feature = "python")]
use hifitime::Duration;
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyType;
pub use gauss_markov::GaussMarkov;
pub use white::WhiteNoise;
pub trait Stochastics {
fn covariance(&self, epoch: Epoch) -> f64;
fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64;
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct StochasticNoise {
pub white_noise: Option<WhiteNoise>,
pub bias: Option<GaussMarkov>,
}
impl StochasticNoise {
pub const ZERO: Self = Self {
white_noise: None,
bias: None,
};
pub const MIN: Self = Self {
white_noise: Some(WhiteNoise {
mean: 0.0,
sigma: 1e-6,
}),
bias: None,
};
pub fn default_range_km() -> Self {
Self {
white_noise: Some(WhiteNoise {
sigma: 2.0e-3, ..Default::default()
}),
bias: None,
}
}
pub fn default_doppler_km_s() -> Self {
Self {
white_noise: Some(WhiteNoise {
sigma: 3e-6, ..Default::default()
}),
bias: None,
}
}
pub fn default_angle_deg() -> Self {
Self {
white_noise: Some(WhiteNoise {
sigma: 1.0e-2, ..Default::default()
}),
bias: None,
}
}
pub fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64 {
let mut sample = 0.0;
if let Some(wn) = &mut self.white_noise {
sample += wn.sample(epoch, rng)
}
if let Some(gm) = &mut self.bias {
sample += gm.sample(epoch, rng);
}
sample
}
pub fn simulate<P: AsRef<Path>>(
self,
path: P,
runs: Option<u32>,
unit: Option<String>,
) -> Result<Vec<StochasticState>, Box<dyn Error>> {
let num_runs = runs.unwrap_or(25);
let start = Epoch::now().unwrap();
let (step, end) = (1.minutes(), start + 1.days());
let capacity = ((end - start).to_seconds() / step.to_seconds()).ceil() as usize;
let mut samples = Vec::with_capacity(capacity);
for run in 0..num_runs {
let mut rng = Pcg64Mcg::try_from_rng(&mut SysRng).unwrap();
let mut mdl = self;
for epoch in TimeSeries::inclusive(start, end, step) {
if epoch > start + 6.hours() && epoch < start + 12.hours() {
continue;
}
let variance = mdl.covariance(epoch);
let sample = mdl.sample(epoch, &mut rng);
samples.push(StochasticState {
run,
dt_s: (epoch - start).to_seconds(),
sample,
variance,
});
}
}
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),
Field::new(format!("Variance {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.sample).collect::<Vec<f64>>(),
)) as ArrayRef,
Arc::new(Float64Array::from(
samples.iter().map(|s| s.variance).collect::<Vec<f64>>(),
)) as ArrayRef,
];
let props = pq_writer(None);
let file = File::create(path)?;
let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap();
let batch = RecordBatch::try_new(schema, record)?;
writer.write(&batch)?;
writer.close()?;
Ok(samples)
}
fn available_data(&self) -> u8 {
let mut bits: u8 = 0;
if self.white_noise.is_some() {
bits |= 1 << 0;
}
if self.bias.is_some() {
bits |= 1 << 1;
}
bits
}
}
#[cfg_attr(feature = "python", pymethods)]
impl StochasticNoise {
#[cfg(feature = "python")]
#[pyo3(signature=(white_noise=None, bias=None, name=None))]
#[new]
fn py_new(
white_noise: Option<WhiteNoise>,
bias: Option<GaussMarkov>,
name: Option<String>,
) -> PyResult<Self> {
if let Some(name) = name {
match name.to_ascii_lowercase().as_str() {
"range" => Ok(Self::default_range_km()),
"doppler" => Ok(Self::default_doppler_km_s()),
"angles" => Ok(Self::default_angle_deg()),
_ => Err(PyValueError::new_err(format!(
"name must be `range`, `doppler`, or `angles` (received `{name}`)"
))),
}
} else {
Ok(Self { white_noise, bias })
}
}
pub fn covariance(&self, epoch: Epoch) -> f64 {
let mut variance = 0.0;
if let Some(wn) = &self.white_noise {
variance += wn.covariance(epoch);
}
if let Some(gm) = &self.bias {
variance += gm.covariance(epoch);
}
variance
}
#[cfg(feature = "python")]
#[pyo3(name = "simulate")]
fn py_simulate(
&self,
path: &str,
runs: Option<u32>,
unit: Option<String>,
) -> PyResult<Vec<StochasticState>> {
self.simulate(path, runs, unit)
.map_err(|e| PyValueError::new_err(e.to_string()))
}
#[cfg(feature = "python")]
#[pyo3(name = "from_hardware_range_km")]
#[classmethod]
fn py_from_hardware_range_km(
_cls: &Bound<'_, PyType>,
allan_deviation: f64,
integration_time: Duration,
chip_rate: link_specific::ChipRate,
s_n0: link_specific::SN0,
) -> Self {
Self::from_hardware_range_km(allan_deviation, integration_time, chip_rate, s_n0)
}
#[cfg(feature = "python")]
#[pyo3(name = "from_hardware_doppler_km_s")]
#[classmethod]
fn py_from_hardware_doppler_km_s(
_cls: &Bound<'_, PyType>,
allan_deviation: f64,
integration_time: Duration,
carrier: link_specific::CarrierFreq,
c_n0: link_specific::CN0,
) -> Self {
Self::from_hardware_doppler_km_s(allan_deviation, integration_time, carrier, c_n0)
}
fn __str__(&self) -> String {
format!("{self}")
}
fn __repr__(&self) -> String {
format!("{self} @ {self:p}")
}
}
impl Display for StochasticNoise {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.white_noise, self.bias) {
(Some(wn), None) => write!(f, "Stochastics with {wn:?}"),
(None, Some(bias)) => write!(f, "Stochastics with bias {bias}"),
(None, None) => write!(f, "Noiseless stochastics"),
(Some(wn), Some(bias)) => write!(f, "Stochastics with {wn:?} and bias {bias}"),
}
}
}
impl Mul<f64> for StochasticNoise {
type Output = Self;
fn mul(mut self, rhs: f64) -> Self::Output {
if let Some(wn) = &mut self.white_noise {
*wn *= rhs;
}
if let Some(gm) = &mut self.bias {
*gm *= rhs;
}
self
}
}
impl MulAssign<f64> for StochasticNoise {
fn mul_assign(&mut self, rhs: f64) {
*self = *self * rhs;
}
}
impl Encode for StochasticNoise {
fn encoded_len(&self) -> der::Result<der::Length> {
let flags = self.available_data();
flags.encoded_len()? + self.white_noise.encoded_len()? + self.bias.encoded_len()?
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
let flags = self.available_data();
flags.encode(encoder)?;
self.white_noise.encode(encoder)?;
self.bias.encode(encoder)
}
}
impl<'a> Decode<'a> for StochasticNoise {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let flags: u8 = decoder.decode()?;
let white_noise = if flags & (1 << 0) != 0 {
Some(decoder.decode()?)
} else {
None
};
let bias = if flags & (1 << 1) != 0 {
Some(decoder.decode()?)
} else {
None
};
Ok(Self { white_noise, bias })
}
}
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all))]
pub struct StochasticState {
pub run: u32,
pub dt_s: f64,
pub sample: f64,
pub variance: f64,
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl StochasticState {
fn __str__(&self) -> String {
format!("{self:?}")
}
fn __repr__(&self) -> String {
format!("{self:?} @ {self:p}")
}
}
#[cfg(test)]
mod ut_stochastics {
use std::path::PathBuf;
use super::{StochasticNoise, white::WhiteNoise};
#[test]
fn test_simulate_zero() {
let path: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"04_output",
"stochastics_zero.parquet",
]
.iter()
.collect();
let noise = StochasticNoise::default();
let rslts = noise.simulate(path, None, None).unwrap();
assert!(!rslts.is_empty());
assert!(rslts.iter().map(|rslt| rslt.sample).sum::<f64>().abs() < f64::EPSILON);
}
#[test]
fn test_simulate_constant() {
let path: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"04_output",
"stochastics_constant.parquet",
]
.iter()
.collect();
let noise = StochasticNoise {
white_noise: Some(WhiteNoise {
mean: 15.0,
sigma: 2.0,
}),
..Default::default()
};
noise.simulate(path, None, None).unwrap();
}
#[test]
fn test_simulate_dsn_range() {
let path: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"04_output",
"stochastics_dsn_range.parquet",
]
.iter()
.collect();
let noise = StochasticNoise::default_range_km();
noise
.simulate(path, None, Some("kilometer".to_string()))
.unwrap();
}
#[test]
fn test_simulate_dsn_range_gm_only() {
let path: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"04_output",
"stochastics_dsn_range_gm_only.parquet",
]
.iter()
.collect();
let mut noise = StochasticNoise::default_range_km();
noise.white_noise = None;
noise
.simulate(path, None, Some("kilometer".to_string()))
.unwrap();
}
}