harness_space/
sheaf.rs

1//! # Sheaf Module
2//!
3//! This module defines the structure and operations for a **sheaf** over a topological space.
4//! In mathematics, a sheaf is a tool for systematically tracking locally defined data
5//! attached to the open sets of a topological space and defined from the open sets to
6//! a category.
7//!
8//! ## Core Concepts
9//!
10//! - **Topological Space ($T$)**: The base space over which the sheaf is defined. In this context,
11//!   the space is also a [`Poset`] (partially ordered set), where the items of the topology (e.g.,
12//!   cells in a cell complex, open sets) are ordered by inclusion or a similar relation. The `Item`
13//!   type of the `Topology` trait represents these fundamental components (e.g., cells).
14//!
15//! - **Category ($\mathcal{C}$)**: A target category where the data (stalks or sections) of the
16//!   sheaf resides. For each item $U$ in the topological space $T$, a sheaf assigns an object
17//!   $F(U)$ from $\mathcal{C}$. The type `C` in `Sheaf<T, C>` represents objects in this category,
18//!   and `C::Morphism` represents the morphisms (e.g., functions, linear maps).
19//!
20//! - **Restriction Maps**: For any pair of items $U, V$ in $T$ such that $U \subseteq V$ (or more
21//!   generally $U \le V$ in the poset structure), there is a **restriction morphism**
22//!   $\text{res}_{U,V}: F(V) \to F(U)$. These morphisms must satisfy certain compatibility
23//!   conditions:
24//!   1. $\text{res}_{U,U} = \text{id}_{F(U)}$ (identity on $U$).
25//!   2. If $W \subseteq V \subseteq U$ (or $W \le V \le U$), then $\text{res}_{W,V} \circ
26//!      \text{res}_{V,U} = \text{res}_{W,U}$ (composition).
27//!
28//! - **Sections**: A section $s$ over an item $U \in T$ is an element $s \in F(U)$. A **global
29//!   section** is a collection of sections $\{s_U \in F(U)\}$ for each $U \in T$ such that for any
30//!   pair $U \le V$, $\text{res}_{U,V}(s_V) = s_U$.
31//!
32//! The [`Sheaf`] struct in this module stores the underlying topological space (which is also a
33//! poset) and the restriction maps between its items.
34//!
35//! ## Implementation Details
36//!
37//! - The `space` field holds the topological space $T$, which must implement both [`Topology`] and
38//!   [`Poset`]. Its items `T::Item` must be `Hash + Eq + Clone` to be used as keys in HashMaps.
39//! - The `restrictions` field is a `HashMap`. For a key `(U, V)` where `U` is a \"parent\" item and
40//!   `V` is a \"child\" item (meaning $U \le V$ in the poset structure of the space), the
41//!   associated value `C::Morphism` is the restriction map $\rho_{UV}: F(V) \to F(U)$.
42//! - The `Category` trait (`C`) provides the structure for the data attached to items, including
43//!   the `Morphism` type and an `apply` method to apply morphisms to objects.
44
45use 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
58// TODO: We should make this have a nice construction setup so you can build the underlying space
59// and the restrictions simultaneously
60
61/// Represents a sheaf over a topological space `T` with values in a category `C`.
62///
63/// A sheaf assigns an object from a category `C` to each item (e.g., open set, cell)
64/// in a topological space `T`. It also defines restriction morphisms that relate
65/// the data assigned to different items, ensuring consistency.
66///
67/// # Type Parameters
68///
69/// * `T`: The underlying topological space, which must also implement [`Poset`]. Its items
70///   (`T::Item`) must be `Hash + Eq + Clone` for use in `HashMap` keys.
71/// * `C`: The target [`Category`] where the data (stalks/sections) of the sheaf reside. Objects of
72///   this category (`C`) must be `Clone + Eq + Debug` (if `is_global_section` is used). Morphisms
73///   (`C::Morphism`) must be `Clone + Debug`.
74pub struct Sheaf<T: Topology, C: Category> {
75  /// The underlying topological space (e.g., a cell complex, an ordered set of open sets).
76  /// This space also implements [`Poset`] to define relationships (e.g., sub-item/super-item)
77  /// between its items.
78  pub space:        T,
79  /// A map defining the restriction morphisms of the sheaf.
80  ///
81  /// For a key `(parent_item, child_item)` where `parent_item <= child_item` according to the
82  /// poset structure of `T`, the associated value `C::Morphism` is the restriction map
83  /// from the data on `child_item` to the data on `parent_item`.
84  /// That is, $\rho_{\text{parent_item}, \text{child_item}}: F(\text{child_item}) \to
85  /// F(\text{parent_item})$.
86  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  /// Creates a new sheaf from a given topological space and a set of restriction maps.
97  ///
98  /// The provided `restrictions` map should contain morphisms for all relevant pairs
99  /// of items $(U, V)$ in the space `T` where $U \le V$. The constructor asserts that
100  /// for every key `(k.0, k.1)` in `restrictions`, the relation `space.leq(&k.0, &k.1)` holds.
101  /// Here, `k.0` is treated as the parent (smaller item) and `k.1` as the child (larger item).
102  /// The restriction map is from $F(k.1)$ to $F(k.0)$.
103  ///
104  /// # Arguments
105  ///
106  /// * `space`: The topological space `T` which also implements `Poset`.
107  /// * `restrictions`: A `HashMap` where keys are `(parent_item, child_item)` tuples from `T::Item`
108  ///   (such that `parent_item <= child_item`), and values are the corresponding restriction
109  ///   morphisms $F(\text{child_item}) \to F(\text{parent_item})$ of type `C::Morphism`.
110  ///
111  /// # Panics
112  ///
113  /// * Panics if any key `(parent, child)` in `restrictions` does not satisfy `space.leq(&parent,
114  ///   &child) == Some(true)`. This includes cases where `space.leq` returns `Some(false)`
115  ///   (relation does not hold) or `None` (incomparable).
116  ///
117  /// # TODO
118  /// - Implement a builder API for more robust construction, ensuring all necessary restrictions
119  ///   are defined (e.g., for all successor relationships in the poset, and identity maps for $U
120  ///   \le U$).
121  /// - Add validation for compatibility of restriction maps (e.g., identity $\text{res}_{U,U} =
122  ///   \text{id}_{F(U)}$ and composition $\text{res}_{W,V} \circ \text{res}_{V,U} =
123  ///   \text{res}_{W,U}$ axioms).
124  /// - For specific categories (e.g., matrices as morphisms), check dimensional compatibility of
125  ///   morphisms.
126  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  /// Restricts data from a larger item to a smaller item using the sheaf's restriction map.
137  ///
138  /// Given a `parent_target_item` $U$ and a `child_source_item` $V$ such that $U \le V$,
139  /// this function retrieves the restriction morphism $\rho_{UV}: F(V) \to F(U)$ from the sheaf's
140  /// definition (stored under the key `(parent_target_item, child_source_item)`).
141  /// It then retrieves the data $s_V \in F(V)$ corresponding to `child_source_item` from the
142  /// provided `section` data. Finally, it applies the morphism to compute $s_U = \rho_{UV}(s_V)$,
143  /// which is the data on $U$ restricted from $V$.
144  ///
145  /// # Arguments
146  ///
147  /// * `parent_target_item`: The smaller item $U$ to which data is being restricted.
148  /// * `child_source_item`: The larger item $V$ from which data is being restricted. Must satisfy
149  ///   `parent_target_item <= child_source_item`.
150  /// * `section_data_on_child`: The data object $s_V \in F(V)$ associated with `child_source_item`.
151  ///
152  /// # Returns
153  ///
154  /// The restricted data $s_U \in F(U)$, result of applying the restriction map.
155  ///
156  /// # Panics
157  ///
158  /// * Panics if `parent_target_item <= child_source_item` is false or if they are incomparable, as
159  ///   asserted by `self.space.leq`.
160  /// * Panics if the restriction map for `(parent_target_item, child_source_item)` is not found in
161  ///   `self.restrictions`.
162  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  /// Checks if a given `section` is a global section of the sheaf.
185  ///
186  /// A section $s = \{s_X\}_{X \in T}$ is a global section if for every pair of items
187  /// `(parent_item, child_item)` in `self.restrictions` (which implies `parent_item <=
188  /// child_item`), the restriction of the data on `child_item` to `parent_item` is equal to the
189  /// data already on `parent_item`.
190  ///
191  /// That is, for each stored morphism $\rho_{\text{parent}, \text{child}}: F(\text{child_item})
192  /// \to F(\text{parent_item})$, it must hold that $\rho_{\text{parent},
193  /// \text{child}}(s_{\text{child_item}}) = s_{\text{parent_item}}$, where $s_{\
194  /// text{child_item}}$ is `section.get(child_item)` and $s_{\text{parent_item}}$ is
195  /// `section.get(parent_item)`.
196  ///
197  /// # Arguments
198  ///
199  /// * `section`: A `HashMap` where keys are items `T::Item` from the space and values are data
200  ///   objects of type `C` (from the target category), representing $s_X$ for each $X$.
201  ///
202  /// # Returns
203  ///
204  /// * `true` if the `section` satisfies the global section condition for all defined restrictions.
205  /// * `false` if any restriction condition is violated, or if data for a required item (involved
206  ///   in a restriction) is missing from the `section`.
207  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
226// TODO: This is a temporary implementation for the coboundary map specifically for the vector
227// stalks.
228impl<T: ComplexElement, F: Field + Copy> Sheaf<Complex<T>, DynamicVector<F>>
229where T: Hash + Eq + Clone + Debug
230{
231  /// Constructs the coboundary matrix δ^k: C^k → C^(k+1) for the sheaf.
232  ///
233  /// The coboundary map is dual to the boundary map of the underlying complex.
234  /// For dimension k, this maps k-cochains (sections over k-dimensional elements)
235  /// to (k+1)-cochains (sections over (k+1)-dimensional elements).
236  ///
237  /// The matrix has:
238  /// - Rows indexed by (k+1)-dimensional elements
239  /// - Columns indexed by k-dimensional elements
240  /// - Entry (σ, τ) equals the orientation coefficient of τ in ∂σ where σ is (k+1)-dimensional and
241  ///   τ is k-dimensional
242  ///
243  /// # Arguments
244  /// * `dimension`: The dimension k of the domain (k-cochains)
245  ///
246  /// # Returns
247  /// A block matrix representing δ^k: C^k → C^(k+1)
248  pub fn coboundary(&self, dimension: usize) -> BlockMatrix<F, RowMajor> {
249    // Get sorted k-dimensional and (k+1)-dimensional elements
250    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      // No source elements or no target elements - return completely empty matrix (0,0)
264      return BlockMatrix::new(vec![], vec![]);
265    }
266
267    // Determine block sizes based on stalk dimensions
268    let mut col_block_sizes = Vec::new();
269    for k_element in &k_elements {
270      // Find any restriction involving this k_element to determine its stalk dimension
271      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()) // When k_element is the source, its stalk dimension is num_cols
277          } else if to.same_content(k_element) {
278            Some(matrix.num_rows()) // When k_element is the target, its stalk dimension is num_rows
279          } else {
280            None
281          }
282        })
283        .unwrap_or(1); // Default to 1 if no restriction found
284      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      // Find any restriction involving this (k+1)-element to determine its stalk dimension
290      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()) // When k_plus_1_element is the source, its stalk dimension is
296                                    // num_cols
297          } else if to.same_content(k_plus_1_element) {
298            Some(matrix.num_rows()) // When k_plus_1_element is the target, its stalk dimension is
299                                    // num_rows
300          } else {
301            None
302          }
303        })
304        .unwrap_or(1); // Default to 1 if no restriction found
305      row_block_sizes.push(stalk_dim);
306    }
307
308    // Create the block matrix with proper structure
309    let mut block_matrix = BlockMatrix::new(row_block_sizes, col_block_sizes);
310
311    // Fill in the blocks
312    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        // Check if k_element appears in the boundary of k_plus_1_element
315        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          // k_element is in the boundary, so we need the restriction matrix
321          if let Some(restriction_matrix) =
322            self.restrictions.get(&(k_element.clone(), k_plus_1_element.clone()))
323          {
324            // Apply the orientation sign to the restriction matrix
325            let signed_matrix = if *orientation_coeff > 0 {
326              restriction_matrix.clone()
327            } else if *orientation_coeff < 0 {
328              // Multiply by -1
329              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              // Zero coefficient - create zero matrix
339              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          // If no restriction found, the block remains zero (not stored)
348        }
349        // If k_element is not in the boundary, the block remains zero (not stored)
350      }
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])), // R^1
408      (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
409      (e1.clone(), DynamicVector::<f64>::new(vec![2.0, 4.0])), // R^2
410    ]);
411    assert!(sheaf.is_global_section(&section));
412
413    let section = HashMap::from([
414      (v1.clone(), DynamicVector::<f64>::new(vec![1.0])), // R^1
415      (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
416      (e1.clone(), DynamicVector::<f64>::new(vec![2.0, 4.0])), // R^2
417    ]);
418    assert!(!sheaf.is_global_section(&section));
419
420    let section = HashMap::from([
421      (v1.clone(), DynamicVector::<f64>::new(vec![2.0])), // R^1
422      (v2.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
423      (e1.clone(), DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
424    ]);
425    assert!(!sheaf.is_global_section(&section));
426
427    let section = HashMap::from([
428      (v1, DynamicVector::<f64>::new(vec![2.0])),      // R^1
429      (v2, DynamicVector::<f64>::new(vec![3.0, 3.0])), // R^2
430      (e1, DynamicVector::<f64>::new(vec![2.0, 4.0])), // R^2
431    ]);
432    assert!(!sheaf.is_global_section(&section));
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    // Expected structure: 1 block row × 2 block columns
442    // Block (0,0): 2×1 (from v0's R¹ stalk to e01's R² stalk)
443    // Block (0,1): 2×2 (from v1's R² stalk to e01's R² stalk)
444    assert_eq!(coboundary.block_structure(), (1, 2));
445    assert_eq!(coboundary.row_block_sizes(), &[2]); // e01 has R² stalk
446    assert_eq!(coboundary.col_block_sizes(), &[1, 2]); // v0 has R¹, v1 has R²
447
448    // Block (0,0): Should be -1 × [[1.0], [2.0]] = [[-1.0], [-2.0]]
449    // (since v0 has orientation coefficient -1 in ∂e01 = v1 - v0)
450    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    // Block (0,1): Should be +1 × [[2.0, 0.0], [0.0, 2.0]] = [[2.0, 0.0], [0.0, 2.0]]
457    // (since v1 has orientation coefficient +1 in ∂e01 = v1 - v0)
458    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    // Verify the flattened matrix is correct
467    let flattened = coboundary.flatten();
468    assert_eq!(flattened.num_rows(), 2);
469    assert_eq!(flattened.num_cols(), 3);
470
471    // Row 0: [-1, 2, 0]
472    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    // Row 1: [-2, 0, 2]
477    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    // Vertices
502    let v0 = Simplex::new(0, vec![0]); // R^1
503    let v1 = Simplex::new(0, vec![1]); // R^2
504    let v2 = Simplex::new(0, vec![2]); // R^3
505                                       // Edges
506    let e01 = Simplex::new(1, vec![0, 1]); // R^2
507    let e02 = Simplex::new(1, vec![0, 2]); // R^2
508    let e12 = Simplex::new(1, vec![1, 2]); // R^2
509
510    // Faces
511    let f012 = Simplex::new(2, vec![0, 1, 2]); // R^3
512
513    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])),      // R^1
588      (v1, DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
589      (v2, DynamicVector::<f64>::new(vec![1.0, 2.0, 3.0])), // R^3
590      (e01, DynamicVector::<f64>::new(vec![1.0, 2.0])), // R^2
591      (e02, DynamicVector::<f64>::new(vec![1.0, 0.0])), // R^2
592      (e12, DynamicVector::<f64>::new(vec![2.0, 4.0])), // R^2
593      (f012, DynamicVector::<f64>::new(vec![2.0, 0.0, 0.0])), // R^3
594    ]);
595    assert!(sheaf.is_global_section(&section));
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    // Create a 2x2 grid of cubes
620    // Vertices (0-cubes) at grid positions
621    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    // Horizontal edges (1-cubes)
627    let e_h1 = Cube::edge(0, 1);
628    let e_h2 = Cube::edge(2, 3);
629
630    // Vertical edges (1-cubes)
631    let e_v1 = Cube::edge(0, 2);
632    let e_v2 = Cube::edge(1, 3);
633
634    // Square face (2-cube)
635    let square = Cube::square([0, 1, 2, 3]); // R^4 stalk
636
637    // Add all elements to complex
638    let v00 = cc.join_element(v00); // R^2
639    let v10 = cc.join_element(v10); // R^2
640    let v01 = cc.join_element(v01); // R^3
641    let v11 = cc.join_element(v11); // R^3
642    let e_h1 = cc.join_element(e_h1); // R^2
643    let e_h2 = cc.join_element(e_h2); // R^3
644    let e_v1 = cc.join_element(e_v1); // R^2
645    let e_v2 = cc.join_element(e_v2); // R^3
646    let square = cc.join_element(square); // R^4
647
648    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])); // R^2 → R^2
652        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])); // R^2 → R^2
658        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])); // R^3 → R^3
664        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])); // R^3 → R^3
671        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])); // R^2 → R^2
678        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])); // R^3 → R^2
684        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])); // R^2 → R^3
690        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])); // R^3 → R^3
697        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])); // R^2 → R^4
704        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])); // R^3 → R^4
712        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])); // R^2 → R^4
720        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])); // R^3 → R^4
728        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    // Test 0-dimensional coboundary (vertices → edges)
746    let coboundary_0 = sheaf.coboundary(0);
747    println!("\n0-dimensional coboundary (vertices → edges):");
748    println!("{coboundary_0}");
749
750    // Expected: 4 block rows (edges) × 4 block columns (vertices)
751    assert_eq!(coboundary_0.block_structure().0, 4); // 4 edges
752    assert_eq!(coboundary_0.block_structure().1, 4); // 4 vertices
753
754    // Verify block sizes
755    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    // Test 1-dimensional coboundary (edges → faces)
759    let coboundary_1 = sheaf.coboundary(1);
760    println!("\n1-dimensional coboundary (edges → faces):");
761    println!("{coboundary_1}");
762
763    // Expected: 1 block row (square) × 4 block columns (edges)
764    assert_eq!(coboundary_1.block_structure().0, 1); // 1 square
765    assert_eq!(coboundary_1.block_structure().1, 4); // 4 edges
766
767    // Verify block sizes
768    assert_eq!(coboundary_1.row_block_sizes(), &[4]);
769    assert_eq!(coboundary_1.col_block_sizes(), &[2, 2, 3, 3]);
770
771    // Test 2-dimensional coboundary (faces → higher dim, should be empty)
772    let coboundary_2 = sheaf.coboundary(2);
773    println!("\n2-dimensional coboundary (faces → 3-cubes, should be empty):");
774    println!("{coboundary_2}");
775
776    // Should be empty since no 3-cubes
777    assert_eq!(coboundary_2.block_structure(), (0, 0));
778  }
779}