use std::num::NonZeroUsize;
use crate::latent::Continuous;
use crate::{Error, Latent, Result};
use super::super::common::{l2_distance, shape};
pub fn grid_distance(
reference: &[Vec<Latent<Continuous>>],
candidate: &[Vec<Latent<Continuous>>],
) -> Result<f32> {
let clusters = grid_cell_distances(reference, candidate)?;
let total: f64 = clusters.iter().flatten().sum();
let cells: usize = clusters.iter().map(Vec::len).sum();
Ok((total / cells as f64) as f32)
}
pub fn grid_cell_distances(
reference: &[Vec<Latent<Continuous>>],
candidate: &[Vec<Latent<Continuous>>],
) -> Result<Vec<Vec<f64>>> {
if reference.is_empty() {
return Err(Error::validation(
"step-gap grids need at least one condition",
));
}
if reference.len() != candidate.len() {
return Err(Error::validation(format!(
"grids must share a condition count; reference {} vs candidate {}",
reference.len(),
candidate.len()
)));
}
let mut clusters: Vec<Vec<f64>> = Vec::with_capacity(reference.len());
let mut expected: Option<[usize; 2]> = None;
for (c, (ref_row, cand_row)) in reference.iter().zip(candidate).enumerate() {
if ref_row.is_empty() {
return Err(Error::validation(format!("condition {c} has no samples")));
}
if ref_row.len() != cand_row.len() {
return Err(Error::validation(format!(
"condition {c} must share a seed budget; reference {} vs candidate {}",
ref_row.len(),
cand_row.len()
)));
}
let mut row = Vec::with_capacity(ref_row.len());
for (s, (r, k)) in ref_row.iter().zip(cand_row).enumerate() {
let rt = r.repr();
let kt = k.repr();
if rt.values().is_empty() {
return Err(Error::validation("samples must have at least one element"));
}
let want = *expected.get_or_insert_with(|| shape(rt));
if shape(rt) != want || shape(kt) != want {
return Err(Error::validation(format!(
"all cells must share a shape {want:?}; cell ({c}, {s}) has \
reference {:?} / candidate {:?}",
shape(rt),
shape(kt)
)));
}
row.push(l2_distance(rt, kt));
}
clusters.push(row);
}
Ok(clusters)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct StepGap {
steps: NonZeroUsize,
gap: f32,
}
impl StepGap {
pub fn steps(&self) -> NonZeroUsize {
self.steps
}
pub fn gap(&self) -> f32 {
self.gap
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StepGapCurve {
reference_steps: NonZeroUsize,
gaps: Vec<StepGap>,
}
impl StepGapCurve {
pub fn measure(
reference_steps: NonZeroUsize,
reference: &[Vec<Latent<Continuous>>],
candidates: &[(NonZeroUsize, &[Vec<Latent<Continuous>>])],
) -> Result<Self> {
if candidates.is_empty() {
return Err(Error::validation(
"step-gap curve needs at least one candidate budget",
));
}
let mut gaps: Vec<StepGap> = Vec::with_capacity(candidates.len());
for &(steps, grid) in candidates {
if gaps.iter().any(|g| g.steps == steps) {
return Err(Error::validation(format!(
"duplicate candidate step budget {steps}"
)));
}
let gap = grid_distance(reference, grid)?;
gaps.push(StepGap { steps, gap });
}
gaps.sort_by_key(|g| g.steps);
Ok(Self {
reference_steps,
gaps,
})
}
pub fn reference_steps(&self) -> NonZeroUsize {
self.reference_steps
}
pub fn gaps(&self) -> &[StepGap] {
&self.gaps
}
pub fn is_converging(&self) -> bool {
self.gaps.windows(2).all(|w| w[1].gap <= w[0].gap)
}
}