use alloc::borrow::Cow;
use alloc::format;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float;
use rand::Rng;
use crate::domain::{BoundingBox, Cut, ensure_dim, ensure_finite};
use crate::error::{RcfError, RcfResult};
use crate::tree::node::{NodeRef, NodeView, NodeViewMut};
use crate::tree::node_store::NodeStore;
use crate::visitor::Visitor;
pub trait PointAccessor<const D: usize> {
fn point(&self, idx: usize) -> Option<&[f64; D]>;
}
impl<const D: usize> PointAccessor<D> for Vec<[f64; D]> {
fn point(&self, idx: usize) -> Option<&[f64; D]> {
self.get(idx)
}
}
impl<const D: usize> PointAccessor<D> for [[f64; D]] {
fn point(&self, idx: usize) -> Option<&[f64; D]> {
self.get(idx)
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RandomCutTree<const D: usize> {
root: Option<NodeRef>,
store: NodeStore<D>,
leaf_index: Vec<Option<NodeRef>>,
distinct_count: usize,
}
impl<const D: usize> RandomCutTree<D> {
pub fn new(capacity: u32) -> RcfResult<Self> {
if D == 0 {
return Err(RcfError::InvalidConfig(
"RandomCutTree dimension must be > 0".into(),
));
}
Ok(Self {
root: None,
store: NodeStore::<D>::new(capacity)?,
leaf_index: Vec::new(),
distinct_count: 0,
})
}
#[inline]
fn leaf_index_set(&mut self, idx: usize, node: NodeRef) {
if idx >= self.leaf_index.len() {
self.leaf_index.resize(idx + 1, None);
}
if self.leaf_index[idx].is_none() {
self.distinct_count += 1;
}
self.leaf_index[idx] = Some(node);
}
#[inline]
fn leaf_index_clear(&mut self, idx: usize) {
if let Some(slot) = self.leaf_index.get_mut(idx)
&& slot.is_some()
{
*slot = None;
self.distinct_count -= 1;
}
}
#[inline]
#[must_use]
fn leaf_index_get(&self, idx: usize) -> Option<NodeRef> {
self.leaf_index.get(idx).copied().flatten()
}
#[inline]
#[must_use]
fn leaf_index_contains(&self, idx: usize) -> bool {
matches!(self.leaf_index.get(idx), Some(Some(_)))
}
#[must_use]
pub fn root(&self) -> Option<NodeRef> {
self.root
}
#[must_use]
pub const fn dimension(&self) -> usize {
D
}
#[must_use]
#[inline]
pub fn distinct_point_count(&self) -> usize {
self.distinct_count
}
#[must_use]
pub fn store(&self) -> &NodeStore<D> {
&self.store
}
#[must_use]
#[inline]
pub fn contains(&self, point_idx: usize) -> bool {
self.leaf_index_contains(point_idx)
}
#[must_use]
#[inline]
pub fn leaf_of(&self, point_idx: usize) -> Option<NodeRef> {
self.leaf_index_get(point_idx)
}
#[must_use]
pub fn max_depth(&self) -> Option<usize> {
let root = self.root?;
Some(self.subtree_depth(root, 0).unwrap_or(0))
}
fn subtree_depth(&self, n: NodeRef, depth: usize) -> RcfResult<usize> {
match self.store.view(n)? {
NodeView::Leaf(_) => Ok(depth),
NodeView::Internal(i) => {
let l = self.subtree_depth(i.left, depth + 1)?;
let r = self.subtree_depth(i.right, depth + 1)?;
Ok(l.max(r))
}
}
}
pub fn add<R, P>(
&mut self,
point_idx: usize,
point: &[f64],
points: &P,
rng: &mut R,
) -> RcfResult<()>
where
R: Rng + ?Sized,
P: PointAccessor<D> + ?Sized,
{
ensure_dim(point, D)?;
ensure_finite(point)?;
if self.leaf_index_contains(point_idx) {
return Err(RcfError::InvalidConfig(
format!("RandomCutTree::add: point_idx {point_idx} already present").into(),
));
}
let Some(root) = self.root else {
let leaf = self.store.add_leaf(point_idx, None, 1)?;
self.leaf_index_set(point_idx, leaf);
self.root = Some(leaf);
return Ok(());
};
self.insert_at(root, point_idx, point, points, rng)?;
Ok(())
}
#[allow(clippy::drop_non_drop)]
fn insert_at<R, P>(
&mut self,
n: NodeRef,
point_idx: usize,
point: &[f64],
points: &P,
rng: &mut R,
) -> RcfResult<NodeRef>
where
R: Rng + ?Sized,
P: PointAccessor<D> + ?Sized,
{
let n_bbox = self.bbox_of(n, points)?;
if n_bbox.augmented_range_sum(point) <= 0.0 {
drop(n_bbox);
return self.absorb_duplicate(n, point_idx);
}
let cut = n_bbox.augmented_random_cut(point, rng)?;
let isolates = isolates_point(&cut, point, &n_bbox);
if isolates {
let mut augmented: BoundingBox<D> = (*n_bbox).clone();
drop(n_bbox);
augmented.extend(point)?;
self.splice_new_internal(n, point_idx, point, cut, augmented)
} else {
drop(n_bbox);
self.descend_or_split(n, point_idx, point, points, rng)
}
}
fn absorb_duplicate(&mut self, n: NodeRef, point_idx: usize) -> RcfResult<NodeRef> {
if !n.is_leaf() {
return Err(RcfError::InvalidConfig(
"RandomCutTree::absorb_duplicate called on internal node".into(),
));
}
let mass = self.store.view(n)?.mass();
self.store.set_mass(n, mass + 1)?;
self.leaf_index_set(point_idx, n);
let mut cur = n;
while let Some(parent) = self.store.parent(cur)? {
let m = self.store.view(parent)?.mass();
self.store.set_mass(parent, m + 1)?;
cur = parent;
}
Ok(n)
}
fn splice_new_internal(
&mut self,
n: NodeRef,
point_idx: usize,
point: &[f64],
cut: Cut,
bbox: BoundingBox<D>,
) -> RcfResult<NodeRef> {
let new_leaf = self.store.add_leaf(point_idx, None, 1)?;
self.leaf_index_set(point_idx, new_leaf);
let parent_of_n = self.store.parent(n)?;
let n_mass = self.store.view(n)?.mass();
let new_mass = n_mass + 1;
let (left, right) = if cut.left_of(point) {
(new_leaf, n)
} else {
(n, new_leaf)
};
let new_internal =
self.store
.add_internal(cut, bbox, left, right, parent_of_n, new_mass)?;
self.store.set_parent(new_leaf, Some(new_internal))?;
self.store.set_parent(n, Some(new_internal))?;
self.replace_in_parent(parent_of_n, n, new_internal)?;
if let Some(parent) = parent_of_n {
self.update_ancestors_after_insert(parent, point)?;
}
Ok(new_internal)
}
fn descend_or_split<R, P>(
&mut self,
n: NodeRef,
point_idx: usize,
point: &[f64],
points: &P,
rng: &mut R,
) -> RcfResult<NodeRef>
where
R: Rng + ?Sized,
P: PointAccessor<D> + ?Sized,
{
let (existing_cut, left, right) = match self.store.view(n)? {
NodeView::Internal(i) => (i.cut, i.left, i.right),
NodeView::Leaf(_) => {
return Err(RcfError::InvalidConfig(
"RandomCutTree::descend_or_split: leaf reached without isolation".into(),
));
}
};
let go_left = existing_cut.left_of(point);
let next = if go_left { left } else { right };
let new_child = self.insert_at(next, point_idx, point, points, rng)?;
if new_child.raw() != next.raw() {
if go_left {
self.store.set_internal_children(n, new_child, right)?;
} else {
self.store.set_internal_children(n, left, new_child)?;
}
self.store.set_parent(new_child, Some(n))?;
}
Ok(n)
}
fn replace_in_parent(
&mut self,
parent_opt: Option<NodeRef>,
old: NodeRef,
new: NodeRef,
) -> RcfResult<()> {
let Some(parent) = parent_opt else {
self.root = Some(new);
return Ok(());
};
let (l, r) = match self.store.view(parent)? {
NodeView::Internal(i) => (i.left, i.right),
NodeView::Leaf(_) => {
return Err(RcfError::InvalidConfig(
"RandomCutTree::replace_in_parent: parent is leaf".into(),
));
}
};
if l.raw() == old.raw() {
self.store.set_internal_children(parent, new, r)?;
} else if r.raw() == old.raw() {
self.store.set_internal_children(parent, l, new)?;
} else {
return Err(RcfError::InvalidConfig(
"RandomCutTree::replace_in_parent: orphan".into(),
));
}
Ok(())
}
fn update_ancestors_after_insert(&mut self, start: NodeRef, point: &[f64]) -> RcfResult<()> {
let mut cur = Some(start);
while let Some(node) = cur {
let parent = match self.store.view_mut(node)? {
NodeViewMut::Internal(i) => {
i.mass += 1;
i.bbox.extend(point)?;
i.parent
}
NodeViewMut::Leaf(l) => {
l.mass += 1;
l.parent
}
};
cur = parent;
}
Ok(())
}
pub fn delete<P>(&mut self, point_idx: usize, points: &P) -> RcfResult<()>
where
P: PointAccessor<D> + ?Sized,
{
let leaf = self.leaf_index_get(point_idx).ok_or_else(|| {
RcfError::InvalidConfig(
format!("RandomCutTree::delete: point_idx {point_idx} not present").into(),
)
})?;
let leaf_mass = self.store.view(leaf)?.mass();
if leaf_mass > 1 {
self.store.set_mass(leaf, leaf_mass - 1)?;
let mut cur = leaf;
while let Some(parent) = self.store.parent(cur)? {
let m = self.store.view(parent)?.mass();
self.store.set_mass(parent, m - 1)?;
cur = parent;
}
self.leaf_index_clear(point_idx);
return Ok(());
}
let parent_of_leaf = self.store.parent(leaf)?;
self.leaf_index_clear(point_idx);
self.store.delete(leaf)?;
let Some(parent) = parent_of_leaf else {
self.root = None;
return Ok(());
};
let sibling = match self.store.view(parent)? {
NodeView::Internal(i) => {
if i.left.raw() == leaf.raw() {
i.right
} else {
i.left
}
}
NodeView::Leaf(_) => {
return Err(RcfError::InvalidConfig(
"RandomCutTree::delete: parent is leaf".into(),
));
}
};
let grandparent = self.store.parent(parent)?;
self.store.set_parent(sibling, grandparent)?;
self.store.delete(parent)?;
match grandparent {
None => {
self.root = Some(sibling);
}
Some(gp) => {
let (l, r) = match self.store.view(gp)? {
NodeView::Internal(i) => (i.left, i.right),
NodeView::Leaf(_) => {
return Err(RcfError::InvalidConfig(
"RandomCutTree::delete: grandparent is leaf".into(),
));
}
};
if l.raw() == parent.raw() {
self.store.set_internal_children(gp, sibling, r)?;
} else if r.raw() == parent.raw() {
self.store.set_internal_children(gp, l, sibling)?;
} else {
return Err(RcfError::InvalidConfig(
"RandomCutTree::delete: parent not registered with grandparent".into(),
));
}
self.recompute_ancestors(gp, points)?;
}
}
Ok(())
}
fn recompute_ancestors<P>(&mut self, start: NodeRef, points: &P) -> RcfResult<()>
where
P: PointAccessor<D> + ?Sized,
{
let mut cur = Some(start);
while let Some(node) = cur {
let m = self.store.view(node)?.mass();
self.store.set_mass(node, m - 1)?;
if node.is_internal() {
let new_bbox = self.compute_internal_bbox(node, points)?;
self.store.set_internal_bbox(node, new_bbox)?;
}
cur = self.store.parent(node)?;
}
Ok(())
}
fn compute_internal_bbox<P>(&self, n: NodeRef, points: &P) -> RcfResult<BoundingBox<D>>
where
P: PointAccessor<D> + ?Sized,
{
let (left, right) = match self.store.view(n)? {
NodeView::Internal(i) => (i.left, i.right),
NodeView::Leaf(_) => {
return Err(RcfError::InvalidConfig(
"RandomCutTree::compute_internal_bbox called on leaf".into(),
));
}
};
let lb = self.bbox_of(left, points)?;
let rb = self.bbox_of(right, points)?;
Ok(lb.merged(&rb))
}
fn bbox_of<'a, P>(&'a self, n: NodeRef, points: &'a P) -> RcfResult<Cow<'a, BoundingBox<D>>>
where
P: PointAccessor<D> + ?Sized,
{
match self.store.view(n)? {
NodeView::Internal(i) => Ok(Cow::Borrowed(&i.bbox)),
NodeView::Leaf(l) => {
let p = points.point(l.point_idx).ok_or(RcfError::OutOfBounds {
index: l.point_idx,
len: 0,
})?;
Ok(Cow::Owned(BoundingBox::<D>::from_point(p)?))
}
}
}
pub fn codisp_stateless(&self, point: &[f64]) -> RcfResult<f64> {
ensure_dim(point, D)?;
ensure_finite(point)?;
let Some(root) = self.root else {
return Err(RcfError::EmptyForest);
};
let mut cur = root;
let mut max_disp = 0.0_f64;
loop {
match self.store.view(cur)? {
crate::tree::NodeView::Leaf(_) => return Ok(max_disp),
crate::tree::NodeView::Internal(i) => {
let (next, sibling) = if i.cut.left_of(point) {
(i.left, i.right)
} else {
(i.right, i.left)
};
let next_mass = self.store.view(next)?.mass();
let sibling_mass = self.store.view(sibling)?.mass();
if next_mass > 0 {
#[allow(clippy::cast_precision_loss)]
let disp = sibling_mass as f64 / next_mass as f64;
if disp > max_disp {
max_disp = disp;
}
}
cur = next;
}
}
}
}
pub fn traverse<V: Visitor<D>>(&self, point: &[f64], mut visitor: V) -> RcfResult<V::Output> {
ensure_dim(point, D)?;
ensure_finite(point)?;
let Some(root) = self.root else {
return Err(RcfError::EmptyForest);
};
self.traverse_inner(root, point, 0, &mut visitor)?;
Ok(visitor.result())
}
fn traverse_inner<V: Visitor<D>>(
&self,
n: NodeRef,
point: &[f64],
depth: usize,
visitor: &mut V,
) -> RcfResult<()> {
match self.store.view(n)? {
NodeView::Leaf(l) => {
visitor.accept_leaf(depth, l.mass, l.point_idx);
Ok(())
}
NodeView::Internal(i) => {
if visitor.needs_per_dim_prob() {
let (prob, per_dim) = i.bbox.probability_of_cut(point)?;
visitor.accept_internal(depth, i.mass, &i.cut, &i.bbox, prob, &per_dim);
} else {
let prob = i.bbox.total_probability_of_cut(point)?;
visitor.accept_internal(depth, i.mass, &i.cut, &i.bbox, prob, &[]);
}
let next = if i.cut.left_of(point) {
i.left
} else {
i.right
};
self.traverse_inner(next, point, depth + 1, visitor)
}
}
}
}
#[inline]
fn isolates_point<const D: usize>(cut: &Cut, point: &[f64], n_bbox: &BoundingBox<D>) -> bool {
let d = cut.dim();
let v = cut.value();
let p_d = point[d];
let n_min = n_bbox.min()[d];
let n_max = n_bbox.max()[d];
if p_d <= v && v < n_min {
return true;
}
if n_max <= v && v < p_d {
return true;
}
false
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use crate::visitor::Visitor;
struct PathRecorder {
depths: Vec<usize>,
leaf_idx: Option<usize>,
}
impl PathRecorder {
fn new() -> Self {
Self {
depths: Vec::new(),
leaf_idx: None,
}
}
}
impl<const D: usize> Visitor<D> for PathRecorder {
type Output = (Vec<usize>, Option<usize>);
fn accept_internal(
&mut self,
depth: usize,
_mass: u64,
_cut: &Cut,
_bbox: &BoundingBox<D>,
_prob: f64,
_per_dim: &[f64],
) {
self.depths.push(depth);
}
fn accept_leaf(&mut self, depth: usize, _mass: u64, point_idx: usize) {
self.depths.push(depth);
self.leaf_idx = Some(point_idx);
}
fn result(self) -> Self::Output {
(self.depths, self.leaf_idx)
}
}
fn fresh_rng(seed: u64) -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(seed)
}
#[test]
fn new_rejects_zero_dimension() {
assert!(matches!(
RandomCutTree::<0>::new(8).unwrap_err(),
RcfError::InvalidConfig(_)
));
}
#[test]
fn new_rejects_zero_capacity() {
assert!(matches!(
RandomCutTree::<2>::new(0).unwrap_err(),
RcfError::InvalidConfig(_)
));
}
#[test]
fn empty_tree_root_is_none() {
let t = RandomCutTree::<2>::new(8).unwrap();
assert!(t.root().is_none());
assert_eq!(t.distinct_point_count(), 0);
assert_eq!(t.dimension(), 2);
}
#[test]
fn add_first_point_creates_root_leaf() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let points: Vec<[f64; 2]> = vec![[1.0, 2.0]];
let mut rng = fresh_rng(42);
t.add(0, &points[0], &points, &mut rng).unwrap();
let root = t.root().unwrap();
assert!(root.is_leaf());
assert_eq!(t.distinct_point_count(), 1);
}
#[test]
fn add_two_points_creates_internal_root() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p0 = [0.0_f64, 0.0];
let p1 = [1.0_f64, 1.0];
let points = vec![p0, p1];
let mut rng = fresh_rng(7);
t.add(0, &p0, &points, &mut rng).unwrap();
t.add(1, &p1, &points, &mut rng).unwrap();
let root = t.root().unwrap();
assert!(root.is_internal());
assert_eq!(t.distinct_point_count(), 2);
assert_eq!(t.store().view(root).unwrap().mass(), 2);
}
#[test]
fn add_rejects_dimension_mismatch() {
let mut t = RandomCutTree::<3>::new(8).unwrap();
let points: Vec<[f64; 3]> = vec![];
let mut rng = fresh_rng(1);
assert!(matches!(
t.add(0, &[1.0, 2.0], &points, &mut rng).unwrap_err(),
RcfError::DimensionMismatch { .. }
));
}
#[test]
fn add_rejects_non_finite() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let points: Vec<[f64; 2]> = vec![];
let mut rng = fresh_rng(1);
assert!(matches!(
t.add(0, &[1.0, f64::NAN], &points, &mut rng).unwrap_err(),
RcfError::NaNValue
));
}
#[test]
fn add_rejects_duplicate_point_idx() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p = [0.0_f64, 0.0];
let points = vec![p];
let mut rng = fresh_rng(1);
t.add(0, &p, &points, &mut rng).unwrap();
assert!(matches!(
t.add(0, &p, &points, &mut rng).unwrap_err(),
RcfError::InvalidConfig(_)
));
}
#[test]
fn duplicate_coordinate_increments_leaf_mass() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p = [3.0_f64, 4.0];
let mut points = vec![p];
let mut rng = fresh_rng(1);
t.add(0, &p, &points, &mut rng).unwrap();
points.push(p);
t.add(1, &p, &points, &mut rng).unwrap();
let root = t.root().unwrap();
assert!(root.is_leaf(), "single-point tree stays a leaf");
assert_eq!(t.store().view(root).unwrap().mass(), 2);
assert_eq!(t.distinct_point_count(), 2);
}
#[test]
fn add_many_distinct_points_keeps_mass_invariant() {
let mut t = RandomCutTree::<4>::new(64).unwrap();
let mut rng = fresh_rng(99);
let mut points: Vec<[f64; 4]> = Vec::new();
for i in 0_u32..32 {
let f = f64::from(i);
let p = [f, f * 2.0, f * 0.5, -f];
points.push(p);
t.add(i as usize, &p, &points, &mut rng).unwrap();
}
let root = t.root().unwrap();
assert!(root.is_internal());
assert_eq!(t.store().view(root).unwrap().mass(), 32);
assert_eq!(t.distinct_point_count(), 32);
}
#[test]
fn delete_unknown_point_idx_is_err() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let points: Vec<[f64; 2]> = vec![];
assert!(matches!(
t.delete(99, &points).unwrap_err(),
RcfError::InvalidConfig(_)
));
}
#[test]
fn delete_root_leaf_clears_tree() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p = [1.0_f64, 2.0];
let points = vec![p];
let mut rng = fresh_rng(1);
t.add(0, &p, &points, &mut rng).unwrap();
t.delete(0, &points).unwrap();
assert!(t.root().is_none());
assert_eq!(t.distinct_point_count(), 0);
assert_eq!(t.store().live_count(), 0);
}
#[test]
fn delete_one_of_two_points_leaves_sibling_as_root() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p0 = [0.0_f64, 0.0];
let p1 = [1.0_f64, 1.0];
let points = vec![p0, p1];
let mut rng = fresh_rng(7);
t.add(0, &p0, &points, &mut rng).unwrap();
t.add(1, &p1, &points, &mut rng).unwrap();
t.delete(0, &points).unwrap();
let root = t.root().unwrap();
assert!(root.is_leaf());
let leaf = t.store().leaf(root).unwrap();
assert_eq!(leaf.point_idx, 1);
assert_eq!(leaf.mass, 1);
assert_eq!(t.store().live_count(), 1);
}
#[test]
fn delete_duplicate_decrements_mass_only() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p = [1.0_f64, 1.0];
let mut points = vec![p, p];
let mut rng = fresh_rng(1);
t.add(0, &p, &points, &mut rng).unwrap();
t.add(1, &p, &points, &mut rng).unwrap();
let root = t.root().unwrap();
assert_eq!(t.store().view(root).unwrap().mass(), 2);
t.delete(1, &points).unwrap();
assert_eq!(t.store().view(root).unwrap().mass(), 1);
assert!(t.root().unwrap().is_leaf());
assert!(!t.contains(1));
assert!(t.contains(0));
assert_eq!(t.store().view(root).unwrap().mass(), 1);
points.pop();
}
#[test]
fn delete_then_re_add_keeps_capacity_bounded() {
let mut t = RandomCutTree::<2>::new(4).unwrap();
let mut rng = fresh_rng(11);
let mut points: Vec<[f64; 2]> = Vec::new();
let mut live_idxs: Vec<usize> = Vec::new();
for i in 0_u32..4 {
let f = f64::from(i);
let p = [f, f + 1.0];
points.push(p);
let idx = i as usize;
t.add(idx, &p, &points, &mut rng).unwrap();
live_idxs.push(idx);
}
for _ in 0..10 {
let old = live_idxs.remove(0);
t.delete(old, &points).unwrap();
let new_idx = points.len();
let p = points[old];
points.push(p);
t.add(new_idx, &p, &points, &mut rng).unwrap();
live_idxs.push(new_idx);
}
assert_eq!(t.distinct_point_count(), 4);
}
#[test]
fn traverse_empty_tree_is_err() {
let t = RandomCutTree::<2>::new(4).unwrap();
let v = PathRecorder::new();
assert!(matches!(
t.traverse(&[1.0, 2.0], v).unwrap_err(),
RcfError::EmptyForest
));
}
#[test]
fn traverse_single_leaf_visits_only_leaf() {
let mut t = RandomCutTree::<2>::new(4).unwrap();
let p = [1.0_f64, 2.0];
let points = vec![p];
let mut rng = fresh_rng(0);
t.add(0, &p, &points, &mut rng).unwrap();
let v = PathRecorder::new();
let (depths, leaf_idx) = t.traverse(&p, v).unwrap();
assert_eq!(depths, vec![0]);
assert_eq!(leaf_idx, Some(0));
}
#[test]
fn traverse_visits_in_root_to_leaf_order() {
let mut t = RandomCutTree::<2>::new(8).unwrap();
let p0 = [0.0_f64, 0.0];
let p1 = [10.0_f64, 10.0];
let points = vec![p0, p1];
let mut rng = fresh_rng(123);
t.add(0, &p0, &points, &mut rng).unwrap();
t.add(1, &p1, &points, &mut rng).unwrap();
let v = PathRecorder::new();
let (depths, leaf_idx) = t.traverse(&p1, v).unwrap();
assert!(depths.array_windows::<2>().all(|[a, b]| a < b));
assert!(leaf_idx == Some(0) || leaf_idx == Some(1));
}
#[test]
fn traverse_rejects_dim_mismatch_and_nan() {
let mut t = RandomCutTree::<2>::new(4).unwrap();
let p = [1.0_f64, 2.0];
let points = vec![p];
let mut rng = fresh_rng(0);
t.add(0, &p, &points, &mut rng).unwrap();
assert!(matches!(
t.traverse(&[1.0], PathRecorder::new()).unwrap_err(),
RcfError::DimensionMismatch { .. }
));
assert!(matches!(
t.traverse(&[f64::NAN, 0.0], PathRecorder::new())
.unwrap_err(),
RcfError::NaNValue
));
}
#[test]
fn deterministic_tree_under_fixed_seed() {
fn build(seed: u64) -> RandomCutTree<3> {
let mut t = RandomCutTree::<3>::new(64).unwrap();
let mut rng = fresh_rng(seed);
let mut points: Vec<[f64; 3]> = Vec::new();
for i in 0_u32..16 {
let f = f64::from(i);
let p = [f, f * 2.0, f * 0.25 + 1.0];
points.push(p);
t.add(i as usize, &p, &points, &mut rng).unwrap();
}
t
}
let t1 = build(42);
let t2 = build(42);
assert_eq!(t1.store().live_count(), t2.store().live_count());
assert_eq!(t1.distinct_point_count(), t2.distinct_point_count());
assert_eq!(
t1.store().live_internal_count(),
t2.store().live_internal_count()
);
assert_eq!(t1.store().live_leaf_count(), t2.store().live_leaf_count());
}
#[test]
fn isolates_point_above_separated() {
let mut bbox = BoundingBox::<2>::from_point(&[0.0, 0.0]).unwrap();
bbox.extend(&[1.0, 1.0]).unwrap();
assert!(isolates_point(&Cut::new(0, 5.0), &[10.0, 0.5], &bbox));
assert!(!isolates_point(&Cut::new(0, 0.5), &[10.0, 0.5], &bbox));
}
#[test]
fn isolates_point_below_separated() {
let mut bbox = BoundingBox::<2>::from_point(&[0.0, 0.0]).unwrap();
bbox.extend(&[1.0, 1.0]).unwrap();
assert!(isolates_point(&Cut::new(1, -2.0), &[0.5, -10.0], &bbox));
}
#[test]
fn isolates_point_inside_never() {
let mut bbox = BoundingBox::<2>::from_point(&[0.0, 0.0]).unwrap();
bbox.extend(&[10.0, 10.0]).unwrap();
assert!(!isolates_point(&Cut::new(0, 5.0), &[5.0, 5.0], &bbox));
}
proptest::proptest! {
#![proptest_config(proptest::test_runner::Config { cases: 32, ..proptest::test_runner::Config::default() })]
#[test]
fn depth_bounded_under_uniform_inserts(seed in 0_u64..10_000) {
const N: usize = 64;
const D: usize = 4;
#[allow(clippy::cast_possible_truncation)]
let mut t = RandomCutTree::<D>::new(N as u32).unwrap();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut points: Vec<[f64; D]> = Vec::with_capacity(N);
for i in 0..N {
let mut p = [0.0_f64; D];
for slot in &mut p {
*slot = <ChaCha8Rng as rand::RngExt>::random::<f64>(&mut rng) * 100.0;
}
points.push(p);
t.add(i, &p, &points, &mut rng).unwrap();
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let log2_n = (N as f64).log2().ceil() as usize;
let bound = 4 * log2_n + 4;
let depth = t.max_depth().expect("non-empty tree");
proptest::prop_assert!(
depth <= bound,
"depth = {} exceeds bound 4·⌈log₂ {}⌉ + 4 = {} (seed={})",
depth, N, bound, seed,
);
}
}
}