Skip to main content

holos_tda/bipersistence/
mod.rs

1//! Exact finite H1 modules over a degree-Rips parameter grid.
2//!
3//! The module stores every canonical cohomology space and every cover
4//! restriction. This is a complete representation on the declared finite
5//! grid. It is not a minimal bigraded presentation or a free resolution.
6
7mod atlas;
8mod circular;
9mod linear;
10mod module;
11mod queries;
12
13pub use atlas::{ClassExtension, ClassExtensionKind, ClassExtensionRegion, CohomologyClassAtlas};
14pub use circular::{
15    CircularCoordinateFamily, CircularCoordinateFamilyEntry, CircularCoordinateFamilyStatus,
16};
17pub use module::BipersistenceModule;
18
19use crate::bifiltration::Bigrade;
20use crate::cohomology::{CohomologyLimits, CohomologySpaceId};
21use crate::{Error, Result};
22
23/// Resource limits for one finite bipersistence module.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct BipersistenceLimits {
27    /// Largest accepted grid-node count.
28    pub max_nodes: usize,
29    /// Largest accepted cover-map count.
30    pub max_cover_maps: usize,
31    /// Largest sum of cohomology ranks across all nodes.
32    pub max_total_rank: usize,
33    /// Largest total nonzero coefficient count across cover maps.
34    pub max_map_terms: usize,
35    /// Largest direct-sum dimension used by one exact query.
36    pub max_linear_variables: usize,
37    /// Largest sparse coefficient count admitted by one exact query.
38    pub max_linear_terms: usize,
39    /// Limits for each canonical cohomology space.
40    pub cohomology: CohomologyLimits,
41}
42
43impl Default for BipersistenceLimits {
44    fn default() -> Self {
45        Self {
46            max_nodes: 100_000,
47            max_cover_maps: 200_000,
48            max_total_rank: 10_000_000,
49            max_map_terms: 100_000_000,
50            max_linear_variables: 100_000,
51            max_linear_terms: 100_000_000,
52            cohomology: CohomologyLimits::default(),
53        }
54    }
55}
56
57/// One canonical vector-space node of a finite bipersistence module.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct BipersistenceNode {
60    /// Position in the parameter grid.
61    pub grade: Bigrade,
62    /// Content identifier of the canonical cohomology space.
63    pub space: CohomologySpaceId,
64    /// Dimension of `H¹` at this grade.
65    pub rank: usize,
66}
67
68/// One nonzero coefficient in canonical basis coordinates.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
70pub struct BipersistenceTerm {
71    /// Zero-based canonical basis position.
72    pub basis_index: usize,
73    /// Coefficient in `1..modulus`.
74    pub coefficient: u32,
75}
76
77/// Image of one source basis vector under a module map.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct BipersistenceMapColumn {
80    /// Zero-based source basis position.
81    pub source_basis_index: usize,
82    /// Nonzero target coordinates in ascending basis order.
83    pub image: Vec<BipersistenceTerm>,
84}
85
86/// Exact contravariant `H¹` map between comparable grid nodes.
87///
88/// `lower_grade` precedes `upper_grade`. The map runs from the cohomology at
89/// `upper_grade` to the cohomology at `lower_grade`.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct BipersistenceMap {
92    /// Smaller filtration grade and map target.
93    pub lower_grade: Bigrade,
94    /// Larger filtration grade and map source.
95    pub upper_grade: Bigrade,
96    /// Canonical source space at `upper_grade`.
97    pub source_space: CohomologySpaceId,
98    /// Canonical target space at `lower_grade`.
99    pub target_space: CohomologySpaceId,
100    /// Rank of the linear map.
101    pub rank: usize,
102    /// Columns in source basis order.
103    pub columns: Vec<BipersistenceMapColumn>,
104}
105
106/// One closed rectangle in the finite parameter grid.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct BipersistenceRectangle {
109    /// Coordinatewise minimum corner.
110    pub lower: Bigrade,
111    /// Coordinatewise maximum corner.
112    pub upper: Bigrade,
113}
114
115impl BipersistenceRectangle {
116    /// Construct a rectangle with comparable corners.
117    pub fn new(lower: Bigrade, upper: Bigrade) -> Result<Self> {
118        if !lower.precedes(upper) {
119            return Err(Error::InvalidInput(
120                "a bipersistence rectangle needs comparable corners".into(),
121            ));
122        }
123        Ok(Self { lower, upper })
124    }
125}
126
127/// One finite connected region of the parameter grid.
128///
129/// Grades are canonicalized in lexicographic order. The module query checks
130/// that every grade lies on its grid and that the comparability graph is
131/// connected. The region need not have a minimum or a maximum.
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
133pub struct BipersistenceRegion {
134    grades: Vec<Bigrade>,
135}
136
137impl BipersistenceRegion {
138    /// Construct a nonempty region from distinct grades.
139    pub fn new(mut grades: Vec<Bigrade>) -> Result<Self> {
140        grades.sort_unstable();
141        if grades.is_empty() {
142            return Err(Error::InvalidInput(
143                "a bipersistence region needs at least one grade".into(),
144            ));
145        }
146        if grades.windows(2).any(|pair| pair[0] == pair[1]) {
147            return Err(Error::InvalidInput(
148                "bipersistence region grades must be distinct".into(),
149            ));
150        }
151        Ok(Self { grades })
152    }
153
154    /// Canonical grades in lexicographic order.
155    pub fn grades(&self) -> &[Bigrade] {
156        &self.grades
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::SparseDistanceMatrix;
164    use crate::bifiltration::{DegreeRipsBifiltration, DegreeRipsParams};
165    use crate::circular::CircularCoordinateParams;
166
167    fn square_with_diagonals() -> SparseDistanceMatrix {
168        SparseDistanceMatrix::from_triplets(
169            4,
170            &[
171                (0, 1, 1.0),
172                (1, 2, 1.0),
173                (2, 3, 1.0),
174                (0, 3, 1.0),
175                (0, 2, 2.0),
176                (1, 3, 2.0),
177            ],
178        )
179        .unwrap()
180    }
181
182    fn module(graph: &SparseDistanceMatrix, modulus: u32) -> BipersistenceModule {
183        let degree_rips =
184            DegreeRipsBifiltration::from_graph(graph, DegreeRipsParams::default()).unwrap();
185        BipersistenceModule::from_degree_rips(&degree_rips, modulus, BipersistenceLimits::default())
186            .unwrap()
187    }
188
189    fn repeated_cycle_grid() -> DegreeRipsBifiltration {
190        let cycle = SparseDistanceMatrix::from_triplets(
191            4,
192            &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 3, 1.0)],
193        )
194        .unwrap();
195        DegreeRipsBifiltration::from_graph_on_grid(
196            &cycle,
197            vec![1.0, 2.0, 3.0],
198            vec![3, 0],
199            DegreeRipsParams {
200                threshold: Some(3.0),
201                ..DegreeRipsParams::default()
202            },
203        )
204        .unwrap()
205    }
206
207    #[test]
208    fn builds_commuting_degree_rips_module() {
209        let module = module(&square_with_diagonals(), 47);
210        assert_eq!(module.scales(), &[0.0, 1.0, 2.0]);
211        assert_eq!(module.minimum_degrees(), &[3, 2, 1, 0]);
212        assert_eq!(module.nodes().len(), 12);
213        assert_eq!(module.cover_maps().len(), 17);
214        assert_eq!(module.node(Bigrade::new(1, 1)).unwrap().rank, 1);
215        assert_eq!(module.node(Bigrade::new(2, 0)).unwrap().rank, 0);
216        assert_eq!(
217            module
218                .map_rank(Bigrade::new(1, 1), Bigrade::new(2, 1))
219                .unwrap(),
220            0
221        );
222    }
223
224    #[test]
225    fn repeated_graph_states_keep_exact_node_and_map_content() {
226        let degree_rips = repeated_cycle_grid();
227        let module =
228            BipersistenceModule::from_degree_rips(&degree_rips, 47, BipersistenceLimits::default())
229                .unwrap();
230        let expected_edges: Vec<(usize, usize, u64)> =
231            vec![(0, 1, 0), (0, 3, 0), (1, 2, 0), (2, 3, 0)];
232        let empty_space = module.cohomology_space(Bigrade::new(0, 0)).unwrap().id();
233        let cycle_space = module.cohomology_space(Bigrade::new(0, 1)).unwrap().id();
234        for scale in 0..3 {
235            assert_eq!(
236                module
237                    .cohomology_space(Bigrade::new(scale, 0))
238                    .unwrap()
239                    .id(),
240                empty_space
241            );
242            let graph = module.h1_graph(Bigrade::new(scale, 1)).unwrap();
243            assert_eq!(graph.len(), 4);
244            assert_eq!(
245                graph
246                    .edges()
247                    .map(|(u, v, value)| (u, v, value.to_bits()))
248                    .collect::<Vec<_>>(),
249                expected_edges
250            );
251            assert_eq!(
252                module
253                    .cohomology_space(Bigrade::new(scale, 1))
254                    .unwrap()
255                    .id(),
256                cycle_space
257            );
258        }
259        assert_eq!(module.nodes().len(), 6);
260        assert_eq!(module.cover_maps().len(), 7);
261        assert_eq!(module.node(Bigrade::new(0, 1)).unwrap().rank, 1);
262        for scale in 0..2 {
263            let map = module
264                .cover_map(Bigrade::new(scale, 1), Bigrade::new(scale + 1, 1))
265                .unwrap();
266            assert_eq!(map.rank, 1);
267            assert_eq!(map.source_space, cycle_space);
268            assert_eq!(map.target_space, cycle_space);
269            assert_eq!(
270                map.columns,
271                vec![BipersistenceMapColumn {
272                    source_basis_index: 0,
273                    image: vec![BipersistenceTerm {
274                        basis_index: 0,
275                        coefficient: 1,
276                    }],
277                }]
278            );
279        }
280        for scale in 0..3 {
281            let map = module
282                .cover_map(Bigrade::new(scale, 0), Bigrade::new(scale, 1))
283                .unwrap();
284            assert_eq!(map.rank, 0);
285            assert_eq!(map.source_space, cycle_space);
286            assert_eq!(map.target_space, empty_space);
287            assert_eq!(map.columns.len(), 1);
288            assert!(map.columns[0].image.is_empty());
289        }
290    }
291
292    #[test]
293    fn repeated_states_still_count_every_node_and_map_term() {
294        let degree_rips = repeated_cycle_grid();
295        let mut limits = BipersistenceLimits {
296            max_total_rank: 2,
297            ..BipersistenceLimits::default()
298        };
299        assert!(BipersistenceModule::from_degree_rips(&degree_rips, 47, limits).is_err());
300
301        limits = BipersistenceLimits {
302            max_map_terms: 1,
303            ..BipersistenceLimits::default()
304        };
305        assert!(BipersistenceModule::from_degree_rips(&degree_rips, 47, limits).is_err());
306    }
307
308    #[test]
309    fn isolated_vertices_remain_in_empty_h1_graphs() {
310        let graph =
311            SparseDistanceMatrix::from_triplets(4, &[(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0)])
312                .unwrap();
313        let degree_rips = DegreeRipsBifiltration::from_graph_on_grid(
314            &graph,
315            vec![1.0],
316            vec![2, 0],
317            DegreeRipsParams {
318                threshold: Some(1.0),
319                ..DegreeRipsParams::default()
320            },
321        )
322        .unwrap();
323        assert_eq!(
324            degree_rips
325                .bifiltration()
326                .slice(Bigrade::new(0, 0))
327                .unwrap()
328                .active_vertices(),
329            &[0]
330        );
331        let module =
332            BipersistenceModule::from_degree_rips(&degree_rips, 47, BipersistenceLimits::default())
333                .unwrap();
334        let empty = module.h1_graph(Bigrade::new(0, 0)).unwrap();
335        assert_eq!(empty.len(), 4);
336        assert_eq!(empty.num_edges(), 0);
337        assert_eq!(
338            module
339                .h1_graph(Bigrade::new(0, 1))
340                .unwrap()
341                .edges()
342                .collect::<Vec<_>>(),
343            vec![(0, 1, 0.0), (0, 2, 0.0), (0, 3, 0.0)]
344        );
345    }
346
347    #[test]
348    fn equal_ranks_with_different_graphs_keep_distinct_spaces() {
349        let graph = SparseDistanceMatrix::from_triplets(
350            4,
351            &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 2, 2.0)],
352        )
353        .unwrap();
354        let degree_rips = DegreeRipsBifiltration::from_graph_on_grid(
355            &graph,
356            vec![1.0, 2.0],
357            vec![0],
358            DegreeRipsParams {
359                threshold: Some(2.0),
360                ..DegreeRipsParams::default()
361            },
362        )
363        .unwrap();
364        let module =
365            BipersistenceModule::from_degree_rips(&degree_rips, 47, BipersistenceLimits::default())
366                .unwrap();
367        assert_eq!(module.node(Bigrade::new(0, 0)).unwrap().rank, 0);
368        assert_eq!(module.node(Bigrade::new(1, 0)).unwrap().rank, 0);
369        assert_ne!(
370            module.cohomology_space(Bigrade::new(0, 0)).unwrap().id(),
371            module.cohomology_space(Bigrade::new(1, 0)).unwrap().id()
372        );
373        assert_ne!(
374            module
375                .h1_graph(Bigrade::new(0, 0))
376                .unwrap()
377                .edges()
378                .collect::<Vec<_>>(),
379            module
380                .h1_graph(Bigrade::new(1, 0))
381                .unwrap()
382                .edges()
383                .collect::<Vec<_>>()
384        );
385    }
386
387    #[test]
388    fn rectangle_rank_agrees_with_constant_cycle() {
389        let cycle = SparseDistanceMatrix::from_triplets(
390            4,
391            &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 3, 1.0)],
392        )
393        .unwrap();
394        let module = module(&cycle, 47);
395        let rectangle =
396            BipersistenceRectangle::new(Bigrade::new(1, 1), Bigrade::new(1, 3)).unwrap();
397        assert_eq!(module.rectangle_rank(rectangle).unwrap(), 1);
398        assert_eq!(
399            module.rectangle_rank(rectangle).unwrap(),
400            module.map_rank(rectangle.lower, rectangle.upper).unwrap()
401        );
402    }
403
404    #[test]
405    fn connected_region_without_extrema_has_generalized_rank() {
406        let cycle = SparseDistanceMatrix::from_triplets(
407            4,
408            &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 3, 1.0)],
409        )
410        .unwrap();
411        let degree_rips = DegreeRipsBifiltration::from_graph_on_grid(
412            &cycle,
413            vec![1.0, 2.0, 3.0],
414            vec![2, 1, 0],
415            DegreeRipsParams {
416                threshold: Some(3.0),
417                ..DegreeRipsParams::default()
418            },
419        )
420        .unwrap();
421        let module =
422            BipersistenceModule::from_degree_rips(&degree_rips, 47, BipersistenceLimits::default())
423                .unwrap();
424        let region = BipersistenceRegion::new(vec![
425            Bigrade::new(0, 1),
426            Bigrade::new(1, 1),
427            Bigrade::new(1, 0),
428            Bigrade::new(2, 0),
429        ])
430        .unwrap();
431        assert_eq!(module.region_rank(&region).unwrap(), 1);
432
433        let disconnected =
434            BipersistenceRegion::new(vec![Bigrade::new(0, 1), Bigrade::new(1, 0)]).unwrap();
435        assert!(module.region_rank(&disconnected).is_err());
436    }
437
438    #[test]
439    fn class_atlas_finds_unique_and_absent_extensions() {
440        let module = module(&square_with_diagonals(), 47);
441        let atlas = module
442            .class_atlas(
443                Bigrade::new(1, 1),
444                &[BipersistenceTerm {
445                    basis_index: 0,
446                    coefficient: 1,
447                }],
448            )
449            .unwrap();
450        assert_eq!(atlas.extensions[0].kind, ClassExtensionKind::Unique);
451        assert!(
452            atlas
453                .extensions
454                .iter()
455                .any(|extension| extension.kind == ClassExtensionKind::NoExtension)
456        );
457    }
458
459    #[test]
460    fn class_atlas_reports_ambiguous_extensions() {
461        let graph = SparseDistanceMatrix::from_triplets(
462            7,
463            &[
464                (0, 1, 1.0),
465                (1, 2, 1.0),
466                (2, 3, 1.0),
467                (0, 3, 1.0),
468                (0, 4, 2.0),
469                (4, 5, 2.0),
470                (5, 6, 2.0),
471                (0, 6, 2.0),
472            ],
473        )
474        .unwrap();
475        let module = module(&graph, 47);
476        let atlas = module
477            .class_atlas(
478                Bigrade::new(1, 6),
479                &[BipersistenceTerm {
480                    basis_index: 0,
481                    coefficient: 1,
482                }],
483            )
484            .unwrap();
485        let extension = atlas
486            .extensions
487            .iter()
488            .find(|extension| extension.grade == Bigrade::new(2, 6))
489            .unwrap();
490        assert_eq!(extension.kind, ClassExtensionKind::Ambiguous);
491        assert_eq!(extension.ambiguity.len(), 1);
492    }
493
494    #[test]
495    fn circular_family_covers_unique_cycle_extensions() {
496        let cycle = SparseDistanceMatrix::from_triplets(
497            4,
498            &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 3, 1.0)],
499        )
500        .unwrap();
501        let module = module(&cycle, 47);
502        let atlas = module
503            .class_atlas(
504                Bigrade::new(1, 1),
505                &[BipersistenceTerm {
506                    basis_index: 0,
507                    coefficient: 1,
508                }],
509            )
510            .unwrap();
511        let family = module
512            .circular_coordinate_family(&atlas, CircularCoordinateParams::default())
513            .unwrap();
514        assert!(family.entries.iter().all(|entry| {
515            matches!(
516                (entry.extension, &entry.status),
517                (
518                    ClassExtensionKind::Unique,
519                    CircularCoordinateFamilyStatus::Success(_)
520                ) | (
521                    ClassExtensionKind::Ambiguous | ClassExtensionKind::NoExtension,
522                    CircularCoordinateFamilyStatus::NotAttempted,
523                )
524            )
525        }));
526
527        let scaled = module
528            .class_atlas(
529                Bigrade::new(1, 1),
530                &[BipersistenceTerm {
531                    basis_index: 0,
532                    coefficient: 2,
533                }],
534            )
535            .unwrap();
536        let scaled_family = module
537            .circular_coordinate_family(&scaled, CircularCoordinateParams::default())
538            .unwrap();
539        assert!(
540            scaled_family
541                .entries
542                .iter()
543                .any(|entry| matches!(&entry.status, CircularCoordinateFamilyStatus::Success(_)))
544        );
545    }
546
547    #[test]
548    fn generalized_rank_matches_map_rank_on_two_node_rectangles() {
549        let module = module(&square_with_diagonals(), 47);
550        for map in module.cover_maps() {
551            let rectangle = BipersistenceRectangle::new(map.lower_grade, map.upper_grade).unwrap();
552            assert_eq!(module.rectangle_rank(rectangle).unwrap(), map.rank);
553        }
554    }
555}