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}