1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct BipersistenceLimits {
27 pub max_nodes: usize,
29 pub max_cover_maps: usize,
31 pub max_total_rank: usize,
33 pub max_map_terms: usize,
35 pub max_linear_variables: usize,
37 pub max_linear_terms: usize,
39 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct BipersistenceNode {
60 pub grade: Bigrade,
62 pub space: CohomologySpaceId,
64 pub rank: usize,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
70pub struct BipersistenceTerm {
71 pub basis_index: usize,
73 pub coefficient: u32,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct BipersistenceMapColumn {
80 pub source_basis_index: usize,
82 pub image: Vec<BipersistenceTerm>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct BipersistenceMap {
92 pub lower_grade: Bigrade,
94 pub upper_grade: Bigrade,
96 pub source_space: CohomologySpaceId,
98 pub target_space: CohomologySpaceId,
100 pub rank: usize,
102 pub columns: Vec<BipersistenceMapColumn>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct BipersistenceRectangle {
109 pub lower: Bigrade,
111 pub upper: Bigrade,
113}
114
115impl BipersistenceRectangle {
116 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
133pub struct BipersistenceRegion {
134 grades: Vec<Bigrade>,
135}
136
137impl BipersistenceRegion {
138 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 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(°ree_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(°ree_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(°ree_rips, 47, limits).is_err());
300
301 limits = BipersistenceLimits {
302 max_map_terms: 1,
303 ..BipersistenceLimits::default()
304 };
305 assert!(BipersistenceModule::from_degree_rips(°ree_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(°ree_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(°ree_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(°ree_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(®ion).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}