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//!
77use ndarray:: Array1 ;
@@ -13,9 +13,9 @@ use crate::traits::ProcessExt;
1313pub 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+ }
0 commit comments