use core::{
cmp::Ordering,
fmt::{Display, Formatter},
hash::{Hash, Hasher},
ops::Range,
};
use distances::Number;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use crate::{utils, Dataset, Instance, PartitionCriteria, PartitionCriterion};
use crate::core::cluster::Ratios;
#[derive(Debug)]
pub struct Cluster<U: Number> {
history: Vec<bool>,
seed: Option<u64>,
offset: usize,
cardinality: usize,
arg_center: usize,
arg_radial: usize,
radius: U,
lfd: f64,
pub(crate) children: Option<Children<U>>,
#[allow(dead_code)]
ratios: Option<Ratios>,
}
#[derive(Debug)]
pub struct Children<U: Number> {
pub(crate) left: Box<Cluster<U>>,
pub(crate) right: Box<Cluster<U>>,
pub(crate) arg_l: usize,
pub(crate) arg_r: usize,
pub(crate) polar_distance: U,
}
impl<U: Number> PartialEq for Cluster<U> {
fn eq(&self, other: &Self) -> bool {
self.history == other.history
}
}
impl<U: Number> Eq for Cluster<U> {}
impl<U: Number> PartialOrd for Cluster<U> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<U: Number> Ord for Cluster<U> {
fn cmp(&self, other: &Self) -> Ordering {
match self.depth().cmp(&other.depth()) {
Ordering::Equal => self.offset.cmp(&other.offset),
ordering => ordering,
}
}
}
impl<U: Number> Hash for Cluster<U> {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.offset, self.cardinality).hash(state);
}
}
impl<U: Number> Display for Cluster<U> {
fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
write!(f, "{}", self.name())
}
}
impl<U: Number> Cluster<U> {
pub const fn cardinality(&self) -> usize {
self.cardinality
}
pub const fn arg_center(&self) -> usize {
self.arg_center
}
pub const fn arg_radial(&self) -> usize {
self.arg_radial
}
pub const fn radius(&self) -> U {
self.radius
}
pub const fn lfd(&self) -> f64 {
self.lfd
}
pub const fn ratios(&self) -> Option<Ratios> {
self.ratios
}
pub fn new_root<I: Instance, D: Dataset<I, U>>(data: &D, seed: Option<u64>) -> Self {
let indices = (0..data.cardinality()).collect::<Vec<_>>();
Self::new(data, seed, vec![true], 0, &indices)
}
fn new<I: Instance, D: Dataset<I, 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 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let n = (indices.len().as_f64().sqrt()) as usize;
data.choose_unique(n, indices, seed)
};
let Some(arg_center) = data.median(&arg_samples) else {
unreachable!("The cluster should have at least one instance.")
};
let center_distances = data.one_to_many(arg_center, indices);
let Some((arg_radial, radius)) = utils::arg_max(¢er_distances) else {
unreachable!("The cluster should have at least one instance.")
};
let arg_radial = indices[arg_radial];
let lfd = utils::compute_lfd(radius, ¢er_distances);
Self {
history,
seed,
offset,
cardinality,
arg_center,
arg_radial,
radius,
lfd,
children: None,
ratios: None,
}
}
#[must_use]
pub fn partition<I: Instance, D: Dataset<I, U>>(mut self, data: &mut D, criteria: &PartitionCriteria<U>) -> Self {
let mut indices = (0..self.cardinality).collect::<Vec<_>>();
(self, indices) = self._partition(data, criteria, indices);
data.permute_instances(&indices)
.unwrap_or_else(|_| unreachable!("All indices are valid."));
self
}
fn _partition<I: Instance, D: Dataset<I, U>>(
mut self,
data: &D,
criteria: &PartitionCriteria<U>,
mut indices: Vec<usize>,
) -> (Self, Vec<usize>) {
if criteria.check(&self) {
let ([(arg_l, l_indices), (arg_r, r_indices)], polar_distance) = self.partition_once(data, indices);
let r_offset = self.offset + l_indices.len();
let (l_history, r_history) = (self.child_history(false), self.child_history(true));
let ((left, l_indices), (right, r_indices)) = rayon::join(
|| Self::new(data, self.seed, l_history, self.offset, &l_indices)._partition(data, criteria, l_indices),
|| Self::new(data, self.seed, r_history, r_offset, &r_indices)._partition(data, criteria, r_indices),
);
let arg_l = utils::pos_val(&l_indices, arg_l)
.map_or_else(|| unreachable!("We know the left pole is in the indices."), |(i, _)| i);
let arg_r = utils::pos_val(&r_indices, arg_r)
.map_or_else(|| unreachable!("We know the right pole is in the indices."), |(i, _)| i);
self.children = Some(Children {
left: Box::new(left),
right: Box::new(right),
arg_l: self.offset + arg_l,
arg_r: r_offset + arg_r,
polar_distance,
});
indices = l_indices.into_iter().chain(r_indices).collect::<Vec<_>>();
}
let arg_center = utils::pos_val(&indices, self.arg_center)
.map_or_else(|| unreachable!("We know the center is in the indices."), |(i, _)| i);
self.arg_center = self.offset + arg_center;
let arg_radial = utils::pos_val(&indices, self.arg_radial)
.map_or_else(|| unreachable!("We know the radial is in the indices."), |(i, _)| i);
self.arg_radial = self.offset + arg_radial;
(self, indices)
}
fn partition_once<I: Instance, D: Dataset<I, U>>(
&self,
data: &D,
indices: Vec<usize>,
) -> ([(usize, Vec<usize>); 2], U) {
let l_distances = data.one_to_many(self.arg_radial, &indices);
let Some((arg_r, polar_distance)) = utils::arg_max(&l_distances) else {
unreachable!("The cluster should have at least one instance.")
};
let arg_r = indices[arg_r];
let r_distances = data.one_to_many(arg_r, &indices);
let (l_indices, r_indices) = indices
.into_iter()
.zip(l_distances)
.zip(r_distances)
.filter(|&((i, _), _)| i != self.arg_radial && i != arg_r)
.partition::<Vec<_>, _>(|&((_, l), r)| l <= r);
let (l_indices, r_indices) = {
let mut l_indices = Self::drop_distances(l_indices);
let mut r_indices = Self::drop_distances(r_indices);
l_indices.push(self.arg_radial);
r_indices.push(arg_r);
(l_indices, r_indices)
};
if l_indices.len() < r_indices.len() {
([(arg_r, r_indices), (self.arg_radial, l_indices)], polar_distance)
} else {
([(self.arg_radial, l_indices), (arg_r, 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
}
#[must_use]
pub(crate) fn set_child_parent_ratios(mut self, parent_ratios: Ratios) -> Self {
let [parent_cardinality, parent_radius, parent_lfd, parent_cardinality_ema, parent_radius_ema, parent_lfd_ema] =
parent_ratios;
let c = self.cardinality.as_f64() / parent_cardinality;
let r = self.radius.as_f64() / parent_radius;
let l = self.lfd / parent_lfd;
let c_ = utils::next_ema(c, parent_cardinality_ema);
let r_ = utils::next_ema(r, parent_radius_ema);
let l_ = utils::next_ema(l, parent_lfd_ema);
let ratios = [c, r, l, c_, r_, l_];
self.ratios = Some(ratios);
if let Some(Children {
left,
right,
arg_l,
arg_r,
polar_distance,
}) = self.children
{
let left = Box::new(left.set_child_parent_ratios(ratios));
let right = Box::new(right.set_child_parent_ratios(ratios));
let children = Children {
left,
right,
arg_l,
arg_r,
polar_distance,
};
self.children = Some(children);
}
self
}
pub(crate) fn set_normalized_ratios(&mut self, means: Ratios, sds: Ratios) {
let normalized_ratios: Vec<_> = self
.ratios
.unwrap_or_else(|| unreachable!("Ratios should have been set first."))
.into_iter()
.zip(means)
.zip(sds)
.map(|((value, mean), std)| (value - mean) / std.mul_add(core::f64::consts::SQRT_2, f64::EPSILON))
.map(libm::erf)
.map(|v| (1. + v) / 2.)
.collect();
if let Ok(normalized_ratios) = normalized_ratios.try_into() {
self.ratios = Some(normalized_ratios);
}
match &mut self.children {
Some(children) => {
children.left.set_normalized_ratios(means, sds);
children.right.set_normalized_ratios(means, sds);
}
None => (),
}
}
pub const fn indices(&self) -> Range<usize> {
self.offset..(self.offset + self.cardinality)
}
pub fn history(&self) -> &[bool] {
&self.history
}
pub fn name(&self) -> String {
Self::history_to_name(&self.history)
}
#[must_use]
pub fn history_to_name(history: &[bool]) -> String {
let rem = history.len() % 4;
let padding = if rem == 0 { 0 } else { 4 - rem };
(0..padding) .map(|_| &false)
.chain(history)
.map(|&b| if b { "1" } else { "0" })
.collect::<Vec<_>>()
.chunks_exact(4)
.map(|s| {
u8::from_str_radix(&s.join(""), 2)
.unwrap_or_else(|_| unreachable!("We know the characters used are only \"0\" and \"1\"."))
})
.fold(String::new(), |mut acc, s| {
write!(&mut acc, "{s:01x}")
.unwrap_or_else(|_| unreachable!("We know the characters used are hexadecimal."));
acc
})
}
#[must_use]
pub fn name_to_history(name: &str) -> Vec<bool> {
name.chars()
.map(|c| {
u8::from_str_radix(&c.to_string(), 16)
.unwrap_or_else(|_| unreachable!("We know the characters used are hexadecimal."))
})
.fold(String::new(), |mut acc, c| {
write!(&mut acc, "{c:04b}")
.unwrap_or_else(|_| unreachable!("We know the characters used are only \"0\" and \"1\"."));
acc
})
.trim_start_matches('0')
.chars()
.map(|c| c == '1')
.collect()
}
pub fn depth(&self) -> usize {
self.history.len() - 1
}
pub fn is_singleton(&self) -> bool {
self.radius == U::zero()
}
pub const 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()])
}
pub fn polar_distance(&self) -> Option<U> {
self.children.as_ref().map(|v| v.polar_distance)
}
pub fn is_ancestor_of(&self, other: &Self) -> bool {
self.depth() < other.depth() && self.history.as_slice() == &other.history[..self.history.len()]
}
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())
.chain(right.subtree())
.collect(),
None => subtree,
}
}
pub fn max_leaf_depth(&self) -> usize {
self.subtree().into_iter().map(Self::depth).max().map_or_else(
|| unreachable!("The subtree of a Cluster should have at least one element, i.e. the Cluster itself."),
|depth| depth,
)
}
pub fn distance_to_instance<I: Instance, D: Dataset<I, U>>(&self, data: &D, instance: &I) -> U {
data.query_to_one(instance, self.arg_center)
}
pub fn distance_to_other<I: Instance, D: Dataset<I, U>>(&self, data: &D, other: &Self) -> U {
data.one_to_one(self.arg_center, other.arg_center)
}
pub fn overlapping_children<I: Instance, D: Dataset<I, U>>(&self, data: &D, query: &I, radius: U) -> Vec<&Self> {
self.children.as_ref().map_or_else(
Vec::new,
|Children {
left,
right,
arg_l,
arg_r,
polar_distance,
..
}| {
let ql = data.query_to_one(query, *arg_l);
let qr = data.query_to_one(query, *arg_r);
let swap = ql < qr;
let (ql, qr) = if swap { (qr, ql) } else { (ql, qr) };
if (ql + qr) * (ql - qr) <= U::from(2) * (*polar_distance) * radius {
vec![left.as_ref(), right.as_ref()]
} else if swap {
vec![left.as_ref()]
} else {
vec![right.as_ref()]
}
},
)
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Serialize, Deserialize)]
pub struct SerializedCluster {
pub name: String,
pub seed: Option<u64>,
pub offset: usize,
pub cardinality: usize,
pub arg_center: usize,
pub arg_radial: usize,
pub radius_bytes: Vec<u8>,
pub lfd: f64,
pub ratios: Option<Ratios>,
}
#[derive(Serialize, Deserialize)]
pub struct SerializedChildren {
pub arg_l: usize,
pub arg_r: usize,
pub polar_distance_bytes: Vec<u8>,
}
impl SerializedCluster {
pub fn from_cluster<U: Number>(cluster: &Cluster<U>) -> (Self, Option<SerializedChildren>) {
let name = cluster.name();
let cardinality = cluster.cardinality;
let offset = cluster.offset;
let seed = cluster.seed;
let radius_bytes = cluster.radius.to_le_bytes();
let arg_center = cluster.arg_center;
let arg_radial = cluster.arg_radial;
let lfd = cluster.lfd;
let ratios = cluster.ratios;
let children = cluster.children.as_ref().map(
|Children {
arg_l,
arg_r,
polar_distance,
..
}| {
SerializedChildren {
arg_l: *arg_l,
arg_r: *arg_r,
polar_distance_bytes: polar_distance.to_le_bytes(),
}
},
);
(
Self {
name,
seed,
offset,
cardinality,
arg_center,
arg_radial,
radius_bytes,
lfd,
ratios,
},
children,
)
}
#[must_use]
pub fn into_partial_cluster<U: Number>(self) -> Cluster<U> {
Cluster {
history: Cluster::<U>::name_to_history(&self.name),
seed: self.seed,
offset: self.offset,
cardinality: self.cardinality,
arg_center: self.arg_center,
arg_radial: self.arg_radial,
radius: U::from_le_bytes(&self.radius_bytes),
lfd: self.lfd,
ratios: self.ratios,
children: None,
}
}
}