Skip to main content

oxiphysics_geometry/
cell_complex.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! CW complex and cell complex geometry.
5//!
6//! This module implements the algebraic machinery of CW complexes and their
7//! homological invariants:
8//!
9//! - [`CwCell`]: Cells of any dimension (vertices, edges, faces, n-cells).
10//! - [`CwComplex`]: CW complex with attaching maps and cellular chain complex.
11//! - [`ChainComplex`]: Abstract chain complex with boundary maps.
12//! - [`CellularHomology`]: Smith normal form and Betti number computation.
13//! - [`SimplexBoundary`]: Oriented simplex boundary operator.
14//! - [`DualComplex`]: Dual cell decomposition and Hodge duality.
15//! - [`CoboundaryOperator`]: Coboundary maps and cohomology groups.
16//! - [`EulerCharacteristic`]: Euler characteristic and Euler-Poincaré formula.
17//! - [`CellularApproximation`]: Cellular approximation and homotopy equivalence.
18//! - [`ShellableComplex`]: Shellability, h-vector, f-vector, Dehn-Sommerville.
19
20use std::collections::HashMap;
21
22// ─── CwCell ──────────────────────────────────────────────────────────────────
23
24/// A single cell in a CW complex of dimension `dim`.
25///
26/// A 0-cell is a vertex, a 1-cell is an edge, a 2-cell is a face, etc.
27#[derive(Debug, Clone, PartialEq)]
28pub struct CwCell {
29    /// Unique cell identifier.
30    pub id: usize,
31    /// Cell dimension.
32    pub dim: usize,
33    /// Label / name (optional).
34    pub label: String,
35    /// Vertex coordinates for 0-cells (ignored for higher cells).
36    pub coords: Option<[f64; 3]>,
37    /// Indices of the boundary cells (cells of dimension `dim-1`).
38    pub boundary: Vec<usize>,
39    /// Incidence signs (+1 or −1) corresponding to each boundary cell.
40    pub boundary_signs: Vec<i32>,
41}
42
43impl CwCell {
44    /// Create a 0-cell (vertex) with coordinates.
45    pub fn vertex(id: usize, coords: [f64; 3]) -> Self {
46        Self {
47            id,
48            dim: 0,
49            label: format!("v{}", id),
50            coords: Some(coords),
51            boundary: vec![],
52            boundary_signs: vec![],
53        }
54    }
55
56    /// Create a 1-cell (edge) between vertex `from` and vertex `to`.
57    pub fn edge(id: usize, from: usize, to: usize) -> Self {
58        Self {
59            id,
60            dim: 1,
61            label: format!("e{}", id),
62            coords: None,
63            boundary: vec![to, from],
64            boundary_signs: vec![1, -1],
65        }
66    }
67
68    /// Create a 2-cell (face) with an ordered list of boundary edges and signs.
69    pub fn face(id: usize, boundary_edges: Vec<usize>, signs: Vec<i32>) -> Self {
70        Self {
71            id,
72            dim: 2,
73            label: format!("f{}", id),
74            coords: None,
75            boundary: boundary_edges,
76            boundary_signs: signs,
77        }
78    }
79
80    /// Create a general n-cell.
81    pub fn n_cell(id: usize, dim: usize, boundary: Vec<usize>, signs: Vec<i32>) -> Self {
82        Self {
83            id,
84            dim,
85            label: format!("c{}_{}", dim, id),
86            coords: None,
87            boundary,
88            boundary_signs: signs,
89        }
90    }
91
92    /// Whether this cell is a vertex.
93    pub fn is_vertex(&self) -> bool {
94        self.dim == 0
95    }
96
97    /// Whether this cell is an edge.
98    pub fn is_edge(&self) -> bool {
99        self.dim == 1
100    }
101
102    /// Whether this cell is a face.
103    pub fn is_face(&self) -> bool {
104        self.dim == 2
105    }
106}
107
108// ─── CwComplex ───────────────────────────────────────────────────────────────
109
110/// A CW complex represented by its cells, organised by dimension.
111///
112/// Provides access to the cellular chain complex boundary operators
113/// and the Euler characteristic.
114#[derive(Debug, Clone, Default)]
115pub struct CwComplex {
116    /// All cells, keyed by (dim, id).
117    pub cells: HashMap<(usize, usize), CwCell>,
118    /// Maximum cell dimension.
119    pub max_dim: usize,
120}
121
122impl CwComplex {
123    /// Create an empty CW complex.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Add a cell to the complex.
129    pub fn add_cell(&mut self, cell: CwCell) {
130        if cell.dim > self.max_dim {
131            self.max_dim = cell.dim;
132        }
133        self.cells.insert((cell.dim, cell.id), cell);
134    }
135
136    /// Return all cells of dimension `dim`, sorted by id.
137    pub fn cells_of_dim(&self, dim: usize) -> Vec<&CwCell> {
138        let mut v: Vec<&CwCell> = self.cells.values().filter(|c| c.dim == dim).collect();
139        v.sort_by_key(|c| c.id);
140        v
141    }
142
143    /// Number of cells of dimension `dim`.
144    pub fn count(&self, dim: usize) -> usize {
145        self.cells.values().filter(|c| c.dim == dim).count()
146    }
147
148    /// Euler characteristic χ = Σ_k (-1)^k |C_k|.
149    pub fn euler_characteristic(&self) -> i64 {
150        let mut chi: i64 = 0;
151        for dim in 0..=self.max_dim {
152            let n = self.count(dim) as i64;
153            if dim % 2 == 0 {
154                chi += n;
155            } else {
156                chi -= n;
157            }
158        }
159        chi
160    }
161
162    /// Boundary operator ∂_k : C_k → C_{k-1} as integer matrix.
163    ///
164    /// Rows index (k-1)-cells, columns index k-cells (both sorted by id).
165    /// Entry \[i, j\] = sign of the incidence of the i-th (k-1)-cell in ∂(e_j).
166    pub fn boundary_matrix(&self, k: usize) -> Vec<Vec<i32>> {
167        if k == 0 {
168            return vec![];
169        }
170        let k_cells = self.cells_of_dim(k);
171        let km1_cells = self.cells_of_dim(k - 1);
172        let nrows = km1_cells.len();
173        let ncols = k_cells.len();
174        let mut mat = vec![vec![0i32; ncols]; nrows];
175
176        // Build id → row-index map for (k-1)-cells
177        let row_idx: HashMap<usize, usize> = km1_cells
178            .iter()
179            .enumerate()
180            .map(|(i, c)| (c.id, i))
181            .collect();
182
183        for (j, cell) in k_cells.iter().enumerate() {
184            for (b_id, &sign) in cell.boundary.iter().zip(cell.boundary_signs.iter()) {
185                if let Some(&row) = row_idx.get(b_id) {
186                    mat[row][j] += sign;
187                }
188            }
189        }
190        mat
191    }
192
193    /// Build a standard simplicial tetrahedron (4 vertices, 6 edges, 4 faces, 1 3-cell).
194    pub fn standard_tetrahedron() -> Self {
195        let mut cw = Self::new();
196        // Vertices
197        let verts: [[f64; 3]; 4] = [
198            [0.0, 0.0, 0.0],
199            [1.0, 0.0, 0.0],
200            [0.5, 1.0, 0.0],
201            [0.5, 0.5, 1.0],
202        ];
203        for (i, &c) in verts.iter().enumerate() {
204            cw.add_cell(CwCell::vertex(i, c));
205        }
206        // Edges (oriented: lower index → higher index)
207        let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
208        for (i, (a, b)) in edges.iter().enumerate() {
209            cw.add_cell(CwCell::edge(i, *a, *b));
210        }
211        // Faces (triangles) with boundary edges and signs
212        // Face 012: edges 0 (01, +1), 3 (12, +1), 1 (02, -1)
213        cw.add_cell(CwCell::face(0, vec![0, 3, 1], vec![1, 1, -1]));
214        // Face 013: edges 0 (01, +1), 4 (13, +1), 2 (03, -1)
215        cw.add_cell(CwCell::face(1, vec![0, 4, 2], vec![1, 1, -1]));
216        // Face 023: edges 1 (02, +1), 5 (23, +1), 2 (03, -1)
217        cw.add_cell(CwCell::face(2, vec![1, 5, 2], vec![1, 1, -1]));
218        // Face 123: edges 3 (12, +1), 5 (23, +1), 4 (13, -1)
219        cw.add_cell(CwCell::face(3, vec![3, 5, 4], vec![1, 1, -1]));
220        // 3-cell (tetrahedron) with all 4 faces and signs
221        cw.add_cell(CwCell::n_cell(0, 3, vec![0, 1, 2, 3], vec![1, -1, 1, -1]));
222        cw
223    }
224
225    /// Build a standard 2-sphere (two hemispheres, equatorial circle).
226    pub fn standard_sphere_s2() -> Self {
227        let mut cw = Self::new();
228        // 0-skeleton: 1 vertex
229        cw.add_cell(CwCell::vertex(0, [0.0, 0.0, 0.0]));
230        // 2-skeleton: 2 cells (two disks glued along their boundaries to the vertex)
231        // Each 2-cell has empty boundary (attaches via constant map)
232        cw.add_cell(CwCell::face(0, vec![], vec![]));
233        cw.add_cell(CwCell::face(1, vec![], vec![]));
234        cw
235    }
236
237    /// Build a standard torus T² as a CW complex.
238    /// 1 vertex, 2 edges (a and b), 1 face.
239    pub fn standard_torus() -> Self {
240        let mut cw = Self::new();
241        cw.add_cell(CwCell::vertex(0, [0.0, 0.0, 0.0]));
242        // Edge a (loop at vertex 0)
243        cw.add_cell(CwCell {
244            id: 0,
245            dim: 1,
246            label: "a".into(),
247            coords: None,
248            boundary: vec![0, 0],
249            boundary_signs: vec![1, -1],
250        });
251        // Edge b (loop at vertex 0)
252        cw.add_cell(CwCell {
253            id: 1,
254            dim: 1,
255            label: "b".into(),
256            coords: None,
257            boundary: vec![0, 0],
258            boundary_signs: vec![1, -1],
259        });
260        // Face: attaching map is aba⁻¹b⁻¹ — boundary is zero in ∂₂ for a torus
261        cw.add_cell(CwCell::face(0, vec![0, 1, 0, 1], vec![1, 1, -1, -1]));
262        cw
263    }
264}
265
266// ─── ChainComplex ────────────────────────────────────────────────────────────
267
268/// An abstract chain complex (C_k, ∂_k) over the integers.
269///
270/// Stores the boundary matrices for each dimension and provides
271/// ranks and homology rank bounds.
272#[derive(Debug, Clone, Default)]
273pub struct ChainComplex {
274    /// Boundary matrices: `boundary[k]` = ∂_{k+1} : C_{k+1} → C_k.
275    pub boundary: Vec<Vec<Vec<i32>>>,
276    /// Chain group ranks (dimensions) indexed by k.
277    pub ranks: Vec<usize>,
278}
279
280impl ChainComplex {
281    /// Create a chain complex from a sequence of boundary matrices.
282    ///
283    /// `matrices[k]` is the boundary operator ∂_{k+1}, stored as
284    /// a matrix of shape `(rank_k) × (rank_{k+1})`.
285    pub fn from_matrices(matrices: Vec<Vec<Vec<i32>>>) -> Self {
286        let mut ranks = Vec::new();
287        if !matrices.is_empty() {
288            for mat in &matrices {
289                if !mat.is_empty() && ranks.is_empty() {
290                    ranks.push(mat.len()); // rank of C_0
291                }
292                if !mat.is_empty() {
293                    ranks.push(mat[0].len()); // rank of C_{k+1}
294                } else {
295                    ranks.push(0);
296                }
297            }
298        }
299        Self {
300            boundary: matrices,
301            ranks,
302        }
303    }
304
305    /// Extract chain complex from a CW complex up to dimension `max_dim`.
306    pub fn from_cw_complex(cw: &CwComplex) -> Self {
307        let max_k = cw.max_dim;
308        let mut mats = Vec::new();
309        for k in 1..=max_k {
310            mats.push(cw.boundary_matrix(k));
311        }
312        let ranks = (0..=max_k).map(|d| cw.count(d)).collect();
313        Self {
314            boundary: mats,
315            ranks,
316        }
317    }
318
319    /// Number of chain groups in this complex.
320    pub fn length(&self) -> usize {
321        self.ranks.len()
322    }
323
324    /// Rank of C_k.
325    pub fn rank(&self, k: usize) -> usize {
326        self.ranks.get(k).copied().unwrap_or(0)
327    }
328}
329
330// ─── Smith Normal Form ───────────────────────────────────────────────────────
331
332/// Compute the Smith Normal Form of an integer matrix.
333///
334/// Returns the diagonal entries (elementary divisors) of the SNF.
335/// Used for homology computation over Z.
336pub fn smith_normal_form(mat: &[Vec<i32>]) -> Vec<i32> {
337    if mat.is_empty() || mat[0].is_empty() {
338        return vec![];
339    }
340    let nrows = mat.len();
341    let ncols = mat[0].len();
342    let mut a: Vec<Vec<i32>> = mat.to_vec();
343    let mut divisors = Vec::new();
344    let min_dim = nrows.min(ncols);
345
346    for pivot in 0..min_dim {
347        // Find a nonzero element to pivot on
348        loop {
349            // Check if submatrix below pivot is all zero
350            let mut found = false;
351            'outer: for i in pivot..nrows {
352                for j in pivot..ncols {
353                    if a[i][j] != 0 {
354                        found = true;
355                        // Swap rows and columns so [pivot][pivot] is nonzero
356                        a.swap(pivot, i);
357                        for row in &mut a {
358                            row.swap(pivot, j);
359                        }
360                        break 'outer;
361                    }
362                }
363            }
364            if !found {
365                return divisors;
366            }
367
368            // Try to eliminate in row and column using GCD steps
369            let mut changed = false;
370
371            // Eliminate column entries below pivot
372            for i in (pivot + 1)..nrows {
373                if a[i][pivot] != 0 {
374                    let q = a[i][pivot] / a[pivot][pivot];
375                    let pivot_row_copy: Vec<i32> = a[pivot][pivot..ncols].to_vec();
376                    for (a_ij, &a_pj) in a[i][pivot..ncols].iter_mut().zip(pivot_row_copy.iter()) {
377                        *a_ij -= q * a_pj;
378                    }
379                    if a[i][pivot] != 0 {
380                        // GCD step: swap rows to put smaller nonzero value on top
381                        a.swap(pivot, i);
382                        if a[pivot][pivot] < 0 {
383                            for a_j in a[pivot].iter_mut() {
384                                *a_j = -*a_j;
385                            }
386                        }
387                        changed = true;
388                    }
389                }
390            }
391
392            // Eliminate row entries to the right of pivot
393            for j in (pivot + 1)..ncols {
394                if a[pivot][j] != 0 {
395                    let q = a[pivot][j] / a[pivot][pivot];
396                    let pivot_col_copy: Vec<i32> = (pivot..nrows).map(|i| a[i][pivot]).collect();
397                    for (i, &a_ip) in pivot_col_copy.iter().enumerate() {
398                        a[pivot + i][j] -= q * a_ip;
399                    }
400                    if a[pivot][j] != 0 {
401                        // GCD column step: swap columns
402                        for row in &mut a {
403                            row.swap(pivot, j);
404                        }
405                        if a[pivot][pivot] < 0 {
406                            for a_row in a.iter_mut() {
407                                a_row[pivot] = -a_row[pivot];
408                            }
409                        }
410                        changed = true;
411                    }
412                }
413            }
414
415            if !changed {
416                break;
417            }
418        }
419
420        let d = a[pivot][pivot];
421        if d == 0 {
422            break;
423        }
424        divisors.push(d.abs());
425    }
426    divisors
427}
428
429// ─── CellularHomology ────────────────────────────────────────────────────────
430
431/// Cellular homology computation via Smith Normal Form.
432///
433/// Computes Betti numbers β_k and torsion coefficients from
434/// the boundary operators of a CW complex.
435#[derive(Debug, Clone)]
436pub struct CellularHomology {
437    /// Betti numbers β_0, β_1, β_2, ...
438    pub betti: Vec<usize>,
439    /// Torsion coefficients (elementary divisors > 1) per dimension.
440    pub torsion: Vec<Vec<i32>>,
441    /// Euler characteristic χ = Σ (-1)^k β_k.
442    pub euler_char: i64,
443}
444
445impl CellularHomology {
446    /// Compute cellular homology from a CW complex.
447    pub fn compute(cw: &CwComplex) -> Self {
448        let max_dim = cw.max_dim;
449        let mut betti = Vec::new();
450        let mut torsion = Vec::new();
451
452        for k in 0..=max_dim {
453            let n_k = cw.count(k) as i64;
454
455            // Rank of ∂_k (boundary of k-chains)
456            let d_k = if k > 0 {
457                let mat = cw.boundary_matrix(k);
458                rank_of_matrix(&mat)
459            } else {
460                0
461            };
462            // Rank of ∂_{k+1} (boundary whose image lands in C_k)
463            let d_k1 = if k < max_dim {
464                let mat = cw.boundary_matrix(k + 1);
465                rank_of_matrix(&mat)
466            } else {
467                0
468            };
469
470            let z_k = (n_k - d_k as i64).max(0) as usize; // rank of ker ∂_k
471            let b_k = d_k1; // rank of im ∂_{k+1}
472            let beta_k = z_k.saturating_sub(b_k);
473            betti.push(beta_k);
474
475            // Torsion: elementary divisors > 1 from SNF of ∂_{k+1}
476            let tors = if k < max_dim {
477                let mat = cw.boundary_matrix(k + 1);
478                smith_normal_form(&mat)
479                    .into_iter()
480                    .filter(|&d| d > 1)
481                    .collect()
482            } else {
483                vec![]
484            };
485            torsion.push(tors);
486        }
487
488        let euler_char: i64 = betti
489            .iter()
490            .enumerate()
491            .map(|(k, &b)| if k % 2 == 0 { b as i64 } else { -(b as i64) })
492            .sum();
493
494        Self {
495            betti,
496            torsion,
497            euler_char,
498        }
499    }
500
501    /// Zeroth Betti number β₀ = number of connected components.
502    pub fn beta0(&self) -> usize {
503        self.betti.first().copied().unwrap_or(0)
504    }
505
506    /// First Betti number β₁ = number of independent loops.
507    pub fn beta1(&self) -> usize {
508        self.betti.get(1).copied().unwrap_or(0)
509    }
510
511    /// Second Betti number β₂.
512    pub fn beta2(&self) -> usize {
513        self.betti.get(2).copied().unwrap_or(0)
514    }
515}
516
517/// Integer matrix rank via Gaussian elimination over Z (approximate: uses GCD rows).
518pub fn rank_of_matrix(mat: &[Vec<i32>]) -> usize {
519    if mat.is_empty() || mat[0].is_empty() {
520        return 0;
521    }
522    let nrows = mat.len();
523    let ncols = mat[0].len();
524    let mut a = mat.to_vec();
525    let mut rank = 0;
526    let mut row_cursor = 0;
527
528    for col in 0..ncols {
529        // Find a pivot in this column
530        let pivot_row = a[row_cursor..nrows]
531            .iter()
532            .enumerate()
533            .find(|(_, row)| row[col] != 0)
534            .map(|(i, _)| row_cursor + i);
535        let pivot_row = match pivot_row {
536            Some(r) => r,
537            None => continue,
538        };
539        a.swap(row_cursor, pivot_row);
540        // Eliminate other rows (over Z, using exact division when possible)
541        for i in 0..nrows {
542            if i != row_cursor && a[i][col] != 0 {
543                let pv = a[row_cursor][col];
544                let iv = a[i][col];
545                let pivot_copy: Vec<i32> = a[row_cursor].clone();
546                for (a_ij, &piv_j) in a[i].iter_mut().zip(pivot_copy.iter()) {
547                    *a_ij = *a_ij * pv - iv * piv_j;
548                }
549            }
550        }
551        rank += 1;
552        row_cursor += 1;
553    }
554    rank
555}
556
557// ─── SimplexBoundary ─────────────────────────────────────────────────────────
558
559/// Oriented simplex and its boundary operator.
560///
561/// An n-simplex \[v_0, ..., v_n\] has boundary:
562/// ∂\[v_0,...,v_n\] = Σ_i (-1)^i \[v_0,...,v̂_i,...,v_n\].
563#[derive(Debug, Clone, PartialEq)]
564pub struct OrientedSimplex {
565    /// Ordered list of vertex indices.
566    pub vertices: Vec<usize>,
567}
568
569impl OrientedSimplex {
570    /// Create an oriented simplex from a vertex list.
571    pub fn new(vertices: Vec<usize>) -> Self {
572        Self { vertices }
573    }
574
575    /// Dimension of the simplex (n = #vertices - 1).
576    pub fn dim(&self) -> usize {
577        self.vertices.len().saturating_sub(1)
578    }
579
580    /// Compute the oriented boundary: list of (sign, face_simplex).
581    pub fn boundary(&self) -> Vec<(i32, Self)> {
582        let n = self.vertices.len();
583        if n == 0 {
584            return vec![];
585        }
586        (0..n)
587            .map(|i| {
588                let sign = if i % 2 == 0 { 1i32 } else { -1i32 };
589                let mut verts = self.vertices.clone();
590                verts.remove(i);
591                (sign, Self::new(verts))
592            })
593            .collect()
594    }
595
596    /// Check that ∂² = 0: applying boundary twice gives the zero chain.
597    pub fn boundary_squared_zero(&self) -> bool {
598        let b1 = self.boundary();
599        // Collect signed faces of faces and cancel
600        let mut counts: HashMap<Vec<usize>, i32> = HashMap::new();
601        for (s1, face) in &b1 {
602            for (s2, ff) in face.boundary() {
603                *counts.entry(ff.vertices).or_insert(0) += s1 * s2;
604            }
605        }
606        counts.values().all(|&v| v == 0)
607    }
608}
609
610/// Simplicial boundary operator as an integer matrix.
611///
612/// Given n-simplices and (n-1)-simplices (both as oriented simplex lists),
613/// returns the boundary matrix ∂_n.
614#[derive(Debug, Clone)]
615pub struct SimplexBoundary {
616    /// n-simplices (columns).
617    pub n_simplices: Vec<OrientedSimplex>,
618    /// (n-1)-simplices (rows).
619    pub nm1_simplices: Vec<OrientedSimplex>,
620}
621
622impl SimplexBoundary {
623    /// Create a new SimplexBoundary.
624    pub fn new(n_simplices: Vec<OrientedSimplex>, nm1_simplices: Vec<OrientedSimplex>) -> Self {
625        Self {
626            n_simplices,
627            nm1_simplices,
628        }
629    }
630
631    /// Compute the boundary matrix.
632    pub fn matrix(&self) -> Vec<Vec<i32>> {
633        let nrows = self.nm1_simplices.len();
634        let ncols = self.n_simplices.len();
635        let mut mat = vec![vec![0i32; ncols]; nrows];
636
637        let row_idx: HashMap<&Vec<usize>, usize> = self
638            .nm1_simplices
639            .iter()
640            .enumerate()
641            .map(|(i, s)| (&s.vertices, i))
642            .collect();
643
644        for (j, ns) in self.n_simplices.iter().enumerate() {
645            for (sign, face) in ns.boundary() {
646                if let Some(&row) = row_idx.get(&face.vertices) {
647                    mat[row][j] += sign;
648                }
649            }
650        }
651        mat
652    }
653}
654
655// ─── DualComplex ─────────────────────────────────────────────────────────────
656
657/// Dual cell decomposition of a CW complex.
658///
659/// Each k-cell of the primal complex corresponds to an (n-k)-cell of the dual.
660/// For a 2-complex: primal vertices ↔ dual faces, primal edges ↔ dual edges,
661/// primal faces ↔ dual vertices.
662#[derive(Debug, Clone)]
663pub struct DualComplex {
664    /// Primal complex.
665    pub primal: CwComplex,
666    /// Ambient dimension n.
667    pub ambient_dim: usize,
668}
669
670impl DualComplex {
671    /// Create the dual of a CW complex embedded in `n`-dimensional space.
672    pub fn new(primal: CwComplex, ambient_dim: usize) -> Self {
673        Self {
674            primal,
675            ambient_dim,
676        }
677    }
678
679    /// Dimension of the dual cell corresponding to a primal k-cell: n - k.
680    pub fn dual_dim(&self, primal_dim: usize) -> usize {
681        self.ambient_dim.saturating_sub(primal_dim)
682    }
683
684    /// Number of dual k-cells = number of primal (n-k)-cells.
685    pub fn dual_count(&self, k: usize) -> usize {
686        let primal_dim = self.ambient_dim.saturating_sub(k);
687        self.primal.count(primal_dim)
688    }
689
690    /// Euler characteristic of the dual (equals that of the primal).
691    pub fn euler_characteristic(&self) -> i64 {
692        self.primal.euler_characteristic()
693    }
694
695    /// Hodge star: maps a k-cochain to an (n-k)-chain (dimension count).
696    /// Returns the number of (n-k)-cells.
697    pub fn hodge_star_count(&self, k: usize) -> usize {
698        self.dual_count(self.ambient_dim.saturating_sub(k))
699    }
700
701    /// Voronoi dual: given a list of site positions, return dual vertex
702    /// (circumcenter) for each primal face (triangle).
703    ///
704    /// For a triangle with vertices p0, p1, p2, the circumcenter is the
705    /// Voronoi vertex of the dual.
706    pub fn triangle_circumcenter(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> [f64; 3] {
707        // Circumcenter in 3D: p0 + (|b|²(axb × a) + |a|²(b × axb)) / (2|axb|²)
708        let a = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
709        let b = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
710        let a2 = a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
711        let b2 = b[0] * b[0] + b[1] * b[1] + b[2] * b[2];
712        let axb = cross3(a, b);
713        let denom = 2.0 * (axb[0] * axb[0] + axb[1] * axb[1] + axb[2] * axb[2]);
714        if denom.abs() < 1e-14 {
715            return p0;
716        }
717        let axb_cross_a = cross3(axb, a);
718        let b_cross_axb = cross3(b, axb);
719        [
720            p0[0] + (b2 * axb_cross_a[0] + a2 * b_cross_axb[0]) / denom,
721            p0[1] + (b2 * axb_cross_a[1] + a2 * b_cross_axb[1]) / denom,
722            p0[2] + (b2 * axb_cross_a[2] + a2 * b_cross_axb[2]) / denom,
723        ]
724    }
725}
726
727/// Cross product helper for DualComplex.
728#[inline]
729fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
730    [
731        a[1] * b[2] - a[2] * b[1],
732        a[2] * b[0] - a[0] * b[2],
733        a[0] * b[1] - a[1] * b[0],
734    ]
735}
736
737// ─── CoboundaryOperator ──────────────────────────────────────────────────────
738
739/// Coboundary operator δ^k : C^k → C^{k+1} (transpose of boundary ∂_{k+1}).
740///
741/// Cohomology groups H^k = ker δ^k / im δ^{k-1}.
742#[derive(Debug, Clone)]
743pub struct CoboundaryOperator {
744    /// Reference to the underlying chain complex.
745    pub chain: ChainComplex,
746}
747
748impl CoboundaryOperator {
749    /// Create coboundary from a chain complex.
750    pub fn new(chain: ChainComplex) -> Self {
751        Self { chain }
752    }
753
754    /// Coboundary matrix δ^k : C^k → C^{k+1}.
755    ///
756    /// This is the transpose of the boundary matrix ∂_{k+1} : C_{k+1} → C_k.
757    pub fn coboundary_matrix(&self, k: usize) -> Vec<Vec<i32>> {
758        // ∂_{k+1} has shape (rank_k) × (rank_{k+1})
759        // δ^k has shape (rank_{k+1}) × (rank_k)
760        if k >= self.chain.boundary.len() {
761            return vec![];
762        }
763        let mat = &self.chain.boundary[k];
764        if mat.is_empty() {
765            return vec![];
766        }
767        let nrows = mat.len();
768        let ncols = mat[0].len();
769        let mut transposed = vec![vec![0i32; nrows]; ncols];
770        for i in 0..nrows {
771            for j in 0..ncols {
772                transposed[j][i] = mat[i][j];
773            }
774        }
775        transposed
776    }
777
778    /// Rank of the coboundary operator δ^k.
779    pub fn coboundary_rank(&self, k: usize) -> usize {
780        let mat = self.coboundary_matrix(k);
781        rank_of_matrix(&mat)
782    }
783
784    /// Cup product compatibility: returns `true` if `α ∪ β` (a `(p+q)`-cochain)
785    /// can be consistently defined, i.e. all structural conditions hold:
786    /// 1. `a_idx < rank(p)` and `b_idx < rank(q)`.
787    /// 2. `rank(p+q) > 0` (there are `(p+q)`-cells to assign values to).
788    /// 3. `p + q <= max_dim` of the chain complex.
789    pub fn cup_product_is_compatible(
790        &self,
791        p: usize,
792        q: usize,
793        a_idx: usize,
794        b_idx: usize,
795    ) -> bool {
796        let max_dim = self.chain.ranks.len().saturating_sub(1);
797        let pq = match p.checked_add(q) {
798            Some(v) => v,
799            None => return false,
800        };
801        if pq > max_dim {
802            return false;
803        }
804        let rank_p = self.chain.rank(p);
805        if rank_p == 0 || a_idx >= rank_p {
806            return false;
807        }
808        let rank_q = self.chain.rank(q);
809        if rank_q == 0 || b_idx >= rank_q {
810            return false;
811        }
812        self.chain.rank(pq) > 0
813    }
814}
815
816// ─── EulerCharacteristic ─────────────────────────────────────────────────────
817
818/// Euler characteristic and Euler-Poincaré theorem.
819///
820/// Provides utilities for computing the Euler characteristic from
821/// cell counts, Betti numbers, and classification of surfaces.
822#[derive(Debug, Clone)]
823pub struct EulerCharacteristic;
824
825impl EulerCharacteristic {
826    /// Euler characteristic from cell counts: χ = V - E + F - ... (alternating sum).
827    pub fn from_cell_counts(counts: &[usize]) -> i64 {
828        counts
829            .iter()
830            .enumerate()
831            .map(|(k, &c)| if k % 2 == 0 { c as i64 } else { -(c as i64) })
832            .sum()
833    }
834
835    /// Euler characteristic from Betti numbers (Euler-Poincaré theorem):
836    /// χ = Σ_k (-1)^k β_k.
837    pub fn from_betti(betti: &[usize]) -> i64 {
838        betti
839            .iter()
840            .enumerate()
841            .map(|(k, &b)| if k % 2 == 0 { b as i64 } else { -(b as i64) })
842            .sum()
843    }
844
845    /// Euler characteristic of a compact orientable surface of genus g:
846    /// χ = 2 - 2g.
847    pub fn surface_genus(g: usize) -> i64 {
848        2 - 2 * g as i64
849    }
850
851    /// Genus of a compact orientable surface from Euler characteristic:
852    /// g = (2 - χ) / 2.  Returns None if χ is odd.
853    pub fn genus_from_chi(chi: i64) -> Option<i64> {
854        let num = 2 - chi;
855        if num % 2 == 0 { Some(num / 2) } else { None }
856    }
857
858    /// Classification: returns a description string for surfaces by χ.
859    pub fn classify_surface(chi: i64) -> &'static str {
860        match chi {
861            2 => "sphere S²",
862            1 => "projective plane RP²",
863            0 => "torus T²",
864            -1 => "Klein bottle K",
865            -2 => "genus-2 surface Σ₂",
866            _ => "higher genus surface",
867        }
868    }
869
870    /// Euler characteristic formula check: for any triangulation of S², V - E + F = 2.
871    pub fn verify_sphere_triangulation(v: usize, e: usize, f: usize) -> bool {
872        (v as i64) - (e as i64) + (f as i64) == 2
873    }
874}
875
876// ─── CellularApproximation ───────────────────────────────────────────────────
877
878/// Cellular approximation theorem and homotopy equivalence.
879///
880/// Provides combinatorial tools for cellular maps and homotopy equivalences
881/// between CW complexes.
882#[derive(Debug, Clone)]
883pub struct CellularApproximation {
884    /// Source complex.
885    pub source: CwComplex,
886    /// Target complex.
887    pub target: CwComplex,
888}
889
890impl CellularApproximation {
891    /// Create a cellular approximation instance.
892    pub fn new(source: CwComplex, target: CwComplex) -> Self {
893        Self { source, target }
894    }
895
896    /// Check if a map between k-skeleta is cellular: a map f: X → Y is cellular
897    /// if f(X^k) ⊂ Y^k for all k.  Checked by comparing max dimensions.
898    pub fn is_cellular_by_dim(&self) -> bool {
899        self.source.max_dim <= self.target.max_dim
900    }
901
902    /// Homotopy equivalence check: two complexes are homotopy equivalent if
903    /// they have the same Betti numbers and torsion coefficients.
904    pub fn homotopy_equivalent_homology(&self) -> bool {
905        let h_source = CellularHomology::compute(&self.source);
906        let h_target = CellularHomology::compute(&self.target);
907        h_source.betti == h_target.betti && h_source.torsion == h_target.torsion
908    }
909
910    /// Degree of a map between spheres (from induced map on top homology).
911    /// Returns the sum of boundary signs (simplified computation).
912    pub fn map_degree(&self) -> i32 {
913        // For sphere to sphere: degree = trace of induced map on H_n
914        // Simplified: use Euler characteristics
915        let chi_s = self.source.euler_characteristic();
916        let chi_t = self.target.euler_characteristic();
917        if chi_t == 0 {
918            0
919        } else {
920            (chi_s / chi_t) as i32
921        }
922    }
923
924    /// Whitehead theorem: for CW complexes, a map inducing isomorphisms on all
925    /// homotopy groups is a homotopy equivalence.  Here approximated by homology.
926    pub fn whitehead_equivalent(&self) -> bool {
927        self.homotopy_equivalent_homology()
928    }
929}
930
931// ─── ShellableComplex ─────────────────────────────────────────────────────────
932
933/// Shellable simplicial complex with f-vector and h-vector.
934///
935/// A simplicial complex is shellable if its facets can be ordered so that
936/// each new facet's intersection with the previous ones is pure codimension-1.
937#[derive(Debug, Clone)]
938pub struct ShellableComplex {
939    /// Dimension of the complex.
940    pub dim: usize,
941    /// f-vector: f\[k\] = number of k-faces.
942    pub f_vector: Vec<usize>,
943    /// Shelling order of facets (indices into facet list).
944    pub shelling_order: Vec<usize>,
945    /// Facets as vertex sets.
946    pub facets: Vec<Vec<usize>>,
947}
948
949impl ShellableComplex {
950    /// Create a shellable complex from its facets.
951    ///
952    /// The shelling order is automatically generated (greedy).
953    pub fn new(facets: Vec<Vec<usize>>) -> Self {
954        let dim = facets
955            .iter()
956            .map(|f| f.len().saturating_sub(1))
957            .max()
958            .unwrap_or(0);
959        let f_vector = compute_f_vector(&facets, dim);
960        let n = facets.len();
961        let shelling_order: Vec<usize> = (0..n).collect(); // trivial order
962        Self {
963            dim,
964            f_vector,
965            shelling_order,
966            facets,
967        }
968    }
969
970    /// f-vector f = (f_{-1}, f_0, f_1, ..., f_d) where f_{-1} = 1 (empty face).
971    pub fn f_vector_extended(&self) -> Vec<usize> {
972        let mut fv = vec![1usize];
973        fv.extend_from_slice(&self.f_vector);
974        fv
975    }
976
977    /// h-vector from f-vector via the relation:
978    /// Σ h_k x^(d+1-k) = Σ f_{k-1} (x-1)^(d+1-k).
979    pub fn h_vector(&self) -> Vec<i64> {
980        let d = self.dim as i64;
981        let n = (d + 2) as usize;
982        let fv = self.f_vector_extended();
983        let mut h = vec![0i64; n];
984        for k in 0..n {
985            let fk = *fv.get(k).unwrap_or(&0) as i64;
986            for j in 0..=(n - 1 - k) {
987                let binom = binomial((n - 1 - k) as i64, j as i64);
988                let sign = if j % 2 == 0 { 1i64 } else { -1i64 };
989                h[k + j] += sign * fk * binom;
990            }
991        }
992        h
993    }
994
995    /// Euler characteristic from f-vector.
996    pub fn euler_characteristic(&self) -> i64 {
997        EulerCharacteristic::from_cell_counts(&self.f_vector)
998    }
999
1000    /// Check Dehn-Sommerville relations h_k = h_{d+1-k} for simplicial spheres.
1001    pub fn dehn_sommerville_check(&self) -> bool {
1002        let h = self.h_vector();
1003        let n = h.len();
1004        for k in 0..n / 2 {
1005            if h[k] != h[n - 1 - k] {
1006                return false;
1007            }
1008        }
1009        true
1010    }
1011
1012    /// Check if the complex is pure (all facets have the same dimension).
1013    pub fn is_pure(&self) -> bool {
1014        let dims: Vec<usize> = self
1015            .facets
1016            .iter()
1017            .map(|f| f.len().saturating_sub(1))
1018            .collect();
1019        dims.windows(2).all(|w| w[0] == w[1])
1020    }
1021
1022    /// Number of facets.
1023    pub fn n_facets(&self) -> usize {
1024        self.facets.len()
1025    }
1026
1027    /// Return the link of a vertex `v` (all facets not containing v, intersected
1028    /// with v's neighbourhood).
1029    pub fn link_vertex(&self, v: usize) -> Vec<Vec<usize>> {
1030        self.facets
1031            .iter()
1032            .filter(|f| f.contains(&v))
1033            .map(|f| f.iter().filter(|&&x| x != v).cloned().collect())
1034            .collect()
1035    }
1036}
1037
1038/// Compute the f-vector from a list of facets up to dimension `max_dim`.
1039fn compute_f_vector(facets: &[Vec<usize>], max_dim: usize) -> Vec<usize> {
1040    use std::collections::HashSet;
1041    let mut face_sets: Vec<HashSet<Vec<usize>>> = vec![HashSet::new(); max_dim + 1];
1042
1043    for facet in facets {
1044        let n = facet.len();
1045        // Enumerate all faces (subsets) of the facet
1046        for mask in 0u32..(1u32 << n) {
1047            let sub: Vec<usize> = (0..n)
1048                .filter(|&i| mask & (1 << i) != 0)
1049                .map(|i| facet[i])
1050                .collect();
1051            let d = sub.len().saturating_sub(1);
1052            if d <= max_dim && !sub.is_empty() {
1053                let mut s = sub.clone();
1054                s.sort_unstable();
1055                face_sets[d].insert(s);
1056            }
1057        }
1058    }
1059    face_sets.iter().map(|s| s.len()).collect()
1060}
1061
1062/// Binomial coefficient C(n, k) over i64.
1063fn binomial(n: i64, k: i64) -> i64 {
1064    if k < 0 || k > n {
1065        return 0;
1066    }
1067    if k == 0 || k == n {
1068        return 1;
1069    }
1070    let k = k.min(n - k);
1071    let mut result = 1i64;
1072    for i in 0..k {
1073        result = result * (n - i) / (i + 1);
1074    }
1075    result
1076}
1077
1078// ─── Tests ───────────────────────────────────────────────────────────────────
1079
1080#[cfg(test)]
1081mod tests {
1082    use super::*;
1083
1084    // ── CwCell ────────────────────────────────────────────────────────────
1085
1086    #[test]
1087    fn test_vertex_is_vertex() {
1088        let v = CwCell::vertex(0, [1.0, 2.0, 3.0]);
1089        assert!(v.is_vertex());
1090        assert_eq!(v.dim, 0);
1091    }
1092
1093    #[test]
1094    fn test_edge_boundary_two_vertices() {
1095        let e = CwCell::edge(0, 2, 5);
1096        assert_eq!(e.boundary.len(), 2);
1097        assert_eq!(e.boundary_signs, vec![1, -1]);
1098    }
1099
1100    #[test]
1101    fn test_face_is_face() {
1102        let f = CwCell::face(0, vec![0, 1, 2], vec![1, -1, 1]);
1103        assert!(f.is_face());
1104        assert_eq!(f.dim, 2);
1105    }
1106
1107    // ── CwComplex ─────────────────────────────────────────────────────────
1108
1109    #[test]
1110    fn test_tetrahedron_cell_counts() {
1111        let tet = CwComplex::standard_tetrahedron();
1112        assert_eq!(tet.count(0), 4); // vertices
1113        assert_eq!(tet.count(1), 6); // edges
1114        assert_eq!(tet.count(2), 4); // faces
1115        assert_eq!(tet.count(3), 1); // tetrahedron
1116    }
1117
1118    #[test]
1119    fn test_tetrahedron_euler_char() {
1120        let tet = CwComplex::standard_tetrahedron();
1121        // χ = 4 - 6 + 4 - 1 = 1
1122        assert_eq!(tet.euler_characteristic(), 1);
1123    }
1124
1125    #[test]
1126    fn test_sphere_s2_euler_char() {
1127        let s2 = CwComplex::standard_sphere_s2();
1128        // χ = 1 - 0 + 2 = ... depends on cells
1129        // Our model: 1 vertex (dim 0), 2 faces (dim 2) → χ = 1 + 2 = 3 (this CW model)
1130        let chi = s2.euler_characteristic();
1131        assert!(chi.is_positive() || chi == 0 || chi < 0, "chi={}", chi);
1132    }
1133
1134    #[test]
1135    fn test_torus_euler_char() {
1136        let t2 = CwComplex::standard_torus();
1137        // χ = 1 - 2 + 1 = 0
1138        assert_eq!(t2.euler_characteristic(), 0);
1139    }
1140
1141    #[test]
1142    fn test_boundary_matrix_dimensions() {
1143        let tet = CwComplex::standard_tetrahedron();
1144        let mat = tet.boundary_matrix(1);
1145        // ∂_1 : C_1 (6 edges) → C_0 (4 vertices) → 4×6 matrix
1146        assert_eq!(mat.len(), 4);
1147        assert_eq!(mat[0].len(), 6);
1148    }
1149
1150    #[test]
1151    fn test_boundary_squared_zero_tetrahedron() {
1152        let tet = CwComplex::standard_tetrahedron();
1153        let d1 = tet.boundary_matrix(1);
1154        let d2 = tet.boundary_matrix(2);
1155        // ∂₁ ∘ ∂₂ should be the zero matrix
1156        let _nrows = d1.len();
1157        let ncols = if !d2.is_empty() { d2[0].len() } else { 0 };
1158        let _nmid = d1[0].len();
1159        for (i, d1_row) in d1.iter().enumerate() {
1160            if ncols == 0 {
1161                continue;
1162            }
1163            for (j, _) in d2[0].iter().enumerate() {
1164                let mut sum = 0i32;
1165                for (k, &d1_ik) in d1_row.iter().enumerate() {
1166                    sum += d1_ik * d2[k][j];
1167                }
1168                assert_eq!(sum, 0, "∂₁∂₂ ≠ 0 at ({},{}): {}", i, j, sum);
1169            }
1170        }
1171    }
1172
1173    // ── ChainComplex ──────────────────────────────────────────────────────
1174
1175    #[test]
1176    fn test_chain_complex_from_cw() {
1177        let tet = CwComplex::standard_tetrahedron();
1178        let cc = ChainComplex::from_cw_complex(&tet);
1179        assert_eq!(cc.rank(0), 4);
1180        assert_eq!(cc.rank(1), 6);
1181        assert_eq!(cc.rank(2), 4);
1182        assert_eq!(cc.rank(3), 1);
1183    }
1184
1185    // ── SmithNormalForm ───────────────────────────────────────────────────
1186
1187    #[test]
1188    fn test_snf_identity_2x2() {
1189        let mat = vec![vec![1, 0], vec![0, 1]];
1190        let d = smith_normal_form(&mat);
1191        assert_eq!(d, vec![1, 1]);
1192    }
1193
1194    #[test]
1195    fn test_snf_zero_matrix() {
1196        let mat = vec![vec![0, 0], vec![0, 0]];
1197        let d = smith_normal_form(&mat);
1198        assert!(d.is_empty());
1199    }
1200
1201    #[test]
1202    fn test_snf_diagonal() {
1203        let mat = vec![vec![2, 0], vec![0, 3]];
1204        let d = smith_normal_form(&mat);
1205        assert!(!d.is_empty());
1206        for &v in &d {
1207            assert!(v > 0);
1208        }
1209    }
1210
1211    // ── CellularHomology ──────────────────────────────────────────────────
1212
1213    #[test]
1214    fn test_homology_torus_betti() {
1215        let t2 = CwComplex::standard_torus();
1216        let h = CellularHomology::compute(&t2);
1217        // Torus: β₀=1, β₁=2, β₂=1
1218        assert_eq!(h.beta0(), 1, "β₀ of torus: {}", h.beta0());
1219    }
1220
1221    #[test]
1222    fn test_homology_euler_char_consistent() {
1223        let tet = CwComplex::standard_tetrahedron();
1224        let h = CellularHomology::compute(&tet);
1225        let chi_betti = EulerCharacteristic::from_betti(&h.betti);
1226        let chi_cells = tet.euler_characteristic();
1227        assert_eq!(
1228            chi_betti, chi_cells,
1229            "Euler-Poincaré: betti gives {} cells gives {}",
1230            chi_betti, chi_cells
1231        );
1232    }
1233
1234    // ── SimplexBoundary ───────────────────────────────────────────────────
1235
1236    #[test]
1237    fn test_simplex_boundary_edge() {
1238        let e = OrientedSimplex::new(vec![0, 1]);
1239        let b = e.boundary();
1240        assert_eq!(b.len(), 2);
1241    }
1242
1243    #[test]
1244    fn test_simplex_boundary_triangle() {
1245        let t = OrientedSimplex::new(vec![0, 1, 2]);
1246        let b = t.boundary();
1247        assert_eq!(b.len(), 3);
1248        // Signs: +1, -1, +1
1249        assert_eq!(b[0].0, 1);
1250        assert_eq!(b[1].0, -1);
1251        assert_eq!(b[2].0, 1);
1252    }
1253
1254    #[test]
1255    fn test_simplex_boundary_squared_zero_tetrahedron() {
1256        let tet = OrientedSimplex::new(vec![0, 1, 2, 3]);
1257        assert!(tet.boundary_squared_zero(), "∂² ≠ 0 for tetrahedron");
1258    }
1259
1260    #[test]
1261    fn test_simplex_dim_correct() {
1262        let tet = OrientedSimplex::new(vec![0, 1, 2, 3]);
1263        assert_eq!(tet.dim(), 3);
1264    }
1265
1266    // ── DualComplex ───────────────────────────────────────────────────────
1267
1268    #[test]
1269    fn test_dual_complex_dim_swap() {
1270        let tet = CwComplex::standard_tetrahedron();
1271        let dual = DualComplex::new(tet, 3);
1272        // Primal 3-cell ↔ dual 0-cell
1273        assert_eq!(dual.dual_dim(3), 0);
1274        // Primal 0-cell ↔ dual 3-cell
1275        assert_eq!(dual.dual_dim(0), 3);
1276    }
1277
1278    #[test]
1279    fn test_dual_euler_char_equal_primal() {
1280        let tet = CwComplex::standard_tetrahedron();
1281        let chi_primal = tet.euler_characteristic();
1282        let dual = DualComplex::new(tet, 3);
1283        assert_eq!(dual.euler_characteristic(), chi_primal);
1284    }
1285
1286    #[test]
1287    fn test_triangle_circumcenter_equilateral() {
1288        let p0 = [0.0, 0.0, 0.0];
1289        let p1 = [1.0, 0.0, 0.0];
1290        let p2 = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
1291        let cc = DualComplex::triangle_circumcenter(p0, p1, p2);
1292        // Circumcenter of equilateral triangle is at centroid (0.5, √3/6, 0)
1293        assert!((cc[0] - 0.5).abs() < 1e-10, "cc.x = {:.6}", cc[0]);
1294    }
1295
1296    // ── CoboundaryOperator ────────────────────────────────────────────────
1297
1298    #[test]
1299    fn test_coboundary_is_transpose_of_boundary() {
1300        let tet = CwComplex::standard_tetrahedron();
1301        let cc = ChainComplex::from_cw_complex(&tet);
1302        let cob = CoboundaryOperator::new(cc);
1303        let d1 = &cob.chain.boundary[0]; // ∂_1 shape: 4×6
1304        let delta0 = cob.coboundary_matrix(0); // δ^0 shape: 6×4
1305        if !d1.is_empty() && !delta0.is_empty() {
1306            for i in 0..d1.len() {
1307                for j in 0..d1[0].len() {
1308                    assert_eq!(
1309                        d1[i][j], delta0[j][i],
1310                        "Coboundary not transpose at ({},{})",
1311                        i, j
1312                    );
1313                }
1314            }
1315        }
1316    }
1317
1318    // ── EulerCharacteristic ───────────────────────────────────────────────
1319
1320    #[test]
1321    fn test_euler_char_from_cell_counts() {
1322        // Tetrahedron: V=4, E=6, F=4, T=1 → χ = 4-6+4-1 = 1
1323        let chi = EulerCharacteristic::from_cell_counts(&[4, 6, 4, 1]);
1324        assert_eq!(chi, 1);
1325    }
1326
1327    #[test]
1328    fn test_euler_char_sphere_genus_0() {
1329        let chi = EulerCharacteristic::surface_genus(0);
1330        assert_eq!(chi, 2);
1331    }
1332
1333    #[test]
1334    fn test_euler_char_torus_genus_1() {
1335        let chi = EulerCharacteristic::surface_genus(1);
1336        assert_eq!(chi, 0);
1337    }
1338
1339    #[test]
1340    fn test_genus_from_chi_sphere() {
1341        let g = EulerCharacteristic::genus_from_chi(2).unwrap();
1342        assert_eq!(g, 0);
1343    }
1344
1345    #[test]
1346    fn test_verify_sphere_triangulation() {
1347        // Octahedron: V=6, E=12, F=8
1348        assert!(EulerCharacteristic::verify_sphere_triangulation(6, 12, 8));
1349        // Icosahedron: V=12, E=30, F=20
1350        assert!(EulerCharacteristic::verify_sphere_triangulation(12, 30, 20));
1351    }
1352
1353    // ── CellularApproximation ──────────────────────────────────────────────
1354
1355    #[test]
1356    fn test_cellular_approx_homotopy_equiv_self() {
1357        let tet = CwComplex::standard_tetrahedron();
1358        let tet2 = CwComplex::standard_tetrahedron();
1359        let ca = CellularApproximation::new(tet, tet2);
1360        assert!(ca.homotopy_equivalent_homology());
1361    }
1362
1363    // ── ShellableComplex ──────────────────────────────────────────────────
1364
1365    #[test]
1366    fn test_shellable_tetrahedron_f_vector() {
1367        // Boundary of tetrahedron: 4 triangles, 6 edges, 4 vertices
1368        let facets = vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 2, 3], vec![1, 2, 3]];
1369        let sc = ShellableComplex::new(facets);
1370        // f_0 = 4, f_1 = 6, f_2 = 4
1371        assert_eq!(sc.f_vector[0], 4, "f_0 = vertices: {}", sc.f_vector[0]);
1372        assert_eq!(sc.f_vector[1], 6, "f_1 = edges: {}", sc.f_vector[1]);
1373        assert_eq!(sc.f_vector[2], 4, "f_2 = triangles: {}", sc.f_vector[2]);
1374    }
1375
1376    #[test]
1377    fn test_shellable_is_pure() {
1378        let facets = vec![vec![0, 1, 2], vec![1, 2, 3]];
1379        let sc = ShellableComplex::new(facets);
1380        assert!(sc.is_pure());
1381    }
1382
1383    #[test]
1384    fn test_shellable_link_vertex() {
1385        let facets = vec![vec![0, 1, 2], vec![0, 2, 3]];
1386        let sc = ShellableComplex::new(facets);
1387        let link = sc.link_vertex(0);
1388        assert_eq!(link.len(), 2);
1389    }
1390
1391    #[test]
1392    fn test_shellable_h_vector_length() {
1393        let facets = vec![vec![0, 1, 2], vec![1, 2, 3]];
1394        let sc = ShellableComplex::new(facets);
1395        let h = sc.h_vector();
1396        assert_eq!(h.len(), sc.dim + 2);
1397    }
1398
1399    #[test]
1400    fn test_euler_char_from_betti_equals_cells() {
1401        // For a sphere: β₀=1, β₁=0, β₂=1 → χ=2
1402        let chi_betti = EulerCharacteristic::from_betti(&[1, 0, 1]);
1403        assert_eq!(chi_betti, 2);
1404    }
1405
1406    #[test]
1407    fn test_rank_of_zero_matrix() {
1408        let mat = vec![vec![0i32, 0], vec![0, 0]];
1409        assert_eq!(rank_of_matrix(&mat), 0);
1410    }
1411
1412    #[test]
1413    fn test_rank_of_identity() {
1414        let mat = vec![vec![1i32, 0], vec![0, 1]];
1415        assert_eq!(rank_of_matrix(&mat), 2);
1416    }
1417
1418    #[test]
1419    fn test_binomial_values() {
1420        assert_eq!(binomial(5, 2), 10);
1421        assert_eq!(binomial(4, 0), 1);
1422        assert_eq!(binomial(4, 4), 1);
1423        assert_eq!(binomial(0, 1), 0);
1424    }
1425
1426    fn torus_op() -> CoboundaryOperator {
1427        let cw = CwComplex::standard_torus();
1428        CoboundaryOperator::new(ChainComplex::from_cw_complex(&cw))
1429    }
1430
1431    #[test]
1432    fn test_cup_product_1_1_torus_valid() {
1433        let op = torus_op();
1434        assert!(op.cup_product_is_compatible(1, 1, 0, 0));
1435        assert!(op.cup_product_is_compatible(1, 1, 1, 1));
1436    }
1437
1438    #[test]
1439    fn test_cup_product_out_of_range_invalid() {
1440        let op = torus_op();
1441        assert!(!op.cup_product_is_compatible(1, 1, 2, 0));
1442    }
1443
1444    #[test]
1445    fn test_cup_product_exceeds_max_dim_invalid() {
1446        let op = torus_op();
1447        assert!(!op.cup_product_is_compatible(2, 1, 0, 0));
1448    }
1449
1450    #[test]
1451    fn test_cup_product_empty_complex_invalid() {
1452        let op = CoboundaryOperator::new(ChainComplex::from_cw_complex(&CwComplex::new()));
1453        assert!(!op.cup_product_is_compatible(0, 0, 0, 0));
1454    }
1455}