use core::hash::{Hash, Hasher};
use distances::Number;
use super::PartitionCriteria;
use crate::{dataset::Dataset, utils::helpers};
pub type Ratios = [f64; 6];
#[derive(Debug)]
pub(crate) struct Cluster<T: Send + Sync + Copy, U: Number> {
pub history: Vec<bool>,
pub seed: Option<u64>,
pub offset: usize,
pub cardinality: usize,
pub center: T,
pub radial: T,
pub radius: U,
#[allow(dead_code)]
pub lfd: f64,
pub children: Option<Children<T, U>>,
#[allow(dead_code)]
pub ratios: Option<Ratios>,
}
#[derive(Debug)]
pub(crate) struct Children<T: Send + Sync + Copy, U: Number> {
pub left: Box<Cluster<T, U>>,
pub right: Box<Cluster<T, U>>,
pub l_pole: T,
pub r_pole: T,
pub polar_distance: U,
}
impl<T: Send + Sync + Copy, U: Number> PartialEq for Cluster<T, U> {
fn eq(&self, other: &Self) -> bool {
self.history == other.history
}
}
impl<T: Send + Sync + Copy, U: Number> Eq for Cluster<T, U> {}
impl<T: Send + Sync + Copy, U: Number> PartialOrd for Cluster<T, U> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.depth() == other.depth() {
self.offset.partial_cmp(&other.offset)
} else {
self.depth().partial_cmp(&other.depth())
}
}
}
impl<T: Send + Sync + Copy, U: Number> Ord for Cluster<T, U> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.depth() == other.depth() {
self.offset.cmp(&other.offset)
} else {
self.depth().cmp(&other.depth())
}
}
}
impl<T: Send + Sync + Copy, U: Number> Hash for Cluster<T, U> {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.offset, self.cardinality).hash(state)
}
}
impl<T: Send + Sync + Copy, U: Number> std::fmt::Display for Cluster<T, U> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl<T: Send + Sync + Copy, U: Number> Cluster<T, U> {
pub fn new_root<D: Dataset<T, U>>(data: &D, indices: &[usize], seed: Option<u64>) -> Self {
Cluster::new(data, seed, vec![true], 0, indices)
}
pub fn new<D: Dataset<T, U>>(
data: &D,
seed: Option<u64>,
history: Vec<bool>,
offset: usize,
indices: &[usize],
) -> Self {
let cardinality = indices.len();
let arg_samples = if cardinality < 100 {
indices.to_vec()
} else {
let n = ((indices.len() as f64).sqrt()) as usize;
data.choose_unique(n, indices, seed)
};
let arg_center = data.median(&arg_samples);
let center = data.get(arg_center);
let center_distances = data.one_to_many(arg_center, indices);
let (arg_radius, radius) = helpers::arg_max(¢er_distances);
let arg_radius = indices[arg_radius];
let radial = data.get(arg_radius);
let lfd = helpers::compute_lfd(radius, ¢er_distances);
Cluster {
history,
seed,
offset,
cardinality,
center,
radial,
radius,
lfd,
children: None,
ratios: None,
}
}
pub fn partition<D: Dataset<T, U>>(mut self, data: &mut D, criteria: &PartitionCriteria<T, U>) -> Self {
assert_eq!(self.depth(), 0, "This method may only be called on a root cluster.");
let mut indices = data.indices().to_vec();
(self, indices) = self._partition(data, criteria, indices);
data.reorder(&indices);
self
}
fn _partition<D: Dataset<T, U>>(
mut self,
data: &D,
criteria: &PartitionCriteria<T, U>,
mut indices: Vec<usize>,
) -> (Self, Vec<usize>) {
if criteria.check(&self) {
let ([(l_pole, l_indices), (r_pole, r_indices)], polar_distance) = self.partition_once(data, indices);
let r_offset = self.offset + l_indices.len();
let ((left, l_indices), (right, r_indices)) = rayon::join(
|| {
Cluster::new(data, self.seed, self.child_history(false), self.offset, &l_indices)
._partition(data, criteria, l_indices)
},
|| {
Cluster::new(data, self.seed, self.child_history(true), r_offset, &r_indices)
._partition(data, criteria, r_indices)
},
);
let (left, right) = (Box::new(left), Box::new(right));
indices = l_indices.into_iter().chain(r_indices.into_iter()).collect::<Vec<_>>();
self.children = Some(Children {
left,
right,
l_pole,
r_pole,
polar_distance,
});
}
(self, indices)
}
fn partition_once<D: Dataset<T, U>>(&self, data: &D, indices: Vec<usize>) -> ([(T, Vec<usize>); 2], U) {
let l_distances = data.query_to_many(self.radial, &indices);
let (arg_r, polar_distance) = helpers::arg_max(&l_distances);
let r_pole = data.get(indices[arg_r]);
let r_distances = data.query_to_many(r_pole, &indices);
let (l_indices, r_indices) = indices
.into_iter()
.zip(l_distances.into_iter())
.zip(r_distances.into_iter())
.partition::<Vec<_>, _>(|&((_, l), r)| l <= r);
let l_indices = Self::drop_distances(l_indices);
let r_indices = Self::drop_distances(r_indices);
if l_indices.len() < r_indices.len() {
([(r_pole, r_indices), (self.radial, l_indices)], polar_distance)
} else {
([(self.radial, l_indices), (r_pole, r_indices)], polar_distance)
}
}
fn drop_distances(indices: Vec<((usize, U), U)>) -> Vec<usize> {
indices.into_iter().map(|((i, _), _)| i).collect()
}
fn child_history(&self, right: bool) -> Vec<bool> {
let mut history = self.history.clone();
history.push(right);
history
}
#[allow(unused_mut, unused_variables, dead_code)]
pub fn with_ratios(mut self, normalized: bool) -> Self {
todo!()
}
#[inline(always)]
#[allow(dead_code)]
fn next_ema(&self, ratio: f64, parent_ema: f64) -> f64 {
let alpha = 2. / 11.;
alpha * ratio + (1. - alpha) * parent_ema
}
#[allow(unused_mut, unused_variables, dead_code)]
fn set_child_parent_ratios(mut self, parent_ratios: Ratios) -> Self {
todo!()
}
#[allow(unused_mut, unused_variables, dead_code)]
fn set_normalized_ratios(&mut self, means: Ratios, sds: Ratios) {
todo!()
}
pub fn indices<'a, D: Dataset<T, U>>(&'a self, data: &'a D) -> &[usize] {
&data.indices()[self.offset..(self.offset + self.cardinality)]
}
#[allow(dead_code)]
pub fn history(&self) -> &[bool] {
&self.history
}
pub fn name(&self) -> String {
let d = self.history.len();
let padding = if d % 4 == 0 { 0 } else { 4 - d % 4 };
let bin_name = (0..padding)
.map(|_| "0")
.chain(self.history.iter().map(|&b| if b { "1" } else { "0" }))
.collect::<Vec<_>>();
bin_name
.chunks_exact(4)
.map(|s| {
let [a, b, c, d] = [s[0], s[1], s[2], s[3]];
let s = format!("{a}{b}{c}{d}");
let s = u8::from_str_radix(&s, 2).unwrap();
format!("{s:01x}")
})
.collect::<Vec<_>>()
.join("")
}
#[allow(dead_code)]
pub fn is_root(&self) -> bool {
self.depth() == 0
}
pub fn depth(&self) -> usize {
self.history.len() - 1
}
pub fn is_singleton(&self) -> bool {
self.radius == U::zero()
}
pub fn is_leaf(&self) -> bool {
self.children.is_none()
}
pub fn children(&self) -> Option<[&Self; 2]> {
self.children.as_ref().map(|v| [v.left.as_ref(), v.right.as_ref()])
}
#[allow(dead_code)]
pub fn polar_distance(&self) -> Option<U> {
self.children.as_ref().map(|v| v.polar_distance)
}
#[allow(dead_code)]
pub fn ratios(&self) -> Ratios {
self.ratios
.expect("Please call `with_ratios` before using this method.")
}
#[allow(dead_code)]
pub fn is_ancestor_of(&self, other: &Self) -> bool {
self.depth() < other.depth() && self.history.as_slice() == &other.history[..self.history.len()]
}
#[allow(dead_code)]
pub fn is_descendant_of(&self, other: &Self) -> bool {
other.is_ancestor_of(self)
}
pub fn subtree(&self) -> Vec<&Self> {
let subtree = vec![self];
match self.children() {
Some([left, right]) => subtree
.into_iter()
.chain(left.subtree().into_iter())
.chain(right.subtree().into_iter())
.collect(),
None => subtree,
}
}
pub fn max_leaf_depth(&self) -> usize {
self.subtree().into_iter().map(|c| c.depth()).max().unwrap()
}
#[allow(dead_code)]
pub fn distance_to_indexed_instance<D: Dataset<T, U>>(&self, data: &D, index: usize) -> U {
data.metric()(data.get(index), self.center)
}
pub fn distance_to_instance<D: Dataset<T, U>>(&self, data: &D, instance: T) -> U {
data.metric()(instance, self.center)
}
#[allow(dead_code)]
pub fn distance_to_other<D: Dataset<T, U>>(&self, data: &D, other: &Self) -> U {
data.metric()(self.center, other.center)
}
pub fn overlapping_children<D: Dataset<T, U>>(&self, data: &D, query: T, radius: U) -> Vec<&Self> {
let children = self
.children
.as_ref()
.expect("This method may only be called on non-leaf clusters.");
let ql = data.metric()(query, children.l_pole);
let qr = data.metric()(query, children.r_pole);
let swap = ql < qr;
let (ql, qr) = if swap { (qr, ql) } else { (ql, qr) };
if (ql + qr) * (ql - qr) <= U::from(2) * children.polar_distance * radius {
vec![&children.left, &children.right]
} else if swap {
vec![&children.left]
} else {
vec![&children.right]
}
}
}
#[cfg(test)]
mod tests {
use distances::vectors::euclidean;
use crate::{
cluster::Tree,
dataset::{Dataset, VecVec},
};
use super::*;
#[test]
fn test_cluster() {
let data: Vec<&[f32]> = vec![&[0., 0., 0.], &[1., 1., 1.], &[2., 2., 2.], &[3., 3., 3.]];
let name = "test".to_string();
let mut data = VecVec::new(data, euclidean::<f32, f32>, name, false);
let indices = data.indices().to_vec();
let partition_criteria = PartitionCriteria::new(true).with_max_depth(3).with_min_cardinality(1);
let cluster = Cluster::new_root(&data, &indices, Some(42)).partition(&mut data, &partition_criteria);
assert_eq!(cluster.depth(), 0);
assert_eq!(cluster.cardinality, 4);
assert_eq!(cluster.subtree().len(), 7);
assert!(cluster.radius > 0.);
assert_eq!(format!("{cluster}"), "1");
let [left, right] = cluster.children().unwrap();
assert_eq!(format!("{left}"), "2");
assert_eq!(format!("{right}"), "3");
for child in [left, right] {
assert_eq!(child.depth(), 1);
assert_eq!(child.cardinality, 2);
assert_eq!(child.subtree().len(), 3);
}
}
#[test]
fn test_leaf_indices() {
let data: Vec<&[f32]> = vec![&[10.], &[1.], &[-5.], &[8.], &[3.], &[2.], &[0.5], &[0.]];
let name = "test".to_string();
let data = VecVec::new(data, euclidean::<f32, f32>, name, false);
let partition_criteria = PartitionCriteria::new(true).with_max_depth(3).with_min_cardinality(1);
let tree = Tree::new(data, Some(42)).partition(&partition_criteria);
let mut leaf_indices = tree.root().indices(tree.data()).to_vec();
leaf_indices.sort();
assert_eq!(leaf_indices, tree.data().indices());
}
#[test]
fn test_end_to_end_reordering() {
let data: Vec<&[f32]> = vec![&[10.], &[1.], &[-5.], &[8.], &[3.], &[2.], &[0.5], &[0.]];
let name = "test".to_string();
let data = VecVec::new(data, euclidean::<f32, f32>, name, false);
let partition_criteria = PartitionCriteria::new(true).with_max_depth(3).with_min_cardinality(1);
let tree = Tree::new(data, Some(42)).partition(&partition_criteria);
assert_eq!(tree.data().cardinality(), tree.indices().len());
assert_eq!((0..tree.cardinality()).collect::<Vec<usize>>(), tree.indices());
}
#[test]
fn cluster() {
let (dimensionality, min_val, max_val) = (10, -1., 1.);
let seed = 42;
let data = symagen::random_data::random_f32(10_000, dimensionality, min_val, max_val, seed);
let data = data.iter().map(|v| v.as_slice()).collect::<Vec<_>>();
let name = "test".to_string();
let mut data = VecVec::<_, f32>::new(data, euclidean, name, false);
let indices = data.indices().to_vec();
let partition_criteria = PartitionCriteria::new(true).with_min_cardinality(1);
let root = Cluster::new_root(&data, &indices, Some(seed)).partition(&mut data, &partition_criteria);
for c in root.subtree() {
assert!(c.cardinality > 0, "Cardinality must be positive.");
assert!(c.radius >= 0., "Radius must be non-negative.");
assert!(c.lfd > 0., "LFD must be positive.");
let radius = data.metric()(c.center, c.radial);
assert_eq!(
c.radius, radius,
"Radius must be equal to the distance to the farthest instance."
);
}
}
}