#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClqaError {
ZeroDim,
DimensionMismatch,
}
impl std::fmt::Display for ClqaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroDim => write!(f, "dim must be positive"),
Self::DimensionMismatch => {
write!(
f,
"centers and offsets must share a length that is a multiple of dim"
)
}
}
}
}
impl std::error::Error for ClqaError {}
#[derive(Debug, Clone, Copy)]
pub struct BoxClqa<'a> {
centers: &'a [f32],
offsets: &'a [f32],
dim: usize,
n: usize,
}
impl<'a> BoxClqa<'a> {
pub fn new(centers: &'a [f32], offsets: &'a [f32], dim: usize) -> Result<Self, ClqaError> {
if dim == 0 {
return Err(ClqaError::ZeroDim);
}
if centers.len() != offsets.len() || !centers.len().is_multiple_of(dim) {
return Err(ClqaError::DimensionMismatch);
}
Ok(Self {
centers,
offsets,
dim,
n: centers.len() / dim,
})
}
pub fn num_concepts(&self) -> usize {
self.n
}
pub fn join(&self, a: usize, b: usize) -> (Vec<f32>, Vec<f32>) {
let (ao, bo) = (a * self.dim, b * self.dim);
let mut jc = vec![0f32; self.dim];
let mut jo = vec![0f32; self.dim];
for i in 0..self.dim {
let lo = (self.centers[ao + i] - self.offsets[ao + i])
.min(self.centers[bo + i] - self.offsets[bo + i]);
let hi = (self.centers[ao + i] + self.offsets[ao + i])
.max(self.centers[bo + i] + self.offsets[bo + i]);
jc[i] = (lo + hi) / 2.0;
jo[i] = (hi - lo) / 2.0;
}
(jc, jo)
}
pub fn score_lca(&self, jc: &[f32], jo: &[f32], x: usize, tau: f32) -> f32 {
let xo = x * self.dim;
let mut incl = 0f32; let mut prox = 0f32; for i in 0..self.dim {
let v = ((jc[i] - self.centers[xo + i]).abs() + jo[i] - self.offsets[xo + i]).max(0.0);
incl += v * v;
prox += (self.centers[xo + i] - jc[i]).abs() + (self.offsets[xo + i] - jo[i]).abs();
}
let gate = (-incl.sqrt()).exp();
let proximity = (-prox / tau).exp();
gate * proximity
}
pub fn rank_lca(&self, a: usize, b: usize, tau: f32) -> Vec<(usize, f32)> {
let (jc, jo) = self.join(a, b);
let mut scored: Vec<(usize, f32)> = (0..self.n)
.filter(|&x| x != a && x != b)
.map(|x| (x, self.score_lca(&jc, &jo, x, tau)))
.collect();
scored.sort_by(|p, r| r.1.partial_cmp(&p.1).unwrap());
scored
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> (Vec<f32>, Vec<f32>, usize) {
let centers = vec![
0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0, 5.0, 5.0, ];
let offsets = vec![
10.0, 10.0, 2.0, 2.0, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, ];
(centers, offsets, 2)
}
#[test]
fn join_is_smallest_enclosing_box() {
let (c, o, dim) = fixture();
let q = BoxClqa::new(&c, &o, dim).unwrap();
let (jc, jo) = q.join(2, 3);
assert!(
(jc[0] - 0.0).abs() < 1e-6 && (jc[1] - 0.0).abs() < 1e-6,
"{jc:?}"
);
assert!((jo[0] - 1.5).abs() < 1e-6, "x half-width: {jo:?}");
assert!((jo[1] - 0.5).abs() < 1e-6, "y half-width: {jo:?}");
}
#[test]
fn lca_is_the_tightest_container_not_the_root() {
let (c, o, dim) = fixture();
let q = BoxClqa::new(&c, &o, dim).unwrap();
let ranked = q.rank_lca(2, 3, 1.0);
assert_eq!(ranked[0].0, 1, "ranking: {ranked:?}");
let par_pos = ranked.iter().position(|&(x, _)| x == 1).unwrap();
let sib_pos = ranked.iter().position(|&(x, _)| x == 4).unwrap();
assert!(
par_pos < sib_pos,
"container must outrank non-container: {ranked:?}"
);
}
#[test]
fn gate_penalizes_non_containers() {
let (c, o, dim) = fixture();
let q = BoxClqa::new(&c, &o, dim).unwrap();
let (jc, jo) = q.join(2, 3);
let s_par = q.score_lca(&jc, &jo, 1, 1.0);
let s_sib = q.score_lca(&jc, &jo, 4, 1.0);
assert!(s_par > s_sib, "par {s_par} should beat sib {s_sib}");
assert!((0.0..=1.0).contains(&s_par) && (0.0..=1.0).contains(&s_sib));
}
#[test]
fn rejects_bad_dimensions() {
assert_eq!(
BoxClqa::new(&[1.0], &[1.0], 0).unwrap_err(),
ClqaError::ZeroDim
);
assert_eq!(
BoxClqa::new(&[1.0, 2.0], &[1.0], 2).unwrap_err(),
ClqaError::DimensionMismatch
);
assert_eq!(
BoxClqa::new(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0], 2).unwrap_err(),
ClqaError::DimensionMismatch
);
}
}