use crate::changepoint::detector::Cost;
use crate::changepoint::signal::Signal;
use crate::error::{ForecastError, Result};
#[derive(Debug, Clone)]
pub struct CostRbf {
gamma: Option<f64>,
n: usize,
cum_diag: Vec<f64>,
gram2d: Vec<f64>,
}
impl CostRbf {
pub fn auto() -> Self {
Self {
gamma: None,
n: 0,
cum_diag: Vec::new(),
gram2d: Vec::new(),
}
}
pub fn with_gamma(gamma: f64) -> Self {
Self {
gamma: Some(gamma),
n: 0,
cum_diag: Vec::new(),
gram2d: Vec::new(),
}
}
#[inline]
fn gram2d_at(&self, a: usize, b: usize) -> f64 {
self.gram2d[a * (self.n + 1) + b]
}
}
impl Cost for CostRbf {
fn fit(&mut self, signal: &Signal) -> Result<()> {
let n = signal.n();
self.n = n;
let gamma = match self.gamma {
Some(g) => g,
None => median_heuristic_gamma(signal),
};
self.cum_diag = vec![0.0; n + 1];
self.gram2d = vec![0.0; (n + 1) * (n + 1)];
for i in 0..n {
let xi = signal.row(i);
self.cum_diag[i + 1] = self.cum_diag[i] + 1.0; for j in 0..n {
let xj = signal.row(j);
let sq: f64 = xi.iter().zip(xj.iter()).map(|(a, b)| (a - b).powi(2)).sum();
let k = (-gamma * sq).exp();
let a = (i + 1) * (n + 1) + (j + 1);
let top = self.gram2d[i * (n + 1) + (j + 1)];
let left = self.gram2d[(i + 1) * (n + 1) + j];
let diag = self.gram2d[i * (n + 1) + j];
self.gram2d[a] = top + left - diag + k;
}
}
self.gamma = Some(gamma);
Ok(())
}
fn error(&self, start: usize, end: usize) -> Result<f64> {
if end <= start || end > self.n {
return Err(ForecastError::InvalidParameter(format!(
"CostRbf: invalid segment [{}, {}) for n = {}",
start, end, self.n
)));
}
let n_seg = (end - start) as f64;
let diag = self.cum_diag[end] - self.cum_diag[start];
let g_ee = self.gram2d_at(end, end);
let g_es = self.gram2d_at(end, start);
let g_se = self.gram2d_at(start, end);
let g_ss = self.gram2d_at(start, start);
let gram = g_ee - g_es - g_se + g_ss;
Ok((diag - gram / n_seg).max(0.0))
}
fn min_size(&self) -> usize {
1
}
fn name(&self) -> &str {
"rbf"
}
}
fn median_heuristic_gamma(signal: &Signal) -> f64 {
let n = signal.n();
if n < 2 {
return 1.0;
}
let max_pairs = 5000;
let total_pairs = n * (n - 1) / 2;
let step = (total_pairs / max_pairs).max(1);
let mut dists = Vec::new();
let mut k = 0;
for i in 0..n {
for j in (i + 1)..n {
if k % step == 0 {
let xi = signal.row(i);
let xj = signal.row(j);
let sq: f64 = xi.iter().zip(xj.iter()).map(|(a, b)| (a - b).powi(2)).sum();
dists.push(sq);
}
k += 1;
}
}
if dists.is_empty() {
return 1.0;
}
dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let med = dists[dists.len() / 2];
if med <= f64::EPSILON {
1.0
} else {
1.0 / med
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rbf_cost_finite_on_level_shift() {
let values: Vec<f64> = (0..40).map(|i| if i < 20 { 0.0 } else { 10.0 }).collect();
let s = Signal::univariate(&values);
let mut c = CostRbf::auto();
c.fit(&s).unwrap();
let e = c.error(0, 40).unwrap();
assert!(e.is_finite() && e >= 0.0);
let left = c.error(0, 20).unwrap();
let right = c.error(20, 40).unwrap();
assert!(left + right < e + 1e-6);
}
#[test]
fn rbf_cost_with_explicit_gamma() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let s = Signal::univariate(&values);
let mut c = CostRbf::with_gamma(0.5);
c.fit(&s).unwrap();
assert!(c.error(0, 5).unwrap().is_finite());
}
}