use crate::solve_ridge;
pub struct ArPredictor {
pub weights: Vec<f64>,
pub bias: f64,
pub p: usize,
}
impl ArPredictor {
pub fn fit(train: &[f64], p: usize, lambda: f64) -> Option<ArPredictor> {
let n = train.len();
if n < p * 3 + 10 {
return None;
}
let mean = train.iter().sum::<f64>() / n as f64;
let sd = {
let v = train.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n as f64;
crate::sqrt(v).max(1e-9)
};
let z: Vec<f64> = train.iter().map(|x| (x - mean) / sd).collect();
let rows = n - p;
let dim = p; let mut xtx = vec![0.0f64; dim * dim];
let mut xty = vec![0.0f64; dim];
for t in p..n {
let y = z[t];
for i in 0..p {
let xi = z[t - 1 - i];
xty[i] += xi * y;
for j in 0..p {
xtx[i * dim + j] += xi * z[t - 1 - j];
}
}
}
let l = lambda * rows as f64;
for i in 0..dim {
xtx[i * dim + i] += l;
}
let mut w = xty.clone();
if !solve_ridge(&mut xtx, &mut w, dim) {
return None;
}
let wsum: f64 = w.iter().sum();
Some(ArPredictor { bias: mean * (1.0 - wsum), weights: w, p })
}
pub fn residuals(&self, series: &[f64]) -> Vec<f64> {
let mut out = vec![0.0f64; series.len()];
for t in self.p..series.len() {
let mut pred = self.bias;
for i in 0..self.p {
pred += self.weights[i] * series[t - 1 - i];
}
out[t] = (series[t] - pred).abs();
}
out
}
}
pub fn ewma(x: &[f64], span: usize) -> Vec<f64> {
let alpha = 2.0 / (span as f64 + 1.0);
let mut out = Vec::with_capacity(x.len());
let mut s = x.first().copied().unwrap_or(0.0);
for &v in x {
s = alpha * v + (1.0 - alpha) * s;
out.push(s);
}
out
}
pub fn find_epsilon(errors: &[f64]) -> f64 {
find_epsilon_from(errors, 2.5)
}
pub fn find_epsilon_from(errors: &[f64], z_min: f64) -> f64 {
let n = errors.len() as f64;
let mean = errors.iter().sum::<f64>() / n;
let sd = crate::sqrt(errors.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n)
.max(1e-12);
let mut best_eps = mean + 12.0 * sd;
let mut best_score = f64::MIN;
let mut z = z_min;
while z <= 12.0 {
let eps = mean + z * sd;
let below: Vec<f64> = errors.iter().cloned().filter(|&e| e < eps).collect();
let n_above = errors.len() - below.len();
if n_above == 0 {
z += 0.5;
continue;
}
let bm = below.iter().sum::<f64>() / below.len() as f64;
let bsd = crate::sqrt(
below.iter().map(|x| (x - bm) * (x - bm)).sum::<f64>() / below.len() as f64,
);
let mut seqs = 0usize;
let mut prev_above = false;
for &e in errors {
let above = e >= eps;
if above && !prev_above {
seqs += 1;
}
prev_above = above;
}
let score = ((mean - bm) / mean + (sd - bsd) / sd)
/ (n_above as f64 + (seqs * seqs) as f64);
if score > best_score {
best_score = score;
best_eps = eps;
}
z += 0.5;
}
best_eps
}
pub fn anomaly_sequences(errors: &[f64], eps: f64, buffer: usize) -> Vec<(usize, usize)> {
let n = errors.len();
let mut seqs: Vec<(usize, usize)> = Vec::new();
let mut start: Option<usize> = None;
for t in 0..n {
if errors[t] >= eps {
if start.is_none() {
start = Some(t);
}
} else if let Some(s) = start.take() {
seqs.push((s.saturating_sub(buffer), (t - 1 + buffer).min(n - 1)));
}
}
if let Some(s) = start {
seqs.push((s.saturating_sub(buffer), n - 1));
}
let mut merged: Vec<(usize, usize)> = Vec::new();
for (s, e) in seqs {
if let Some(last) = merged.last_mut() {
if s <= last.1 + 1 {
last.1 = last.1.max(e);
continue;
}
}
merged.push((s, e));
}
merged
}
pub fn prune_sequences(
errors: &[f64],
seqs: &[(usize, usize)],
p_prune: f64,
) -> Vec<(usize, usize)> {
if seqs.is_empty() {
return Vec::new();
}
let mut scored: Vec<(f64, (usize, usize))> = seqs
.iter()
.map(|&(s, e)| {
let m = errors[s..=e].iter().cloned().fold(0.0f64, f64::max);
(m, (s, e))
})
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
let mut in_seq = vec![false; errors.len()];
for &(s, e) in seqs {
for slot in in_seq.iter_mut().take(e + 1).skip(s) {
*slot = true;
}
}
let floor = errors
.iter()
.zip(in_seq.iter())
.filter(|(_, &m)| !m)
.map(|(&e, _)| e)
.fold(0.0f64, f64::max);
let mut keep = scored.len();
for i in 0..scored.len() {
let next = if i + 1 < scored.len() { scored[i + 1].0 } else { floor };
let drop = if scored[i].0 > 1e-12 {
(scored[i].0 - next) / scored[i].0
} else {
0.0
};
if drop < p_prune {
keep = i;
break;
}
}
scored.truncate(keep);
let mut out: Vec<(usize, usize)> = scored.into_iter().map(|(_, se)| se).collect();
out.sort_unstable();
out
}
pub fn detect_channel(train: &[f64], test: &[f64], p: usize) -> Vec<(usize, usize)> {
detect_channel_tuned(train, test, p, 2.5, 0.13, 50)
}
pub fn detect_channel_tuned(
train: &[f64],
test: &[f64],
p: usize,
z_min: f64,
p_prune: f64,
buffer: usize,
) -> Vec<(usize, usize)> {
let chosen_p = if p == 0 {
let split = train.len() * 4 / 5;
let (tr, va) = train.split_at(split);
let mut best = (5usize, f64::MAX);
for &cand in &[3usize, 5, 10, 25, 50] {
if let Some(m) = ArPredictor::fit(tr, cand, 1e-4) {
let res = m.residuals(va);
let mse: f64 = res[cand.min(res.len())..]
.iter()
.map(|e| e * e)
.sum::<f64>()
/ res.len().max(1) as f64;
if mse < best.1 {
best = (cand, mse);
}
}
}
best.0
} else {
p
};
let p = chosen_p;
let pred = match ArPredictor::fit(train, p, 1e-4) {
Some(m) => m,
None => return Vec::new(),
};
let res = pred.residuals(test);
let span = (test.len() / 30).clamp(10, 300);
let var_window = span.max(20);
let mut res_var = vec![0.0f64; res.len()];
for t in var_window..res.len() {
let w = &res[t + 1 - var_window..t + 1];
let m = w.iter().sum::<f64>() / var_window as f64;
res_var[t] = w.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / var_window as f64;
}
let sm_mag = ewma(&res, span);
let sm_var = ewma(&res_var, span);
let train_res = pred.residuals(train);
let train_sm_var = {
let mut rv = vec![0.0f64; train_res.len()];
for t in var_window..train_res.len() {
let w = &train_res[t + 1 - var_window..t + 1];
let m = w.iter().sum::<f64>() / var_window as f64;
rv[t] = w.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / var_window as f64;
}
ewma(&rv, span)
};
let var_mean = train_sm_var[p..].iter().sum::<f64>() / (train_sm_var.len() - p).max(1) as f64;
let var_sd = crate::sqrt(
train_sm_var[p..].iter().map(|x| (x - var_mean) * (x - var_mean)).sum::<f64>()
/ (train_sm_var.len() - p).max(1) as f64,
).max(1e-12);
let sm = sm_mag
.iter()
.zip(sm_var.iter())
.map(|(&m, &v)| {
let zv = ((v - var_mean) / var_sd).max(0.0);
m + (zv * var_sd * 0.3).max(0.0) })
.collect::<Vec<f64>>();
const EPS_WINDOW: usize = 2100;
let n = sm.len();
let gfloor = {
let mut s: Vec<f64> = sm[p..].to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = s[s.len() / 2];
let mut dev: Vec<f64> = s.iter().map(|x| (x - med).abs()).collect();
dev.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mad = dev[dev.len() / 2];
med + 3.0 * 1.4826 * mad
};
let mut seqs: Vec<(usize, usize)> = Vec::new();
let global_eps = find_epsilon_from(&sm[p..], z_min);
seqs.extend(anomaly_sequences(&sm, global_eps, buffer));
for phase in [0usize] {
let mut start = p + phase;
while start < n {
let end = (start + EPS_WINDOW).min(n);
let w = &sm[start..end];
if w.len() >= 200 {
let eps = find_epsilon_from(w, z_min).max(gfloor);
for (s, e) in anomaly_sequences(w, eps, buffer) {
seqs.push((start + s, (start + e).min(n - 1)));
}
}
if end == n {
break;
}
start = end;
}
}
seqs.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::new();
for (s, e) in seqs {
if let Some(last) = merged.last_mut() {
if s <= last.1 + 1 {
last.1 = last.1.max(e);
continue;
}
}
merged.push((s, e));
}
prune_sequences(&sm, &merged, p_prune)
}
pub fn dfa_sequences(
train: &[f64],
test: &[f64],
window: usize,
z_min: f64,
buffer: usize,
) -> Vec<(usize, usize)> {
let stride = 8usize;
if train.len() < 3 * window || test.len() < window {
return Vec::new();
}
let mut buf = Vec::new();
let alphas_of = |series: &[f64], buf: &mut Vec<f64>| -> Vec<f64> {
let mut out = Vec::new();
let mut end = window;
while end <= series.len() {
out.push(crate::dfa_fast_into(&series[end - window..end], buf).alpha);
end += stride;
}
out
};
let train_a = alphas_of(train, &mut buf);
let m = train_a.iter().sum::<f64>() / train_a.len() as f64;
let sd = crate::sqrt(
train_a.iter().map(|a| (a - m) * (a - m)).sum::<f64>() / train_a.len() as f64,
)
.max(1e-6);
let test_a = alphas_of(test, &mut buf);
let z: Vec<f64> = test_a.iter().map(|a| (a - m).abs() / sd).collect();
let eps = find_epsilon_from(&z, z_min.max(4.0));
let raw = anomaly_sequences(&z, eps, buffer / stride);
prune_sequences(&z, &raw, 0.13)
.into_iter()
.map(|(s, e)| (s * stride, (e * stride + window - 1).min(test.len() - 1)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ar_predictor_learns_a_sine() {
let train: Vec<f64> = (0..2000).map(|i| crate::sin(i as f64 * 0.1)).collect();
let m = ArPredictor::fit(&train, 10, 1e-4).expect("fit");
let test: Vec<f64> = (2000..3000).map(|i| crate::sin(i as f64 * 0.1)).collect();
let res = m.residuals(&test);
let mean_res = res[10..].iter().sum::<f64>() / (res.len() - 10) as f64;
assert!(mean_res < 1e-3, "sine must be nearly perfectly predicted, got {}", mean_res);
}
#[test]
fn injected_burst_is_detected_and_isolated() {
let mut rng = 987654321u64;
let mut noise = || {
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
(rng >> 33) as f64 / (1u64 << 31) as f64 - 0.5
};
let train: Vec<f64> = (0..3000)
.map(|i| crate::sin(i as f64 * 0.05) + 0.05 * noise())
.collect();
let mut test: Vec<f64> = (0..3000)
.map(|i| crate::sin(i as f64 * 0.05) + 0.05 * noise())
.collect();
for item in test.iter_mut().skip(1500).take(120) {
*item += 1.5;
}
let seqs = detect_channel(&train, &test, 20);
assert!(!seqs.is_empty(), "burst must be detected");
let hit = seqs.iter().any(|&(s, e)| s <= 1620 && e >= 1500);
assert!(hit, "detected sequences {:?} must overlap the burst", seqs);
assert!(seqs.len() <= 3, "pruning must keep it tight, got {:?}", seqs);
}
}