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->get_simplices_map()) {
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 the positive side of the cut. Each cut set assigns copy ids while walking its originals in ascending id order, with the non-periodic originals first and the periodic pairs in the order encountered by that walk. The sets are processed in sorted order of their pattern hex strings. collect_duplicates then appends the abstract nodes and the copies in ascending copy-id order, so a copy's position never depends on the hash order of a duplicate map
- 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: A chain is a formal sum of k-simplices with integer coefficients, keyed by the mesh entity's index (Node::index(), Edge::index(), Face::index() for the triangles of a 3D mesh, Element::index() for the top-dimensional cells), never by its id. It can also carry its own boundary and coboundary chains, which the simplicial-complex builder fills alongside it.
File: cl_Chain.{hpp,cpp}
Structure:
class Chain
{
int mDim ;
Mesh * mMesh ;
OrderedMap< index_t, int > mSimplicesMap ;
Chain * mBoundary ;
Chain * mCoboundary ;
};
The constructor Chain( aDim, aMesh, aIsBound, aIsCobound ) allocates mBoundary unless aDim == 0 or aIsBound is set, and allocates mCoboundary unless aDim == 3 or aIsCobound is set. The two flags mark a chain that is someone's boundary or coboundary. A chain built with both flags false therefore owns a boundary (unless aDim == 0) and a coboundary (unless aDim == 3). Each of those owns at most one chain in the opposite direction (the boundary's coboundary and the coboundary's boundary), and those own nothing. The destructor deletes both members.
Key operations: operator+, operator-, and operator* modify this in place. operator() changes neither chain and returns the pairing. None of the operators returns a new chain.
void operator+( Chain * aChain ) ;
void operator-( Chain * aChain ) ;
void operator*( const int aMult ) ;
int operator()( Chain * aChain ) ;
void add_simplex_to_chain( const index_t aIndex, const int aCoeff ) ;
void remove_simplex_from_chain( const index_t aIndex ) ;
void add_chain_to_chain( Chain * aChain, int aCoeff ) ;
void remove_chain_from_chain( Chain * aChain ) ;
OrderedMap< index_t, int > & get_simplices_map() ;
int get_coefficient( const index_t aIndex ) ;
void set_coefficient( const index_t aIndex, const int aCoeff ) ;
int get_dim() ;
Chain * get_boundary() ;
Chain * get_coboundary() ;
void add_simplex_to_boundary( index_t aIndex, const int aCoeff ) ;
void add_simplex_to_coboundary( index_t aIndex, const int aCoeff ) ;
void print() ;
add_chain_to_chain refuses a dimension mismatch (it prints and returns). Otherwise, it also propagates into mBoundary and mCoboundary wherever this chain owns them, so a generator assembled from other chains keeps its boundary consistent. It adds the stored boundary and coboundary of aChain; it does not recompute them from the coefficients. Therefore, aChain must own every member that this chain owns: a chain built with a flag set has a null member in that direction. remove_chain_from_chain does not propagate.
Usage:
Chain tC1( 1, tMesh, false, false ) ;
tC1.add_simplex_to_chain( tEdgeA->index(), 1 ) ;
tC1.add_simplex_to_chain( tEdgeB->index(), -1 ) ;
Chain tC2( 1, tMesh, false, false ) ;
tC2.add_simplex_to_chain( tEdgeA->index(), 2 ) ;
tC1 + &tC2 ;
int tPairing = tC1( &tC2 ) ;
for ( const auto & [ tIndex, tCoeff ] : tC1.get_simplices_map() )
{
}
Cochain
Purpose: A Cochain is dual to Chain: a formal sum of k-cosimplices keyed by the same simplex indices. The generators that Cohomology hands out are Cochain *.
File: cl_Cochain.{hpp,cpp}
Structure: The structure is identical to Chain, but uses Cochain * boundary and coboundary members. The constructor and destructor follow the same rules.
Key operations: The in-place API corresponds to Chain, with cochain in the method names, with one difference: add_simplex_to_cochain and add_cochain_to_cochain return immediately for a zero coefficient. A zero-coefficient add_cochain_to_cochain therefore neither checks the dimension nor propagates. The Chain versions do not return early.
void operator+( Cochain * aCochain ) ;
void operator-( Cochain * aCochain ) ;
int operator()( Chain * aChain ) ;
void add_simplex_to_cochain( const index_t aIndex, const int aCoeff ) ;
void remove_simplex_from_cochain( const index_t aIndex ) ;
void add_cochain_to_cochain( Cochain * aCochain, const int aCoeff ) ;
void remove_cochain_from_cochain( Cochain * aCochain ) ;
OrderedMap< index_t, int > & get_simplices_map() ;
int get_coefficient( const index_t aIndex ) ;
void set_coefficient( const index_t aIndex, const int aCoeff ) ;
int get_dim() ;
Cochain * get_coboundary() ;
Cochain * get_boundary() ;
void add_simplex_to_coboundary( index_t aIndex, const int aCoeff ) ;
void add_simplex_to_boundary( index_t aIndex, const int aCoeff ) ;
bool isBound() ;
bool isCobound() ;
void print() ;
There is no scalar operator* on Cochain. add_cochain_to_cochain accumulates (this += aCoeff * aCochain), so to scale a cochain, add it with the factor to a new, empty cochain of the same dimension. Cohomology recombines generators this way into fresh cochains.
Usage:
Cochain tPhi( 1, tMesh, false, false ) ;
tPhi.add_simplex_to_cochain( tEdgeA->index(), 1 ) ;
tPhi.add_simplex_to_cochain( tEdgeB->index(), -1 ) ;
Chain * tC = ... ;
int tValue = tPhi( tC ) ;
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. A node is copied once per interface set, except on the conductor side. Each tie-weight set whose master block is a conductor takes its copy from one shared map keyed by the node's own id. A final sweep installs that copy in every element of every conductor block that still holds the original. Two conductor blocks that meet the air along a common line therefore reference the same copy, and the conductor edges leaving that line along their common face are created once. Without this sharing, each block had its own copy, so the edges were doubled and untied, and tangential h could jump across the air-adjacent row of the conductor|conductor face: a lossless sheet. Ferro, buffer and air-master sets keep private per-set copies; a ferro|ferro sideset stays split, as its material jump requires. On a periodic mesh, the copies of a periodic node pair are also tied to each other, either within one interface set or across two; this pairing runs at the end of node duplication, before the relink. Across sets, a pair is tied only when both sets have the same treatment and, for tie-weight sets, the same master domain type (pair_within). When no compatible set holds the partner's copy, because it lies only on a buffer|air, ferro|air, or air-master set against a conductor|air set, or only on a decoupled coil set against a tie-weight set, setup refuses the pair with a message naming both nodes and both domain types or treatments. The reason is the tie itself: it identifies like unknowns, h on conductor edges and φ elsewhere (Messe et al. 2023, §1), and cannot represent a change of formulation across the plane. The rule is applied to the set's master domain type, so it also refuses ferro against buffer, although both are φ blocks. A periodic plane that coincides with a change of domain type, or that a coil touches on one side only, can be avoided by shifting the plane to a section where the interface has one domain type and the same treatment on both sides. The plain "not duplicated in any interface set" refusal remains for a partner that lies on no interface set at all. Two rules come first. First, every interface sideset, that is, every sideset with a facet this processor duplicates at (one side air or ferro, not both air), must have one master domain type on all of its facets that have a slave; a sideset without such a facet is left alone. A sideset is admitted to an interface set, and the set is classed as shared or private and paired across a periodic plane, by its first facet, as in 0.9.2: a native Gmsh mesh builds one sideset per geometric entity, so its facets cannot differ in kind. refuse_mixed_master_types checks, after CutProcessor has run, every sideset that existed before it (generated cut sidesets are not examined, and thin-shell sidesets have no slave by then), before any sideset is admitted and before any interface copy exists. If it finds a mismatch, it reports the sideset id, the two domain types (name and enumerator value, since several types print under one name), the two facets with their master elements, and the remedy: split the sideset so that each piece has one master domain type. A conductor|air facet beside an air|air facet is refused the same way, since the air|air facet's master is air. A sideset over several blocks of one domain type stays accepted. A reused .bfm (one whose checksum and configuration tag match the deck) skips all of this: it replays the enrichment it was written with. Second, interface-copy numbering is a function of the mesh alone. Copies are allocated one set at a time, in set order (the sorted hex strings of the block-index bitsets of each set's block pair, a fixed function of the mesh's block order), and within each set in ascending order of the original id. A shared conductor copy is allocated by the first set that reaches the original. Copies are appended to the mesh in ascending copy-id order, which is the allocation order. Periodic pairs are backed up set by set: first, all pairs tied within one set, in ascending order of the original id; then the next set; finally, pairs tied across sets, again set by set and in ascending order. The maps that hold originals and copies are hash maps, but their iteration order never reaches the mesh. Therefore, interface-copy ids and positions are the same on every platform for the same incoming node order. The cut copies, which precede the interface copies in the mesh, are appended the same way: their ids come from a sorted walk of each cut set's originals (see "CutSet"), and CutProcessor::collect_duplicates sorts the kept copies by id after the abstract nodes before appending them. Therefore, no hash-map iteration order reaches a copy's position. Cut copies and interface copies occupy positions determined by the node order entering the cut processor. That is not yet a platform-independent node numbering. CutFactory renumbers the mesh with reverse Cuthill-McKee before the cohomology, and MaxwellFactory renumbers it again after enrichment, in the order that reaches the .bfm and the dof manager. symrcm sorts a node's neighbors by degree with std::sort, so equal degrees follow whatever order the C++ standard library's sort happens to leave them in. That behavior is reproducible on one toolchain but is not guaranteed across toolchains until the graph sort has a tie-break. The thin-shell copies were already appended in ascending index order of their originals. This does not cover a reused .bfm, which replays the numbering with which it was written, the dof manager's own ordering, or partitioned runs.
- 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 the cut in ascending id (CutProcessor::collect_nodes sorts the originals): the non-periodic originals first, then the periodic pairs in the order encountered by the walk
- Create a new Node with a unique ID in that order
- Store it in the mNodeDuplicates map (a hash map; its iteration order never reaches the mesh, because CutProcessor::collect_duplicates sorts the copies by id before appending them)
- 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_tree_field( Mesh * aEdgeMesh, const string aFieldName );
void create_cohomology_field( Mesh * aEdgeMesh );
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->get_simplices_map().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)