Skip to content

Commit c930e4a

Browse files
committed
fix: fgn padded n/dt
1 parent 1452aaa commit c930e4a

4 files changed

Lines changed: 75 additions & 11 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stochastic-rs"
3-
version = "1.2.0"
3+
version = "1.2.1"
44
edition = "2024"
55
license = "MIT"
66
description = "A Rust library for quant finance and simulating stochastic processes."

src/stochastic/diffusion/fou.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! # fOU
22
//!
33
//! $$
4-
//! dX_t=\kappa(\theta-X_t)\,dt+\sigma\,dB_t^H
4+
//! dX_t=\theta(\mu-X_t)\,dt+\sigma\,dB_t^H
55
//! $$
66
//!
77
use ndarray::Array1;
@@ -13,9 +13,9 @@ use crate::traits::ProcessExt;
1313
pub struct FOU<T: FloatExt> {
1414
/// Hurst exponent controlling roughness and long-memory.
1515
pub hurst: T,
16-
/// Long-run target level / model location parameter.
16+
/// Mean-reversion speed.
1717
pub theta: T,
18-
/// Drift / long-run mean-level parameter.
18+
/// Long-run mean level.
1919
pub mu: T,
2020
/// Diffusion / noise scale parameter.
2121
pub sigma: T,
@@ -66,3 +66,42 @@ py_process_1d!(PyFOU, FOU,
6666
sig: (hurst, theta, mu, sigma, n, x0=None, t=None, dtype=None),
6767
params: (hurst: f64, theta: f64, mu: f64, sigma: f64, n: usize, x0: Option<f64>, t: Option<f64>)
6868
);
69+
70+
#[cfg(test)]
71+
mod tests {
72+
use super::FOU;
73+
use crate::traits::ProcessExt;
74+
75+
#[test]
76+
#[should_panic(expected = "n must be at least 2")]
77+
fn fou_requires_at_least_two_points() {
78+
let _ = FOU::<f64>::new(0.7, 1.0, 0.0, 0.2, 1, Some(0.0), Some(1.0));
79+
}
80+
81+
#[test]
82+
fn fou_sigma_zero_matches_deterministic_euler() {
83+
let theta = 1.3_f64;
84+
let mu = 0.8_f64;
85+
let n = 129_usize;
86+
let x0 = 0.2_f64;
87+
let t = 1.0_f64;
88+
89+
let p = FOU::<f64>::new(0.7, theta, mu, 0.0, n, Some(x0), Some(t));
90+
let x = p.sample();
91+
92+
let dt = t / (n as f64 - 1.0);
93+
let mut expected = x0;
94+
for i in 1..n {
95+
expected = expected + theta * (mu - expected) * dt;
96+
assert!((x[i] - expected).abs() < 1e-12, "mismatch at index {i}");
97+
}
98+
}
99+
100+
#[test]
101+
fn fou_sample_is_finite() {
102+
let p = FOU::<f64>::new(0.65, 1.0, 0.0, 0.5, 256, Some(0.1), Some(1.0));
103+
let x = p.sample();
104+
assert_eq!(x.len(), 256);
105+
assert!(x.iter().all(|v| v.is_finite()));
106+
}
107+
}

src/stochastic/noise/fgn/core.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::traits::FloatExt;
1818
pub struct FGN<T: FloatExt> {
1919
/// Hurst exponent controlling roughness and long-memory.
2020
pub hurst: T,
21-
/// Number of discrete simulation points (or samples).
21+
/// Internal FFT length (power-of-two padded).
2222
pub n: usize,
2323
/// Total simulation horizon (defaults to 1 when omitted).
2424
pub t: Option<T>,
@@ -34,7 +34,8 @@ pub struct FGN<T: FloatExt> {
3434

3535
impl<T: FloatExt> FGN<T> {
3636
pub fn dt(&self) -> T {
37-
self.t.unwrap_or(T::one()) / T::from_usize_(self.n)
37+
let step_count = self.out_len.max(1);
38+
self.t.unwrap_or(T::one()) / T::from_usize_(step_count)
3839
}
3940

4041
#[must_use]
@@ -78,13 +79,15 @@ impl<T: FloatExt> FGN<T> {
7879
};
7980
}
8081

82+
let scale_n = out_len.max(1);
83+
8184
Self {
8285
hurst,
8386
n,
8487
offset,
8588
out_len,
8689
t,
87-
scale: T::from_usize_(n).powf(-hurst) * t.unwrap_or(T::one()).powf(hurst),
90+
scale: T::from_usize_(scale_n).powf(-hurst) * t.unwrap_or(T::one()).powf(hurst),
8891
sqrt_eigenvalues: Arc::new(sqrt_eigenvalues),
8992
fft_handler: Arc::new(FftHandler::new(2 * n)),
9093
}
@@ -114,3 +117,23 @@ impl<T: FloatExt> FGN<T> {
114117
fgn
115118
}
116119
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::FGN;
124+
125+
#[test]
126+
fn dt_and_scale_use_requested_length_not_fft_padding() {
127+
let n = 1000_usize;
128+
let h = 0.7_f64;
129+
let t = 2.0_f64;
130+
let fgn = FGN::<f64>::new(h, n, Some(t));
131+
132+
// Internal FFT length is padded, but time-step must follow requested n.
133+
assert_eq!(fgn.n, 1024);
134+
assert!((fgn.dt() - (t / n as f64)).abs() < 1e-15);
135+
136+
let expected_scale = (n as f64).powf(-h) * t.powf(h);
137+
assert!((fgn.scale - expected_scale).abs() < 1e-15);
138+
}
139+
}

src/stochastic/noise/fgn/cuda.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,11 @@ impl<T: FloatExt> FGN<T> {
169169
fn sample_cuda_f32(&self, m: usize) -> Result<Either<Array1<T>, Array2<T>>> {
170170
let n = self.n;
171171
let offset = self.offset;
172+
let out_size = n - offset;
173+
let scale_steps = out_size.max(1);
172174
let hurst = self.hurst.to_f64().unwrap();
173175
let t = self.t.unwrap_or(T::one()).to_f64().unwrap();
174-
let scale = (n as f32).powf(-(hurst as f32)) * (t as f32).powf(hurst as f32);
175-
let out_size = n - offset;
176+
let scale = (scale_steps as f32).powf(-(hurst as f32)) * (t as f32).powf(hurst as f32);
176177

177178
let need_init = {
178179
let guard = CUDA_CONTEXT_F32.lock().unwrap();
@@ -260,10 +261,11 @@ impl<T: FloatExt> FGN<T> {
260261
fn sample_cuda_f64(&self, m: usize) -> Result<Either<Array1<T>, Array2<T>>> {
261262
let n = self.n;
262263
let offset = self.offset;
264+
let out_size = n - offset;
265+
let scale_steps = out_size.max(1);
263266
let hurst = self.hurst.to_f64().unwrap();
264267
let t = self.t.unwrap_or(T::one()).to_f64().unwrap();
265-
let scale = (n as f64).powf(-hurst) * t.powf(hurst);
266-
let out_size = n - offset;
268+
let scale = (scale_steps as f64).powf(-hurst) * t.powf(hurst);
267269

268270
let need_init = {
269271
let guard = CUDA_CONTEXT_F64.lock().unwrap();

0 commit comments

Comments
 (0)