#![cfg(feature = "std")]
use crate::config::ForestBuilder;
use crate::domain::{AnomalyScore, DiVector};
use crate::error::{RcfError, RcfResult};
use crate::forest::RandomCutForest;
#[derive(Debug)]
pub struct DynamicForest<const MAX_D: usize> {
forest: RandomCutForest<MAX_D>,
active_dim: usize,
}
impl<const MAX_D: usize> DynamicForest<MAX_D> {
pub fn new(builder: ForestBuilder<MAX_D>, active_dim: usize) -> RcfResult<Self> {
if active_dim == 0 {
return Err(RcfError::InvalidConfig(
"DynamicForest: active_dim must be > 0".into(),
));
}
if active_dim > MAX_D {
return Err(RcfError::InvalidConfig(
format!("DynamicForest: active_dim {active_dim} exceeds MAX_D {MAX_D}").into(),
));
}
let forest = builder.build()?;
Ok(Self { forest, active_dim })
}
#[must_use]
pub fn active_dim(&self) -> usize {
self.active_dim
}
#[must_use]
pub const fn max_dim(&self) -> usize {
MAX_D
}
#[must_use]
pub fn forest(&self) -> &RandomCutForest<MAX_D> {
&self.forest
}
pub fn score(&self, point: &[f64]) -> RcfResult<AnomalyScore> {
let padded = self.pad(point)?;
self.forest.score(&padded)
}
pub fn update(&mut self, point: &[f64]) -> RcfResult<()> {
let padded = self.pad(point)?;
self.forest.update(padded)
}
pub fn attribution(&self, point: &[f64]) -> RcfResult<DiVector> {
let padded = self.pad(point)?;
let di_full = self.forest.attribution(&padded)?;
let mut di = DiVector::zeros(self.active_dim);
for d in 0..self.active_dim {
let _ = di.add_high(d, di_full.high()[d]);
let _ = di.add_low(d, di_full.low()[d]);
}
Ok(di)
}
fn pad(&self, point: &[f64]) -> RcfResult<[f64; MAX_D]> {
if point.len() != self.active_dim {
return Err(RcfError::DimensionMismatch {
expected: self.active_dim,
got: point.len(),
});
}
if !point.iter().all(|v| v.is_finite()) {
return Err(RcfError::NaNValue);
}
let mut padded = [0.0_f64; MAX_D];
padded[..self.active_dim].copy_from_slice(point);
Ok(padded)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
clippy::float_cmp,
clippy::cast_precision_loss
)]
mod tests {
use super::*;
fn builder() -> ForestBuilder<16> {
ForestBuilder::<16>::new()
.num_trees(50)
.sample_size(64)
.seed(2026)
}
#[test]
fn new_rejects_zero_active_dim() {
let err = DynamicForest::<16>::new(builder(), 0).unwrap_err();
assert!(matches!(err, RcfError::InvalidConfig(_)));
}
#[test]
fn new_rejects_active_dim_above_max() {
let err = DynamicForest::<16>::new(builder(), 32).unwrap_err();
assert!(matches!(err, RcfError::InvalidConfig(_)));
}
#[test]
fn update_then_score_preserves_dim_contract() {
let mut f = DynamicForest::<16>::new(builder(), 4).unwrap();
for i in 0..200 {
let v = f64::from(i) * 0.01;
f.update(&[v, v + 0.5, v * 2.0, v - 0.1]).unwrap();
}
let s = f.score(&[10.0, 10.0, 10.0, 10.0]).unwrap();
let raw: f64 = s.into();
assert!(raw.is_finite());
assert!(raw > 0.0);
}
#[test]
fn length_mismatch_rejected() {
let mut f = DynamicForest::<16>::new(builder(), 4).unwrap();
for _ in 0..50 {
f.update(&[0.1, 0.2, 0.3, 0.4]).unwrap();
}
assert!(matches!(
f.score(&[0.1, 0.2, 0.3]).unwrap_err(),
RcfError::DimensionMismatch { .. }
));
assert!(matches!(
f.score(&[0.1, 0.2, 0.3, 0.4, 0.5]).unwrap_err(),
RcfError::DimensionMismatch { .. }
));
}
#[test]
fn non_finite_rejected() {
let mut f = DynamicForest::<16>::new(builder(), 4).unwrap();
assert!(matches!(
f.update(&[f64::NAN, 0.0, 0.0, 0.0]).unwrap_err(),
RcfError::NaNValue
));
}
#[test]
fn attribution_truncated_to_active_dim() {
let mut f = DynamicForest::<16>::new(builder(), 3).unwrap();
for i in 0..200 {
let v = f64::from(i) * 0.01;
f.update(&[v, v + 0.5, v * 2.0]).unwrap();
}
let di = f.attribution(&[10.0, 10.0, 10.0]).unwrap();
assert_eq!(di.dim(), 3);
}
}