1use crate::io::watermark::pq_writer;
20use arrow::array::{ArrayRef, Float64Array, UInt32Array};
21use arrow::datatypes::{DataType, Field, Schema};
22use arrow::record_batch::RecordBatch;
23use der::{Decode, Encode, Reader};
24use hifitime::{Epoch, TimeSeries, TimeUnits};
25use parquet::arrow::ArrowWriter;
26use rand::rngs::SysRng;
27use rand::{Rng, SeedableRng};
28use rand_pcg::Pcg64Mcg;
29use serde::{Deserialize, Serialize};
30use std::error::Error;
31use std::fs::File;
32use std::ops::{Mul, MulAssign};
33use std::path::Path;
34use std::sync::Arc;
35
36pub mod gauss_markov;
37#[cfg(feature = "premium")]
38pub mod link_specific;
39pub mod white;
40
41#[cfg(feature = "python")]
42use pyo3::prelude::*;
43
44pub use gauss_markov::GaussMarkov;
45pub use white::WhiteNoise;
46
47pub trait Stochastics {
49 fn covariance(&self, epoch: Epoch) -> f64;
51
52 fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64;
54}
55
56#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
60#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
61pub struct StochasticNoise {
62 pub white_noise: Option<WhiteNoise>,
63 pub bias: Option<GaussMarkov>,
64}
65
66impl StochasticNoise {
67 pub const ZERO: Self = Self {
69 white_noise: None,
70 bias: None,
71 };
72
73 pub const MIN: Self = Self {
75 white_noise: Some(WhiteNoise {
76 mean: 0.0,
77 sigma: 1e-6,
78 }),
79 bias: None,
80 };
81
82 pub fn default_range_km() -> Self {
85 Self {
86 white_noise: Some(WhiteNoise {
87 sigma: 2.0e-3, ..Default::default()
89 }),
90 bias: None,
93 }
94 }
95
96 pub fn default_doppler_km_s() -> Self {
98 Self {
99 white_noise: Some(WhiteNoise {
100 sigma: 3e-6, ..Default::default()
102 }),
103 bias: None,
106 }
107 }
108
109 pub fn default_angle_deg() -> Self {
112 Self {
113 white_noise: Some(WhiteNoise {
114 sigma: 1.0e-2, ..Default::default()
116 }),
117 bias: None,
120 }
121 }
122
123 pub fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64 {
125 let mut sample = 0.0;
126 if let Some(wn) = &mut self.white_noise {
127 sample += wn.sample(epoch, rng)
128 }
129 if let Some(gm) = &mut self.bias {
130 sample += gm.sample(epoch, rng);
131 }
132 sample
133 }
134
135 pub fn covariance(&self, epoch: Epoch) -> f64 {
137 let mut variance = 0.0;
138 if let Some(wn) = &self.white_noise {
139 variance += wn.covariance(epoch);
140 }
141 if let Some(gm) = &self.bias {
142 variance += gm.covariance(epoch);
143 }
144 variance
145 }
146
147 pub fn simulate<P: AsRef<Path>>(
154 self,
155 path: P,
156 runs: Option<u32>,
157 unit: Option<String>,
158 ) -> Result<Vec<StochasticState>, Box<dyn Error>> {
159 let num_runs = runs.unwrap_or(25);
160
161 let start = Epoch::now().unwrap();
162 let (step, end) = (1.minutes(), start + 1.days());
163
164 let capacity = ((end - start).to_seconds() / step.to_seconds()).ceil() as usize;
165
166 let mut samples = Vec::with_capacity(capacity);
167
168 for run in 0..num_runs {
169 let mut rng = Pcg64Mcg::try_from_rng(&mut SysRng).unwrap();
170
171 let mut mdl = self;
172 for epoch in TimeSeries::inclusive(start, end, step) {
173 if epoch > start + 6.hours() && epoch < start + 12.hours() {
174 continue;
176 }
177 let variance = mdl.covariance(epoch);
178 let sample = mdl.sample(epoch, &mut rng);
179 samples.push(StochasticState {
180 run,
181 dt_s: (epoch - start).to_seconds(),
182 sample,
183 variance,
184 });
185 }
186 }
187
188 let bias_unit = match unit {
189 Some(unit) => format!("({unit})"),
190 None => "(unitless)".to_string(),
191 };
192
193 let hdrs = vec![
195 Field::new("Run", DataType::UInt32, false),
196 Field::new("Delta Time (s)", DataType::Float64, false),
197 Field::new(format!("Bias {bias_unit}"), DataType::Float64, false),
198 Field::new(format!("Variance {bias_unit}"), DataType::Float64, false),
199 ];
200
201 let schema = Arc::new(Schema::new(hdrs));
202 let record = vec![
203 Arc::new(UInt32Array::from(
204 samples.iter().map(|s| s.run).collect::<Vec<u32>>(),
205 )) as ArrayRef,
206 Arc::new(Float64Array::from(
207 samples.iter().map(|s| s.dt_s).collect::<Vec<f64>>(),
208 )) as ArrayRef,
209 Arc::new(Float64Array::from(
210 samples.iter().map(|s| s.sample).collect::<Vec<f64>>(),
211 )) as ArrayRef,
212 Arc::new(Float64Array::from(
213 samples.iter().map(|s| s.variance).collect::<Vec<f64>>(),
214 )) as ArrayRef,
215 ];
216
217 let props = pq_writer(None);
218
219 let file = File::create(path)?;
220 let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap();
221
222 let batch = RecordBatch::try_new(schema, record)?;
223 writer.write(&batch)?;
224 writer.close()?;
225
226 Ok(samples)
227 }
228
229 fn available_data(&self) -> u8 {
230 let mut bits: u8 = 0;
231
232 if self.white_noise.is_some() {
233 bits |= 1 << 0;
234 }
235
236 if self.bias.is_some() {
237 bits |= 1 << 1;
238 }
239
240 bits
241 }
242}
243
244impl Mul<f64> for StochasticNoise {
245 type Output = Self;
246
247 fn mul(mut self, rhs: f64) -> Self::Output {
248 if let Some(mut wn) = &mut self.white_noise {
249 wn *= rhs;
250 }
251 if let Some(mut gm) = &mut self.bias {
252 gm *= rhs;
253 }
254
255 self
256 }
257}
258
259impl MulAssign<f64> for StochasticNoise {
260 fn mul_assign(&mut self, rhs: f64) {
261 *self = *self * rhs;
262 }
263}
264
265impl Encode for StochasticNoise {
266 fn encoded_len(&self) -> der::Result<der::Length> {
267 let flags = self.available_data();
268 flags.encoded_len()? + self.white_noise.encoded_len()? + self.bias.encoded_len()?
269 }
270
271 fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
272 let flags = self.available_data();
273
274 flags.encode(encoder)?;
275 self.white_noise.encode(encoder)?;
276 self.bias.encode(encoder)
277 }
278}
279
280impl<'a> Decode<'a> for StochasticNoise {
281 fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
282 let flags: u8 = decoder.decode()?;
283
284 let white_noise = if flags & (1 << 0) != 0 {
285 Some(decoder.decode()?)
286 } else {
287 None
288 };
289
290 let bias = if flags & (1 << 1) != 0 {
291 Some(decoder.decode()?)
292 } else {
293 None
294 };
295
296 Ok(Self { white_noise, bias })
297 }
298}
299
300pub struct StochasticState {
301 pub run: u32,
302 pub dt_s: f64,
303 pub sample: f64,
304 pub variance: f64,
305}
306
307#[cfg(test)]
308mod ut_stochastics {
309 use std::path::PathBuf;
310
311 use super::{white::WhiteNoise, StochasticNoise};
312
313 #[test]
314 fn test_simulate_zero() {
315 let path: PathBuf = [
316 env!("CARGO_MANIFEST_DIR"),
317 "data",
318 "04_output",
319 "stochastics_zero.parquet",
320 ]
321 .iter()
322 .collect();
323
324 let noise = StochasticNoise::default();
325
326 let rslts = noise.simulate(path, None, None).unwrap();
327 assert!(!rslts.is_empty());
328 assert!(rslts.iter().map(|rslt| rslt.sample).sum::<f64>().abs() < f64::EPSILON);
329 }
330
331 #[test]
332 fn test_simulate_constant() {
333 let path: PathBuf = [
334 env!("CARGO_MANIFEST_DIR"),
335 "data",
336 "04_output",
337 "stochastics_constant.parquet",
338 ]
339 .iter()
340 .collect();
341
342 let noise = StochasticNoise {
343 white_noise: Some(WhiteNoise {
344 mean: 15.0,
345 sigma: 2.0,
346 }),
347 ..Default::default()
348 };
349
350 noise.simulate(path, None, None).unwrap();
351 }
352
353 #[test]
354 fn test_simulate_dsn_range() {
355 let path: PathBuf = [
356 env!("CARGO_MANIFEST_DIR"),
357 "data",
358 "04_output",
359 "stochastics_dsn_range.parquet",
360 ]
361 .iter()
362 .collect();
363
364 let noise = StochasticNoise::default_range_km();
365
366 noise
367 .simulate(path, None, Some("kilometer".to_string()))
368 .unwrap();
369 }
370
371 #[test]
372 fn test_simulate_dsn_range_gm_only() {
373 let path: PathBuf = [
374 env!("CARGO_MANIFEST_DIR"),
375 "data",
376 "04_output",
377 "stochastics_dsn_range_gm_only.parquet",
378 ]
379 .iter()
380 .collect();
381
382 let mut noise = StochasticNoise::default_range_km();
383 noise.white_noise = None;
384
385 noise
386 .simulate(path, None, Some("kilometer".to_string()))
387 .unwrap();
388 }
389}