holos_tda/distances/matrix.rs
1/// Symmetric dissimilarity matrix.
2/// No metric assumptions: entries need not satisfy the triangle inequality.
3/// Entries must be non-negative and not NaN; +inf is legal and equivalent
4/// to an absent edge.
5///
6/// The matrix has two storage forms. The compact form holds the condensed
7/// lower triangle, `n(n-1)/2` entries, and is the form every constructor
8/// builds. The full form holds both triangles row-major, `n * n` entries,
9/// so that a cofacet diameter fold reads one contiguous row per simplex
10/// vertex instead of one strided column.
11#[derive(Debug, Clone)]
12pub struct DistanceMatrix {
13 pub(super) n: usize,
14 /// Row-major n by n in the full form, the condensed lower triangle in
15 /// the compact one. Row `i` holds its entries below the diagonal first
16 /// in both, which is what [`DistanceMatrix::lower_row`] returns.
17 pub(super) data: Vec<f64>,
18 pub(super) square: bool,
19}
20
21impl DistanceMatrix {
22 /// Number of points.
23 pub fn len(&self) -> usize {
24 self.n
25 }
26
27 /// True when there are no points.
28 pub fn is_empty(&self) -> bool {
29 self.n == 0
30 }
31
32 /// Distance between points `i` and `j` (0 on the diagonal).
33 #[inline]
34 pub fn get(&self, i: usize, j: usize) -> f64 {
35 debug_assert!(i < self.n && j < self.n);
36 if self.square {
37 return self.data[i * self.n + j];
38 }
39 match i.cmp(&j) {
40 std::cmp::Ordering::Equal => 0.0,
41 std::cmp::Ordering::Greater => self.data[i * (i - 1) / 2 + j],
42 std::cmp::Ordering::Less => self.data[j * (j - 1) / 2 + i],
43 }
44 }
45
46 /// Row `i` up to the diagonal: the distances from `i` to every point
47 /// below it, in index order. Both forms store that run contiguously.
48 #[inline]
49 pub(super) fn lower_row(&self, i: usize) -> &[f64] {
50 let start = if self.square {
51 i * self.n
52 } else {
53 i * (i - 1) / 2
54 };
55 &self.data[start..start + i]
56 }
57
58 /// Count the pairs that enter the complex at `threshold`: finite and at
59 /// or below it. One pass over the condensed triangle, no allocation.
60 pub(crate) fn count_edges_at(&self, threshold: f64) -> usize {
61 (1..self.n)
62 .map(|i| {
63 self.lower_row(i)
64 .iter()
65 .filter(|d| d.is_finite() && **d <= threshold)
66 .count()
67 })
68 .sum()
69 }
70 /// Default threshold: the minimum over i of the maximum over j of d(i, j).
71 /// Past that radius the complex is a cone and acquires no further homology.
72 pub fn enclosing_radius(&self) -> f64 {
73 if self.n < 2 {
74 return 0.0;
75 }
76 // Each distance folds into both endpoints' running maxima, so one
77 // pass over the lower triangle is enough. Row `i` holds its own
78 // maximum in a local until the row ends: no earlier row writes
79 // `row_max[i]`, because every column index it touches is below it.
80 let mut row_max = vec![0.0f64; self.n];
81 for i in 1..self.n {
82 let mut max_i = 0.0f64;
83 for (m, &d) in row_max[..i].iter_mut().zip(self.lower_row(i)) {
84 max_i = max_i.max(d);
85 *m = m.max(d);
86 }
87 row_max[i] = max_i;
88 }
89 row_max.into_iter().fold(f64::INFINITY, f64::min)
90 }
91}
92
93#[cfg(test)]
94thread_local! {
95 /// Conversions to the full form on this thread. Each test runs on its
96 /// own thread, so the count belongs to one test and no other test can
97 /// disturb it.
98 pub(crate) static SQUARE_BUILDS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
99}
100
101impl DistanceMatrix {
102 /// True when this matrix holds both triangles.
103 #[cfg(test)]
104 pub(crate) fn is_square(&self) -> bool {
105 self.square
106 }
107
108 /// The same distances in the full row-major form. Every constructor
109 /// builds the compact form, so this is the only way a run reaches the
110 /// full one.
111 pub(crate) fn to_square(&self) -> Self {
112 #[cfg(test)]
113 SQUARE_BUILDS.with(|c| c.set(c.get() + 1));
114 let n = self.n;
115 let mut data = vec![0.0f64; n * n];
116 for i in 1..n {
117 let row = self.lower_row(i);
118 data[i * n..i * n + i].copy_from_slice(row);
119 for (j, &d) in row.iter().enumerate() {
120 data[j * n + i] = d;
121 }
122 }
123 Self {
124 n,
125 data,
126 square: true,
127 }
128 }
129}