Date: 2026-01-16 Module: src/homology Purpose: Comprehensive usage guide for BELFEM's computational topology module
Revision History:
| Date | Changes |
| 2026-01-16 | Initial documentation |
| 2026-01-16 | Applied external review feedback: Added Critical Constraints section (mesh mutation, MPI, memory ownership, thread safety, topological vs FEM correctness, mesh requirements, scratch-field usage); Added Quick-Start section; Added comprehensive Glossary; Moved Common Pitfalls to top with 10 concrete examples |
| 2026-08-31 | Marked the manifold-filtering description as a design of record with no implementation: manifold_filter_3d() and check_surface() exist nowhere in the tree. Added a design note under "Manifold Cleanup"; corrected the glossary, the Internal Workflow step, and the troubleshooting entry that told readers to tune thresholds that do not exist. Named the shipped path, Cohomology::clean_spfa() → remove_cut_pockets(). Content status remains Gregory Giard's to confirm |
Module Contracts
Before diving into usage details, understand these fundamental design contracts:
- Mesh Dependency: All topology operations require a valid, connected Mesh object
- Integer Arithmetic: Homology computations use exact integer arithmetic (no floating-point errors)
- Algorithm Independence: Results are topologically equivalent across algorithms; no performance comparison is recorded
- Manifold Requirement: Thin cuts must form valid 2-manifolds for FEM assembly
- MPI Awareness: Cohomology computation is currently serial (mesh must be on a single process)
Critical Constraints
⚠️ READ THIS SECTION BEFORE USING THE HOMOLOGY MODULE ⚠️
Mesh Mutation Contract
The homology module directly modifies mesh topology in invasive, irreversible ways:
Operations performed:
- Duplicates nodes on cut surfaces (creates new Node objects with new IDs)
- Relinks elements to point to duplicated nodes (modifies Element connectivity arrays)
- Creates new SideSets for cut surfaces (adds to mesh's sideset collection)
- Creates abstract nodes not belonging to any element (new DOF carriers)
- Calls mesh->unfinalize() and mesh->finalize() multiple times during processing
Consequences:
- NOT REVERSIBLE: Cannot undo node duplication or element relinking
- Invalidates indices: Any code relying on original node indices will break
- Invalidates fields: Custom mesh fields indexed by original nodes become inconsistent
- Changes mesh size: Node count, element connectivity, and sideset count all increase
Execution order requirement:
Mesh* tMesh = new Mesh("problem.exo");
mesh::CutFactory tFactory(tMesh, ...);
tFactory.run();
fem::DofManager* tDofManager = new fem::DofManager(tMesh);
fem::DofManager* tDofManager = new fem::DofManager(tMesh);
tFactory.run();
Cannot run twice:
tFactory.run();
tFactory.run();
MPI Serial Limitation
CRITICAL: Cohomology computation is serial-only and must run on an undistributed mesh.
What breaks if you ignore this:
- Running on partitioned mesh → incorrect simplicial complexes (missing connectivity across ranks)
- Running on distributed mesh → incomplete generators (topological holes span process boundaries)
- Running on multiple ranks → rank-dependent results (nondeterministic cut generation)
Correct MPI workflow:
Mesh* tMesh = nullptr;
if (comm_rank() == 0) {
tMesh = new Mesh("problem.h5");
mesh::CutFactory tFactory(tMesh, ...);
tFactory.run();
}
if (comm_rank() == 0) {
tMesh->partition(comm_size());
}
if (comm_rank() == 0) {
tMesh->distribute();
} else {
tMesh = new Mesh();
tMesh->receive(0);
}
Future work: Parallel cohomology is planned but not yet implemented.
Memory Ownership
Critical ownership rules:
| Object | Created By | Owned By | Caller Action |
| SideSet* (cuts) | CutFactory | Mesh | Do NOT delete |
| Node* (abstract) | CutFactory | Mesh | Do NOT delete |
| Node* (duplicates) | CutFactory | Mesh | Do NOT delete |
| Cochain* (generators) | Cohomology | Cohomology | Do NOT delete |
| Chain* (generators) | Homology | Homology | Do NOT delete |
| Protoshell* | User | User | Caller must delete |
Key rule: CutFactory transfers all created mesh entities (nodes, sidesets) to the mesh. The mesh takes ownership and will delete them in its destructor.
CutFactory tFactory(tMesh, ...);
tFactory.run();
Cell<SideSet*>& cuts = tFactory.cuts();
delete tMesh;
Cell<SideSet*>& cuts = tFactory.cuts();
delete cuts(0);
Thread Safety
The homology module is NOT thread-safe:
- SimplicialComplex::reduce_*() modifies internal state (chain/cochain maps)
- CutFactory::run() modifies mesh topology (node duplication, element relinking)
- smithForm() and friends modify their input matrix in place (aMat is taken by reference)
OpenMP: Do not call homology functions from parallel regions
MPI: Must run on single rank (see MPI Serial Limitation above)
Topological vs FEM Correctness
Critical distinction: The homology module provides two separate guarantees:
Topological Guarantees
✅ What homology guarantees:
- Generators span correct cohomology groups H^k
- Algorithms produce topologically equivalent results (different bases, same groups)
- Betti numbers are exact (integer arithmetic, no rounding errors)
- Orientation is consistent after cleanup
FEM Guarantees
⚠️ What homology does NOT automatically guarantee:
- Thin cuts form valid 2-manifolds → Requires manifold filtering (may fail)
- Node duplication yields independent DOFs → Requires proper element relinking (usually works)
- No element has mixed φ⁺/φ⁻ nodes → Requires correct cut-side detection (usually works)
- Static condensation produces correct constraints → Requires correct FEM implementation (user responsibility)
Implication: A topologically correct cohomology generator may still produce a non-manifold thin cut that breaks FEM assembly. Always inspect the cut sidesets (debug output, ParaView) — there is no is_manifold() query on SideSet.
Mesh Requirements
Preconditions for correctness:
✅ Required mesh properties:
- Mesh must be connected (single component, or separate components with distinct domain types)
- Mesh must be orientable (no Möbius strips)
- Elements must form a manifold (no hanging faces, no non-manifold edges)
- Facet orientations must be computable (consistent normal directions)
⚠️ Topology requirements:
- Phi/non-phi domains must be correctly tagged (see Topology::run())
- Thin-shell interfaces must be properly labeled as sidesets
- Terminals (if used) must exist as nodes in the mesh
❌ Unsupported configurations:
- Non-orientable manifolds (Möbius strip, Klein bottle)
- Meshes with dangling tetrahedra (elements connected only at nodes/edges)
- Multiply-connected phi-domains where cuts span >10^7 faces (memory limit)
Validation pattern:
BELFEM_ERROR(tMesh->number_of_elements() > 0,
"Empty mesh");
BELFEM_ERROR(tMesh->number_of_blocks() > 0,
"No blocks defined");
mesh::Topology tTopology(tMesh);
tTopology.run();
"No phi-domain blocks identified");
#define BELFEM_ERROR(aCheck,...)
Definition assert.hpp:264
Scratch-Field Usage
Warning: the module uses the mesh entities' flag bits (flag_*() / unflag_*()) as scratch and, in the SPFA infeasibility path, writes Element::level (cl_Cohomology.cpp:510-528). It does not write owner or index.
Consequences:
- Do not rely on entity flags or Element::level after CutFactory::run()
- Run homology before partitioning because cohomology needs the undistributed mesh (see "MPI Serial Limitation"), not because of owner
tMesh->partition();
tFactory.run();
tFactory.run();
tMesh->partition();
Table of Contents
- Quick-Start
- Glossary
- Common Pitfalls
- Introduction
- Module Architecture
- CutFactory - Main Entry Point
- Cohomology Computation
- Homology Computation
- Cut Algorithm Selection
- Topology Analysis
- Thin Shell Handling
- Cut Processing
- Data Structures
- Advanced Topics
- Common Patterns
- Troubleshooting
- Usage Examples
Quick-Start
Minimal example to generate cuts for a toroidal mesh:
Mesh* tMesh = new Mesh("toroid.exo");
mesh::Topology tTopology(tMesh);
tTopology.run();
mesh::CutFactory tFactory(
tMesh, &tTopology, {},
mesh::CutAlgorithm::PellikkaGeneralized
);
tFactory.run();
for (mesh::SideSet* cut : tFactory.cuts()) {
cut->set_domain_type(mesh::DomainType::Cut);
}
That's it! The tFactory.cuts() are now ready for FEM assembly with jump conditions.
Glossary
Core Concepts:
| Term | Definition |
| Chain | Formal sum of k-simplices with integer coefficients (homology side): c = Σ aᵢ σᵢ where aᵢ ∈ ℤ |
| Cochain | Dual to chain; element of cochain complex (cohomology side): φ : C_k → ℤ |
| Thick Cut | Cohomology generator as a set of directed edges with ±1 coefficients (topological object) |
| Thin Cut | FEM-compatible surface where φ is discontinuous (minimal 2-manifold derived from thick cut). Worked example: thick_thin_cuts_and_conjugate_edges.md |
| Conjugate Edges | Face-loop edges bounding the thin cut, dual to the apex edges the thick cut intersects (see thick_thin_cuts_and_conjugate_edges.md) |
| Abstract Node | Node not belonging to any element; carries DOF for jump condition [φ] = φ⁺ - φ⁻ |
| Duplicate Node | Copy of original node created during cut processing; allows φ⁺ ≠ φ⁻ on cut surface |
| Simplicial Complex | Collection of simplices (nodes, edges, faces, elements) with incidence relations |
| Boundary Operator | Map ∂_k : C_k → C_{k-1} (takes k-chain to its (k-1)-boundary) |
| Coboundary Operator | Map δ^k : C^k → C^{k+1} (dual to boundary operator) |
| Smith Normal Form | Diagonal matrix decomposition QAR = D revealing Betti numbers and torsion |
| Betti Number | Rank of k-th homology/cohomology group; counts k-dimensional "holes" |
| Homology Group | H_k = Ker(∂_k) / Im(∂_{k+1}) (k-cycles modulo k-boundaries) |
| Cohomology Group | H^k = Ker(δ^k) / Im(δ^{k-1}) (k-cocycles modulo k-coboundaries) |
| Terminal | Electrical connection point (set of nodes) for current injection/extraction |
| Protoshell | Configuration object for thin shell structures (sidesets, terminals, material, thickness) |
| Phi-domain | Region where magnetic scalar potential φ is solved (non-conducting) |
| Non-phi-domain | Conducting region or external boundary (not part of φ solution domain) |
| Manifold | Surface where every edge belongs to exactly 2 faces (topologically valid) |
| Non-manifold | Surface with edges shared by >2 faces or other defects (invalid for FEM) |
| Double Pocket | Topological defect: small cluster of faces attached via single articulation point |
Algorithm Terms:
| Term | Definition |
| Elementary Collapse | Removal of k-simplex and its free (k-1)-face (preserves homology) |
| Interior Face Reduction | Merging two k-simplices sharing a common face |
| Generalized Combine | Enhanced reduction updating boundaries after removing shared faces |
| Pellikka Algorithm | Original reduction via elementary collapses and combines |
| PellikkaGeneralized | Enhanced Giard algorithm combining CCR with Pellikka |
| CCR | Chain Complex Reduction (Kaczynski); classical acyclic matching |
| BeltedTree | Spanning tree approach with belt fasteners for cohomology |
| Manifold Filtering | Post-processing to remove non-manifold artifacts (double pockets, etc.). Design of record — not implemented; see the design note under "Manifold Cleanup" |
| Tarjan's Algorithm | DFS-based articulation point detection for manifold cleanup. Exists only as an archived prototype, archive/graph/fn_Graph_tarjan.{hpp,cpp}, which is outside the build |
FEM Integration:
| Term | Definition |
| Jump Condition | Discontinuity enforced on cut: [φ] = φ⁺ - φ⁻ = I (Ampere current) |
| Static Condensation | FEM technique to eliminate internal DOFs via transformation matrix T |
| Poincaré-Lefschetz Duality | Theorem allowing thick-to-thin cut conversion (edge set → surface) |
| h-φ Formulation | Magnetostatic FEM using magnetic field H = -∇φ in non-conducting regions |
| Ampere's Law | ∮ H·dl = I for any loop encircling conductor with current I |
Common Pitfalls
⚠️ Read this section to avoid frequent mistakes.
1. Building the CutFactory Before the Topology Has Run
Problem: Topology not analyzed before creating CutFactory.
mesh::Topology tTopology(aMesh);
mesh::CutFactory tFactory(aMesh, &tTopology, {});
tFactory.run();
mesh::Topology tTopology(aMesh);
tTopology.run();
mesh::CutFactory tFactory(aMesh, &tTopology, {});
tFactory.run();
Consequence: phi_block_ids() is empty → no cohomology generators → no cuts.
2. Running Homology After Mesh Partitioning
Problem: Partitioning before homology breaks simplicial complex.
tMesh->partition(comm_size());
tFactory.run();
tFactory.run();
tMesh->partition(comm_size());
Consequence: Incomplete generators, missing cuts, or crashes due to missing connectivity.
3. Deleting Cuts or Abstract Nodes
Problem: Manually deleting mesh entities owned by the mesh.
Cell<SideSet*>& cuts = tFactory.cuts();
delete cuts(0);
Cell<SideSet*>& cuts = tFactory.cuts();
Consequence: Double delete → crash or memory corruption.
4. Forgetting Edge/Face Creation
Problem: SimplicialComplex needs edges and faces, but mesh doesn't have them.
Mesh* tMesh = new Mesh("mesh.msh");
mesh::SimplicialComplex tComplex(tMesh);
Mesh* tMesh = new Mesh("mesh.msh");
tMesh->create_edges();
tMesh->create_faces();
mesh::SimplicialComplex tComplex(tMesh);
Consequence: number_of_ksimplices(1) == 0 → cohomology computation fails.
5. Mixing Chain and Cochain
Problem: Using Chain where Cochain expected (or vice versa).
Chain* tChain = tHomology.get_Generators()( 1 )(0);
Cochain* tCochain = tCohomology.get_Generators()( 1 )(0);
Consequence: Compile error or incorrect cut generation.
6. Not Checking Manifold Status
Problem: Non-manifold cuts break FEM assembly.
tFactory.run();
tFactory.run();
for (SideSet* cut : tFactory.cuts()) {
uint nonManifoldEdges = 0;
for (Edge* edge : cut->edges()) {
if (edge->number_of_facets() != 2) {
++nonManifoldEdges;
}
}
"Cut %lu has %u non-manifold edges",
(luint)cut->id(), nonManifoldEdges);
}
Consequence: FEM assembly fails due to edges shared by >2 faces.
7. Using Wrong Algorithm for Mesh Size
Problem: Using CCR (the theory/debugging path) for production meshes.
mesh::CutFactory tFactory(..., mesh::CutAlgorithm::CCR);
tFactory.run();
mesh::CutFactory tFactory(..., mesh::CutAlgorithm::PellikkaGeneralized);
tFactory.run();
Consequence: No timing comparison between the algorithms is recorded (see README, "Cut Algorithms").
8. Running CutFactory Twice
Problem: Calling run() multiple times on same factory.
tFactory.run();
tFactory.run();
tFactory.run();
Consequence: Crash, incorrect cuts, or memory corruption.
9. Ignoring Orphaned Nodes
Problem: Orphaned nodes increase memory usage and may leak.
tFactory.run();
if (tFactory.orphaned_nodes().size() > 0) {
message(InfoLevel::Default,
"Warning: %lu orphaned nodes",
tFactory.orphaned_nodes().size());
}
void message(const belfem::InfoLevel aInfoLevel, const std::string &aFormat, const Args ... aArgs)
Definition cl_Logger.hpp:144
Consequence: Memory leak or unexpected behavior if orphaned nodes are referenced elsewhere.
10. Incorrect Terminal-to-Shell Mapping
Problem: Terminal indices don't match protoshell indices.
Cell<Cell<id_t>> tTerminals = {{101}, {201}};
Cell<id_t> tShellIndices = {1, 0};
Cell<Cell<id_t>> tTerminals = {{101}, {201}};
Cell<id_t> tShellIndices = {0, 0};
Consequence: Terminals assigned to wrong thin shell → incorrect current distribution.
Introduction
The homology module provides computational topology tools for analyzing and processing finite element meshes, with a focus on electromagnetic field simulations in multiply-connected domains.
Primary use case: Generating topological cuts for enforcing current constraints in magnetostatic h-φ formulations.
What is a Cohomology Cut?
In multiply-connected domains (e.g., toroidal magnets), the magnetic scalar potential φ must satisfy Ampere's law:
Where C is any closed loop encircling a conductor carrying current I. To enforce this in FEM:
- Compute cohomology generator: Identifies topological "hole" in the mesh
- Convert to thin cut: Minimal surface where φ is discontinuous
- Duplicate nodes: Create separate DOFs for φ⁺ and φ⁻ on cut surface
- Enforce jump condition: [φ] = φ⁺ - φ⁻ = I via static condensation
Location: src/homology/ CMake Target: belfem_homology
Module Architecture
File Organization
src/homology/
├── Entry Point
│ └── cl_CutFactory.{hpp,cpp}
│
├── Computational Core
│ ├── cl_Cohomology.{hpp,cpp}
│ ├── cl_Homology.{hpp,cpp}
│ ├── cl_SimplicialComplex.{hpp,cpp}
│ └── fn_Smith.{hpp,cpp}
│
├── Data Structures
│ ├── cl_Chain.{hpp,cpp}
│ ├── cl_Cochain.{hpp,cpp}
│ ├── cl_CutData.{hpp,cpp}
│ └── cl_CutSet.{hpp,cpp}
│
├── Processing
│ ├── cl_CutProcessor.{hpp,cpp}
│ ├── cl_CutProcessorManual.{hpp,cpp}
│ └── cl_InterfaceProcessor.{hpp,cpp}
│
├── Configuration
│ ├── cl_Topology.{hpp,cpp}
│ └── en_CutAlgorithm.hpp
│
└── Algorithms
└── cl_BeltedTree.{hpp,cpp}
Dependencies
- mesh: Mesh, Node, Element, Facet, SideSet classes
- linalg: Matrix, Vector for boundary/coboundary matrices
- containers: Cell, OrderedMap, Set for data structures
- sparse: Integer matrix solvers for Smith Normal Form
- comm: MPI support (computations currently serial)
CutFactory - Main Entry Point
Purpose
CutFactory is the primary user-facing class that orchestrates the entire cut generation pipeline.
File: cl_CutFactory.{hpp,cpp}
Constructor
CutFactory::CutFactory(
Mesh* aMesh,
Topology* aTopology,
Cell<Protoshell*>& aProtoshells,
const CutAlgorithm aAlgorithm,
const bool aUseEnrichment
);
Parameters:
- aMesh: Finite element mesh to process
- aTopology: Pre-analyzed mesh topology (identifies domains, interfaces)
- aProtoshells: Configuration for thin shell structures
- aAlgorithm: Cut generation algorithm (see Cut Algorithm Selection)
- aUseEnrichment: Use XFEM enrichment (experimental)
Neither argument has a default; both are passed explicitly.
⚠️ Ownership: CutFactory does not take ownership of aMesh or aTopology. Caller must ensure they outlive the factory.
Key Methods
Workflow Methods
void set_terminals(
const Cell<Cell<id_t>>& aTerminals,
const Cell<id_t>& aThinShellIndices
);
Sets up electrical terminal definitions for current injection/extraction points.
- aTerminals: Cell of node ID lists, one per terminal
- aThinShellIndices: Maps terminals to thin shell indices
Main execution pipeline. Performs:
- Thin shell preprocessing
- Cohomology computation
- Thick-to-thin cut conversion
- Node duplication
- Manifold cleanup
⚠️ Call exactly once. Calling run() multiple times is undefined behavior.
Result Retrieval
Cell<Node*>& abstract_nodes();
Returns abstract nodes (new degrees of freedom) created for cut discontinuities.
Usage: Assign these to FEM DOF manager for φ⁺ and φ⁻ values.
Returns generated cut surfaces as SideSet objects.
Usage: Mark as DomainType::Cut and apply discontinuity constraints in FEM assembly.
Cell<Node*>& orphaned_nodes();
Returns nodes not belonging to any element after node duplication.
Usage: Typically freed after FEM setup, but may be used for debugging.
Vector<id_t>& thin_shell_boundaries();
Returns IDs of boundaries created for thin shell sidesets.
Cell<Facet*>& thin_shell_facets(const id_t aID);
Returns facets for a specific thin shell ID.
Debugging
void save_debug_meshes();
Saves one debug mesh per cut, named cut_<index>.vtk (cl_CutProcessor.cpp:132-140). It does not write separate thick-cut, thin-cut or manifold files — those filenames appear in older documentation but no code emits them.
Usage Pattern
mesh::Topology tTopology(aMesh);
tTopology.run();
mesh::CutFactory tFactory(
aMesh,
&tTopology,
mProtoshells,
mesh::CutAlgorithm::PellikkaGeneralized,
false
);
Cell<Cell<id_t>> tTerminals = {{101, 102, 103}, {201, 202, 203}};
Cell<id_t> tShellIndices = {0, 0};
tFactory.set_terminals(tTerminals, tShellIndices);
tFactory.run();
for (mesh::SideSet* cut : tFactory.cuts()) {
cut->set_domain_type(mesh::DomainType::Cut);
}
for (
mesh::Node* node : tFactory.abstract_nodes()) {
aDofManager->add_abstract_dof(node);
}
Cohomology Computation
Purpose
Cohomology computes cohomology groups H^k = Ker(δ^k) / Im(δ^{k+1}) from a simplicial complex.
File: cl_Cohomology.{hpp,cpp}
Constructor
Cohomology::Cohomology(SimplicialComplex* aComplex, Mesh* aFullMesh);
Cohomology::Cohomology(SimplicialComplex* aComplex, Mesh* aFullMesh, BeltedTree* aTree);
Parameters:
- aComplex: Pre-constructed (and reduced) simplicial complex
- aFullMesh: Original full mesh (for node/element lookups)
- aTree: Belted tree for alternative algorithm (optional)
Both constructors leave the object with computed, cleaned generators, ready to use. The SimplicialComplex constructor computes all H^k groups; the BeltedTree constructor takes its H^1 generators from the tree.
Key Methods
void cohomologyGroupOfChainComplex();
Main computation method. Performs:
- Kernel/image decomposition of the coboundary maps
- Quotient group computation (Smith Normal Form)
Complex reduction happens beforehand on the SimplicialComplex; generator extraction is generatorsOfCohomology() and cleaning is clean(), both of which the constructor runs afterward.
⚠️ Called internally by the SimplicialComplex-based constructor. The BeltedTree constructor copies its generators from the tree instead. Do not call it yourself.
Computes quotient space H^k = Ker(δ^k) / Im(δ^{k+1}) via Smith Normal Form.
Note: Typically called internally by cohomologyGroupOfChainComplex().
void generatorsOfCohomology();
Extracts cohomology generators from Smith decomposition.
Output: Populates mGenerators with Cochain objects representing basis elements.
Cleans generator coefficients so every coefficient is in {-1, 0, 1}.
Method: Dispatches to clean_spfa(): SPFA feasibility certificate, greedy coboundary rectification, then remove_cut_pockets().
Note: Every constructor already calls clean(), so a constructed Cohomology always has unit-coefficient generators. CutFactory calls it a second time after updatekGeneratorsFromHomology(), because integer recombination of unit generators is generally non-unit again. Both runs are required: the second run is the only clean of the final generators, and the downstream CutData machinery can only represent coefficients in {-1, 0, 1}. See cohomology_algorithms.md, "Cleaning runs twice on the factory path".
Cell<Cell<Cochain*>>& get_Generators();
Returns cohomology generators organized by dimension k.
Structure: mGenerators[k] contains generators for H^k.
Cell<Matrix<int>>& get_CoboundaryMatrix();
Returns coboundary operator matrices δ^k.
Structure: mCoboundaryMatrix[k] is the δ^k : C^k → C^{k+1} matrix.
Visualization
void create_kGeneratorsField(uint k, Mesh* aMesh, string aLabel);
Creates mesh fields to visualize cohomology generators.
Parameters:
- k: Dimension (0 for nodes, 1 for edges, 2 for faces)
- aMesh: Mesh to attach fields to
- aLabel: Field name prefix
Output: Creates fields showing generator support with ±1 coefficients.
Usage Pattern
mesh::SimplicialComplex tComplex(aFullMesh, true);
tComplex.coreduce_complexPellikkaGeneralized();
mesh::Cohomology tCohomology(&tComplex, aFullMesh);
Cell<Cochain*>& generators = tCohomology.get_Generators()( 1 );
message(InfoLevel::Default,
"Found %d cohomology generators", generators.size());
tCohomology.create_kGeneratorsField(1, aFullMesh, "H1_gen");
Cochain* gen = generators(0);
for (auto& [edge_id, coeff] : gen->getSimplicesMap()) {
message(InfoLevel::Verbose,
"Edge %lu: coefficient %d", edge_id, coeff);
}
Homology Computation
Purpose
Homology computes homology groups H_k = Ker(∂_k) / Im(∂_{k+1}), the dual counterpart to cohomology.
File: cl_Homology.{hpp,cpp}
Use cases:
- Relative homology for terminal definitions
- Orientation computation for current flow direction
- Topological analysis of conductor geometries
Constructor
Homology::Homology(
Mesh* aMesh,
Cell<Cell<id_t>> aTerminals,
Cell<id_t> aThinShells
);
Homology::Homology(SimplicialComplex* aComplex, Mesh* aFullMesh);
Key Methods
void homologyGroupOfChainComplex();
Main computation method. Performs:
- Simplicial complex reduction
- Quotient group computation (Ker/Im decomposition)
- Generator extraction
void suggest_Homology(
Cell<Cell<id_t>> aTerminals,
Cell<id_t> aThinShellIndices
);
Suggests homology generators based on terminal connectivity.
Use case: Pre-seeds homology computation with physically meaningful cycles.
Cell<int> generators_orientation(Cell<Vector<real>>& aVectors);
Computes orientation of generators relative to specified direction vectors.
Returns: Cell of ±1 values indicating generator orientation.
Use case: Ensures current flow direction matches physical conventions.
void reorient_generators();
Flips generator orientation based on computed directions.
Cell<Cell<Chain*>>& get_Generators();
Returns homology generators organized by dimension k.
Cell<Matrix<int>>& get_BoundaryMatrix();
Returns boundary operator matrices ∂_k.
Usage Pattern
Cell<Cell<id_t>> tTerminals = {{101, 102}, {201, 202}};
Cell<id_t> tShellIndices = {0, 0};
mesh::Homology tHomology(aMesh, tTerminals, tShellIndices);
tHomology.homologyGroupOfChainComplex();
Cell<Chain*>& generators = tHomology.get_Generators()( 1 );
Cell<Vector<real>> tDirections = {{1, 0, 0}, {0, 1, 0}};
Cell<int> orientations = tHomology.generators_orientation(tDirections);
tHomology.reorient_generators();
Cut Algorithm Selection
Available Algorithms
Four algorithms are available via the CutAlgorithm enum:
File: en_CutAlgorithm.hpp
enum class CutAlgorithm : uint
{
Pellikka = 0,
CCR = 1,
BeltedTree = 2,
PellikkaGeneralized = 3,
UNDEFINED = 4
};
Algorithm Comparison
| Algorithm | Complexity | Performance | Manifold Issues | Recommendation |
| PellikkaGeneralized | unmeasured | unmeasured | Rare | ✅ Use this (production default) |
| Pellikka | unmeasured | unmeasured | Occasional | Legacy support |
| CCR | unmeasured | unmeasured | Frequent | Theory/debugging |
| BeltedTree | unmeasured | unmeasured | Rare | Alternative |
Implementation Details
PellikkaGeneralized (Giard et al., in preparation):
- Location: cl_SimplicialComplex.cpp — pGeneralizedCombine(), reduce_complexPellikkaGeneralized(), pGeneralizedCocombine(), coreduce_complexPellikkaGeneralized()
- Method:
- Downward pass: pReduce(k) for k from d to 1 (elementary collapses)
- Second pass (again from d down to 1): interleaves pGeneralizedCombine(k) with pReduce(k-1); the cochain variant runs both passes upward, k = 0 … d−1
- For cohomology: Reversed dimensions (k from 0 to d-1)
- Advantages:
- Near-linear complexity for large meshes
- Minimal Smith Normal Form computation
- Compact generator representations
Pellikka (Pellikka et al., 2013):
- Location: cl_SimplicialComplex.cpp — pReduce(), pCombine(), reduceOmit()
- Method:
- pReduce(): Elementary collapses (remove k-simplex and free (k-1)-face)
- pCombine(): Interior face reductions (merge neighboring simplices)
- reduceOmit(): Initialization pass
- Advantages:
- Well-tested in electromagnetic community
- Widely used in GetDP, Sparselizard
- Stable for moderate mesh sizes
CCR (Kaczynski et al., 2004):
- Location: cl_SimplicialComplex.cpp — reduce_complexCCR(), coreduce_complexCCR()
- Method: Classical chain complex reduction via acyclic matching
- Advantages:
- Mathematically pure approach
- Good for theoretical understanding
- Reliable for small meshes
- Disadvantages:
- Slowest for large meshes
- May produce non-manifold cuts requiring extensive cleanup
BeltedTree:
- Location: cl_BeltedTree.{hpp,cpp}
- Method: Spanning tree construction with belt identification
- Use case: Alternative approach when reduction methods struggle
Selection Guidance
tFactory = new CutFactory(
aMesh, aTopology, aProtoshells,
CutAlgorithm::PellikkaGeneralized
);
tFactory = new CutFactory(
aMesh, aTopology, aProtoshells,
CutAlgorithm::Pellikka
);
tFactory = new CutFactory(
aMesh, aTopology, aProtoshells,
CutAlgorithm::CCR
);
Benchmark results: none recorded — see README, "Cut Algorithms".
Topology Analysis
Purpose
Topology analyzes mesh structure to identify domain types, interfaces, and boundary conditions.
File: cl_Topology.{hpp,cpp}
Constructor
Topology::Topology(Mesh* aMesh);
Key Methods
Performs mesh topology analysis:
- Categorizes blocks by domain type (phi/non-phi)
- Identifies interfaces between domains
- Classifies sidesets by type
- Builds connectivity maps
DomainType block_domain_type(const id_t aBlockID) const;
Returns domain type for a given block ID.
Domain types:
- DomainType::Phi: Region where scalar potential φ is solved
- DomainType::Conductor: Conducting region (source of magnetic field)
- DomainType::Air: Air/vacuum region
- DomainType::Boundary: Domain boundary
- DomainType::Interface: Interface between domains
- DomainType::Cut: Cohomology cut surface
const Cell<id_t>& phi_block_ids() const;
const Cell<id_t>& nonphi_block_ids() const;
Returns lists of block IDs categorized by domain type.
const Cell<id_t>& interface_ids() const;
Returns IDs of interface sidesets.
Usage Pattern
mesh::Topology tTopology(aMesh);
tTopology.run();
for (id_t blockID : tTopology.phi_block_ids()) {
message(InfoLevel::Default,
"Block %lu: Phi domain", blockID);
}
for (id_t ssID : tTopology.interface_ids()) {
SideSet* interface = aMesh->sideset(ssID);
}
if (tTopology.block_domain_type(5) == DomainType::Conductor) {
}
Thin Shell Handling
Purpose
Protoshell configures thin shell structures for layered conductors (e.g., superconducting tapes, cables).
File: ../mesh/cl_Protoshell.hpp (header-only, namespace belfem)
Structure
class Protoshell
{
Cell<id_t> mSideSetIDs;
Cell<id_t> mTerminalIDs;
Vector<real> mThicknesses;
Cell<string> mMaterials;
Cell<id_t> mCurveIDs;
};
Key Methods
void add_sideset(const id_t aID);
void add_terminal(const id_t aID);
void add_thickness(const real aValue);
void add_material(const string& aName);
void add_curve(const id_t aID);
Adds components to protoshell configuration.
const Cell<id_t>& sideset_ids() const;
const Cell<id_t>& terminal_ids() const;
const Vector<real>& thicknesses() const;
Accessors for configuration data.
Usage Pattern
mesh::Protoshell* tShell = new mesh::Protoshell();
tShell->add_sideset(101);
tShell->add_sideset(102);
tShell->add_terminal(201);
tShell->add_terminal(202);
tShell->add_material("YBCO");
tShell->add_thickness(1e-6);
Cell<Protoshell*> tProtoshells = {tShell};
CutFactory tFactory(aMesh, &tTopology, tProtoshells, ...);
⚠️ Memory Management: CutFactory does not take ownership of Protoshell objects. Caller must delete them after use.
Cut Processing
Purpose
CutProcessor converts cohomology generators (thick cuts) to FEM-compatible thin cuts.
File: cl_CutProcessor.{hpp,cpp}
Key operations:
- Converts thick cuts (edge sets) to thin cuts (face sets)
- Duplicates nodes on cut surface
- Relinks elements to create separate DOFs for φ⁺ and φ⁻
- Creates abstract nodes for jump conditions
- Performs manifold cleanup
Constructor
CutProcessor::CutProcessor(
Mesh* aMesh,
Cohomology* aCohomology,
const Vector<id_t>& aPhiBlocks,
const Vector<id_t>& aNonPhiBlocks,
const Vector<id_t>& aPhiInterfaces,
const Vector<id_t>& aShellBlocks
);
Parameters:
- aMesh: Mesh to process
- aCohomology: Cohomology computation results
- aPhiBlocks: Block IDs for phi domains
- aNonPhiBlocks: Block IDs for non-phi domains
- aPhiInterfaces: Interface sideset IDs
- aShellBlocks: Thin shell block IDs
Key Methods
Cell<Node*>& abstract_nodes();
Returns abstract nodes created for cut DOFs.
id_t max_node_id() const;
id_t max_sideset_id() const;
Returns maximum IDs after node duplication and cut creation.
Use case: Ensures unique IDs when creating additional mesh entities.
void save_debug_meshes();
Exports one debug mesh per cut for visualization, as cut_<index>.vtk (cl_CutProcessor.cpp:139-147).
Internal Workflow
The cut processing pipeline (called internally by CutFactory):
- Thick cut extraction: Extract edges from cohomology generators
- Face identification: Find faces incident to thick cut edges
- Manifold filtering: Remove non-manifold artifacts (double pockets, etc.) — design of record; the step that actually runs is Cohomology::remove_cut_pockets(), see "Manifold Cleanup" below
- Node duplication: Duplicate nodes on positive side of cut
- Element relinking: Update element connectivity to duplicated nodes
- Abstract node creation: Create DOFs for [φ] = φ⁺ - φ⁻
- SideSet creation: Package cut as SideSet for FEM assembly
Manifold Cleanup
Design note — design of record, not wired in. manifold_filter_3d() and check_surface() do not exist anywhere in the tree, and no shipped code applies the three-phase scheme or the metric thresholds below. Tarjan articulation detection exists only as an archived prototype, archive/graph/fn_Graph_tarjan.{hpp,cpp}, which is outside the build — nothing in CMakeLists.txt references archive/.
What runs today is Cohomology::clean_spfa() → remove_cut_pockets() (cl_Cohomology.hpp:135,145), applied after SPFA rectification. Both are private — they run internally during cohomology computation and are not part of the caller-facing API. There is no CutProcessor method of either name.
The description is kept because it may be the design of record; it is marked so that no reader goes looking for the functions or tunes the thresholds. Its status is Gregory Giard's to confirm (doc/ai_collaboration_protocol.md §7.1). The fuller note is in cohomology_theory_and_implementation.md, "Post-Processing Cleanup".
Problem: Cohomology generators may produce non-manifold surfaces (edges shared by >2 faces).
Solution as designed: Multi-phase cleanup:
- Phase 1: Tarjan's algorithm to detect articulation points (necks)
- Phase 2: Region growing to detect embedded pockets
- Phase 3: BFS layering with metric validation
Metrics as designed:
- Cycle density > 0.3
- Compactness < 2.0
- Size < 20 faces
Implementation as shipped: Cohomology::clean_spfa() → remove_cut_pockets( const bool aFireTierA ) (cl_Cohomology.hpp:135,145). The designed filter above is not among the code paths that run.
Data Structures
Chain
Purpose: Represents formal sums of k-simplices with integer coefficients.
File: cl_Chain.{hpp,cpp}
Structure:
class Chain
{
OrderedMap<id_t, int> mData;
uint mDimension;
};
Key operations:
Chain operator+(const Chain& aOther) const;
Chain operator-(const Chain& aOther) const;
Chain operator*(const int aValue) const;
int inner_product(const Chain& aOther) const;
const OrderedMap<id_t, int>& data() const;
Usage:
Chain c1;
c1.add(101, 1);
c1.add(102, -1);
Chain c2;
c2.add(101, 2);
Chain c3 = c1 + c2;
Cochain
Purpose: Dual to Chain; represents cochains in the cochain complex.
File: cl_Cochain.{hpp,cpp}
Structure:
class Cochain
{
OrderedMap<index_t, int> mData;
uint mDimension;
};
Key operations:
Cochain operator+(const Cochain& aOther) const;
Cochain operator-(const Cochain& aOther) const;
Cochain operator*(const int aValue) const;
int evaluate(const Chain& aChain) const;
const OrderedMap<index_t, int>& data() const;
Usage:
Cochain phi;
phi.add(0, 1);
phi.add(1, -1);
Chain c = ...;
int value = phi.evaluate(c);
CutData
Purpose: Stores topology and metadata for a single cut.
File: cl_CutData.{hpp,cpp}
Structure:
class CutData
{
Cell<id_t> mEdgeIDs;
Cell<id_t> mElementIDs;
Cell<Facet*> mFacets;
DynamicBitset mEdgeBitset;
DynamicBitset mNodeBitset;
DomainType mType;
};
Key methods:
void determine_cut_case(Element* aElement);
Determines how cut intersects an element (which faces are on cut).
Use case: Guides node duplication and element relinking.
bool node_is_on_cut(const id_t aNodeID) const;
bool edge_is_on_cut(const id_t aEdgeID) const;
Fast membership tests using bitsets.
SimplicialComplex
Purpose: Builds and reduces simplicial chain/cochain complexes.
File: cl_SimplicialComplex.{hpp,cpp}
Structure:
class SimplicialComplex
{
Cell< Map< index_t, Chain * > > mChainsMap;
Cell< Map< index_t, Cochain * > > mCochainsMap;
Mesh * mMesh;
};
Key methods:
void create_complex(Mesh* aMesh, bool aPeriodicity);
Builds chain/cochain complex from flagged mesh entities.
Flags:
- Nodes: 0-simplices
- Edges: 1-simplices
- Faces: 2-simplices
- Elements: 3-simplices
void reduce_complexPellikkaGeneralized();
void reduce_complexPellikka();
void reduce_complexCCR();
Reduction algorithms (see Cut Algorithm Selection).
Cell<Matrix<int>> createMatrixFromBoundaryMap();
Cell<Matrix<int>> createMatrixFromCoboundaryMap();
Exports boundary/coboundary operators as integer matrices.
Structure: ∂_k : C_k → C_{k-1}, δ^k : C^k → C^{k+1}
Cut Sideset Creation
Purpose: Cohomology generators become mesh sidesets (the thin cuts).
Live path: CutFactory::run() calls compute_thin_cuts_and_duplicate_interface_nodes(), which constructs a CutProcessor. The CutProcessor constructor calls create_thin_cut_sidesets(), which delegates to CutData::add_thin_cut_sidesets_to_mesh(). Users do not call any of this directly; it runs inside CutFactory::run().
Note: The earlier SideSetFactory class and the CutFactory::create_sidesets_2d/3d methods that built sidesets directly from generator support were retired in 2026. CutProcessor/CutData is now the only sideset path.
InterfaceProcessor
Purpose: Handles node duplication and element relinking at domain interfaces (e.g., air-conductor boundaries).
File: cl_InterfaceProcessor.{hpp,cpp}
Structure:
class InterfaceProcessor
{
Mesh* mMesh;
Topology* mTopology;
Cell<Node*>& mAbstractNodes;
Map<id_t, InterfaceSet*> mSetsMap;
DynamicBitset* mIsAirBlock;
DynamicBitset* mIsFerroBlock;
};
Key methods:
InterfaceProcessor(
Mesh* aMesh,
Topology* aTopology,
Cell<Node*>& aAbstractNodes,
const uint aNumOriginalSideSets,
const id_t aMaxNodeID
);
Constructor performs full interface processing pipeline:
- Creates interface sets (groups of related interfaces)
- Connects sidesets to sets
- Duplicates nodes at interfaces
- Relinks elements to duplicated nodes
- Ties each duplicate to its original (InterfaceTreatment::TieWeight1) or decouples it when a coil touches the interface (InterfaceTreatment::Decouple)
Helper class: InterfaceSet
class InterfaceSet
{
Mesh* mMesh;
DynamicBitset* mBitset;
Cell<Element*> mElements;
Map<id_t, Node*> mOriginals;
Map<id_t, Node*> mDuplicates;
Cell<SideSet*> mSideSets;
};
Physical context: Duplicates the nodes of every sideset that separates a φ block (Air/Buffer/Ferro) from a non-φ block or Ferro (conductor-air, conductor-ferro, ferro-air), so the φ side and the other side carry distinct DOFs; coil-touching interfaces are decoupled (InterfaceTreatment::Decouple). It does not create abstract nodes; it receives the factory's list only to keep them out of the duplicate scan.
Usage:
Cell<Node*> tAbstractNodes;
mesh::InterfaceProcessor tProcessor(
aMesh, &tTopology, tAbstractNodes,
originalSidesetCount, maxNodeID
);
Note: Runs unconditionally from CutFactory::compute_thin_cuts_and_duplicate_interface_nodes(), thin shells or not.
CutSet
Purpose: Manages node duplication for a single cohomology cut.
File: cl_CutSet.{hpp,cpp}
Structure:
class CutSet
{
Mesh* mMesh;
Cell<Node*>& mNodeOriginals;
DynamicBitset* mBitset;
DynamicBitset* mNodeBitset;
Map<id_t, Node*> mNodeDuplicates;
};
Key methods:
CutSet(
Mesh* aMesh,
Cell<Node*>& aNodeOriginals,
const string& aHexString,
const index_t aNumberOfCuts
);
Constructor parses hex-encoded bitset describing cut topology.
bool test(const Node* aNode) const;
Tests if node is on the cut surface.
Returns: true if node index is set in cut bitset.
void create_duplicates(id_t& aMaxNodeID, Cell<Node*>& aAbstractNodes);
Creates duplicate nodes for positive side of cut.
Algorithm:
- Iterate through nodes on cut
- Create new Node with unique ID
- Store in mNodeDuplicates map
- Tie each duplicate to its original plus the abstract nodes of the cuts in this set's pattern (weight 1 each; the abstract nodes are created by CutProcessor::create_abstract_nodes())
Node* duplicate(Node* aNode);
Retrieves previously created duplicate for a node.
Returns: Duplicate node pointer (must exist in map).
DynamicBitset* node_bitset();
Map<id_t, Node*>& duplicate_map();
Accessors for internal data structures.
Usage:
mesh::CutSet tCutSet(
aMesh, nodeOriginals, hexBitset, numCuts
);
if (tCutSet.test(someNode)) {
}
tCutSet.create_duplicates(maxNodeID, abstractNodes);
mesh::Node* dup = tCutSet.duplicate(originalNode);
Bitset encoding: Uses DynamicBitset with hex string serialization for efficient cut pattern storage and transmission.
Note: This is an internal class used by CutProcessor during thick-to-thin conversion.
Algorithm Classes and Functions
BeltedTree
Purpose: Alternative algorithm for cohomology computation using spanning tree construction with belt identification.
File: cl_BeltedTree.{hpp,cpp}
Structure:
class BeltedTree
{
Cell<Chain*> m1HomologyGenerators;
Cell<Cochain*> m1CohomologyGenerators;
Cell<Edge*> mBeltFasteners;
Cell<index_t> mTree;
SimplicialComplex* mSimplicialComplex;
Mesh* mMesh;
};
Algorithm concept:
The belted tree method constructs cohomology generators by:
- Building a spanning tree of the simplicial complex
- Identifying "belts" (cycles not in the tree) corresponding to homology generators
- Selecting "belt fasteners" (edges connecting tree to belts)
- Computing dual cochains from the fastener configuration
Key methods:
BeltedTree(
Mesh* aMesh,
SimplicialComplex* aSimplicialComplex,
Cell<Chain*> a1HomologyGenerators
);
Constructor requires pre-computed H_1 homology generators.
Precondition: Homology must be computed first.
void select_belt_fasteners();
Identifies edges that connect the spanning tree to independent cycles.
Algorithm: For each homology generator (1-cycle), find minimal edge set that completes the cycle when added to the tree.
Constructs spanning tree of the 1-skeleton (edge graph).
Method: Breadth-first search or depth-first search from arbitrary root node.
void compute_cohomology();
Computes H^1 cochains from belt fastener configuration.
Duality: Each belt fastener corresponds to a cohomology generator via Poincaré duality.
Cell<Cochain*>& get_cohomology();
Returns computed cohomology generators.
Visualization:
void create_TreeField(Mesh* tEdgeMesh, string tFieldName);
void create_cohomologyField(Mesh* tMeshEdge);
Creates mesh fields to visualize spanning tree and cohomology generators.
Usage pattern:
mesh::Homology tHomology(tComplex, aFullMesh);
Cell<Chain*>& h1_gens = tHomology.get_Generators()( 1 );
mesh::BeltedTree tTree(aMesh, tComplex, h1_gens);
tTree.select_belt_fasteners();
tTree.create_tree();
tTree.compute_cohomology();
Cell<Cochain*>& cohom_gens = tTree.get_cohomology();
When to use:
- Alternative to reduction-based methods (Pellikka, CCR)
- Useful when homology generators have specific physical meaning
- Educational: Explicit geometric interpretation of duality
Comparison to other algorithms:
- Advantage: Direct geometric construction (easy to visualize)
- Disadvantage: Requires pre-computed homology (extra step)
- Performance: unmeasured (no timing comparison recorded)
Selection in CutFactory:
mesh::CutFactory tFactory(
aMesh, &tTopology, tProtoshells,
mesh::CutAlgorithm::BeltedTree
);
fn_Smith - Smith Normal Form Functions
Purpose: Integer matrix decomposition for computing homology/cohomology groups.
File: fn_Smith.{hpp,cpp}
Mathematical background:
The Smith Normal Form decomposes an integer matrix A into:
Where:
- Q, R are unimodular matrices (det = ±1)
- D is diagonal matrix with d_1 | d_2 | ... | d_r (divisibility chain)
- Diagonal entries d_i are invariant factors
- Number of nonzero entries r is the rank
Homology application:
Given boundary operator ∂_k : C_k → C_{k-1} with matrix representation A:
- Ker(∂_k) dimension = n - r (n = number of k-chains)
- Im(∂_{k+1}) dimension = r
- H_k rank (Betti number) = dim(Ker) - dim(Im)
- Torsion subgroup = {coefficients d_i where d_i > 1}
Core functions:
std::tuple<Matrix<int>, Matrix<int>, uint>
rowEchelon(Matrix<int>& aMat);
Computes row echelon form of integer matrix.
Returns:
- Q: Row transformation matrix
- Q_: Inverse of Q
- rank: Number of nonzero rows
Algorithm: Euclidean-style elimination in exact integer arithmetic: pivot on the smallest non-zero entry, subtract integer quotients of rows (floor(a/b)), swap; no fractions ever appear.
std::tuple<Matrix<int>, Matrix<int>>
kernelImage(Matrix<int>& aMat);
Computes kernel and image of integer matrix.
Returns:
- Kernel basis (columns span Ker(A))
- Image basis (columns span Im(A))
Method: Row echelon form followed by basis extraction.
std::tuple<Matrix<int>, Matrix<int>, Matrix<int>, Matrix<int>, uint, uint>
smithForm(Matrix<int>& aMat);
Main Smith Normal Form decomposition.
Returns:
- Q: Left transformation matrix
- Q_: Inverse of Q
- R: Right transformation matrix
- R_: Inverse of R
- rank: Number of nonzero diagonal entries
- Number of rows/columns
Algorithm:
- Row reduction to echelon form
- Column reduction to eliminate above-diagonal entries
- Iterative divisibility checking (ensure d_i | d_{i+1})
- Minimize diagonal entries via elementary operations
Helper functions:
std::pair<uint, uint> minNonzero(Matrix<int>& aMat, const uint k);
Finds smallest nonzero entry in submatrix (optimization for faster reduction).
void moveMinNonzero(Matrix<int>& aMat, Matrix<int>& aQ, Matrix<int>& aQ_,
Matrix<int>& aR, Matrix<int>& aR_, const uint k);
Swaps rows/columns to move minimal element to pivot position.
std::tuple<bool, uint, uint, int>
checkForDivisibility(Matrix<int>& aMat, const uint k);
Checks whether the pivot B[k,k] divides every entry of the trailing submatrix B[k+1:end, k+1:end] (Kaczynski et al., Smith-form step); returns the first offending (row, col, quotient).
Returns: (divisible, row_index, col_index, violating_entry)
void partSmithForm(Matrix<int>& aMat, Matrix<int>& aQ, Matrix<int>& aQ_,
Matrix<int>& aR, Matrix<int>& aR_, const uint k);
Performs partial Smith form reduction for diagonal entry k.
Method: Euclidean algorithm to eliminate non-divisible entries.
Matrix<int> SolveInt(Matrix<int> aMat, Matrix<int>& aVec);
Solves integer linear system A*x = b using Smith decomposition.
Returns: Integer solution vector (if exists).
Usage in homology:
Cell<Matrix<int>> boundary = tComplex->createMatrixFromBoundaryMap();
auto [Q, Q_, R, R_, rank, size] = smithForm(boundary[k]);
Performance notes:
- Complexity: O(n³) for dense matrices (n = dimension)
- Optimization: Uses smallest-entry pivoting for faster convergence
- Integer overflow: No protection for very large matrices (TODO: arbitrary-precision)
- Sparsity: Does not exploit sparsity (future improvement)
Elementary operations:
The following operations preserve equivalence and update transformation matrices:
- rowExchange(i, j): Swap rows i and j
- columnExchange(i, j): Swap columns i and j
- rowMultiply(i): Multiply row i by -1
- columnMultiply(i): Multiply column i by -1
- rowAdd(i, j, q): Add q times row j to row i
- columnAdd(i, j, q): Add q times column i to column j
Corresponding operations (with transformation tracking):
- rowExchangeOperation(): Updates Q, Q_, and matrix
- columnExchangeOperation(): Updates R, R_, and matrix
- rowAddOperation(): Updates Q, Q_, and matrix
- columnAddOperation(): Updates R, R_, and matrix
Example:
Matrix<int> boundary2(4, 6);
auto [Q, Q_, R, R_, rank, dim] = smithForm(boundary2);
uint betti = dim - rank;
message(InfoLevel::Default,
"H_2 Betti number: %u", betti);
Matrix<int> generators(R.n_rows(), betti);
uint col = 0;
for (uint i = rank; i < dim; ++i) {
for (uint row = 0; row < R.n_rows(); ++row) {
generators(row, col) = R(row, i);
}
col++;
}
Thread safety: ⚠️ Not thread-safe on shared inputs: modifies the input matrix in place.
Common Patterns
Pattern 1: Basic Cut Generation
void generate_cuts(Mesh* aMesh)
{
mesh::Topology tTopology(aMesh);
tTopology.run();
Cell<mesh::Protoshell*> tProtoshells;
mesh::CutFactory tFactory(
aMesh,
&tTopology,
tProtoshells,
mesh::CutAlgorithm::PellikkaGeneralized
);
tFactory.run();
for (mesh::SideSet* cut : tFactory.cuts()) {
cut->set_domain_type(mesh::DomainType::Cut);
message(InfoLevel::Default,
"Cut %lu: %u facets",
cut->id(), cut->number_of_facets());
}
}
Pattern 2: Terminal-Driven Cuts
void generate_terminal_cuts(Mesh* aMesh)
{
mesh::Topology tTopology(aMesh);
tTopology.run();
Cell<Cell<id_t>> tTerminals;
tTerminals(0) = {101, 102, 103};
tTerminals(1) = {201, 202, 203};
Cell<id_t> tShellIndices = {0, 0};
mesh::CutFactory tFactory(
aMesh, &tTopology, {},
mesh::CutAlgorithm::PellikkaGeneralized
);
tFactory.set_terminals(tTerminals, tShellIndices);
tFactory.run();
message(InfoLevel::Default,
"Generated %lu cuts",
tFactory.cuts().size());
}
Pattern 3: Direct Cohomology Computation
void compute_cohomology_groups(Mesh* aMesh)
{
mesh::SimplicialComplex tComplex(aMesh, true);
tComplex.coreduce_complexPellikkaGeneralized();
mesh::Cohomology tCohomology(&tComplex, aMesh);
Cell<Cochain*>& generators = tCohomology.get_Generators()( 1 );
tCohomology.create_kGeneratorsField(1, aMesh, "H1");
for (uint i = 0; i < generators.size(); ++i) {
Cochain* gen = generators(i);
message(InfoLevel::Default,
"Generator %u: %lu edges",
i, gen->getSimplicesMap().size());
}
}
Pattern 4: Algorithm Performance Comparison
void benchmark_algorithms(Mesh* aMesh)
{
mesh::Topology tTopology(aMesh);
tTopology.run();
Cell<mesh::Protoshell*> tProtoshells;
CutAlgorithm algorithms[] = {
CutAlgorithm::PellikkaGeneralized,
CutAlgorithm::Pellikka,
CutAlgorithm::CCR,
CutAlgorithm::BeltedTree
};
for (auto alg : algorithms) {
Timer timer;
mesh::CutFactory tFactory(aMesh, &tTopology, tProtoshells, alg);
tFactory.run();
uint64_t elapsed = timer.stop();
message(InfoLevel::Default,
"Algorithm %d: %lu ms, %lu cuts",
static_cast<uint>(alg), elapsed, tFactory.cuts().size());
}
}
Pattern 5: Thin Shell with Terminals
void process_thin_shell_coil(Mesh* aMesh)
{
mesh::Topology tTopology(aMesh);
tTopology.run();
mesh::Protoshell* tShell = new mesh::Protoshell();
tShell->add_sideset(101);
tShell->add_sideset(102);
tShell->add_terminal(201);
tShell->add_terminal(202);
tShell->add_material("YBCO");
tShell->add_thickness(1e-6);
Cell<mesh::Protoshell*> tProtoshells = {tShell};
Cell<Cell<id_t>> tTerminals = {{301, 302}, {401, 402}};
Cell<id_t> tShellIndices = {0, 0};
mesh::CutFactory tFactory(
aMesh, &tTopology, tProtoshells,
mesh::CutAlgorithm::PellikkaGeneralized
);
tFactory.set_terminals(tTerminals, tShellIndices);
tFactory.run();
delete tShell;
for (mesh::SideSet* cut : tFactory.cuts()) {
}
}
Troubleshooting
Problem: Non-Manifold Cut Surfaces
Symptom: Error message "Cut surface is non-manifold" or FEM assembly fails.
Cause: Cohomology generator contains "double pockets" or other topological defects.
Solution:
- Try different algorithm:
CutFactory tFactory(..., CutAlgorithm::Pellikka);
- Enable debug output: For curve meshes, CutFactory::save_curve_debug_meshes() is public (cl_CutFactory.hpp:184) and writes curve_<id>.vtk. Inspect either family in ParaView.
- Inspect the cleanup that actually runs:
- The shipped path is Cohomology::clean_spfa() → remove_cut_pockets() (cl_Cohomology.hpp:135,145), not the metric-threshold filter described under "Manifold Cleanup" — that scheme is a design of record with no implementation, so there are no cycle-density or size thresholds to tune. See the design note there before going looking for them.
Problem: Incorrect Number of Cuts
Symptom: Expected N cuts, got M cuts (M ≠ N).
Cause: Mesh topology doesn't match expected domain connectivity.
Diagnosis:
mesh::Topology tTopology(aMesh);
tTopology.run();
message(InfoLevel::Default,
"Phi blocks: %lu",
tTopology.phi_block_ids().size());
message(InfoLevel::Default,
"Interfaces: %lu",
tTopology.interface_ids().size());
Solution:
- Check block domain types: Ensure phi/non-phi classification is correct
- Verify mesh connectivity: Use mesh viewer to confirm expected topology
- Terminal hints: Provide explicit terminals via set_terminals() to guide computation
Problem: Slow Cohomology Computation
Symptom: run() takes minutes/hours for moderate mesh sizes.
Cause: Using suboptimal algorithm (CCR or original Pellikka).
Solution:
CutFactory tFactory(..., CutAlgorithm::PellikkaGeneralized);
If still slow:
- Profile: Use Profiler to identify hotspot
- Mesh quality: Simplify mesh if possible (remove small features)
- MPI: Current implementation is serial (future parallelization planned)
Problem: Abstract Nodes Not Created
Symptom: tFactory.abstract_nodes().size() == 0 after run().
Cause: No cuts were generated (topology is simply-connected).
Diagnosis:
message(InfoLevel::Default,
"Num cuts: %lu", tFactory.cuts().size());
Solution:
- Check mesh: Verify domain is multiply-connected (e.g., toroid, solenoid)
- Topology: Ensure phi/non-phi domains are correctly identified
- Terminals: Cuts may not be needed if terminals span all connections
Problem: Jump Condition Not Satisfied
Symptom: FEM solution violates Ampere's law (∮ H·dl ≠ I).
Cause: Cut orientation or DOF assignment incorrect.
Diagnosis:
- Visualize cut:
tCohomology.create_kGeneratorsField(1, aMesh, "H1_gen");
- Check abstract node count:
BELFEM_ERROR(tFactory.abstract_nodes().size() == tFactory.cuts().size(),
"Abstract node count mismatch");
Solution:
- Orientation: Use Homology::generators_orientation() to ensure consistent direction
- Static condensation: Verify transformation matrix T incorporates [φ] = I correctly
- Gauge: Confirm φ is gauged (set to zero at reference point)
Usage Examples
Example 1: Toroidal Magnet
{
Mesh* tMesh = new Mesh("toroid.exo");
mesh::Topology tTopology(tMesh);
tTopology.run();
mesh::CutFactory tFactory(
tMesh,
&tTopology,
{},
mesh::CutAlgorithm::PellikkaGeneralized
);
Timer timer;
tFactory.run();
uint64_t elapsed = timer.stop();
"Generated %lu cuts in %lu ms",
tFactory.cuts().size(), elapsed);
for (mesh::SideSet* cut : tFactory.cuts()) {
cut->set_domain_type(mesh::DomainType::Cut);
}
fem::DofManager* tDofManager = new fem::DofManager(tMesh);
for (
mesh::Node* node : tFactory.abstract_nodes()) {
tDofManager->add_abstract_dof(node, "phi");
}
delete tDofManager;
delete tMesh;
return 0;
}
Example 2: Superconducting Cable
{
Mesh* tMesh = new Mesh("cable.h5");
mesh::Topology tTopology(tMesh);
tTopology.run();
mesh::Protoshell* tShell = new mesh::Protoshell();
tShell->add_sideset(101);
tShell->add_terminal(201);
tShell->add_terminal(202);
tShell->add_material("BSCCO");
tShell->add_thickness(0.2e-3);
Cell<mesh::Protoshell*> tProtoshells = {tShell};
Cell<Cell<id_t>> tTerminals;
tTerminals(0) = {1001, 1002, 1003};
tTerminals(1) = {2001, 2002, 2003};
Cell<id_t> tShellIndices = {0, 0};
mesh::CutFactory tFactory(
tMesh, &tTopology, tProtoshells,
mesh::CutAlgorithm::PellikkaGeneralized
);
tFactory.set_terminals(tTerminals, tShellIndices);
tFactory.run();
message(InfoLevel::Default,
"Cuts: %lu", tFactory.cuts().size());
message(InfoLevel::Default,
"Abstract DOFs: %lu",
tFactory.abstract_nodes().size());
delete tShell;
delete tMesh;
return 0;
}
Example 3: Performance Analysis
void analyze_cohomology_performance(Mesh* aMesh)
{
Timer t1;
mesh::SimplicialComplex* tComplex = new mesh::SimplicialComplex(aMesh, false);
uint64_t time_build = t1.stop();
message(InfoLevel::Default,
"Complex construction: %lu ms", time_build);
message(InfoLevel::Default,
" 0-simplices: %u", tComplex->number_of_ksimplices(0));
message(InfoLevel::Default,
" 1-simplices: %u", tComplex->number_of_ksimplices(1));
message(InfoLevel::Default,
" 2-simplices: %u", tComplex->number_of_ksimplices(2));
message(InfoLevel::Default,
" 3-simplices: %u", tComplex->number_of_ksimplices(3));
Timer t2;
tComplex->reduce_complexPellikkaGeneralized();
uint64_t time_reduce = t2.stop();
message(InfoLevel::Default,
"Reduction: %lu ms", time_reduce);
message(InfoLevel::Default,
" 0-simplices: %u", tComplex->number_of_ksimplices(0));
message(InfoLevel::Default,
" 1-simplices: %u", tComplex->number_of_ksimplices(1));
message(InfoLevel::Default,
" 2-simplices: %u", tComplex->number_of_ksimplices(2));
message(InfoLevel::Default,
" 3-simplices: %u", tComplex->number_of_ksimplices(3));
Timer t3;
mesh::Cohomology tCohomology(tComplex, aMesh);
uint64_t time_cohomology = t3.stop();
message(InfoLevel::Default,
"Cohomology: %lu ms", time_cohomology);
message(InfoLevel::Default,
" H^0: %lu", tCohomology.get_Generators()( 0 ).size());
message(InfoLevel::Default,
" H^1: %lu", tCohomology.get_Generators()( 1 ).size());
message(InfoLevel::Default,
" H^2: %lu", tCohomology.get_Generators()( 2 ).size());
delete tComplex;
delete tFlagged;
}
Summary
The homology module provides a complete computational topology toolkit for finite element electromagnetics:
| Component | Purpose | Entry Point |
| CutFactory | Orchestrates cut generation | Main user class |
| Cohomology | Computes H^k groups | Direct access for advanced use |
| Homology | Computes H_k groups | Dual to cohomology |
| SimplicialComplex | Reduction engine | Internal use |
| Topology | Mesh analysis | Prerequisite for cuts |
| Protoshell | Thin shell config | Specialized geometries |
Recommended Workflow
- Load mesh and ensure proper block/sideset labeling
- Analyze topology with Topology::run()
- Create CutFactory with PellikkaGeneralized algorithm
- Set terminals if using current constraints
- Run pipeline via CutFactory::run()
- Extract results (cuts, abstract nodes)
- Integrate with FEM (DOF assignment, jump conditions)
Performance Best Practices
- Use PellikkaGeneralized (the production default; no timing comparison is recorded)
- Enable debug output only when troubleshooting (overhead ~10%)
- Pre-filter mesh to remove unnecessary entities before cohomology computation
- For very large meshes (>10^7 elements), consider domain decomposition (future feature)
Further Reading
Document version: 1.0 Last updated: 2026-08-31 Author: Claude Code (claude.ai/code)