use crate::complexity::{Complexity, ComplexityClass};
use crate::types::Precision;
use alloc::collections::BinaryHeap;
use alloc::vec::Vec;
use core::cmp::Ordering;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnomalyRow {
pub row: usize,
pub baseline: Precision,
pub current: Precision,
pub anomaly: Precision,
}
#[derive(Debug, Clone)]
struct MinHeapEntry(AnomalyRow);
impl PartialEq for MinHeapEntry {
fn eq(&self, other: &Self) -> bool {
self.0.anomaly == other.0.anomaly && self.0.row == other.0.row
}
}
impl Eq for MinHeapEntry {}
impl PartialOrd for MinHeapEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MinHeapEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.0
.anomaly
.partial_cmp(&self.0.anomaly)
.unwrap_or(Ordering::Equal)
.then_with(|| other.0.row.cmp(&self.0.row))
}
}
pub fn find_anomalous_rows(
baseline: &[Precision],
current: &[Precision],
k: usize,
) -> Vec<AnomalyRow> {
assert_eq!(
baseline.len(),
current.len(),
"find_anomalous_rows: baseline.len()={} != current.len()={}",
baseline.len(),
current.len(),
);
if k == 0 || baseline.is_empty() {
return Vec::new();
}
let mut heap: BinaryHeap<MinHeapEntry> = BinaryHeap::with_capacity(k.min(baseline.len()) + 1);
for (row, (&b, &c)) in baseline.iter().zip(current.iter()).enumerate() {
let anomaly = (c - b).abs();
let entry = MinHeapEntry(AnomalyRow {
row,
baseline: b,
current: c,
anomaly,
});
if heap.len() < k {
heap.push(entry);
} else if let Some(smallest) = heap.peek() {
if anomaly > smallest.0.anomaly {
heap.pop();
heap.push(entry);
}
}
}
let mut out: Vec<AnomalyRow> = heap.into_iter().map(|e| e.0).collect();
out.sort_by(|a, b| {
b.anomaly
.partial_cmp(&a.anomaly)
.unwrap_or(Ordering::Equal)
.then_with(|| a.row.cmp(&b.row))
});
out
}
pub fn find_rows_above_threshold(
baseline: &[Precision],
current: &[Precision],
threshold: Precision,
) -> Vec<AnomalyRow> {
assert_eq!(
baseline.len(),
current.len(),
"find_rows_above_threshold: dim mismatch {} vs {}",
baseline.len(),
current.len(),
);
baseline
.iter()
.zip(current.iter())
.enumerate()
.filter_map(|(row, (&b, &c))| {
let anomaly = (c - b).abs();
if anomaly > threshold {
Some(AnomalyRow {
row,
baseline: b,
current: c,
anomaly,
})
} else {
None
}
})
.collect()
}
pub struct FindAnomalousRowsOp;
impl Complexity for FindAnomalousRowsOp {
const CLASS: ComplexityClass = ComplexityClass::Adaptive {
default: &ComplexityClass::Linear,
worst: &ComplexityClass::Linear,
};
const DETAIL: &'static str =
"O(n log k) full-scan baseline today; phase-2 lowers to O(k · log n) via the \
sublinear-Neumann single-entry primitive.";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_inputs_return_empty() {
let v: Vec<Precision> = alloc::vec![];
assert_eq!(find_anomalous_rows(&v, &v, 5), alloc::vec![]);
assert_eq!(find_anomalous_rows(&v, &v, 0), alloc::vec![]);
}
#[test]
fn k_zero_returns_empty() {
let b = alloc::vec![1.0, 2.0, 3.0];
let c = alloc::vec![10.0, 20.0, 30.0];
assert_eq!(find_anomalous_rows(&b, &c, 0), alloc::vec![]);
}
#[test]
fn top_k_is_correct_for_small_case() {
let b = alloc::vec![1.0, 1.0, 1.0, 1.0, 1.0];
let c = alloc::vec![1.0, 5.0, 1.0, 9.0, 2.0];
let top = find_anomalous_rows(&b, &c, 3);
assert_eq!(top.len(), 3);
assert_eq!(top[0].row, 3);
assert_eq!(top[0].anomaly, 8.0);
assert_eq!(top[1].row, 1);
assert_eq!(top[1].anomaly, 4.0);
assert_eq!(top[2].row, 4);
assert_eq!(top[2].anomaly, 1.0);
}
#[test]
fn k_larger_than_n_returns_all_sorted() {
let b = alloc::vec![0.0, 0.0, 0.0];
let c = alloc::vec![3.0, 1.0, 2.0];
let top = find_anomalous_rows(&b, &c, 10);
assert_eq!(top.len(), 3);
assert!(top[0].anomaly >= top[1].anomaly);
assert!(top[1].anomaly >= top[2].anomaly);
}
#[test]
fn tie_breaks_on_row_index_ascending() {
let b = alloc::vec![0.0, 0.0, 0.0];
let c = alloc::vec![5.0, 5.0, 5.0]; let top = find_anomalous_rows(&b, &c, 2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].row, 0);
assert_eq!(top[1].row, 1);
}
#[test]
fn anomaly_is_absolute_value() {
let b = alloc::vec![0.0, 10.0];
let c = alloc::vec![-7.0, 3.0];
let top = find_anomalous_rows(&b, &c, 2);
assert_eq!(top[0].anomaly, 7.0);
assert_eq!(top[1].anomaly, 7.0);
assert_eq!(top[0].row, 0);
}
#[test]
#[should_panic(expected = "dim mismatch")]
fn threshold_panics_on_dim_mismatch() {
let b = alloc::vec![1.0, 2.0];
let c = alloc::vec![1.0];
let _ = find_rows_above_threshold(&b, &c, 0.5);
}
#[test]
fn threshold_filters_correctly() {
let b = alloc::vec![0.0, 0.0, 0.0, 0.0];
let c = alloc::vec![0.1, 0.5, 2.0, 0.05];
let above = find_rows_above_threshold(&b, &c, 0.3);
assert_eq!(above.len(), 2);
assert_eq!(above[0].row, 1);
assert_eq!(above[1].row, 2);
}
#[test]
fn threshold_returns_empty_when_nothing_crosses() {
let b = alloc::vec![0.0; 5];
let c = alloc::vec![0.01, 0.02, 0.03, 0.04, 0.05];
let above = find_rows_above_threshold(&b, &c, 1.0);
assert!(above.is_empty(), "no entry above threshold should return empty");
}
}