1use crate::io::watermark::pq_writer;
20use arrow::array::{ArrayRef, Float64Array, UInt32Array};
21use arrow::datatypes::{DataType, Field, Schema};
22use arrow::record_batch::RecordBatch;
23use hifitime::{Epoch, TimeSeries, TimeUnits};
24use parquet::arrow::ArrowWriter;
25use rand::rngs::SysRng;
26use rand::{Rng, SeedableRng};
27use rand_pcg::Pcg64Mcg;
28use serde_derive::{Deserialize, Serialize};
29use std::error::Error;
30use std::fs::File;
31use std::ops::{Mul, MulAssign};
32use std::path::Path;
33use std::sync::Arc;
34
35pub mod gauss_markov;
36#[cfg(feature = "premium")]
37pub mod link_specific;
38pub mod white;
39
40pub use gauss_markov::GaussMarkov;
41pub use white::WhiteNoise;
42
43pub trait Stochastics {
45 fn covariance(&self, epoch: Epoch) -> f64;
47
48 fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64;
50}
51
52#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
56pub struct StochasticNoise {
57 pub white_noise: Option<WhiteNoise>,
58 pub bias: Option<GaussMarkov>,
59}
60
61impl StochasticNoise {
62 pub const ZERO: Self = Self {
64 white_noise: None,
65 bias: None,
66 };
67
68 pub const MIN: Self = Self {
70 white_noise: Some(WhiteNoise {
71 mean: 0.0,
72 sigma: 1e-6,
73 }),
74 bias: None,
75 };
76
77 pub fn default_range_km() -> Self {
80 Self {
81 white_noise: Some(WhiteNoise {
82 sigma: 2.0e-3, ..Default::default()
84 }),
85 bias: None,
88 }
89 }
90
91 pub fn default_doppler_km_s() -> Self {
93 Self {
94 white_noise: Some(WhiteNoise {
95 sigma: 3e-6, ..Default::default()
97 }),
98 bias: None,
101 }
102 }
103
104 pub fn default_angle_deg() -> Self {
107 Self {
108 white_noise: Some(WhiteNoise {
109 sigma: 1.0e-2, ..Default::default()
111 }),
112 bias: None,
115 }
116 }
117
118 pub fn sample<R: Rng>(&mut self, epoch: Epoch, rng: &mut R) -> f64 {
120 let mut sample = 0.0;
121 if let Some(wn) = &mut self.white_noise {
122 sample += wn.sample(epoch, rng)
123 }
124 if let Some(gm) = &mut self.bias {
125 sample += gm.sample(epoch, rng);
126 }
127 sample
128 }
129
130 pub fn covariance(&self, epoch: Epoch) -> f64 {
132 let mut variance = 0.0;
133 if let Some(wn) = &self.white_noise {
134 variance += wn.covariance(epoch);
135 }
136 if let Some(gm) = &self.bias {
137 variance += gm.covariance(epoch);
138 }
139 variance
140 }
141
142 pub fn simulate<P: AsRef<Path>>(
149 self,
150 path: P,
151 runs: Option<u32>,
152 unit: Option<String>,
153 ) -> Result<Vec<StochasticState>, Box<dyn Error>> {
154 let num_runs = runs.unwrap_or(25);
155
156 let start = Epoch::now().unwrap();
157 let (step, end) = (1.minutes(), start + 1.days());
158
159 let capacity = ((end - start).to_seconds() / step.to_seconds()).ceil() as usize;
160
161 let mut samples = Vec::with_capacity(capacity);
162
163 for run in 0..num_runs {
164 let mut rng = Pcg64Mcg::try_from_rng(&mut SysRng).unwrap();
165
166 let mut mdl = self;
167 for epoch in TimeSeries::inclusive(start, end, step) {
168 if epoch > start + 6.hours() && epoch < start + 12.hours() {
169 continue;
171 }
172 let variance = mdl.covariance(epoch);
173 let sample = mdl.sample(epoch, &mut rng);
174 samples.push(StochasticState {
175 run,
176 dt_s: (epoch - start).to_seconds(),
177 sample,
178 variance,
179 });
180 }
181 }
182
183 let bias_unit = match unit {
184 Some(unit) => format!("({unit})"),
185 None => "(unitless)".to_string(),
186 };
187
188 let hdrs = vec![
190 Field::new("Run", DataType::UInt32, false),
191 Field::new("Delta Time (s)", DataType::Float64, false),
192 Field::new(format!("Bias {bias_unit}"), DataType::Float64, false),
193 Field::new(format!("Variance {bias_unit}"), DataType::Float64, false),
194 ];
195
196 let schema = Arc::new(Schema::new(hdrs));
197 let record = vec![
198 Arc::new(UInt32Array::from(
199 samples.iter().map(|s| s.run).collect::<Vec<u32>>(),
200 )) as ArrayRef,
201 Arc::new(Float64Array::from(
202 samples.iter().map(|s| s.dt_s).collect::<Vec<f64>>(),
203 )) as ArrayRef,
204 Arc::new(Float64Array::from(
205 samples.iter().map(|s| s.sample).collect::<Vec<f64>>(),
206 )) as ArrayRef,
207 Arc::new(Float64Array::from(
208 samples.iter().map(|s| s.variance).collect::<Vec<f64>>(),
209 )) as ArrayRef,
210 ];
211
212 let props = pq_writer(None);
213
214 let file = File::create(path)?;
215 let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap();
216
217 let batch = RecordBatch::try_new(schema, record)?;
218 writer.write(&batch)?;
219 writer.close()?;
220
221 Ok(samples)
222 }
223}
224
225impl Mul<f64> for StochasticNoise {
226 type Output = Self;
227
228 fn mul(mut self, rhs: f64) -> Self::Output {
229 if let Some(mut wn) = &mut self.white_noise {
230 wn *= rhs;
231 }
232 if let Some(mut gm) = &mut self.bias {
233 gm *= rhs;
234 }
235
236 self
237 }
238}
239
240impl MulAssign<f64> for StochasticNoise {
241 fn mul_assign(&mut self, rhs: f64) {
242 *self = *self * rhs;
243 }
244}
245
246pub struct StochasticState {
247 pub run: u32,
248 pub dt_s: f64,
249 pub sample: f64,
250 pub variance: f64,
251}
252
253#[cfg(test)]
254mod ut_stochastics {
255 use std::path::PathBuf;
256
257 use super::{white::WhiteNoise, StochasticNoise};
258
259 #[test]
260 fn test_simulate_zero() {
261 let path: PathBuf = [
262 env!("CARGO_MANIFEST_DIR"),
263 "data",
264 "04_output",
265 "stochastics_zero.parquet",
266 ]
267 .iter()
268 .collect();
269
270 let noise = StochasticNoise::default();
271
272 let rslts = noise.simulate(path, None, None).unwrap();
273 assert!(!rslts.is_empty());
274 assert!(rslts.iter().map(|rslt| rslt.sample).sum::<f64>().abs() < f64::EPSILON);
275 }
276
277 #[test]
278 fn test_simulate_constant() {
279 let path: PathBuf = [
280 env!("CARGO_MANIFEST_DIR"),
281 "data",
282 "04_output",
283 "stochastics_constant.parquet",
284 ]
285 .iter()
286 .collect();
287
288 let noise = StochasticNoise {
289 white_noise: Some(WhiteNoise {
290 mean: 15.0,
291 sigma: 2.0,
292 }),
293 ..Default::default()
294 };
295
296 noise.simulate(path, None, None).unwrap();
297 }
298
299 #[test]
300 fn test_simulate_dsn_range() {
301 let path: PathBuf = [
302 env!("CARGO_MANIFEST_DIR"),
303 "data",
304 "04_output",
305 "stochastics_dsn_range.parquet",
306 ]
307 .iter()
308 .collect();
309
310 let noise = StochasticNoise::default_range_km();
311
312 noise
313 .simulate(path, None, Some("kilometer".to_string()))
314 .unwrap();
315 }
316
317 #[test]
318 fn test_simulate_dsn_range_gm_only() {
319 let path: PathBuf = [
320 env!("CARGO_MANIFEST_DIR"),
321 "data",
322 "04_output",
323 "stochastics_dsn_range_gm_only.parquet",
324 ]
325 .iter()
326 .collect();
327
328 let mut noise = StochasticNoise::default_range_km();
329 noise.white_noise = None;
330
331 noise
332 .simulate(path, None, Some("kilometer".to_string()))
333 .unwrap();
334 }
335}