Skip to main content

oxiphysics_geometry/
cell_complex.rs

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