1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct PersistenceDiagram {
11 pub points: Vec<(f64, f64)>,
13 pub betti_numbers: Vec<usize>,
15}
16
17struct UnionFind {
18 parent: Vec<usize>,
19 rank: Vec<usize>,
20}
21
22impl UnionFind {
23 fn new(n: usize) -> Self {
24 Self {
25 parent: (0..n).collect(),
26 rank: vec![0; n],
27 }
28 }
29
30 fn find(&mut self, x: usize) -> usize {
31 if self.parent[x] != x {
32 self.parent[x] = self.find(self.parent[x]);
33 }
34 self.parent[x]
35 }
36
37 fn union(&mut self, a: usize, b: usize) -> bool {
38 let ra = self.find(a);
39 let rb = self.find(b);
40 if ra == rb {
41 return false;
42 }
43 if self.rank[ra] < self.rank[rb] {
44 self.parent[ra] = rb;
45 } else if self.rank[ra] > self.rank[rb] {
46 self.parent[rb] = ra;
47 } else {
48 self.parent[rb] = ra;
49 self.rank[ra] += 1;
50 }
51 true
52 }
53}
54
55impl PersistenceDiagram {
56 pub fn from_distance_matrix(dist: &[f64], n: usize) -> Self {
62 if n <= 1 {
63 return Self {
64 points: vec![],
65 betti_numbers: vec![1],
66 };
67 }
68
69 let mut uf = UnionFind::new(n);
70
71 let mut edges: Vec<(f64, usize, usize)> = Vec::new();
73 for i in 0..n {
74 for j in (i + 1)..n {
75 let d = dist.get(i * n + j).copied().unwrap_or(f64::INFINITY);
76 edges.push((d, i, j));
77 }
78 }
79 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
80
81 let mut points = Vec::new();
83
84 for (d, i, j) in &edges {
85 if d.is_infinite() {
86 continue; }
88 if uf.find(*i) != uf.find(*j) {
89 points.push((0.0, *d));
91 uf.union(*i, *j);
92 }
93 }
94
95 let mut roots = std::collections::HashSet::new();
97 for i in 0..n {
98 roots.insert(uf.find(i));
99 }
100 let beta0 = roots.len();
101
102 let beta1 = if edges.len() + beta0 > n {
104 edges.len() - n + beta0
105 } else {
106 0
107 };
108
109 let betti_numbers = vec![beta0, beta1];
110
111 Self { points, betti_numbers }
112 }
113
114 pub fn from_ndarray_matrix(mat: &ndarray::Array2<f64>) -> Self {
116 let n = mat.nrows();
117 let dist: Vec<f64> = mat.iter().copied().collect();
118 Self::from_distance_matrix(&dist, n)
119 }
120
121 pub fn persistent_features(&self) -> Vec<(f64, f64)> {
123 self.points
124 .iter()
125 .filter(|(b, d)| d > b)
126 .copied()
127 .collect()
128 }
129
130 pub fn total_persistence(&self, p: f64) -> f64 {
132 self.points
133 .iter()
134 .filter(|(b, d)| d > b)
135 .map(|(b, d)| (d - b).powf(p))
136 .sum()
137 }
138
139 pub fn is_valid(&self) -> bool {
141 self.points.iter().all(|(b, d)| *d >= *b - 1e-12)
142 }
143}
144
145pub fn entropy_landscape(distributions: &[Vec<f64>]) -> ndarray::Array2<f64> {
148 let n = distributions.len();
149 let mut dist_mat = ndarray::Array2::<f64>::zeros((n, n));
150
151 for i in 0..n {
152 for j in (i + 1)..n {
153 let d = crate::entropy::js_divergence(&distributions[i], &distributions[j]);
154 let d_sqrt = d.sqrt().max(0.0);
155 dist_mat[[i, j]] = d_sqrt;
156 dist_mat[[j, i]] = d_sqrt;
157 }
158 }
159 dist_mat
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn persistence_diagram_points_above_diagonal() {
168 let dist = vec![
169 0.0, 1.0, 2.0,
170 1.0, 0.0, 1.5,
171 2.0, 1.5, 0.0,
172 ];
173 let pd = PersistenceDiagram::from_distance_matrix(&dist, 3);
174 assert!(pd.is_valid(), "all points should be on or above diagonal");
175 }
176
177 #[test]
178 fn betti_number_single_component() {
179 let dist = vec![
180 0.0, 0.1, 0.2,
181 0.1, 0.0, 0.15,
182 0.2, 0.15, 0.0,
183 ];
184 let pd = PersistenceDiagram::from_distance_matrix(&dist, 3);
185 assert_eq!(pd.betti_numbers[0], 1, "connected graph → β₀ = 1");
186 }
187
188 #[test]
189 fn betti_number_two_components() {
190 let inf = f64::INFINITY;
191 let dist = vec![
192 0.0, 0.5, inf,
193 0.5, 0.0, inf,
194 inf, inf, 0.0,
195 ];
196 let pd = PersistenceDiagram::from_distance_matrix(&dist, 3);
197 assert_eq!(pd.betti_numbers[0], 2, "two components → β₀ = 2");
198 }
199
200 #[test]
201 fn total_persistence_non_negative() {
202 let dist = vec![
203 0.0, 1.0, 2.0,
204 1.0, 0.0, 1.0,
205 2.0, 1.0, 0.0,
206 ];
207 let pd = PersistenceDiagram::from_distance_matrix(&dist, 3);
208 let tp = pd.total_persistence(1.0);
209 assert!(tp >= 0.0);
210 }
211
212 #[test]
213 fn empty_diagram() {
214 let pd = PersistenceDiagram::from_distance_matrix(&[], 0);
215 assert!(pd.points.is_empty());
216 }
217
218 #[test]
219 fn entropy_landscape_shape() {
220 let dists = vec![
221 vec![0.5, 0.5],
222 vec![0.3, 0.7],
223 vec![0.1, 0.9],
224 ];
225 let mat = entropy_landscape(&dists);
226 assert_eq!(mat.nrows(), 3);
227 assert_eq!(mat.ncols(), 3);
228 for i in 0..3 {
229 assert!((mat[[i, i]]).abs() < 1e-12);
230 }
231 assert!((mat[[0, 1]] - mat[[1, 0]]).abs() < 1e-12);
232 }
233}