use super::super::dist::Gaussian;
use super::super::leaf::Leaf;
pub struct StandardizeWrapper {
inner: Box<dyn Leaf + Send>,
alpha: f64,
mu: f64,
var: f64,
initialized: bool,
label: String,
}
impl StandardizeWrapper {
pub fn new(inner: Box<dyn Leaf + Send>, alpha: f64) -> Self {
let alpha = alpha.clamp(1e-4, 0.999);
let label = format!("{}@std{:.2}", inner.name(), alpha);
Self {
inner,
alpha,
mu: 0.0,
var: 0.0,
initialized: false,
label,
}
}
fn sigma(&self) -> f64 {
if self.var.is_finite() && self.var > 1e-16 {
self.var.sqrt().max(1e-8)
} else {
1e-8
}
}
}
impl Leaf for StandardizeWrapper {
fn name(&self) -> &'static str {
Box::leak(self.label.clone().into_boxed_str())
}
fn predict(&self, horizon: usize) -> Vec<Gaussian> {
let sigma = self.sigma();
let mu = self.mu;
let inner = self.inner.predict(horizon);
inner
.into_iter()
.map(|g| Gaussian::new(mu + sigma * g.mean, (sigma * g.std).max(1e-9)))
.collect()
}
#[inline]
fn predict_one(&self) -> Gaussian {
let sigma = self.sigma();
let mu = self.mu;
let g = self.inner.predict_one();
Gaussian::new(mu + sigma * g.mean, (sigma * g.std).max(1e-9))
}
fn observe(&mut self, y: f64) {
if !y.is_finite() {
return;
}
if !self.initialized {
self.mu = y;
self.var = 0.0;
self.initialized = true;
self.inner.observe(0.0);
return;
}
let diff = y - self.mu;
self.mu += self.alpha * diff;
self.var = (1.0 - self.alpha) * self.var + self.alpha * diff * diff;
let sigma = self.sigma();
let y_prime = diff / sigma;
self.inner.observe(y_prime);
}
}
#[cfg(test)]
mod tests {
use super::super::EmaLeaf;
use super::*;
fn gauss_ll(y: f64, mu: f64, sigma: f64) -> f64 {
-0.5 * ((y - mu) / sigma).powi(2)
- sigma.max(1e-30).ln()
- 0.5 * (2.0 * std::f64::consts::PI).ln()
}
#[test]
fn mean_tracks_series_after_warmup() {
let mut w = StandardizeWrapper::new(Box::new(EmaLeaf::new(0.1)), 0.05);
for _ in 0..500 {
w.observe(42.0);
}
let g = w.predict(1)[0];
assert!(
(g.mean - 42.0).abs() < 1.0,
"prediction mean {} not near 42",
g.mean
);
}
#[test]
fn scale_tracks_residual_std() {
let mut w = StandardizeWrapper::new(Box::new(EmaLeaf::new(0.1)), 0.05);
let true_sigma = 3.0;
for i in 1..=2000 {
let u1 = ((i as f64 * 3.111).sin() * 43758.5453)
.fract()
.abs()
.max(1e-9);
let u2 = ((i as f64 * 5.777).cos() * 12345.6789)
.fract()
.abs()
.max(1e-9);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
w.observe(true_sigma * z);
}
assert!(
(w.sigma() - true_sigma).abs() < 1.0,
"sigma {} not near true {}",
w.sigma(),
true_sigma
);
}
#[test]
fn beats_raw_ema_on_step_change() {
let mk = || Box::new(EmaLeaf::new(0.1)) as Box<dyn Leaf + Send>;
let mut raw = mk();
let mut wrapped = StandardizeWrapper::new(mk(), 0.05);
let mut ys = Vec::new();
for i in 1..=1500 {
let u1 = ((i as f64 * 3.111).sin() * 43758.5453)
.fract()
.abs()
.max(1e-9);
let u2 = ((i as f64 * 5.777).cos() * 12345.6789)
.fract()
.abs()
.max(1e-9);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let scale = if i <= 500 { 1.0 } else { 5.0 };
ys.push(scale * z);
}
let mut ll_raw = 0.0;
let mut ll_wrap = 0.0;
for (i, y) in ys.iter().enumerate() {
let gr = raw.predict(1)[0];
let gw = wrapped.predict(1)[0];
if i > 1200 {
ll_raw += gauss_ll(*y, gr.mean, gr.std);
ll_wrap += gauss_ll(*y, gw.mean, gw.std);
}
raw.observe(*y);
wrapped.observe(*y);
}
assert!(
ll_wrap > ll_raw,
"standardize LL {ll_wrap} did not beat raw LL {ll_raw} on step-change scale"
);
}
}