use crate::models::laplace::dist::Gaussian;
use crate::models::laplace::leaf::Leaf;
pub struct OuLeaf {
alpha_mean: f64,
theta: f64,
mean: Option<f64>,
last: Option<f64>,
s_xx: f64,
s_xdy: f64,
n: usize,
ss: f64,
mean_resid: f64,
}
impl OuLeaf {
pub fn new(alpha_mean: f64) -> Self {
Self {
alpha_mean: alpha_mean.clamp(1e-3, 1.0 - 1e-3),
theta: 0.0,
mean: None,
last: None,
s_xx: 0.0,
s_xdy: 0.0,
n: 0,
ss: 0.0,
mean_resid: 0.0,
}
}
fn sigma(&self) -> f64 {
if self.n < 2 {
return 1.0;
}
(self.ss / (self.n as f64 - 1.0)).sqrt().max(1e-9)
}
}
impl Leaf for OuLeaf {
fn name(&self) -> &'static str {
"ou"
}
fn predict(&self, horizon: usize) -> Vec<Gaussian> {
let mu = self.mean.unwrap_or(0.0);
let last = self.last.unwrap_or(mu);
let sigma = self.sigma();
let phi = (1.0 - self.theta).clamp(-0.999, 0.999);
let phi2 = phi * phi;
(1..=horizon)
.map(|h| {
let mean = mu + phi.powi(h as i32) * (last - mu);
let var_scale = if (1.0 - phi2).abs() < 1e-12 {
h as f64
} else {
(1.0 - phi2.powi(h as i32)) / (1.0 - phi2)
};
Gaussian::new(mean, sigma * var_scale.sqrt())
})
.collect()
}
fn observe(&mut self, y: f64) {
let mu_before = self.mean.unwrap_or(y);
let last = self.last.unwrap_or(mu_before);
let phi = (1.0 - self.theta).clamp(-0.999, 0.999);
let predicted = mu_before + phi * (last - mu_before);
let resid = y - predicted;
self.n += 1;
let delta = resid - self.mean_resid;
self.mean_resid += delta / self.n as f64;
self.ss += delta * (resid - self.mean_resid);
let x = last - mu_before;
let dy = y - last;
self.s_xx += x * x;
self.s_xdy += x * dy;
if self.s_xx > 1e-12 {
let theta = -self.s_xdy / self.s_xx;
self.theta = theta.clamp(1e-3, 1.0 - 1e-3);
}
self.mean = Some(match self.mean {
Some(m) => self.alpha_mean * y + (1.0 - self.alpha_mean) * m,
None => y,
});
self.last = Some(y);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn feed_ou(leaf: &mut OuLeaf, theta: f64, mu: f64, n: usize) {
let mut y = mu;
for i in 0..n {
let noise = ((i as f64 * 12.9898).sin() * 43758.5453).fract() - 0.5;
y += theta * (mu - y) + noise;
leaf.observe(y);
}
}
#[test]
fn theta_is_positive_on_mean_reverting_process() {
let mut leaf = OuLeaf::new(0.05);
feed_ou(&mut leaf, 0.3, 5.0, 1000);
assert!(
leaf.theta > 0.0 && leaf.theta < 1.0,
"θ = {} not in (0, 1)",
leaf.theta
);
}
#[test]
fn far_horizon_forecast_reverts_toward_mu() {
let mut leaf = OuLeaf::new(0.05);
feed_ou(&mut leaf, 0.4, 10.0, 500);
leaf.observe(50.0);
let preds = leaf.predict(20);
let mu_estimate = leaf.mean.unwrap();
assert!(
(preds[19].mean - mu_estimate).abs() < (50.0 - mu_estimate).abs() * 0.5,
"h=20 forecast {} should have reverted toward μ≈{}",
preds[19].mean,
mu_estimate
);
}
#[test]
fn cold_start_produces_finite_predictions() {
let mut leaf = OuLeaf::new(0.1);
leaf.observe(3.0);
leaf.observe(4.0);
let preds = leaf.predict(5);
for (h, p) in preds.iter().enumerate() {
assert!(p.mean.is_finite(), "h={}: mean not finite", h + 1);
assert!(p.std.is_finite() && p.std > 0.0, "h={}: std invalid", h + 1);
}
}
}