Skip to main content

scirs2_transform/tda/
zigzag.rs

1//! Zigzag persistence for sequences of simplicial complexes.
2//!
3//! Zigzag persistence generalises ordinary persistence to handle sequences where
4//! simplices can be both added and removed. The "zigzag" refers to the sequence
5//! of maps alternating between forward (inclusion) and backward (deletion)
6//! directions:
7//!
8//! ```text
9//! K_0 → K_1 ← K_2 → K_3 ← K_4 → …
10//! ```
11//!
12//! This implementation uses a vineyard-inspired approach:
13//! 1. Maintain a totally ordered filtration of currently-active simplices.
14//! 2. On addition: append the simplex and reduce.
15//! 3. On removal: locate the simplex, perform transpositions to move it to the
16//!    end, then delete it and close any open intervals.
17//!
18//! ## References
19//!
20//! - Carlsson & de Silva (2010). Zigzag Persistence. FoCM.
21//! - Cohen-Steiner, Edelsbrunner & Morozov (2006). Vines and Vineyards.
22
23use crate::tda::alpha_complex::{sym_diff_sorted, Simplex};
24use std::collections::HashMap;
25
26// ─── ZigzagDirection ─────────────────────────────────────────────────────────
27
28/// Direction of a zigzag step.
29#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ZigzagDirection {
32    /// The simplex is being added (forward inclusion).
33    Forward,
34    /// The simplex is being removed (backward deletion).
35    Backward,
36}
37
38// ─── ZigzagStep ──────────────────────────────────────────────────────────────
39
40/// A single step in a zigzag filtration sequence.
41#[derive(Debug, Clone)]
42pub struct ZigzagStep {
43    /// Direction of this step.
44    pub direction: ZigzagDirection,
45    /// The simplex being added or removed.
46    pub simplex: Simplex,
47}
48
49// ─── ZigzagPersistence ───────────────────────────────────────────────────────
50
51/// Incremental zigzag persistence computation.
52///
53/// Maintains the current filtration and reduced boundary matrix, updating
54/// persistance intervals as simplices are added and removed.
55#[derive(Debug, Clone)]
56pub struct ZigzagPersistence {
57    /// Active simplices in filtration order.
58    filtration: Vec<Simplex>,
59    /// Boundary matrix (reduced), stored as columns of sorted row indices.
60    columns: Vec<Vec<usize>>,
61    /// Pivot map: pivot row → column index.
62    pivot_col: HashMap<usize, usize>,
63    /// Currently open intervals: simplex key → (birth_value, dimension).
64    open_intervals: HashMap<Vec<usize>, (f64, usize)>,
65    /// Completed persistence pairs collected so far.
66    completed: Vec<(f64, f64, usize)>,
67    /// Current "time" counter used as a synthetic filtration value when needed.
68    time: usize,
69}
70
71impl ZigzagPersistence {
72    /// Create a new empty zigzag persistence structure.
73    pub fn new() -> Self {
74        Self {
75            filtration: Vec::new(),
76            columns: Vec::new(),
77            pivot_col: HashMap::new(),
78            open_intervals: HashMap::new(),
79            completed: Vec::new(),
80            time: 0,
81        }
82    }
83
84    /// Process a forward step: add a simplex and return any newly closed intervals.
85    ///
86    /// In standard persistence, adding a simplex either:
87    /// - Creates a new homology class (no pivot after reduction) → opens an interval.
88    /// - Destroys an existing class (reduction yields a pivot) → closes an interval.
89    pub fn add_simplex(&mut self, s: Simplex) -> Vec<(f64, f64, usize)> {
90        self.time += 1;
91        let idx = self.filtration.len();
92        self.filtration.push(s.clone());
93
94        // Build boundary column for the new simplex
95        let simplex_index: HashMap<Vec<usize>, usize> = self
96            .filtration
97            .iter()
98            .enumerate()
99            .map(|(i, sx)| (sx.vertices.clone(), i))
100            .collect();
101
102        let mut col: Vec<usize> = s
103            .boundary_faces()
104            .iter()
105            .filter_map(|face| simplex_index.get(face).copied())
106            .collect();
107        col.sort_unstable();
108
109        // Reduce column
110        while let Some(&pivot) = col.last() {
111            if let Some(&k) = self.pivot_col.get(&pivot) {
112                let col_k = self.columns[k].clone();
113                sym_diff_sorted(&mut col, &col_k);
114            } else {
115                break;
116            }
117        }
118
119        self.columns.push(col.clone());
120
121        let mut newly_closed = Vec::new();
122
123        if let Some(&pivot) = col.last() {
124            // Simplex kills homology class born at `pivot`
125            self.pivot_col.insert(pivot, idx);
126            // Find the open interval for the simplex at position `pivot`
127            let birth_key = self.filtration[pivot].vertices.clone();
128            if let Some((birth_val, dim)) = self.open_intervals.remove(&birth_key) {
129                let death_val = s.filtration_value;
130                newly_closed.push((birth_val, death_val, dim));
131                self.completed.push((birth_val, death_val, dim));
132            }
133        } else {
134            // Simplex creates a new homology class
135            let dim = s.dimension();
136            self.open_intervals
137                .insert(s.vertices.clone(), (s.filtration_value, dim));
138        }
139
140        newly_closed
141    }
142
143    /// Process a backward step: remove a simplex and return any newly closed intervals.
144    ///
145    /// To remove simplex `s` at position `pos`, we perform adjacent transpositions
146    /// (vineyard updates) to bubble it to the last position, then delete it.
147    ///
148    /// After removal, if `s` had an open interval it gets closed with death = infinity
149    /// (the feature survives through the deletion). If `s` was killing another class,
150    /// the killed class re-opens.
151    pub fn remove_simplex(&mut self, s: Simplex) -> Vec<(f64, f64, usize)> {
152        self.time += 1;
153        let mut newly_closed = Vec::new();
154
155        // Find the simplex in the current filtration
156        let pos = match self
157            .filtration
158            .iter()
159            .position(|x| x.vertices == s.vertices)
160        {
161            Some(p) => p,
162            None => return newly_closed, // Simplex not found, nothing to do
163        };
164
165        // Bubble the simplex to the end via adjacent transpositions
166        let last = self.filtration.len() - 1;
167        for i in pos..last {
168            self.transpose_adjacent(i);
169        }
170
171        // The simplex is now at the last position; remove it
172        let removed = self.filtration.pop();
173        self.columns.pop();
174
175        // Rebuild pivot map from scratch (cheaper than incremental for correctness)
176        self.rebuild_pivot_map();
177
178        // If the removed simplex had an open interval, close it with death = removal_value
179        if let Some(rem) = removed {
180            let death_val = rem.filtration_value;
181            if let Some((birth_val, dim)) = self.open_intervals.remove(&rem.vertices) {
182                // Feature born at birth_val, removed at death_val
183                newly_closed.push((birth_val, death_val, dim));
184                self.completed.push((birth_val, death_val, dim));
185            }
186        }
187
188        newly_closed
189    }
190
191    /// Return all completed persistence pairs collected so far.
192    pub fn pairs(&self) -> &[(f64, f64, usize)] {
193        &self.completed
194    }
195
196    // ─── Internal helpers ────────────────────────────────────────────────────
197
198    /// Perform a single adjacent transposition of simplices at positions i and i+1.
199    ///
200    /// This implements the core vineyard transposition step: swap the two simplices
201    /// in the filtration and update the boundary matrix accordingly.
202    fn transpose_adjacent(&mut self, i: usize) {
203        // Swap simplices in the filtration
204        self.filtration.swap(i, i + 1);
205
206        // Update boundary matrix columns for i and i+1
207        // The boundary of the swapped simplices may reference each other.
208
209        // Check if column i+1 has a pivot at i (i.e., column i+1 contains row i)
210        let col_i1_has_i = self.columns[i + 1].binary_search(&i).is_ok();
211        // Check if column i has a pivot at i+1
212        let col_i_has_i1 = self.columns[i].binary_search(&(i + 1)).is_ok();
213
214        if col_i1_has_i {
215            // Add column i to column i+1 (mod-2) to remove row i
216            let col_i = self.columns[i].clone();
217            sym_diff_sorted(&mut self.columns[i + 1], &col_i);
218        }
219
220        // Swap row references: replace any occurrence of i with i+1 and vice versa
221        // in ALL columns (since row i and row i+1 swapped their simplices)
222        for col in self.columns.iter_mut() {
223            let had_i = col.binary_search(&i).is_ok();
224            let had_i1 = col.binary_search(&(i + 1)).is_ok();
225
226            if had_i && !had_i1 {
227                // Replace i with i+1
228                if let Ok(pos) = col.binary_search(&i) {
229                    col[pos] = i + 1;
230                    col.sort_unstable();
231                }
232            } else if had_i1 && !had_i {
233                // Replace i+1 with i
234                if let Ok(pos) = col.binary_search(&(i + 1)) {
235                    col[pos] = i;
236                    col.sort_unstable();
237                }
238            } else if had_i && had_i1 {
239                // Both present: they cancel each other (mod-2 swap has no effect)
240                // but positions are already correct since we just swapped simplices
241                // Nothing to do — both row indices present means they cancel
242            }
243        }
244
245        // Swap the columns themselves
246        self.columns.swap(i, i + 1);
247
248        // If the new column i+1 had a pivot issue (was killing something via i), fix it
249        if col_i_has_i1 {
250            // Column i used to reference row i+1; after swap, this reference is still valid
251            // but needs to remain reduced
252            let last_i = self.columns[i].last().copied();
253            if let Some(piv) = last_i {
254                if let Some(&k) = self.pivot_col.get(&piv) {
255                    if k != i {
256                        let col_k = self.columns[k].clone();
257                        sym_diff_sorted(&mut self.columns[i], &col_k);
258                    }
259                }
260            }
261        }
262    }
263
264    /// Rebuild the pivot map from the current (partially reduced) columns.
265    fn rebuild_pivot_map(&mut self) {
266        self.pivot_col.clear();
267        for (j, col) in self.columns.iter().enumerate() {
268            if let Some(&pivot) = col.last() {
269                self.pivot_col.insert(pivot, j);
270            }
271        }
272    }
273}
274
275impl Default for ZigzagPersistence {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281// ─── Batch computation ────────────────────────────────────────────────────────
282
283/// Compute zigzag persistence for a sequence of steps.
284///
285/// Processes all steps in order and returns all completed `(birth, death, dim)` pairs.
286pub fn compute_zigzag(steps: &[ZigzagStep]) -> Vec<(f64, f64, usize)> {
287    let mut zz = ZigzagPersistence::new();
288    for step in steps {
289        match step.direction {
290            ZigzagDirection::Forward => {
291                zz.add_simplex(step.simplex.clone());
292            }
293            ZigzagDirection::Backward => {
294                zz.remove_simplex(step.simplex.clone());
295            }
296        }
297    }
298    // Close any surviving open intervals (essential features)
299    // These are not added to completed; return only finite pairs.
300    zz.completed
301}
302
303// ─── Tests ────────────────────────────────────────────────────────────────────
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    fn make_simplex(verts: Vec<usize>, fv: f64) -> Simplex {
310        Simplex {
311            vertices: verts,
312            filtration_value: fv,
313        }
314    }
315
316    #[test]
317    fn test_add_vertices_creates_intervals() {
318        let mut zz = ZigzagPersistence::new();
319        let v0 = make_simplex(vec![0], 0.0);
320        let v1 = make_simplex(vec![1], 1.0);
321        zz.add_simplex(v0);
322        zz.add_simplex(v1);
323        // Two vertices added → two open intervals, no closed yet
324        assert_eq!(zz.open_intervals.len(), 2);
325        assert!(zz.completed.is_empty());
326    }
327
328    #[test]
329    fn test_add_edge_closes_one_component() {
330        let mut zz = ZigzagPersistence::new();
331        let v0 = make_simplex(vec![0], 0.0);
332        let v1 = make_simplex(vec![1], 0.0);
333        let edge = make_simplex(vec![0, 1], 1.0);
334
335        zz.add_simplex(v0);
336        zz.add_simplex(v1);
337        let closed = zz.add_simplex(edge);
338
339        // Adding edge should close one H0 interval
340        assert_eq!(closed.len(), 1, "Adding edge should close one interval");
341        let (birth, death, dim) = closed[0];
342        assert_eq!(dim, 0, "Should be H0 (connected component)");
343        assert!(birth < death, "birth < death expected");
344    }
345
346    #[test]
347    fn test_remove_simplex_closes_interval() {
348        let mut zz = ZigzagPersistence::new();
349        let v0 = make_simplex(vec![0], 0.0);
350        zz.add_simplex(v0.clone());
351
352        // The vertex has an open interval; removing it should close it
353        let closed = zz.remove_simplex(v0);
354        // After removal, the open interval for v0 is closed
355        assert_eq!(zz.open_intervals.len(), 0);
356        // The closed interval should appear
357        assert!(!closed.is_empty() || !zz.completed.is_empty());
358    }
359
360    #[test]
361    fn test_zigzag_batch_add_then_remove() {
362        let v0 = make_simplex(vec![0], 0.0);
363        let v1 = make_simplex(vec![1], 0.5);
364        let edge = make_simplex(vec![0, 1], 1.0);
365
366        let steps = vec![
367            ZigzagStep {
368                direction: ZigzagDirection::Forward,
369                simplex: v0.clone(),
370            },
371            ZigzagStep {
372                direction: ZigzagDirection::Forward,
373                simplex: v1.clone(),
374            },
375            ZigzagStep {
376                direction: ZigzagDirection::Forward,
377                simplex: edge.clone(),
378            },
379            ZigzagStep {
380                direction: ZigzagDirection::Backward,
381                simplex: edge,
382            },
383        ];
384
385        let pairs = compute_zigzag(&steps);
386        // Adding the edge closed one H0 interval
387        assert!(!pairs.is_empty(), "Expected at least one completed pair");
388        for (birth, death, _) in &pairs {
389            assert!(birth <= death, "birth={birth} > death={death}");
390        }
391    }
392
393    #[test]
394    fn test_directions_are_non_exhaustive() {
395        // Ensure the enum is non_exhaustive (just compile-check the variant usage)
396        let d = ZigzagDirection::Forward;
397        assert_eq!(d, ZigzagDirection::Forward);
398    }
399}