use ndarray::Array1;
use stochastic_rs_core::simd_rng::Deterministic;
use stochastic_rs_core::simd_rng::SeedExt;
use stochastic_rs_core::simd_rng::Unseeded;
use crate::noise::fgn::Fgn;
use crate::traits::FloatExt;
use crate::traits::ProcessExt;
pub struct Fou<T: FloatExt, S: SeedExt = Unseeded> {
pub hurst: T,
pub theta: T,
pub mu: T,
pub sigma: T,
pub n: usize,
pub x0: Option<T>,
pub t: Option<T>,
pub seed: S,
fgn: Fgn<T>,
}
impl<T: FloatExt> Fou<T> {
#[must_use]
pub fn new(hurst: T, theta: T, mu: T, sigma: T, n: usize, x0: Option<T>, t: Option<T>) -> Self {
assert!(n >= 2, "n must be at least 2");
Self {
hurst,
theta,
mu,
sigma,
n,
x0,
t,
seed: Unseeded,
fgn: Fgn::new(hurst, n - 1, t),
}
}
}
impl<T: FloatExt> Fou<T, Deterministic> {
#[must_use]
pub fn seeded(
hurst: T,
theta: T,
mu: T,
sigma: T,
n: usize,
x0: Option<T>,
t: Option<T>,
seed: u64,
) -> Self {
assert!(n >= 2, "n must be at least 2");
Self {
hurst,
theta,
mu,
sigma,
n,
x0,
t,
seed: Deterministic::new(seed),
fgn: Fgn::new(hurst, n - 1, t),
}
}
}
impl<T: FloatExt, S: SeedExt> ProcessExt<T> for Fou<T, S> {
type Output = Array1<T>;
fn sample(&self) -> Self::Output {
let dt = self.fgn.dt();
let fgn = self.fgn.sample_cpu_impl(&self.seed.derive());
let mut fou = Array1::<T>::zeros(self.n);
fou[0] = self.x0.unwrap_or(T::zero());
for i in 1..self.n {
fou[i] = fou[i - 1] + self.theta * (self.mu - fou[i - 1]) * dt + self.sigma * fgn[i - 1];
}
fou
}
}
py_process_1d!(PyFou, Fou,
sig: (hurst, theta, mu, sigma, n, x0=None, t=None, seed=None, dtype=None),
params: (hurst: f64, theta: f64, mu: f64, sigma: f64, n: usize, x0: Option<f64>, t: Option<f64>)
);
#[cfg(test)]
mod tests {
use super::Fou;
use crate::traits::ProcessExt;
#[test]
#[should_panic(expected = "n must be at least 2")]
fn fou_requires_at_least_two_points() {
let _ = Fou::<f64>::new(0.7, 1.0, 0.0, 0.2, 1, Some(0.0), Some(1.0));
}
#[test]
fn fou_sigma_zero_matches_deterministic_euler() {
let theta = 1.3_f64;
let mu = 0.8_f64;
let n = 129_usize;
let x0 = 0.2_f64;
let t = 1.0_f64;
let p = Fou::<f64>::new(0.7, theta, mu, 0.0, n, Some(x0), Some(t));
let x = p.sample();
let dt = t / (n as f64 - 1.0);
let mut expected = x0;
for i in 1..n {
expected = expected + theta * (mu - expected) * dt;
assert!((x[i] - expected).abs() < 1e-12, "mismatch at index {i}");
}
}
#[test]
fn fou_dt_alignment_holds_for_multiple_grid_sizes() {
let theta = 0.9_f64;
let mu = -0.1_f64;
let x0 = 0.35_f64;
let hs = [0.55_f64, 0.9_f64];
let ns = [3_usize, 17, 129, 1000];
let ts = [0.7_f64, 2.0_f64];
for &h in &hs {
for &n in &ns {
for &t in &ts {
let p = Fou::<f64>::new(h, theta, mu, 0.0, n, Some(x0), Some(t));
let x = p.sample();
let dt = t / (n as f64 - 1.0);
let mut expected = x0;
for i in 1..n {
expected = expected + theta * (mu - expected) * dt;
assert!(
(x[i] - expected).abs() < 1e-12,
"mismatch at i={i}, n={n}, t={t}, h={h}"
);
}
}
}
}
}
#[test]
fn fou_sample_is_finite() {
let p = Fou::<f64>::new(0.65, 1.0, 0.0, 0.5, 256, Some(0.1), Some(1.0));
let x = p.sample();
assert_eq!(x.len(), 256);
assert!(x.iter().all(|v| v.is_finite()));
}
}