use nalgebra::Vector3;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rand_distr::Normal;
pub trait NoiseModel: Send {
fn apply(&mut self, true_value: Vector3<f64>) -> Vector3<f64>;
}
pub struct GaussianNoise {
dist_x: Normal<f64>,
dist_y: Normal<f64>,
dist_z: Normal<f64>,
rng: StdRng,
}
impl GaussianNoise {
pub fn new(sigma: Vector3<f64>, seed: u64) -> Self {
Self {
dist_x: Normal::new(0.0, sigma.x).expect("sigma.x must be non-negative"),
dist_y: Normal::new(0.0, sigma.y).expect("sigma.y must be non-negative"),
dist_z: Normal::new(0.0, sigma.z).expect("sigma.z must be non-negative"),
rng: StdRng::seed_from_u64(seed),
}
}
pub fn isotropic(sigma: f64, seed: u64) -> Self {
Self::new(Vector3::new(sigma, sigma, sigma), seed)
}
}
impl NoiseModel for GaussianNoise {
fn apply(&mut self, true_value: Vector3<f64>) -> Vector3<f64> {
Vector3::new(
true_value.x + self.rng.sample(self.dist_x),
true_value.y + self.rng.sample(self.dist_y),
true_value.z + self.rng.sample(self.dist_z),
)
}
}
pub struct BiasRandomWalk {
bias: Vector3<f64>,
dist_x: Normal<f64>,
dist_y: Normal<f64>,
dist_z: Normal<f64>,
rng: StdRng,
}
impl BiasRandomWalk {
pub fn new(sigma_drift: Vector3<f64>, dt: f64, seed: u64) -> Self {
assert!(dt > 0.0, "dt must be positive");
let scale = dt.sqrt();
Self {
bias: Vector3::zeros(),
dist_x: Normal::new(0.0, sigma_drift.x * scale)
.expect("sigma_drift.x must be non-negative"),
dist_y: Normal::new(0.0, sigma_drift.y * scale)
.expect("sigma_drift.y must be non-negative"),
dist_z: Normal::new(0.0, sigma_drift.z * scale)
.expect("sigma_drift.z must be non-negative"),
rng: StdRng::seed_from_u64(seed),
}
}
pub fn isotropic(sigma_drift: f64, dt: f64, seed: u64) -> Self {
Self::new(
Vector3::new(sigma_drift, sigma_drift, sigma_drift),
dt,
seed,
)
}
}
impl NoiseModel for BiasRandomWalk {
fn apply(&mut self, true_value: Vector3<f64>) -> Vector3<f64> {
let step = Vector3::new(
self.rng.sample(self.dist_x),
self.rng.sample(self.dist_y),
self.rng.sample(self.dist_z),
);
self.bias += step;
true_value + self.bias
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gaussian_noise_is_deterministic() {
let mut n1 = GaussianNoise::isotropic(1e-5, 42);
let mut n2 = GaussianNoise::isotropic(1e-5, 42);
let v = Vector3::new(1.0, 2.0, 3.0);
assert_eq!(n1.apply(v), n2.apply(v));
}
#[test]
fn gaussian_noise_different_seeds_differ() {
let mut n1 = GaussianNoise::isotropic(1e-3, 42);
let mut n2 = GaussianNoise::isotropic(1e-3, 99);
let v = Vector3::new(1.0, 2.0, 3.0);
assert_ne!(n1.apply(v), n2.apply(v));
}
#[test]
fn gaussian_noise_zero_sigma_is_identity() {
let mut n = GaussianNoise::isotropic(0.0, 42);
let v = Vector3::new(1.0, 2.0, 3.0);
assert_eq!(n.apply(v), v);
}
#[test]
fn bias_random_walk_accumulates() {
let mut brw = BiasRandomWalk::isotropic(1e-4, 1.0, 42);
let v = Vector3::zeros();
let first = brw.apply(v);
let second = brw.apply(v);
assert_ne!(first, second);
}
#[test]
fn bias_random_walk_is_deterministic() {
let mut b1 = BiasRandomWalk::isotropic(1e-4, 1.0, 42);
let mut b2 = BiasRandomWalk::isotropic(1e-4, 1.0, 42);
let v = Vector3::new(1.0, 2.0, 3.0);
assert_eq!(b1.apply(v), b2.apply(v));
assert_eq!(b1.apply(v), b2.apply(v));
}
#[test]
fn gaussian_noise_magnitude_is_reasonable() {
let sigma = 1e-5;
let mut n = GaussianNoise::isotropic(sigma, 42);
let v = Vector3::zeros();
let n_samples = 10_000;
let mut sum_sq = 0.0;
for _ in 0..n_samples {
let noisy = n.apply(v);
sum_sq += noisy.magnitude_squared();
}
let empirical_rms = (sum_sq / n_samples as f64).sqrt();
let expected_rms = (3.0_f64).sqrt() * sigma;
assert!(
(empirical_rms - expected_rms).abs() < 0.5 * expected_rms,
"empirical RMS {empirical_rms:.3e} too far from expected {expected_rms:.3e}"
);
}
}