use crate::error::{AnomalyError, AnomalyResult};
use crate::handle::LcgRng;
#[derive(Debug, Clone, PartialEq)]
pub struct BoundingBox {
pub low: Vec<f32>,
pub high: Vec<f32>,
}
impl BoundingBox {
#[must_use]
pub fn point(x: &[f32]) -> Self {
Self {
low: x.to_vec(),
high: x.to_vec(),
}
}
#[must_use]
#[inline]
pub fn side(&self, i: usize) -> f32 {
self.high[i] - self.low[i]
}
#[must_use]
#[inline]
pub fn span(&self) -> f32 {
self.low
.iter()
.zip(self.high.iter())
.map(|(l, h)| h - l)
.sum()
}
#[must_use]
pub fn union(&self, other: &BoundingBox) -> BoundingBox {
let low = self
.low
.iter()
.zip(other.low.iter())
.map(|(a, b)| a.min(*b))
.collect();
let high = self
.high
.iter()
.zip(other.high.iter())
.map(|(a, b)| a.max(*b))
.collect();
BoundingBox { low, high }
}
fn expand_to_point(&mut self, x: &[f32]) {
for ((lo, hi), &xi) in self.low.iter_mut().zip(self.high.iter_mut()).zip(x.iter()) {
if xi < *lo {
*lo = xi;
}
if xi > *hi {
*hi = xi;
}
}
}
}
#[derive(Debug, Clone)]
pub struct RrcNode {
pub parent: Option<usize>,
pub left: Option<usize>,
pub right: Option<usize>,
pub cut_dim: usize,
pub cut_value: f32,
pub bbox: BoundingBox,
pub count: usize,
pub point: Option<Vec<f32>>,
pub multiplicity: usize,
}
impl RrcNode {
#[must_use]
#[inline]
pub fn is_leaf(&self) -> bool {
self.left.is_none() && self.right.is_none()
}
}
#[derive(Debug, Clone)]
pub struct RrcTree {
pub nodes: Vec<RrcNode>,
pub root: Option<usize>,
free: Vec<usize>,
dim: usize,
}
impl RrcTree {
#[must_use]
pub fn new(dim: usize) -> Self {
Self {
nodes: Vec::new(),
root: None,
free: Vec::new(),
dim,
}
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.root.map_or(0, |r| self.nodes[r].count)
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.root.is_none()
}
fn alloc(&mut self, node: RrcNode) -> usize {
if let Some(slot) = self.free.pop() {
self.nodes[slot] = node;
slot
} else {
self.nodes.push(node);
self.nodes.len() - 1
}
}
fn choose_cut(bbox: &BoundingBox, rng: &mut LcgRng) -> (usize, f32) {
let span = bbox.span();
let d = bbox.low.len();
if span <= 0.0 {
return (0, bbox.low[0]);
}
let r = rng.next_f32() * span;
let mut acc = 0.0_f32;
let mut dim = d - 1;
for i in 0..d {
acc += bbox.side(i);
if r < acc {
dim = i;
break;
}
}
let value = bbox.low[dim] + rng.next_f32() * bbox.side(dim);
(dim, value)
}
fn build(&mut self, points: &[(Vec<f32>, usize)], rng: &mut LcgRng) -> usize {
debug_assert!(!points.is_empty());
let mut bbox = BoundingBox::point(&points[0].0);
let mut total = points[0].1;
for (coords, mult) in &points[1..] {
bbox.expand_to_point(coords);
total += *mult;
}
if points.len() == 1 {
return self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim: 0,
cut_value: 0.0,
bbox,
count: total,
point: Some(points[0].0.clone()),
multiplicity: points[0].1,
});
}
if bbox.span() <= 0.0 {
return self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim: 0,
cut_value: 0.0,
bbox,
count: total,
point: Some(points[0].0.clone()),
multiplicity: total,
});
}
let (cut_dim, cut_value, left_pts, right_pts) = loop {
let (cd, cv) = Self::choose_cut(&bbox, rng);
let mut lhs: Vec<(Vec<f32>, usize)> = Vec::new();
let mut rhs: Vec<(Vec<f32>, usize)> = Vec::new();
for (coords, mult) in points {
if coords[cd] <= cv {
lhs.push((coords.clone(), *mult));
} else {
rhs.push((coords.clone(), *mult));
}
}
if !lhs.is_empty() && !rhs.is_empty() {
break (cd, cv, lhs, rhs);
}
};
let this = self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim,
cut_value,
bbox: bbox.clone(),
count: total,
point: None,
multiplicity: 0,
});
let l = self.build(&left_pts, rng);
let r = self.build(&right_pts, rng);
self.nodes[l].parent = Some(this);
self.nodes[r].parent = Some(this);
self.nodes[this].left = Some(l);
self.nodes[this].right = Some(r);
this
}
fn fit_matrix(&mut self, data: &[f32], n: usize, rng: &mut LcgRng) {
let points: Vec<(Vec<f32>, usize)> = (0..n)
.map(|i| (data[i * self.dim..(i + 1) * self.dim].to_vec(), 1))
.collect();
if points.is_empty() {
return;
}
let root = self.build(&points, rng);
self.root = Some(root);
}
pub fn insert_point(&mut self, x: &[f32], rng: &mut LcgRng) -> AnomalyResult<()> {
if x.len() != self.dim {
return Err(AnomalyError::DimensionMismatch {
expected: self.dim,
got: x.len(),
});
}
match self.root {
None => {
let leaf = self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim: 0,
cut_value: 0.0,
bbox: BoundingBox::point(x),
count: 1,
point: Some(x.to_vec()),
multiplicity: 1,
});
self.root = Some(leaf);
Ok(())
}
Some(root) => {
let new_leaf = self.insert_below(root, x, rng);
if let Some(nl) = new_leaf {
self.root = Some(nl);
}
Ok(())
}
}
}
fn insert_below(&mut self, node: usize, x: &[f32], rng: &mut LcgRng) -> Option<usize> {
if self.nodes[node].is_leaf()
&& let Some(p) = &self.nodes[node].point
&& p.as_slice() == x
{
self.nodes[node].multiplicity += 1;
self.nodes[node].count += 1;
return None;
}
let merged = self.nodes[node].bbox.union(&BoundingBox::point(x));
let (cut_dim, cut_value) = Self::choose_cut(&merged, rng);
let x_side_left = x[cut_dim] <= cut_value;
let sub_low = self.nodes[node].bbox.low[cut_dim];
let sub_high = self.nodes[node].bbox.high[cut_dim];
let subtree_all_left = sub_high <= cut_value;
let subtree_all_right = sub_low > cut_value;
if (x_side_left && subtree_all_right) || (!x_side_left && subtree_all_left) {
let leaf = self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim: 0,
cut_value: 0.0,
bbox: BoundingBox::point(x),
count: 1,
point: Some(x.to_vec()),
multiplicity: 1,
});
let old_count = self.nodes[node].count;
let parent = self.nodes[node].parent;
let branch = self.alloc(RrcNode {
parent,
left: None,
right: None,
cut_dim,
cut_value,
bbox: merged,
count: old_count + 1,
point: None,
multiplicity: 0,
});
if x_side_left {
self.nodes[branch].left = Some(leaf);
self.nodes[branch].right = Some(node);
} else {
self.nodes[branch].left = Some(node);
self.nodes[branch].right = Some(leaf);
}
self.nodes[leaf].parent = Some(branch);
self.nodes[node].parent = Some(branch);
return Some(branch);
}
if self.nodes[node].is_leaf() {
let leaf = self.alloc(RrcNode {
parent: None,
left: None,
right: None,
cut_dim: 0,
cut_value: 0.0,
bbox: BoundingBox::point(x),
count: 1,
point: Some(x.to_vec()),
multiplicity: 1,
});
let old_count = self.nodes[node].count;
let parent = self.nodes[node].parent;
let (fd, fv, x_left) = self.forced_split(node, x);
let branch = self.alloc(RrcNode {
parent,
left: None,
right: None,
cut_dim: fd,
cut_value: fv,
bbox: merged,
count: old_count + 1,
point: None,
multiplicity: 0,
});
if x_left {
self.nodes[branch].left = Some(leaf);
self.nodes[branch].right = Some(node);
} else {
self.nodes[branch].left = Some(node);
self.nodes[branch].right = Some(leaf);
}
self.nodes[leaf].parent = Some(branch);
self.nodes[node].parent = Some(branch);
return Some(branch);
}
let go_left = x[self.nodes[node].cut_dim] <= self.nodes[node].cut_value;
let child = if go_left {
self.nodes[node].left
} else {
self.nodes[node].right
};
if let Some(child) = child {
let replaced = self.insert_below(child, x, rng);
if let Some(new_child) = replaced {
if go_left {
self.nodes[node].left = Some(new_child);
} else {
self.nodes[node].right = Some(new_child);
}
self.nodes[new_child].parent = Some(node);
}
self.nodes[node].count += 1;
self.nodes[node].bbox.expand_to_point(x);
}
None
}
fn forced_split(&self, node: usize, x: &[f32]) -> (usize, f32, bool) {
let Some(p) = self.nodes[node].point.as_ref() else {
return (0, x[0], true);
};
for d in 0..self.dim {
if (p[d] - x[d]).abs() > 0.0 {
let mid = 0.5 * (p[d] + x[d]);
let x_left = x[d] <= mid;
return (d, mid, x_left);
}
}
(0, x[0], true)
}
#[inline]
fn sibling_of(&self, parent: usize, child: usize) -> Option<usize> {
let l = self.nodes[parent].left?;
let r = self.nodes[parent].right?;
if l == child { Some(r) } else { Some(l) }
}
pub fn forget_point(&mut self, x: &[f32]) -> AnomalyResult<bool> {
if x.len() != self.dim {
return Err(AnomalyError::DimensionMismatch {
expected: self.dim,
got: x.len(),
});
}
let Some(root) = self.root else {
return Ok(false);
};
let Some(leaf) = self.find_leaf(root, x) else {
return Ok(false);
};
if self.nodes[leaf].multiplicity > 1 {
self.nodes[leaf].multiplicity -= 1;
self.nodes[leaf].count -= 1;
self.repair_ancestors(self.nodes[leaf].parent);
return Ok(true);
}
match self.nodes[leaf].parent {
None => {
self.recycle(leaf);
self.root = None;
}
Some(parent) => {
let Some(sibling) = self.sibling_of(parent, leaf) else {
return Ok(true);
};
let grandparent = self.nodes[parent].parent;
self.nodes[sibling].parent = grandparent;
match grandparent {
None => self.root = Some(sibling),
Some(gp) => {
if self.nodes[gp].left == Some(parent) {
self.nodes[gp].left = Some(sibling);
} else {
self.nodes[gp].right = Some(sibling);
}
}
}
self.recycle(leaf);
self.recycle(parent);
self.repair_ancestors(grandparent);
}
}
Ok(true)
}
fn find_leaf(&self, node: usize, x: &[f32]) -> Option<usize> {
let mut cur = node;
loop {
if self.nodes[cur].is_leaf() {
return match &self.nodes[cur].point {
Some(p) if p.as_slice() == x => Some(cur),
_ => None,
};
}
let go_left = x[self.nodes[cur].cut_dim] <= self.nodes[cur].cut_value;
cur = if go_left {
self.nodes[cur].left?
} else {
self.nodes[cur].right?
};
}
}
fn repair_ancestors(&mut self, mut node: Option<usize>) {
while let Some(n) = node {
let (l, r) = (self.nodes[n].left, self.nodes[n].right);
if let (Some(l), Some(r)) = (l, r) {
self.nodes[n].count = self.nodes[l].count + self.nodes[r].count;
self.nodes[n].bbox = self.nodes[l].bbox.union(&self.nodes[r].bbox);
}
node = self.nodes[n].parent;
}
}
fn recycle(&mut self, idx: usize) {
self.free.push(idx);
}
#[must_use]
pub fn codisp(&self, x: &[f32]) -> f32 {
let Some(root) = self.root else {
return 0.0;
};
let Some(leaf) = self.find_leaf(root, x) else {
return 0.0;
};
let mut best = 0.0_f32;
let mut cur = leaf;
while let Some(parent) = self.nodes[cur].parent {
let Some(sibling) = self.sibling_of(parent, cur) else {
break;
};
let sub_size = self.nodes[cur].count as f32;
let sibling_size = self.nodes[sibling].count as f32;
let disp = sibling_size / sub_size;
if disp > best {
best = disp;
}
cur = parent;
}
best
}
#[must_use]
pub fn depth_of(&self, x: &[f32]) -> Option<usize> {
let root = self.root?;
let mut cur = root;
let mut depth = 0_usize;
loop {
if self.nodes[cur].is_leaf() {
return match &self.nodes[cur].point {
Some(p) if p.as_slice() == x => Some(depth),
_ => None,
};
}
let go_left = x[self.nodes[cur].cut_dim] <= self.nodes[cur].cut_value;
cur = if go_left {
self.nodes[cur].left?
} else {
self.nodes[cur].right?
};
depth += 1;
}
}
pub fn codisp_query(&mut self, x: &[f32], rng: &mut LcgRng) -> AnomalyResult<f32> {
self.insert_point(x, rng)?;
let score = self.codisp(x);
self.forget_point(x)?;
Ok(score)
}
}
#[derive(Debug, Clone)]
pub struct RrcfConfig {
pub n_trees: usize,
pub tree_size: usize,
pub seed: u64,
}
impl Default for RrcfConfig {
fn default() -> Self {
Self {
n_trees: 40,
tree_size: 256,
seed: 42,
}
}
}
pub struct RobustRandomCutForest {
config: RrcfConfig,
trees: Vec<RrcTree>,
rngs: Vec<LcgRng>,
n_features: usize,
fitted: bool,
}
impl RobustRandomCutForest {
#[must_use]
pub fn new(config: RrcfConfig) -> Self {
Self {
config,
trees: Vec::new(),
rngs: Vec::new(),
n_features: 0,
fitted: false,
}
}
pub fn fit(&mut self, data: &[f32], n_samples: usize, n_features: usize) -> AnomalyResult<()> {
if n_samples == 0 {
return Err(AnomalyError::EmptyInput);
}
if n_features == 0 {
return Err(AnomalyError::InvalidFeatureCount { n: 0 });
}
if data.len() != n_samples * n_features {
return Err(AnomalyError::DimensionMismatch {
expected: n_samples * n_features,
got: data.len(),
});
}
if self.config.n_trees == 0 {
return Err(AnomalyError::Internal {
msg: "n_trees must be > 0".into(),
});
}
let ss = self.config.tree_size.min(n_samples).max(1);
let mut rng = LcgRng::new(self.config.seed);
let mut trees = Vec::with_capacity(self.config.n_trees);
let mut rngs = Vec::with_capacity(self.config.n_trees);
for _ in 0..self.config.n_trees {
let mut tree_rng = LcgRng::new(rng.next_u32() as u64 ^ (rng.next_u32() as u64) << 32);
let sub = sample_without_replacement(n_samples, ss, &mut rng);
let mut sub_data = Vec::with_capacity(ss * n_features);
for &idx in &sub {
sub_data.extend_from_slice(&data[idx * n_features..(idx + 1) * n_features]);
}
let mut tree = RrcTree::new(n_features);
tree.fit_matrix(&sub_data, ss, &mut tree_rng);
trees.push(tree);
rngs.push(tree_rng);
}
self.trees = trees;
self.rngs = rngs;
self.n_features = n_features;
self.fitted = true;
Ok(())
}
pub fn score(&mut self, x: &[f32]) -> AnomalyResult<f32> {
if !self.fitted {
return Err(AnomalyError::NotFitted);
}
if x.len() != self.n_features {
return Err(AnomalyError::FeatureCountMismatch {
expected: self.n_features,
got: x.len(),
});
}
let mut sum = 0.0_f32;
for (tree, rng) in self.trees.iter_mut().zip(self.rngs.iter_mut()) {
sum += tree.codisp_query(x, rng)?;
}
Ok(sum / self.trees.len() as f32)
}
pub fn score_batch(&mut self, x: &[f32], n: usize) -> AnomalyResult<Vec<f32>> {
if !self.fitted {
return Err(AnomalyError::NotFitted);
}
if x.len() != n * self.n_features {
return Err(AnomalyError::DimensionMismatch {
expected: n * self.n_features,
got: x.len(),
});
}
let mut scores = Vec::with_capacity(n);
for i in 0..n {
let sample = x[i * self.n_features..(i + 1) * self.n_features].to_vec();
scores.push(self.score(&sample)?);
}
Ok(scores)
}
pub fn insert_point(&mut self, x: &[f32]) -> AnomalyResult<()> {
if !self.fitted {
return Err(AnomalyError::NotFitted);
}
if x.len() != self.n_features {
return Err(AnomalyError::FeatureCountMismatch {
expected: self.n_features,
got: x.len(),
});
}
for (tree, rng) in self.trees.iter_mut().zip(self.rngs.iter_mut()) {
tree.insert_point(x, rng)?;
}
Ok(())
}
pub fn forget_point(&mut self, x: &[f32]) -> AnomalyResult<()> {
if !self.fitted {
return Err(AnomalyError::NotFitted);
}
if x.len() != self.n_features {
return Err(AnomalyError::FeatureCountMismatch {
expected: self.n_features,
got: x.len(),
});
}
for tree in self.trees.iter_mut() {
tree.forget_point(x)?;
}
Ok(())
}
#[must_use]
#[inline]
pub fn n_trees(&self) -> usize {
self.trees.len()
}
#[must_use]
#[inline]
pub fn n_features(&self) -> usize {
self.n_features
}
#[must_use]
#[inline]
pub fn trees(&self) -> &[RrcTree] {
&self.trees
}
}
fn sample_without_replacement(n: usize, k: usize, rng: &mut LcgRng) -> Vec<usize> {
let k = k.min(n);
let mut indices: Vec<usize> = (0..n).collect();
for i in 0..k {
let j = i + rng.next_usize(n - i);
indices.swap(i, j);
}
indices.truncate(k);
indices
}
#[cfg(test)]
mod tests {
use super::*;
fn cluster_with_outlier(n_inliers: usize, seed: u64) -> (Vec<f32>, usize) {
let mut rng = LcgRng::new(seed);
let mut data = Vec::with_capacity((n_inliers + 1) * 2);
for _ in 0..n_inliers {
data.push(rng.next_normal() * 0.2);
data.push(rng.next_normal() * 0.2);
}
data.push(20.0);
data.push(20.0);
(data, n_inliers + 1)
}
#[test]
fn outlier_has_higher_codisp() {
let (data, n) = cluster_with_outlier(120, 7);
let mut forest = RobustRandomCutForest::new(RrcfConfig {
n_trees: 60,
tree_size: 128,
seed: 11,
});
forest.fit(&data, n, 2).expect("fit");
let outlier = [20.0_f32, 20.0];
let inlier = [0.0_f32, 0.0];
let s_out = forest.score(&outlier).expect("score outlier");
let s_in = forest.score(&inlier).expect("score inlier");
assert!(
s_out > s_in,
"outlier CoDisp {s_out} should exceed inlier CoDisp {s_in}"
);
}
#[test]
fn cut_dimension_proportional_to_side_length() {
let bbox = BoundingBox {
low: vec![0.0, 0.0],
high: vec![1.0, 3.0],
};
let mut rng = LcgRng::new(123);
let trials = 40_000;
let mut counts = [0usize; 2];
for _ in 0..trials {
let (dim, _val) = RrcTree::choose_cut(&bbox, &mut rng);
counts[dim] += 1;
}
let p0 = counts[0] as f64 / trials as f64;
let p1 = counts[1] as f64 / trials as f64;
assert!((p0 - 0.25).abs() < 0.02, "p0={p0} expected ≈0.25");
assert!((p1 - 0.75).abs() < 0.02, "p1={p1} expected ≈0.75");
}
#[test]
fn cut_value_within_box_range() {
let bbox = BoundingBox {
low: vec![-2.0, 5.0, 100.0],
high: vec![3.0, 9.0, 110.0],
};
let mut rng = LcgRng::new(456);
for _ in 0..10_000 {
let (dim, val) = RrcTree::choose_cut(&bbox, &mut rng);
assert!(
val >= bbox.low[dim] && val <= bbox.high[dim],
"cut value {val} out of range [{},{}] in dim {dim}",
bbox.low[dim],
bbox.high[dim]
);
}
}
#[test]
fn insert_then_forget_restores_structure() {
let mut rng = LcgRng::new(99);
let mut tree = RrcTree::new(2);
let pts = [
[0.0_f32, 0.0],
[1.0, 0.5],
[0.3, 2.0],
[2.0, 1.0],
[-1.0, -1.0],
[0.7, 1.3],
];
for p in &pts {
tree.insert_point(p, &mut rng).expect("insert");
}
let before_len = tree.len();
let before_depths: Vec<Option<usize>> = pts.iter().map(|p| tree.depth_of(p)).collect();
let new_pt = [5.0_f32, 5.0];
tree.insert_point(&new_pt, &mut rng).expect("insert new");
let removed = tree.forget_point(&new_pt).expect("forget");
assert!(removed, "point should have been removed");
assert_eq!(tree.len(), before_len, "length must be restored");
let after_depths: Vec<Option<usize>> = pts.iter().map(|p| tree.depth_of(p)).collect();
assert_eq!(
before_depths, after_depths,
"tree structure (depths) must be restored after insert+forget"
);
assert_eq!(
tree.depth_of(&new_pt),
None,
"forgotten point still present"
);
}
#[test]
fn codisp_non_negative() {
let (data, n) = cluster_with_outlier(80, 3);
let mut forest = RobustRandomCutForest::new(RrcfConfig {
n_trees: 30,
tree_size: 64,
seed: 5,
});
forest.fit(&data, n, 2).expect("fit");
for i in 0..n {
let row = &data[i * 2..(i + 1) * 2];
let s = forest.score(row).expect("score");
assert!(s >= 0.0, "CoDisp must be ≥ 0, got {s}");
assert!(s.is_finite(), "CoDisp must be finite, got {s}");
}
}
#[test]
fn codisp_handles_collusion_better_than_depth() {
let mut rng = LcgRng::new(2024);
let mut tree = RrcTree::new(2);
for _ in 0..40 {
let p = [rng.next_normal() * 0.05, rng.next_normal() * 0.05];
tree.insert_point(&p, &mut rng).expect("insert inlier");
}
let mut colluders = Vec::new();
for k in 0..6 {
let p = [10.0 + k as f32 * 1e-3, 10.0 + k as f32 * 1e-3];
colluders.push(p);
tree.insert_point(&p, &mut rng).expect("insert colluder");
}
let colluder = colluders[0];
let depth = tree.depth_of(&colluder).expect("colluder depth");
let codisp = tree.codisp(&colluder);
let inlier = [0.0_f32, 0.0];
let codisp_inlier = tree
.codisp_query(&inlier, &mut rng)
.expect("inlier codisp query");
assert!(
codisp > codisp_inlier,
"colluder CoDisp {codisp} should exceed inlier CoDisp {codisp_inlier} (depth={depth})"
);
assert!(codisp > 1.0, "colluder CoDisp {codisp} should exceed 1.0");
}
#[test]
fn more_trees_reduce_score_variance() {
let (data, n) = cluster_with_outlier(100, 17);
let query = [0.1_f32, -0.1];
let var_small = score_variance_across_seeds(&data, n, &query, 4, 64);
let var_large = score_variance_across_seeds(&data, n, &query, 64, 64);
assert!(
var_large < var_small,
"larger forest variance {var_large} should be below small forest {var_small}"
);
}
fn score_variance_across_seeds(
data: &[f32],
n: usize,
query: &[f32],
n_trees: usize,
tree_size: usize,
) -> f32 {
let mut scores = Vec::new();
for seed in 0..8_u64 {
let mut forest = RobustRandomCutForest::new(RrcfConfig {
n_trees,
tree_size,
seed: 1000 + seed,
});
forest.fit(data, n, 2).expect("fit");
scores.push(forest.score(query).expect("score"));
}
let mean = scores.iter().sum::<f32>() / scores.len() as f32;
scores.iter().map(|s| (s - mean).powi(2)).sum::<f32>() / scores.len() as f32
}
#[test]
fn bbox_union_and_span() {
let a = BoundingBox {
low: vec![0.0, 1.0],
high: vec![2.0, 3.0],
};
let b = BoundingBox {
low: vec![-1.0, 2.0],
high: vec![1.0, 5.0],
};
let u = a.union(&b);
assert_eq!(u.low, vec![-1.0, 1.0]);
assert_eq!(u.high, vec![2.0, 5.0]);
assert!((u.span() - 7.0).abs() < 1e-6, "span={}", u.span());
}
#[test]
fn unfitted_score_errors() {
let mut forest = RobustRandomCutForest::new(RrcfConfig::default());
match forest.score(&[0.0_f32, 0.0]) {
Err(AnomalyError::NotFitted) => {}
other => panic!("expected NotFitted, got {other:?}"),
}
}
#[test]
fn empty_input_errors() {
let mut forest = RobustRandomCutForest::new(RrcfConfig::default());
match forest.fit(&[], 0, 2) {
Err(AnomalyError::EmptyInput) => {}
other => panic!("expected EmptyInput, got {other:?}"),
}
}
#[test]
fn feature_count_mismatch_on_score() {
let (data, n) = cluster_with_outlier(20, 1);
let mut forest = RobustRandomCutForest::new(RrcfConfig {
n_trees: 5,
tree_size: 16,
seed: 2,
});
forest.fit(&data, n, 2).expect("fit");
match forest.score(&[0.0_f32, 0.0, 0.0]) {
Err(AnomalyError::FeatureCountMismatch {
expected: 2,
got: 3,
}) => {}
other => panic!("expected FeatureCountMismatch, got {other:?}"),
}
}
#[test]
fn dimension_mismatch_on_fit() {
let mut forest = RobustRandomCutForest::new(RrcfConfig::default());
match forest.fit(&[1.0_f32, 2.0, 3.0], 2, 2) {
Err(AnomalyError::DimensionMismatch {
expected: 4,
got: 3,
}) => {}
other => panic!("expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn duplicate_insert_forget_multiplicity() {
let mut rng = LcgRng::new(321);
let mut tree = RrcTree::new(1);
tree.insert_point(&[1.0_f32], &mut rng).expect("ins1");
tree.insert_point(&[2.0_f32], &mut rng).expect("ins2");
for _ in 0..3 {
tree.insert_point(&[2.0_f32], &mut rng).expect("dup ins");
}
assert_eq!(tree.len(), 5, "5 points total (with multiplicity)");
for _ in 0..3 {
assert!(tree.forget_point(&[2.0_f32]).expect("forget dup"));
}
assert_eq!(tree.len(), 2, "back to 2 points");
assert!(tree.forget_point(&[2.0_f32]).expect("forget last"));
assert_eq!(tree.len(), 1, "one point left");
assert_eq!(tree.depth_of(&[2.0_f32]), None, "all copies gone");
}
#[test]
fn forget_absent_returns_false() {
let mut rng = LcgRng::new(7);
let mut tree = RrcTree::new(2);
tree.insert_point(&[0.0_f32, 0.0], &mut rng).expect("ins");
assert!(
!tree.forget_point(&[9.0_f32, 9.0]).expect("forget absent"),
"forgetting an absent point should return false"
);
assert_eq!(tree.len(), 1, "length unchanged");
}
}