use core::cmp::{min, Ordering};
use distances::Number;
use crate::{Cluster, Dataset, Instance, Tree};
#[derive(Clone, Copy, Debug)]
enum Grain<'a, U: Number, C: Cluster<U>> {
Hit {
d: U,
index: usize,
},
Cluster {
c: &'a C,
d: U,
diameter: U,
multiplicity: usize,
is_leaf: bool,
},
}
impl<'a, U: Number, C: Cluster<U>> Grain<'a, U, C> {
fn new_cluster(c: &'a C, d: U) -> Self {
let r = c.radius();
Self::Cluster {
c,
d: d + r,
diameter: r + r,
multiplicity: c.cardinality(),
is_leaf: c.is_leaf(),
}
}
const fn new_hit(d: U, index: usize) -> Self {
Self::Hit { d, index }
}
fn d_min(&self) -> U {
match self {
Grain::Hit { d, .. } => *d,
Grain::Cluster { d, diameter, .. } => *d - *diameter,
}
}
const fn d(&self) -> U {
match self {
Grain::Hit { d, .. } | Grain::Cluster { d, .. } => *d,
}
}
const fn is_small(&self, k: usize) -> bool {
match self {
Grain::Hit { .. } => true,
Grain::Cluster {
multiplicity, is_leaf, ..
} => *multiplicity <= k || *is_leaf,
}
}
fn is_outside(&self, threshold: U) -> bool {
self.d_min() > threshold
}
fn cluster_to_hits<I: Instance, D: Dataset<I, U>>(self, data: &D, query: &I) -> Vec<Self> {
match self {
Grain::Hit { .. } => unreachable!("This is only called on non-hits."),
Grain::Cluster { c, .. } => {
let distances = data.query_to_many(query, &c.indices().collect::<Vec<_>>());
c.indices()
.zip(distances)
.map(|(index, d)| Grain::new_hit(d, index))
.collect::<Vec<_>>()
}
}
}
fn cluster_to_children(self) -> [&'a C; 2] {
match self {
Grain::Hit { .. } => unreachable!("This is only called on non-hits."),
Grain::Cluster { c, .. } => c
.children()
.unwrap_or_else(|| unreachable!("This is only called on non-leaves.")),
}
}
const fn multiplicity(&self) -> usize {
match self {
Grain::Hit { .. } => 1,
Grain::Cluster { multiplicity, .. } => *multiplicity,
}
}
pub fn partition(grains: &mut [Self], k: usize) -> usize {
Self::_partition(grains, k, 0, grains.len() - 1)
}
#[allow(clippy::many_single_char_names)]
fn _partition(grains: &mut [Self], k: usize, l: usize, r: usize) -> usize {
if l >= r {
min(l, r)
} else {
let mean_cardinality: usize = grains.iter().map(Grain::multiplicity).sum::<usize>() / grains.len();
let pivot = if mean_cardinality > k { l } else { l + (r - l) / 2 };
let p = Self::partition_once(grains, l, r, pivot);
let g = grains.iter().take(p + 1).map(Grain::multiplicity).sum::<usize>();
match g.cmp(&k) {
Ordering::Equal => p,
Ordering::Less => Self::_partition(grains, k, p + 1, r),
Ordering::Greater => {
if (p > 0) && (g > (k + grains[p - 1].multiplicity())) {
Self::_partition(grains, k, l, p - 1)
} else if (p > 0) && (g == k + grains[p - 1].multiplicity()) {
p - 1
} else {
p
}
}
}
}
}
#[allow(clippy::many_single_char_names)]
fn partition_once(grains: &mut [Self], l: usize, r: usize, pivot: usize) -> usize {
if pivot == 0 {
let min = grains
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.d()
.partial_cmp(&b.d())
.unwrap_or_else(|| unreachable!("Should never be called on a slice with a NaN."))
})
.unwrap_or_else(|| unreachable!("Should never be called on a slice with a NaN."))
.0;
grains.swap(pivot, min);
0
} else {
grains.swap(pivot, r);
let (mut a, mut b) = (l, l);
while b < r {
if grains[b].d() < grains[r].d() {
grains.swap(a, b);
a += 1;
}
b += 1;
}
grains.swap(a, r);
a
}
}
fn index(&self) -> usize {
match self {
Grain::Hit { index, .. } => *index,
Grain::Cluster { .. } => unreachable!("This is only called on hits."),
}
}
}
#[allow(clippy::many_single_char_names)]
pub fn search<I, U, D, C>(tree: &Tree<I, U, D, C>, query: &I, k: usize) -> Vec<(usize, U)>
where
I: Instance,
U: Number,
D: Dataset<I, U>,
C: Cluster<U>,
{
let data = tree.data();
let c = &tree.root;
let d = c.distance_to_instance(data, query);
let mut grains = vec![Grain::new_cluster(c, d)];
let [mut insiders, mut non_insiders]: [Vec<_>; 2];
loop {
let i = Grain::partition(&mut grains, k);
let threshold = grains[i].d();
non_insiders = grains.split_off(i + 1);
insiders = grains;
let non_insiders = non_insiders.into_iter().filter(|g| !g.is_outside(threshold));
let (clusters, mut hits) = insiders
.into_iter()
.chain(non_insiders)
.partition::<Vec<_>, _>(|g| matches!(g, Grain::Cluster { .. }));
let (small_clusters, clusters) = clusters.into_iter().partition::<Vec<_>, _>(|g| g.is_small(k));
for cluster in small_clusters {
hits.append(&mut cluster.cluster_to_hits(data, query));
}
if clusters.is_empty() {
Grain::partition(&mut hits, k);
let l = core::cmp::min(k, hits.len());
return hits[..l].iter().map(|g| (g.index(), g.d())).collect();
}
grains = clusters
.into_iter()
.flat_map(Grain::cluster_to_children)
.map(|c| (c, c.distance_to_instance(data, query)))
.map(|(c, d)| Grain::new_cluster(c, d))
.chain(hits)
.collect();
}
}