Skip to main content

koho/math/
cell.rs

1//! Core traits and definitions for constructing a cell complex over arbitrary types.
2//!
3//! This module provides the fundamental building blocks for topological spaces and
4//! CW complexes (cell complexes). It allows for the construction of spaces by
5//! attaching cells of various dimensions along their boundaries.
6//!
7//! # Mathematical Background
8//!
9//! ## Topology
10//!
11//! A topology on a set X is a collection of subsets (called open sets) that satisfies:
12//! - The empty set and X itself are open
13//! - Arbitrary unions of open sets are open
14//! - Finite intersections of open sets are open
15//!
16//! ## Cell Complex (CW Complex)
17//!
18//! A CW complex is built incrementally by:
19//! - Starting with discrete points (0-cells)
20//! - Attaching n-dimensional cells along their boundaries to the (n-1)-skeleton
21//! - The n-skeleton consists of all cells of dimension ≤ n
22//!
23//! CW complexes provide a way to decompose topological spaces into simple building blocks:
24//! - 0-cells: points
25//! - 1-cells: line segments
26//! - 2-cells: disks
27//! - 3-cells: solid balls, etc.
28
29use crate::error::KohoError;
30
31/// A k-cell of arbitrary type and dimension
32pub struct Cell {
33    /// `k`, the dimension of the k-cell
34    pub dimension: usize,
35    /// Collection of upper incident cell IDs within the cell complex.
36    ///
37    /// Incident cells are those that share boundary components with this cell.
38    /// For example, a 1-cell (edge) is incident to its endpoint 0-cells (vertices)
39    pub upper: Vec<usize>,
40    /// Collection of lower incident cell IDs within the cell complex.
41    pub lower: Vec<usize>,
42}
43
44impl Cell {
45    /// Generate a new k-cell
46    pub fn new(dimension: usize) -> Self {
47        Self {
48            dimension,
49            upper: Vec::new(),
50            lower: Vec::new(),
51        }
52    }
53
54    /// Find all the boundary cells and push incidence indexes to neighbors and itself
55    fn attach(
56        &mut self,
57        skeleton: &mut Skeleton,
58        upper: Option<&[usize]>,
59        lower: Option<&[usize]>,
60    ) {
61        // Ensure skeleton has enough dimension layers
62        while skeleton.cells.len() <= self.dimension {
63            skeleton.cells.push(Vec::new());
64        }
65
66        let max = skeleton.cells[self.dimension].len();
67
68        if let Some(upper) = upper {
69            for i in upper {
70                // Ensure the upper dimension exists
71                while skeleton.cells.len() <= self.dimension + 1 {
72                    skeleton.cells.push(Vec::new());
73                }
74                skeleton.cells[self.dimension + 1][*i].lower.push(max);
75                self.upper.push(*i);
76            }
77        }
78
79        if let Some(lower) = lower {
80            for i in lower {
81                skeleton.cells[self.dimension - 1][*i].upper.push(max);
82                self.lower.push(*i);
83            }
84        }
85    }
86}
87
88/// A `Skeleton` is a collection of `Cells` that have been glued together along `Cell::attach` maps.
89///
90/// In CW complex terminology, the n-skeleton consists of all cells of dimension ≤ n.
91/// Building a CW complex involves constructing successive skeletons by attaching cells
92/// of increasing dimension.
93pub struct Skeleton {
94    /// Dimension of the `Skeleton` (maximum cell dimension contained)
95    pub dimension: usize,
96    /// The collection of `Cells` forming the skeleton with [dimension][idx]
97    pub cells: Vec<Vec<Cell>>,
98}
99
100impl Skeleton {
101    /// Initialize a new `Skeleton`
102    pub fn init() -> Self {
103        Self {
104            dimension: 0,
105            cells: vec![Vec::new()],
106        }
107    }
108
109    /// Attach a cell to the existing cell complex.
110    ///
111    /// This implements the core operation in building a CW complex: attaching new cells
112    /// to the existing skeleton. The process involves:
113    /// 1. Verifying the dimensional constraints (can only attach n-cells to (n-1)-skeleton)
114    /// 2. Finding boundary points and their images under the attachment map
115    /// 3. Updating incidence relationships between cells
116    /// 4. Updating the skeleton's dimension if needed
117    pub fn attach(
118        &mut self,
119        mut cell: Cell,
120        upper: Option<&[usize]>,
121        lower: Option<&[usize]>,
122    ) -> Result<usize, KohoError> {
123        let incoming_dim = cell.dimension as i64;
124        if incoming_dim - self.dimension as i64 > 1 {
125            return Err(KohoError::DimensionMismatch);
126        }
127        cell.attach(self, upper, lower);
128        if cell.dimension < self.cells.len() {
129            self.cells[cell.dimension].push(cell);
130        }
131        if incoming_dim > self.dimension as i64 {
132            self.dimension = incoming_dim as usize
133        }
134        Ok(self.cells[incoming_dim as usize].len() - 1)
135    }
136
137    /// Returns the collection of incident cells to `cell_idx` with exactly 1 dimension difference.
138    ///
139    /// This separates boundary relationships (cells of dimension k-1, forming the boundary)
140    /// from coboundary relationships (cells of dimension k+1, having this cell in their boundary).
141    /// This distinction is important for homology and cohomology calculations.
142    pub fn incidences(
143        &self,
144        k: usize,
145        cell_idx: usize,
146    ) -> Result<(&Vec<usize>, &Vec<usize>), KohoError> {
147        if k >= self.cells.len() {
148            return Err(KohoError::DimensionMismatch);
149        }
150        if cell_idx >= self.cells[k].len() {
151            return Err(KohoError::InvalidCellIdx);
152        }
153        let upper = &self.cells[k][cell_idx].upper;
154        let lower = &self.cells[k][cell_idx].lower;
155        Ok((upper, lower))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_attach_zero_cells() {
165        let mut sk = Skeleton::init();
166        // Attach a single 0-cell
167        let idx = sk.attach(Cell::new(0), None, None).unwrap();
168        assert_eq!(idx, 0, "Index for first 0-cell should be 1");
169        assert_eq!(
170            sk.cells[0].len(),
171            1,
172            "There should be one 0-cell in the skeleton"
173        );
174    }
175
176    #[test]
177    fn test_incidences_empty_and_invalid() {
178        let sk = Skeleton::init();
179        // No cells yet: incidences should error on invalid index
180        let err = sk.incidences(0, 0).unwrap_err();
181        assert!(matches!(err, KohoError::InvalidCellIdx));
182
183        // Out-of-range dimension
184        let err = sk.incidences(1, 0).unwrap_err();
185        assert!(matches!(err, KohoError::DimensionMismatch));
186    }
187
188    #[test]
189    fn test_dimension_mismatch_on_attach() {
190        let mut sk = Skeleton::init();
191        // Can't attach a 2-cell to an empty 0-skeleton
192        let err = sk.attach(Cell::new(2), None, None).unwrap_err();
193        assert!(matches!(err, KohoError::DimensionMismatch));
194    }
195
196    #[test]
197    fn test_attach_edge_and_incidence_relations() {
198        let mut sk = Skeleton::init();
199        // Create two vertices (0-cells)
200        sk.attach(Cell::new(0), None, None).unwrap(); // v0
201        sk.attach(Cell::new(0), None, None).unwrap(); // v1
202        assert_eq!(sk.cells[0].len(), 2, "There should be two 0-cells");
203
204        // Attach an edge (1-cell) between v0 and v1
205        let e_idx = sk.attach(Cell::new(1), None, Some(&[0, 1])).unwrap();
206        assert_eq!(e_idx, 0, "Index for first 1-cell should be 0");
207        assert_eq!(sk.cells[1].len(), 1, "There should be one 1-cell");
208
209        // Check that the edge recorded the lower incidences
210        let edge = &sk.cells[1][0];
211        assert_eq!(
212            edge.lower,
213            vec![0, 1],
214            "Edge should be incident to vertices 0 and 1"
215        );
216
217        // Check that each vertex recorded the edge in its upper incidence
218        assert_eq!(
219            sk.cells[0][0].upper,
220            vec![0],
221            "Vertex 0 should have edge 0 in its upper incidences"
222        );
223        assert_eq!(
224            sk.cells[0][1].upper,
225            vec![0],
226            "Vertex 1 should have edge 0 in its upper incidences"
227        );
228
229        // Incidences API for a 0-cell
230        let (upper, _lower) = sk.incidences(0, 0).unwrap();
231        assert_eq!(
232            upper,
233            &[0],
234            "Upper incidences of vertex 0 should contain the edge"
235        );
236    }
237
238    #[test]
239    fn test_invalid_cell_idx_in_incidences() {
240        let mut sk = Skeleton::init();
241        sk.attach(Cell::new(0), None, None).unwrap();
242        // Querying an out-of-bounds cell index should give an error
243        let err = sk.incidences(0, 1).unwrap_err();
244        assert!(matches!(err, KohoError::InvalidCellIdx));
245    }
246}