use crate::{
ebi_traits::ebi_trait_finite_language::EbiTraitFiniteLanguage,
math::distances::TriangularDistanceMatrix,
};
use ebi_objects::{
FiniteLanguage,
anyhow::{Result, anyhow},
ebi_arithmetic::{Fraction, One, Signed, Zero, f},
};
use std::sync::Arc;
pub trait MedoidNonStochastic {
fn medoid(&self, number_of_traces: usize) -> Result<FiniteLanguage>;
fn k_medoids_clustering(&self, number_of_clusters: usize) -> Result<FiniteLanguage>;
}
impl<T: ?Sized> MedoidNonStochastic for T
where
T: EbiTraitFiniteLanguage,
{
fn medoid(&self, number_of_traces: usize) -> Result<FiniteLanguage> {
let activity_key = self.activity_key().clone();
let mut result = FiniteLanguage::new_hashmap();
let distances = TriangularDistanceMatrix::new(self);
if number_of_traces.is_one() {
if let Some(trace_number) = medoid_single(self, &distances) {
result.insert(
self.iter_traces()
.nth(trace_number)
.unwrap()
.to_owned(),
);
return Ok((activity_key, result).into());
} else {
return Err(anyhow!(
"1 trace was requested, but the stochastic language contains none."
));
}
}
if self.number_of_traces() < number_of_traces {
return Err(anyhow!(
"{} traces were requested, but the stochastic language contains only {} traces.",
number_of_traces,
self.number_of_traces()
));
}
let mut sum_distance = sum_distances(self, &distances);
let mut list = Vec::new();
while list.len() < number_of_traces {
let mut min_pos = 0;
for i in 1..sum_distance.len() {
if sum_distance[i] < sum_distance[min_pos] {
min_pos = i;
}
}
list.push(min_pos);
sum_distance[min_pos] = f!(2);
}
list.sort();
let mut list_i = 0;
for (i1, trace1) in self.iter_traces().enumerate() {
if list_i < list.len() && i1 == list[list_i] {
result.insert(trace1.to_vec());
list_i += 1;
}
}
Ok((activity_key, result).into())
}
fn k_medoids_clustering(&self, number_of_clusters: usize) -> Result<FiniteLanguage> {
if self.number_of_traces() < number_of_clusters {
return Err(anyhow!(
"Language contains only {} different traces, and {} clusters were requested.",
self.number_of_traces(),
number_of_clusters
));
}
if number_of_clusters <= 0 {
return Err(anyhow!("No clusters were requested"));
}
let distances = TriangularDistanceMatrix::new(self);
let mut rng = rand::rng();
let mut medoids =
random_initialization(self.number_of_traces(), number_of_clusters, &mut rng);
fasterpam(&distances, &mut medoids, 500);
let mut result = FiniteLanguage::new_hashmap();
medoids.sort();
let mut list_i = 0;
for (i1, trace1) in self.iter_traces().enumerate() {
if list_i < medoids.len() && i1 == medoids[list_i] {
result.insert(trace1.to_vec());
list_i += 1;
}
}
Ok((self.activity_key().clone(), result).into())
}
}
pub fn medoid_single<T>(log: &T, distances: &TriangularDistanceMatrix) -> Option<usize>
where
T: EbiTraitFiniteLanguage + ?Sized,
{
let sum_distance = sum_distances(log, distances);
if distances.len() == 0 {
return None;
}
let mut min_pos = 0;
for (pos, _) in sum_distance.iter().enumerate() {
if sum_distance[pos] < sum_distance[min_pos] {
min_pos = pos;
}
}
return Some(min_pos);
}
pub fn sum_distances<T>(log: &T, distances: &TriangularDistanceMatrix) -> Vec<Fraction>
where
T: EbiTraitFiniteLanguage + ?Sized,
{
let mut sum_distance = vec![Fraction::zero(); log.number_of_traces()];
for (i, j, _, distance) in distances {
sum_distance[i] += distance.as_ref();
sum_distance[j] += distance.as_ref();
}
sum_distance
}
pub fn fasterpam(
distances: &TriangularDistanceMatrix,
medoids: &mut Vec<usize>,
maxiter: usize,
) -> (Fraction, Vec<usize>, usize, usize) {
let (n, k) = (distances.len(), medoids.len());
if k == 1 {
let assi = vec![0; n];
let (swapped, loss) = choose_medoid_within_partition(distances, &assi, medoids, 0);
return (loss, assi, 1, if swapped { 1 } else { 0 });
}
let (mut loss, mut data) = initial_assignment(distances, medoids);
debug_assert_assignment(distances, medoids, &data);
let mut removal_loss = vec![Fraction::zero(); k];
update_removal_loss(&data, &mut removal_loss);
let (mut lastswap, mut n_swaps, mut iter) = (n, 0, 0);
while iter < maxiter {
iter += 1;
let (swaps_before, lastloss) = (n_swaps, loss.clone());
for j in 0..n {
if j == lastswap {
break;
}
if j == medoids[data[j].near.index] {
continue; }
let (change, b) = find_best_swap(distances, &removal_loss, &data, j);
if !change.is_negative() {
continue; }
n_swaps += 1;
lastswap = j;
loss = do_swap(distances, medoids, &mut data, b, j);
update_removal_loss(&data, &mut removal_loss);
}
if n_swaps == swaps_before || loss >= lastloss {
break; }
}
let assi = data.iter().map(|x| x.near.index).collect();
(loss, assi, iter, n_swaps)
}
#[inline]
pub fn random_initialization(n: usize, k: usize, rng: &mut impl rand::Rng) -> Vec<usize> {
rand::seq::index::sample(rng, n, k).into_vec()
}
#[inline]
pub(crate) fn initial_assignment(
distances: &TriangularDistanceMatrix,
medoids: &[usize],
) -> (Fraction, Vec<Rec>) {
let (n, k) = (distances.len(), medoids.len());
assert!(n <= usize::MAX, "N is too large");
assert!(k > 0 && k < usize::MAX, "invalid N");
assert!(k <= n, "k must be at most N");
let mut data = vec![Rec::empty(); distances.len()];
let firstcenter = medoids[0];
let loss = data
.iter_mut()
.enumerate()
.map(|(i, cur)| {
*cur = Rec::new(
0,
distances.get(i, firstcenter).clone(),
usize::MAX,
distances.get_zero(),
);
for (m, &me) in medoids.iter().enumerate().skip(1) {
let d = distances.get(i, me);
if d < &cur.near.distance || i == me {
cur.seco = cur.near.clone();
cur.near = DistancePair {
index: m,
distance: d.clone(),
};
} else if cur.seco.index == usize::MAX || d < &cur.seco.distance {
cur.seco = DistancePair {
index: m,
distance: d.clone(),
};
}
}
cur.near.distance.clone()
})
.fold(Fraction::zero(), |acc, b| &acc + b.as_ref());
(loss, data)
}
#[inline]
pub(crate) fn find_best_swap(
distances: &TriangularDistanceMatrix,
removal_loss: &[Fraction],
data: &[Rec],
j: usize,
) -> (Fraction, usize) {
let mut ploss = removal_loss.to_vec();
let mut acc = Fraction::zero();
for (o, reco) in data.iter().enumerate() {
let distance_o_j = distances.get(o, j);
if distance_o_j < &reco.near.distance {
acc += distance_o_j.clone();
acc -= reco.near.distance.clone();
ploss[reco.near.index] += reco.near.distance.clone();
ploss[reco.near.index] -= reco.seco.distance.clone();
} else if distance_o_j < &reco.seco.distance {
ploss[reco.near.index] += distance_o_j.clone();
ploss[reco.near.index] -= reco.seco.distance.clone();
}
}
let (b, bloss) = find_min(&mut ploss.iter());
acc += bloss;
(acc, b) }
pub(crate) fn update_removal_loss(data: &[Rec], loss: &mut Vec<Fraction>) {
loss.fill(Fraction::zero()); for rec in data.iter() {
loss[rec.near.index] += rec.seco.distance.as_ref();
loss[rec.near.index] -= rec.near.distance.as_ref();
}
}
#[inline]
pub(crate) fn update_second_nearest(
distances: &TriangularDistanceMatrix,
medoids: &[usize],
n: usize,
b: usize,
o: usize,
doj: Arc<Fraction>,
) -> DistancePair {
let mut s = DistancePair::new(b, doj);
for (i, &mi) in medoids.iter().enumerate() {
if i == n || i == b {
continue;
}
let d = distances.get(o, mi);
if d < &s.distance {
s = DistancePair::new(i, d.clone());
}
}
s
}
#[inline]
pub(crate) fn do_swap(
distances: &TriangularDistanceMatrix,
medoids: &mut Vec<usize>,
data: &mut Vec<Rec>,
b: usize,
j: usize,
) -> Fraction {
let n = distances.len();
assert!(b < medoids.len(), "invalid medoid number");
assert!(j < n, "invalid object number");
medoids[b] = j;
data.iter_mut()
.enumerate()
.map(|(o, reco)| {
if o == j {
if reco.near.index != b {
reco.seco = reco.near.clone();
}
reco.near = DistancePair::new(b, distances.get_zero());
return distances.get_zero();
}
let doj = distances.get(o, j);
if reco.near.index == b {
if doj < &reco.seco.distance {
reco.near = DistancePair::new(b, doj.clone());
} else {
reco.near = reco.seco.clone();
reco.seco = update_second_nearest(
distances,
medoids,
reco.near.index,
b,
o,
doj.clone(),
);
}
} else {
if doj < &reco.near.distance {
reco.seco = reco.near.clone();
reco.near = DistancePair::new(b, doj.clone());
} else if doj < &reco.seco.distance {
reco.seco = DistancePair::new(b, doj.clone());
} else if reco.seco.index == b {
reco.seco = update_second_nearest(
distances,
medoids,
reco.near.index,
b,
o,
doj.clone(),
);
}
}
reco.near.distance.clone()
})
.fold(Fraction::zero(), |acc, b| &acc + b.as_ref())
}
#[derive(Debug, Clone)]
pub(crate) struct DistancePair {
pub(crate) index: usize,
pub(crate) distance: Arc<Fraction>,
}
impl DistancePair {
pub(crate) fn new(i: usize, d: Arc<Fraction>) -> Self {
DistancePair {
index: i,
distance: d,
}
}
}
impl DistancePair {
pub(crate) fn empty() -> Self {
DistancePair {
index: usize::MAX,
distance: Arc::new(Fraction::zero()),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Rec {
pub(crate) near: DistancePair,
pub(crate) seco: DistancePair,
}
impl Rec {
pub(crate) fn new(i1: usize, d1: Arc<Fraction>, i2: usize, d2: Arc<Fraction>) -> Rec {
Rec {
near: DistancePair {
index: i1,
distance: d1,
},
seco: DistancePair {
index: i2,
distance: d2,
},
}
}
}
impl Rec {
pub(crate) fn empty() -> Self {
Rec {
near: DistancePair::empty(),
seco: DistancePair::empty(),
}
}
}
pub(crate) fn choose_medoid_within_partition(
distances: &TriangularDistanceMatrix,
assi: &[usize],
medoids: &mut [usize],
m: usize,
) -> (bool, Fraction) {
let first = medoids[m];
let mut best = first;
let mut sumb = Fraction::zero();
for (i, &a) in assi.iter().enumerate() {
if first != i && a == m {
sumb += distances.get(first, i).clone();
}
}
for (j, &aj) in assi.iter().enumerate() {
if j != first && aj == m {
let mut sumj = Fraction::zero();
for (i, &ai) in assi.iter().enumerate() {
if i != j && ai == m {
sumj += distances.get(j, i).clone();
}
}
if sumj < sumb {
best = j;
sumb = sumj;
}
}
}
medoids[m] = best;
(best != first, sumb)
}
pub(crate) fn debug_assert_assignment(
distances: &TriangularDistanceMatrix,
medoids: &[usize],
_data: &[Rec],
) {
for o in 0..distances.len() {
debug_assert!(
distances.get(o, medoids[_data[o].near.index]) == &_data[o].near.distance,
"primary assignment inconsistent"
);
debug_assert!(
distances.get(o, medoids[_data[o].seco.index]) == &_data[o].seco.distance,
"secondary assignment inconsistent"
);
debug_assert!(
_data[o].near.distance <= _data[o].seco.distance,
"nearest is farther than second nearest"
);
}
}
#[inline]
pub(crate) fn find_min<'a, I: 'a>(a: &mut I) -> (usize, &'a Fraction)
where
I: std::iter::Iterator<Item = &'a Fraction>,
{
let mut a = a.enumerate();
let mut best = (0, a.next().unwrap().1);
for (ik, iv) in a {
if iv < &best.1 {
best = (ik, iv);
}
}
best
}
#[cfg(test)]
mod tests {
use std::fs;
use ebi_objects::FiniteStochasticLanguage;
use crate::techniques::medoid_non_stochastic::MedoidNonStochastic;
#[test]
fn non_stochastic_clustering() {
let fin = fs::read_to_string("testfiles/aa-ab-ba.slang").unwrap();
let slang = fin.parse::<FiniteStochasticLanguage>().unwrap();
let cluster = slang.k_medoids_clustering(1).unwrap();
let fout = fs::read_to_string("testfiles/aa.lang").unwrap();
assert_eq!(fout, cluster.to_string())
}
#[test]
fn non_stochastic_medoid() {
let fin = fs::read_to_string("testfiles/aa-ab-ba.slang").unwrap();
let slang = fin.parse::<FiniteStochasticLanguage>().unwrap();
let fout = fs::read_to_string("testfiles/aa.lang").unwrap();
let medoid = slang.medoid(1).unwrap();
assert_eq!(fout, medoid.to_string())
}
}