1use std::{collections::HashMap, fmt::Debug, hash::Hash};
46
47use harness_algebra::tensors::dynamic::{
48 block::BlockMatrix, matrix::RowMajor, vector::DynamicVector,
49};
50
51use super::*;
52use crate::{
53 complexes::{Complex, ComplexElement},
54 definitions::Topology,
55 set::Poset,
56};
57
58pub struct Sheaf<T: Topology, C: Category> {
75 pub space: T,
79 pub restrictions: HashMap<(T::Item, T::Item), C::Morphism>,
87}
88
89impl<T, C> Sheaf<T, C>
90where
91 T: Topology + Poset,
92 T::Item: Hash + Eq + Clone + Debug,
93 C: Category + Clone + PartialEq + Debug,
94 C::Morphism: Clone + Debug,
95{
96 pub fn new(space: T, restrictions: HashMap<(T::Item, T::Item), C::Morphism>) -> Self {
127 assert!(
128 restrictions.iter().all(|(k, _v)| space.leq(&k.0, &k.1) == Some(true)),
129 "Restriction map defined for a pair (parent, child) where parent is not less than or equal \
130 to child, or they are incomparable."
131 );
132
133 Self { space, restrictions }
134 }
135
136 pub fn restrict(
163 &self,
164 parent_target_item: &T::Item,
165 child_source_item: &T::Item,
166 section_data_on_child: C,
167 ) -> C {
168 assert!(
169 self.space.leq(parent_target_item, child_source_item) == Some(true),
170 "Cannot restrict: parent_target_item is not less than or equal to child_source_item, or \
171 items are incomparable."
172 );
173
174 let restriction_map = self
175 .restrictions
176 .get(&(parent_target_item.clone(), child_source_item.clone()))
177 .unwrap_or_else(|| {
178 panic!("Restriction map not found for ({parent_target_item:?}, {child_source_item:?})")
179 });
180
181 C::apply(restriction_map.clone(), section_data_on_child)
182 }
183
184 pub fn is_global_section(&self, section: &HashMap<T::Item, C>) -> bool {
208 for ((parent_item, child_item), restriction_map) in &self.restrictions {
209 let Some(parent_data) = section.get(parent_item) else {
210 return false;
211 };
212 let Some(child_data) = section.get(child_item) else {
213 return false;
214 };
215 let restricted_parent_data = C::apply(restriction_map.clone(), parent_data.clone());
216 if restricted_parent_data != *child_data {
217 return false;
218 }
219 }
220 true
221 }
222}
223
224use harness_algebra::tensors::dynamic::matrix::DynamicDenseMatrix;
225
226impl<T: ComplexElement, F: Field + Copy> Sheaf<Complex<T>, DynamicVector<F>>
229where T: Hash + Eq + Clone + Debug
230{
231 pub fn coboundary(&self, dimension: usize) -> BlockMatrix<F, RowMajor> {
249 let k_elements = {
251 let mut elements = self.space.elements_of_dimension(dimension);
252 elements.sort_unstable();
253 elements
254 };
255
256 let k_plus_1_elements = {
257 let mut elements = self.space.elements_of_dimension(dimension + 1);
258 elements.sort_unstable();
259 elements
260 };
261
262 if k_elements.is_empty() || k_plus_1_elements.is_empty() {
263 return BlockMatrix::new(vec![], vec![]);
265 }
266
267 let mut col_block_sizes = Vec::new();
269 for k_element in &k_elements {
270 let stalk_dim = self
272 .restrictions
273 .iter()
274 .find_map(|((from, to), matrix)| {
275 if from.same_content(k_element) {
276 Some(matrix.num_cols()) } else if to.same_content(k_element) {
278 Some(matrix.num_rows()) } else {
280 None
281 }
282 })
283 .unwrap_or(1); col_block_sizes.push(stalk_dim);
285 }
286
287 let mut row_block_sizes = Vec::new();
288 for k_plus_1_element in &k_plus_1_elements {
289 let stalk_dim = self
291 .restrictions
292 .iter()
293 .find_map(|((from, to), matrix)| {
294 if from.same_content(k_plus_1_element) {
295 Some(matrix.num_cols()) } else if to.same_content(k_plus_1_element) {
298 Some(matrix.num_rows()) } else {
301 None
302 }
303 })
304 .unwrap_or(1); row_block_sizes.push(stalk_dim);
306 }
307
308 let mut block_matrix = BlockMatrix::new(row_block_sizes, col_block_sizes);
310
311 for (row_idx, k_plus_1_element) in k_plus_1_elements.iter().enumerate() {
313 for (col_idx, k_element) in k_elements.iter().enumerate() {
314 let boundary_with_orientations = k_plus_1_element.boundary_with_orientations();
316
317 if let Some((_, orientation_coeff)) =
318 boundary_with_orientations.iter().find(|(face, _)| face.same_content(k_element))
319 {
320 if let Some(restriction_matrix) =
322 self.restrictions.get(&(k_element.clone(), k_plus_1_element.clone()))
323 {
324 let signed_matrix = if *orientation_coeff > 0 {
326 restriction_matrix.clone()
327 } else if *orientation_coeff < 0 {
328 let mut negated = restriction_matrix.clone();
330 for i in 0..negated.num_rows() {
331 for j in 0..negated.num_cols() {
332 let val = *negated.get_component(i, j);
333 negated.set_component(i, j, -val);
334 }
335 }
336 negated
337 } else {
338 DynamicDenseMatrix::<F, RowMajor>::zeros(
340 block_matrix.row_block_sizes()[row_idx],
341 block_matrix.col_block_sizes()[col_idx],
342 )
343 };
344
345 block_matrix.set_block(row_idx, col_idx, signed_matrix);
346 }
347 }
349 }
351 }
352
353 block_matrix
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 #![allow(clippy::type_complexity)]
360 #![allow(clippy::too_many_lines)]
361 #![allow(clippy::float_cmp)]
362 use harness_algebra::tensors::dynamic::{
363 matrix::{DynamicDenseMatrix, RowMajor},
364 vector::DynamicVector,
365 };
366
367 use super::*;
368 use crate::complexes::{Cube, CubicalComplex, Simplex, SimplicialComplex};
369
370 fn simplicial_complex_1d() -> (
371 SimplicialComplex,
372 HashMap<(Simplex, Simplex), DynamicDenseMatrix<f64, RowMajor>>,
373 Simplex,
374 Simplex,
375 Simplex,
376 ) {
377 let mut cc = SimplicialComplex::new();
378 let v0 = Simplex::new(0, vec![0]);
379 let v1 = Simplex::new(0, vec![1]);
380 let e01 = Simplex::new(1, vec![0, 1]);
381 let v0 = cc.join_element(v0);
382 let v1 = cc.join_element(v1);
383 let e01 = cc.join_element(e01);
384 let restrictions = HashMap::from([
385 ((v0.clone(), e01.clone()), {
386 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
387 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 2.0]));
388 mat
389 }),
390 ((v1.clone(), e01.clone()), {
391 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
392 mat.append_column(&DynamicVector::<f64>::new(vec![2.0, 0.0]));
393 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 2.0]));
394 mat
395 }),
396 ]);
397 (cc, restrictions, v0, v1, e01)
398 }
399
400 #[test]
401 fn test_simplicial_sheaf_global_section_1d() {
402 let (cc, restrictions, v1, v2, e1) = simplicial_complex_1d();
403
404 let sheaf = Sheaf::<SimplicialComplex, DynamicVector<f64>>::new(cc, restrictions);
405
406 let section = HashMap::from([
407 (v1.clone(), DynamicVector::<f64>::new(vec![2.0])), (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), (e1.clone(), DynamicVector::<f64>::new(vec![2.0, 4.0])), ]);
411 assert!(sheaf.is_global_section(§ion));
412
413 let section = HashMap::from([
414 (v1.clone(), DynamicVector::<f64>::new(vec![1.0])), (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), (e1.clone(), DynamicVector::<f64>::new(vec![2.0, 4.0])), ]);
418 assert!(!sheaf.is_global_section(§ion));
419
420 let section = HashMap::from([
421 (v1.clone(), DynamicVector::<f64>::new(vec![2.0])), (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), (e1.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), ]);
425 assert!(!sheaf.is_global_section(§ion));
426
427 let section = HashMap::from([
428 (v1, DynamicVector::<f64>::new(vec![2.0])), (v2, DynamicVector::<f64>::new(vec![3.0, 3.0])), (e1, DynamicVector::<f64>::new(vec![2.0, 4.0])), ]);
432 assert!(!sheaf.is_global_section(§ion));
433 }
434
435 #[test]
436 fn test_simplicial_sheaf_coboundary_1d() {
437 let (cc, restrictions, ..) = simplicial_complex_1d();
438 let sheaf = Sheaf::<SimplicialComplex, DynamicVector<f64>>::new(cc, restrictions);
439 let coboundary = sheaf.coboundary(0);
440
441 assert_eq!(coboundary.block_structure(), (1, 2));
445 assert_eq!(coboundary.row_block_sizes(), &[2]); assert_eq!(coboundary.col_block_sizes(), &[1, 2]); let block_00 = coboundary.get_block(0, 0).expect("Block (0,0) should exist");
451 assert_eq!(block_00.num_rows(), 2);
452 assert_eq!(block_00.num_cols(), 1);
453 assert_eq!(*block_00.get_component(0, 0), -1.0);
454 assert_eq!(*block_00.get_component(1, 0), -2.0);
455
456 let block_01 = coboundary.get_block(0, 1).expect("Block (0,1) should exist");
459 assert_eq!(block_01.num_rows(), 2);
460 assert_eq!(block_01.num_cols(), 2);
461 assert_eq!(*block_01.get_component(0, 0), 2.0);
462 assert_eq!(*block_01.get_component(0, 1), 0.0);
463 assert_eq!(*block_01.get_component(1, 0), 0.0);
464 assert_eq!(*block_01.get_component(1, 1), 2.0);
465
466 let flattened = coboundary.flatten();
468 assert_eq!(flattened.num_rows(), 2);
469 assert_eq!(flattened.num_cols(), 3);
470
471 assert_eq!(*flattened.get_component(0, 0), -1.0);
473 assert_eq!(*flattened.get_component(0, 1), 2.0);
474 assert_eq!(*flattened.get_component(0, 2), 0.0);
475
476 assert_eq!(*flattened.get_component(1, 0), -2.0);
478 assert_eq!(*flattened.get_component(1, 1), 0.0);
479 assert_eq!(*flattened.get_component(1, 2), 2.0);
480
481 println!("Coboundary matrix:");
482 println!("{coboundary}");
483
484 let coboundary = sheaf.coboundary(1);
485 println!("{coboundary}");
486 assert_eq!(coboundary.block_structure(), (0, 0));
487 }
488
489 fn simplicial_complex_2d() -> (
490 SimplicialComplex,
491 HashMap<(Simplex, Simplex), DynamicDenseMatrix<f64, RowMajor>>,
492 Simplex,
493 Simplex,
494 Simplex,
495 Simplex,
496 Simplex,
497 Simplex,
498 Simplex,
499 ) {
500 let mut cc = SimplicialComplex::new();
501 let v0 = Simplex::new(0, vec![0]); let v1 = Simplex::new(0, vec![1]); let v2 = Simplex::new(0, vec![2]); let e01 = Simplex::new(1, vec![0, 1]); let e02 = Simplex::new(1, vec![0, 2]); let e12 = Simplex::new(1, vec![1, 2]); let f012 = Simplex::new(2, vec![0, 1, 2]); let v0 = cc.join_element(v0);
514 let v1 = cc.join_element(v1);
515 let v2 = cc.join_element(v2);
516 let e01 = cc.join_element(e01);
517 let e02 = cc.join_element(e02);
518 let e12 = cc.join_element(e12);
519 let f012 = cc.join_element(f012);
520
521 let restrictions = HashMap::from([
522 ((v0.clone(), e01.clone()), {
523 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
524 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 2.0]));
525 mat
526 }),
527 ((v1.clone(), e01.clone()), {
528 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
529 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 0.0]));
530 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 1.0]));
531 mat
532 }),
533 ((v0.clone(), e02.clone()), {
534 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
535 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 0.0]));
536 mat
537 }),
538 ((v2.clone(), e02.clone()), {
539 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
540 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 0.0]));
541 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 0.0]));
542 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 0.0]));
543 mat
544 }),
545 ((v1.clone(), e12.clone()), {
546 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
547 mat.append_column(&DynamicVector::<f64>::new(vec![2.0, 0.0]));
548 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 2.0]));
549 mat
550 }),
551 ((v2.clone(), e12.clone()), {
552 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
553 mat.append_column(&DynamicVector::<f64>::new(vec![2.0, 0.0]));
554 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 2.0]));
555 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 0.0]));
556 mat
557 }),
558 ((e01.clone(), f012.clone()), {
559 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
560 mat.append_column(&DynamicVector::<f64>::new(vec![2.0, 0.0, 0.0]));
561 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
562 mat
563 }),
564 ((e02.clone(), f012.clone()), {
565 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
566 mat.append_column(&DynamicVector::<f64>::new(vec![2.0, 0.0, 0.0]));
567 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 1.0, 0.0]));
568 mat
569 }),
570 ((e12.clone(), f012.clone()), {
571 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
572 mat.append_column(&DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0]));
573 mat.append_column(&DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
574 mat
575 }),
576 ]);
577 (cc, restrictions, v0, v1, v2, e01, e02, e12, f012)
578 }
579
580 #[test]
581 fn test_simplicial_sheaf_global_section_2d() {
582 let (cc, restrictions, v0, v1, v2, e01, e02, e12, f012) = simplicial_complex_2d();
583
584 let sheaf = Sheaf::<SimplicialComplex, DynamicVector<f64>>::new(cc, restrictions);
585
586 let section = HashMap::from([
587 (v0, DynamicVector::<f64>::new(vec![1.0])), (v1, DynamicVector::<f64>::new(vec![1.0, 2.0])), (v2, DynamicVector::<f64>::new(vec![1.0, 2.0, 3.0])), (e01, DynamicVector::<f64>::new(vec![1.0, 2.0])), (e02, DynamicVector::<f64>::new(vec![1.0, 0.0])), (e12, DynamicVector::<f64>::new(vec![2.0, 4.0])), (f012, DynamicVector::<f64>::new(vec![2.0, 0.0, 0.0])), ]);
595 assert!(sheaf.is_global_section(§ion));
596 }
597
598 #[test]
599 fn test_simplicial_sheaf_coboundary_2d() {
600 let (cc, restrictions, ..) = simplicial_complex_2d();
601 let sheaf = Sheaf::<SimplicialComplex, DynamicVector<f64>>::new(cc, restrictions);
602 let coboundary = sheaf.coboundary(0);
603 println!("{coboundary}");
604 assert_eq!(coboundary.block_structure(), (3, 3));
605
606 let coboundary = sheaf.coboundary(1);
607 println!("{coboundary}");
608 assert_eq!(coboundary.block_structure(), (1, 3));
609
610 let coboundary = sheaf.coboundary(2);
611 println!("{coboundary}");
612 assert_eq!(coboundary.block_structure(), (0, 0));
613 }
614
615 fn cubical_complex_2d(
616 ) -> (CubicalComplex, HashMap<(Cube, Cube), DynamicDenseMatrix<f64, RowMajor>>) {
617 let mut cc = CubicalComplex::new();
618
619 let v00 = Cube::vertex(0);
622 let v10 = Cube::vertex(1);
623 let v01 = Cube::vertex(2);
624 let v11 = Cube::vertex(3);
625
626 let e_h1 = Cube::edge(0, 1);
628 let e_h2 = Cube::edge(2, 3);
629
630 let e_v1 = Cube::edge(0, 2);
632 let e_v2 = Cube::edge(1, 3);
633
634 let square = Cube::square([0, 1, 2, 3]); let v00 = cc.join_element(v00); let v10 = cc.join_element(v10); let v01 = cc.join_element(v01); let v11 = cc.join_element(v11); let e_h1 = cc.join_element(e_h1); let e_h2 = cc.join_element(e_h2); let e_v1 = cc.join_element(e_v1); let e_v2 = cc.join_element(e_v2); let square = cc.join_element(square); let restrictions = HashMap::from([
649 ((v00.clone(), e_h1.clone()), {
650 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
651 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.5])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
653 mat
654 }),
655 ((v10.clone(), e_h1.clone()), {
656 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
657 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0]));
659 mat
660 }),
661 ((v01.clone(), e_h2.clone()), {
662 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
663 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
665 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
666 mat
667 }),
668 ((v11.clone(), e_h2.clone()), {
669 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
670 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 1.0]));
672 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0]));
673 mat
674 }),
675 ((v00, e_v1.clone()), {
676 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
677 mat.append_row(DynamicVector::<f64>::new(vec![2.0, 1.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
679 mat
680 }),
681 ((v01, e_v1.clone()), {
682 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
683 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
685 mat
686 }),
687 ((v10, e_v2.clone()), {
688 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
689 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0]));
691 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
692 mat
693 }),
694 ((v11, e_v2.clone()), {
695 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
696 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0, 0.0]));
698 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
699 mat
700 }),
701 ((e_h1, square.clone()), {
702 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
703 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0]));
705 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
706 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
707 mat
708 }),
709 ((e_h2, square.clone()), {
710 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
711 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 1.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
713 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
714 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0, 0.0]));
715 mat
716 }),
717 ((e_v1, square.clone()), {
718 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
719 mat.append_row(DynamicVector::<f64>::new(vec![1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
721 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
722 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0]));
723 mat
724 }),
725 ((e_v2, square), {
726 let mut mat = DynamicDenseMatrix::<f64, RowMajor>::new();
727 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 1.0, 0.0])); mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
729 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
730 mat.append_row(DynamicVector::<f64>::new(vec![0.0, 0.0, 0.0]));
731 mat
732 }),
733 ]);
734
735 (cc, restrictions)
736 }
737
738 #[test]
739 fn test_cubical_sheaf_coboundary_2d() {
740 let (cc, restrictions) = cubical_complex_2d();
741 let sheaf = Sheaf::<CubicalComplex, DynamicVector<f64>>::new(cc, restrictions);
742
743 println!("=== 2D Cubical Sheaf Analysis ===");
744
745 let coboundary_0 = sheaf.coboundary(0);
747 println!("\n0-dimensional coboundary (vertices → edges):");
748 println!("{coboundary_0}");
749
750 assert_eq!(coboundary_0.block_structure().0, 4); assert_eq!(coboundary_0.block_structure().1, 4); assert_eq!(coboundary_0.row_block_sizes(), &[2, 2, 3, 3]);
756 assert_eq!(coboundary_0.col_block_sizes(), &[2, 2, 3, 3]);
757
758 let coboundary_1 = sheaf.coboundary(1);
760 println!("\n1-dimensional coboundary (edges → faces):");
761 println!("{coboundary_1}");
762
763 assert_eq!(coboundary_1.block_structure().0, 1); assert_eq!(coboundary_1.block_structure().1, 4); assert_eq!(coboundary_1.row_block_sizes(), &[4]);
769 assert_eq!(coboundary_1.col_block_sizes(), &[2, 2, 3, 3]);
770
771 let coboundary_2 = sheaf.coboundary(2);
773 println!("\n2-dimensional coboundary (faces → 3-cubes, should be empty):");
774 println!("{coboundary_2}");
775
776 assert_eq!(coboundary_2.block_structure(), (0, 0));
778 }
779}