use ndarray::Array2;
use petgraph::Undirected;
use petgraph::graph::{Graph, NodeIndex};
use rayon::prelude::*;
use std::cmp::min;
#[derive(Clone, Copy, Debug)]
struct Component {
first: usize,
second: Option<usize>,
}
impl Component {
fn singleton(first: usize) -> Self {
Self {
first,
second: None,
}
}
fn pair(a: usize, b: usize) -> Self {
Self {
first: a,
second: Some(b),
}
}
fn size(&self) -> usize {
if self.second.is_some() { 2 } else { 1 }
}
fn values(&self) -> [Option<usize>; 2] {
[Some(self.first), self.second]
}
fn other(&self, p: usize) -> usize {
match self.second {
Some(b) => {
if p == self.first {
b
} else {
self.first
}
}
None => panic!("other() called on singleton"),
}
}
fn first(&self) -> usize {
self.first
}
fn second(&self) -> usize {
self.second.expect("not a pair")
}
fn _is_singleton(&self) -> bool {
self.second.is_none()
}
}
pub fn compute_order_huson_2023(dist: &Array2<f64>) -> Vec<usize> {
let n_tax = dist.nrows();
assert_eq!(
n_tax,
dist.ncols(),
"Distance matrix must be square (n x n)"
);
if n_tax <= 3 {
return create_array_upward_count(n_tax);
}
let mut graph: Graph<usize, (), Undirected> = Graph::default();
let mut node_map: Vec<NodeIndex> = vec![NodeIndex::end(); n_tax + 1];
let mut components: Vec<Component> = (1..=n_tax).map(Component::singleton).collect();
for t in 1..=n_tax {
node_map[t] = graph.add_node(t);
}
let mut d = Array2::<f64>::zeros((n_tax + 1, n_tax + 1));
for i in 1..=n_tax {
for j in 1..=n_tax {
d[[i, j]] = dist[[i - 1, j - 1]];
}
}
while components.len() >= 2 {
debug!("Updated distance matrix D:\n{:?}", d);
let (ip, iq) = select_closest_pair(&components, &d);
let (p_comp, q_comp) = (components[ip], components[iq]);
debug!("Selected: P={} Q={}", p_comp.first(), q_comp.first());
debug!("Sizes: P={} Q={}", p_comp.size(), q_comp.size());
if p_comp.size() == 1 && q_comp.size() == 1 {
let p = p_comp.first();
let q = q_comp.first();
graph.add_edge(node_map[p], node_map[q], ());
let new_component = Component::pair(p, q);
components[ip] = new_component;
debug!(
"First case: P={} Q={} NewComponent={:?}",
p, q, components[ip]
);
components.remove(iq);
} else if p_comp.size() == 1 && q_comp.size() == 2 {
let p = p_comp.first();
let q = select_closest_1_vs_2(ip, iq, &d, &components);
let qb = q_comp.other(q);
d[[p, qb]] = (d[[p, qb]] + d[[q, qb]] + d[[p, q]]) / 3.0;
d[[qb, p]] = d[[p, qb]];
for i in 0..components.len() {
if i == ip || i == iq {
continue; }
for &r_opt in components[i].values().iter() {
if let Some(r) = r_opt {
if r != p && r != q && r != qb {
d[[p, r]] = (2.0 * d[[p, r]] + d[[q, r]]) / 3.0;
d[[r, p]] = d[[p, r]];
d[[qb, r]] = (2.0 * d[[qb, r]] + d[[q, r]]) / 3.0;
d[[r, qb]] = d[[qb, r]];
}
}
}
}
graph.add_edge(node_map[p], node_map[q], ());
let new_component = Component::pair(p, qb);
components[ip] = new_component;
debug!(
"Second case: P={} Q={} NewComponent={:?}",
p, q, components[ip]
);
components.remove(iq);
} else if p_comp.size() == 2 && q_comp.size() == 2 {
let (p, q) = select_closest_2_vs_2(ip, iq, &d, &components);
let pb = p_comp.other(p);
let qb = q_comp.other(q);
d[[pb, qb]] =
(d[[pb, p]] + d[[pb, q]] + d[[pb, qb]] + d[[p, q]] + d[[p, qb]] + d[[q, qb]]) / 6.0;
d[[qb, pb]] = d[[pb, qb]];
for i in 0..components.len() {
if i == ip || i == iq {
continue; }
let other = components[i];
for &r_opt in other.values().iter() {
if let Some(r) = r_opt {
if r != p && r != q && r != pb && r != qb {
let pb_r = d[[pb, r]] / 2.0 + d[[p, r]] / 3.0 + d[[q, r]] / 6.0;
let qb_r = d[[p, r]] / 6.0 + d[[q, r]] / 3.0 + d[[qb, r]] / 2.0;
d[[pb, r]] = pb_r;
d[[r, pb]] = pb_r;
d[[qb, r]] = qb_r;
d[[r, qb]] = qb_r;
}
}
}
}
graph.add_edge(node_map[p], node_map[q], ());
let new_component = Component::pair(pb, qb);
debug!(
"Third case: P={} Q={} NewComponent={:?}",
p, q, new_component
);
components[ip] = new_component;
components.remove(iq);
} else {
panic!(
"Internal error: |P|={} and |Q|={}",
p_comp.size(),
q_comp.size()
);
}
}
let p = components[0].first();
let q = components[0].second();
graph.add_edge(node_map[p], node_map[q], ());
debug!("Final edge: P={} Q={}", p, q);
debug!("Graph: {:?}", graph);
extract_ordering(&graph, &node_map)
}
fn create_array_upward_count(n: usize) -> Vec<usize> {
let mut v = Vec::with_capacity(n + 1);
v.push(0);
for t in 1..=n {
v.push(t);
}
v
}
#[inline]
fn avg_d_comp_comp(d: &Array2<f64>, p: &Component, q: &Component) -> f64 {
match (p.second, q.second) {
(None, None) => {
d[[p.first, q.first]]
}
(None, Some(qb)) => {
(d[[p.first, q.first]] + d[[p.first, qb]]) / 2.0
}
(Some(pb), None) => {
(d[[p.first, q.first]] + d[[pb, q.first]]) / 2.0
}
(Some(pb), Some(qb)) => {
(d[[p.first, q.first]] + d[[p.first, qb]] + d[[pb, q.first]] + d[[pb, qb]]) / 4.0
}
}
}
#[inline]
fn avg_d_p_comp(d: &Array2<f64>, p: usize, q: &Component) -> f64 {
match q.second {
None => d[[p, q.first]],
Some(qb) => (d[[p, q.first]] + d[[p, qb]]) / 2.0,
}
}
fn select_closest_pair(components: &[Component], d: &Array2<f64>) -> (usize, usize) {
let m = components.len();
if m == 2 {
if components[0].size() < components[1].size() {
return (0, 1);
} else {
return (1, 0);
}
}
let (_best_val, best_ip, best_iq) = (0..m)
.into_par_iter()
.filter(|&ip| ip + 1 < m)
.map(|ip| {
let p = components[ip];
let mut local_best_val = f64::INFINITY;
let mut local_best_iq = ip + 1;
for iq in (ip + 1)..m {
let q = components[iq];
let mut sum_p = 0.0;
let mut sum_q = 0.0;
for is in 0..m {
if is == ip || is == iq {
continue;
}
let s = components[is];
sum_p += avg_d_comp_comp(d, &p, &s);
sum_q += avg_d_comp_comp(d, &q, &s);
}
let pq = avg_d_comp_comp(d, &p, &q);
let adjusted = (m as f64 - 2.0) * pq - sum_p - sum_q;
if adjusted < local_best_val {
local_best_val = adjusted;
local_best_iq = iq;
}
}
(local_best_val, ip, local_best_iq)
})
.reduce(
|| (f64::INFINITY, 0usize, 1usize),
|a, b| {
let cmp = a.0.total_cmp(&b.0);
if cmp == std::cmp::Ordering::Less {
a
} else if cmp == std::cmp::Ordering::Greater {
b
} else if a.1 < b.1 || (a.1 == b.1 && a.2 <= b.2) {
a
} else {
b
}
},
);
if components[best_ip].size() > components[best_iq].size() {
return (best_iq, best_ip);
}
(best_ip, best_iq)
}
fn select_closest_1_vs_2(ip: usize, iq: usize, d: &Array2<f64>, components: &[Component]) -> usize {
let m = components.len();
let p = components[ip].first();
let q1 = components[iq].first();
let q2 = components[iq].second();
let (p_r_extra, q1_r_extra, q2_r_extra) = (0..m)
.into_par_iter()
.filter(|&i| i != ip && i != iq)
.map(|i| {
let other = components[i];
(
avg_d_p_comp(d, p, &other),
avg_d_p_comp(d, q1, &other),
avg_d_p_comp(d, q2, &other),
)
})
.reduce(
|| (0.0, 0.0, 0.0),
|a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2),
);
let p_r = d[[q1, p]] + d[[q2, p]] + p_r_extra;
let q1_r = d[[q1, q2]] + d[[q1, p]] + q1_r_extra;
let q2_r = d[[q1, q2]] + d[[q2, p]] + q2_r_extra;
let mm1 = m as f64 - 1.0;
let q1p_adj = mm1 * d[[q1, p]] - q1_r - p_r;
let q2p_adj = mm1 * d[[q2, p]] - q2_r - p_r;
debug!(
"1x2 @ P={} Q={} -> q1p_adj:{:.9} q2p_adj:{:.9}",
p, q1, q1p_adj, q2p_adj
);
if q1p_adj <= q2p_adj { q1 } else { q2 }
}
fn select_closest_2_vs_2(
ip: usize,
iq: usize,
d: &Array2<f64>,
components: &[Component],
) -> (usize, usize) {
let m = components.len();
let p1 = components[ip].first();
let p2 = components[ip].second();
let q1 = components[iq].first();
let q2 = components[iq].second();
let (p1_r_extra, p2_r_extra, q1_r_extra, q2_r_extra) = (0..m)
.into_par_iter()
.filter(|&i| i != ip && i != iq)
.map(|i| {
let other = components[i];
(
avg_d_p_comp(d, p1, &other),
avg_d_p_comp(d, p2, &other),
avg_d_p_comp(d, q1, &other),
avg_d_p_comp(d, q2, &other),
)
})
.reduce(
|| (0.0, 0.0, 0.0, 0.0),
|a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3),
);
let p1_r = d[[p1, q1]] + d[[p1, q2]] + p1_r_extra;
let p2_r = d[[p2, q1]] + d[[p2, q2]] + p2_r_extra;
let q1_r = d[[p1, q1]] + d[[p2, q1]] + q1_r_extra;
let q2_r = d[[p1, q2]] + d[[p2, q2]] + q2_r_extra;
let m_f = m as f64;
let p1q1 = m_f * d[[p1, q1]] - p1_r - q1_r;
let p2q1 = m_f * d[[p2, q1]] - p2_r - q1_r;
let p1q2 = m_f * d[[p1, q2]] - p1_r - q2_r;
let p2q2 = m_f * d[[p2, q2]] - p2_r - q2_r;
debug!(
"2x2 inputs P={{{}, {}}} Q={{{}, {}}} | \
D: p1q1={:.12}, p2q1={:.12}, p1q2={:.12}, p2q2={:.12} | \
R: p1R={:.12}, p2R={:.12}, q1R={:.12}, q2R={:.12}",
p1,
p2,
q1,
q2,
d[[p1, q1]],
d[[p2, q1]],
d[[p1, q2]],
d[[p2, q2]],
p1_r,
p2_r,
q1_r,
q2_r
);
match rank_of_min(&[p1q1, p2q1, p1q2, p2q2]) {
0 => (p1, q1),
1 => (p2, q1),
2 => (p1, q2),
_ => (p2, q2),
}
}
#[inline]
fn rank_of_min(vals: &[f64]) -> usize {
let mut idx = 0usize;
for i in 1..vals.len() {
if vals[i] < vals[idx] {
idx = i;
}
}
idx
}
fn extract_ordering(graph: &Graph<usize, (), Undirected>, node_map: &[NodeIndex]) -> Vec<usize> {
let n = node_map.len() - 1;
let mut order: Vec<usize> = Vec::with_capacity(n + 1);
order.push(0);
debug!("Node Map: {:?}", node_map);
if n == 0 {
return order;
}
if n <= 3 {
for t in 1..=n {
order.push(t);
}
return order;
}
let v1 = node_map[1];
let neigh: Vec<NodeIndex> = graph.neighbors(v1).collect();
assert!(
neigh.len() == 2,
"ordering graph must be a simple cycle: node 1 should have degree 2"
);
let mut prev = v1;
let mut cur = min(neigh[0], neigh[1]);
debug!(
"Starting at v1={} cur={}: neighbours {:?}",
graph[v1], graph[cur], neigh
);
order.push(graph[v1]); while order.len() - 1 < n {
order.push(graph[cur]);
let it = graph.neighbors(cur).collect::<Vec<_>>();
debug!("prev {:?} -> cur {:?}: neighbours {:?}", prev, cur, it);
let a = it[0];
let b = it[1];
let nxt = if a == prev { b } else { a };
prev = cur;
cur = nxt;
}
order
}
fn _debug_pair(components: &[Component], d: &Array2<f64>, a: usize, b: usize) {
let ip = components
.iter()
.position(|c| c.first == a || c.second == Some(a))
.unwrap();
let iq = components
.iter()
.position(|c| c.first == b || c.second == Some(b))
.unwrap();
let m = components.len();
let p = &components[ip];
let q = &components[iq];
let mut sum_p = 0.0;
let mut sum_q = 0.0;
debug!("-- adj breakdown for P={:?} Q={:?} (m={})", p, q, m);
for (is, s) in components.iter().enumerate() {
if is == ip || is == iq {
continue;
}
let aps = avg_d_comp_comp(d, p, s);
let aqs = avg_d_comp_comp(d, q, s);
debug!(" S={:?}: avg(P,S)={:.9} avg(Q,S)={:.9}", s, aps, aqs);
sum_p += aps;
sum_q += aqs;
}
let pq = avg_d_comp_comp(d, p, q);
let adjusted = (m as f64 - 2.0) * pq - sum_p - sum_q;
debug!(
" avg(P,Q)={:.9} sumP={:.9} sumQ={:.9} -> adjusted={:.9}",
pq, sum_p, sum_q, adjusted
);
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::arr2;
#[test]
fn small_triangle() {
let d = arr2(&[[0.0, 1.0, 2.0], [1.0, 0.0, 1.5], [2.0, 1.5, 0.0]]);
let ord = compute_order_huson_2023(&d);
assert_eq!(ord, vec![0, 1, 2, 3]);
}
#[test]
fn small_square() {
let d = arr2(&[
[0.0, 1.0, 2.0, 3.0],
[1.0, 0.0, 1.5, 2.5],
[2.0, 1.5, 0.0, 1.5],
[3.0, 2.5, 1.5, 0.0],
]);
let ord = compute_order_huson_2023(&d);
assert_eq!(ord, vec![0, 1, 2, 4, 3]);
}
#[test]
fn smoke_5_1() {
let d = arr2(&[
[0.0, 5.0, 9.0, 9.0, 8.0],
[5.0, 0.0, 10.0, 10.0, 9.0],
[9.0, 10.0, 0.0, 8.0, 7.0],
[9.0, 10.0, 8.0, 0.0, 3.0],
[8.0, 9.0, 7.0, 3.0, 0.0],
]);
let ord = compute_order_huson_2023(&d);
let exp = vec![0, 1, 2, 5, 4, 3];
assert_eq!(ord, exp);
}
#[test]
fn smoke_5_2() {
let d = arr2(&[
[0.0, 2.0, 3.0, 4.0, 5.0],
[2.0, 0.0, 6.0, 7.0, 8.0],
[3.0, 6.0, 0.0, 9.0, 1.0],
[4.0, 7.0, 9.0, 0.0, 2.0],
[5.0, 8.0, 1.0, 2.0, 0.0],
]);
let ord = compute_order_huson_2023(&d);
let exp = vec![0, 1, 2, 4, 5, 3];
assert_eq!(ord, exp);
}
#[test]
fn smoke_10_1() {
let d = arr2(&[
[0.0, 5.0, 12.0, 7.0, 3.0, 9.0, 11.0, 6.0, 4.0, 10.0],
[5.0, 0.0, 8.0, 2.0, 14.0, 5.0, 13.0, 7.0, 12.0, 1.0],
[12.0, 8.0, 0.0, 4.0, 9.0, 3.0, 8.0, 2.0, 5.0, 6.0],
[7.0, 2.0, 4.0, 0.0, 11.0, 7.0, 10.0, 4.0, 6.0, 9.0],
[3.0, 14.0, 9.0, 11.0, 0.0, 8.0, 1.0, 13.0, 2.0, 7.0],
[9.0, 5.0, 3.0, 7.0, 8.0, 0.0, 12.0, 5.0, 3.0, 4.0],
[11.0, 13.0, 8.0, 10.0, 1.0, 12.0, 0.0, 6.0, 2.0, 8.0],
[6.0, 7.0, 2.0, 4.0, 13.0, 5.0, 6.0, 0.0, 9.0, 7.0],
[4.0, 12.0, 5.0, 6.0, 2.0, 3.0, 2.0, 9.0, 0.0, 5.0],
[10.0, 1.0, 6.0, 9.0, 7.0, 4.0, 8.0, 7.0, 5.0, 0.0],
]);
let ord = compute_order_huson_2023(&d);
assert_eq!(ord, vec![0, 1, 5, 7, 9, 3, 8, 4, 2, 10, 6]);
}
#[test]
fn smoke_10_2() {
let d = arr2(&[
[0.0, 3.0, 7.0, 4.0, 5.0, 9.0, 2.0, 8.0, 6.0, 1.0],
[3.0, 0.0, 5.0, 2.0, 10.0, 4.0, 11.0, 7.0, 9.0, 8.0],
[7.0, 5.0, 0.0, 6.0, 3.0, 8.0, 4.0, 2.0, 5.0, 9.0],
[4.0, 2.0, 6.0, 0.0, 7.0, 5.0, 9.0, 3.0, 4.0, 6.0],
[5.0, 10.0, 3.0, 7.0, 0.0, 2.0, 6.0, 12.0, 1.0, 5.0],
[9.0, 4.0, 8.0, 5.0, 2.0, 0.0, 7.0, 4.0, 3.0, 2.0],
[2.0, 11.0, 4.0, 9.0, 6.0, 7.0, 0.0, 5.0, 2.0, 8.0],
[8.0, 7.0, 2.0, 3.0, 12.0, 4.0, 5.0, 0.0, 6.0, 7.0],
[6.0, 9.0, 5.0, 4.0, 1.0, 3.0, 2.0, 6.0, 0.0, 4.0],
[1.0, 8.0, 9.0, 6.0, 5.0, 2.0, 8.0, 7.0, 4.0, 0.0],
]);
let ord = compute_order_huson_2023(&d);
assert_eq!(ord, vec![0, 1, 2, 4, 8, 3, 7, 9, 5, 6, 10]);
}
#[test]
fn smoke_15_1() {
let d = arr2(&[
[
0.0, 14.0, 9.0, 4.0, 16.0, 11.0, 17.0, 12.0, 7.0, 19.0, 14.0, 9.0, 15.0, 10.0, 5.0,
],
[
14.0, 0.0, 17.0, 12.0, 7.0, 13.0, 8.0, 3.0, 15.0, 10.0, 22.0, 11.0, 6.0, 18.0, 13.0,
],
[
9.0, 17.0, 0.0, 20.0, 9.0, 4.0, 16.0, 11.0, 6.0, 18.0, 7.0, 2.0, 14.0, 9.0, 21.0,
],
[
4.0, 12.0, 20.0, 0.0, 17.0, 12.0, 7.0, 19.0, 14.0, 3.0, 15.0, 10.0, 5.0, 17.0, 12.0,
],
[
16.0, 7.0, 9.0, 17.0, 0.0, 20.0, 15.0, 10.0, 16.0, 11.0, 6.0, 18.0, 13.0, 8.0, 14.0,
],
[
11.0, 13.0, 4.0, 12.0, 20.0, 0.0, 6.0, 12.0, 7.0, 19.0, 14.0, 9.0, 21.0, 10.0, 5.0,
],
[
17.0, 8.0, 16.0, 7.0, 15.0, 6.0, 0.0, 3.0, 15.0, 10.0, 5.0, 17.0, 6.0, 18.0, 13.0,
],
[
12.0, 3.0, 11.0, 19.0, 10.0, 12.0, 3.0, 0.0, 6.0, 18.0, 13.0, 2.0, 14.0, 9.0, 4.0,
],
[
7.0, 15.0, 6.0, 14.0, 16.0, 7.0, 15.0, 6.0, 0.0, 9.0, 15.0, 10.0, 5.0, 17.0, 12.0,
],
[
19.0, 10.0, 18.0, 3.0, 11.0, 19.0, 10.0, 18.0, 9.0, 0.0, 6.0, 18.0, 13.0, 8.0, 20.0,
],
[
14.0, 22.0, 7.0, 15.0, 6.0, 14.0, 5.0, 13.0, 15.0, 6.0, 0.0, 9.0, 21.0, 16.0, 5.0,
],
[
9.0, 11.0, 2.0, 10.0, 18.0, 9.0, 17.0, 2.0, 10.0, 18.0, 9.0, 0.0, 12.0, 1.0, 13.0,
],
[
15.0, 6.0, 14.0, 5.0, 13.0, 21.0, 6.0, 14.0, 5.0, 13.0, 21.0, 12.0, 0.0, 9.0, 4.0,
],
[
10.0, 18.0, 9.0, 17.0, 8.0, 10.0, 18.0, 9.0, 17.0, 8.0, 16.0, 1.0, 9.0, 0.0, 12.0,
],
[
5.0, 13.0, 21.0, 12.0, 14.0, 5.0, 13.0, 4.0, 12.0, 20.0, 5.0, 13.0, 4.0, 12.0, 0.0,
],
]);
let ord = compute_order_huson_2023(&d);
assert_eq!(
ord,
vec![0, 1, 9, 3, 6, 12, 14, 5, 11, 10, 4, 7, 8, 2, 13, 15]
);
}
}