Module: src/mesh Purpose: Critical contracts, invariants, and rules for safe mesh usage
Date: 2026-01-16 Revision: 1.0
Overview
This document specifies hard contracts that must be respected when using the mesh module. Violating these contracts leads to undefined behavior, data corruption, or crashes.
Finalization Contracts
1. MPI Finalization Semantics
Contract:
- Before distribution: Only the master rank (mMasterProc, default 0) owns valid topology after finalize().
- After Distributor::run() / partial_mesh(): Each rank owns a local mesh that must be finalized independently.
- Non-master ranks before distribution: Accessing full topology on non-master ranks before distribution is undefined behavior.
Implementation detail (from cl_Mesh.cpp):
void Mesh::finalize() {
if (comm_rank() == mMasterProc) {
this->update_element_indices();
this->update_node_indices();
}
mIsFinalized = true;
this->set_block_ids();
this->compute_facet_orientations();
}
Safe usage:
#ifdef BELFEM_MPI
Mesh* globalMesh = nullptr;
if (comm_rank() == 0) {
globalMesh = new Mesh("mesh.msh");
globalMesh->finalize();
}
mesh::Distributor dist(globalMesh);
dist.run();
Mesh* localMesh = globalMesh;
if (comm_rank() != 0) {
localMesh = dist.partial_mesh();
}
if (comm_rank() == 0) { localMesh->finalize(); }
#endif
2. unfinalize() is NOT the Inverse of finalize()
Contract:
unfinalize() is cache invalidation, not rollback. It:
- Clears mElements and mFacets containers (derived lists)
- Selectively resets connectivity bitset
- Does not restore mesh to pre-finalize state
- Assumes elements will not change topology
Implementation detail (from cl_Mesh.cpp):
void Mesh::unfinalize() {
mElements.clear();
mFacets.clear();
reset_connectivity(Connectivity::NodeToElement);
mIsFinalized = false;
}
Warning: unfinalize() is intended for advanced workflows only (e.g., mesh modification before re-finalization). Do NOT use casually.
Dangerous example:
tMesh->finalize();
tMesh->unfinalize();
tMesh->partition(comm_size());
Safe example:
tMesh->finalize();
tMesh->unfinalize();
tMesh->create_edges();
tMesh->finalize();
Ownership and Container Contracts
3. Ownership vs. Derived Containers
Contract:
- Mesh owns all entity objects (Nodes, Elements, Edges, Faces, Facets).
- Some containers are derived views (e.g., mElements, mFacets) and may be cleared/rebuilt during finalize()/unfinalize().
- Do not store raw pointers to containers across finalize/unfinalize boundaries.
Primary ownership (from cl_Mesh.hpp):
Cell<mesh::Node*> mNodes;
Cell<mesh::Edge*> mEdges;
Cell<mesh::Face*> mFaces;
Cell<mesh::Block*> mBlocks;
Cell<mesh::SideSet*> mSideSets;
Derived containers (rebuilt from blocks/sidesets):
Cell<mesh::Element*> mElements;
Cell<mesh::Facet*> mFacets;
Safe pattern:
Cell<mesh::Element*>& elems = tMesh->elements();
}
Cell<mesh::Element*>* pElems = &tMesh->elements();
tMesh->finalize();
Index Stability Contract
4. Indices are NOT Stable Across Operations
Rule: Never store indices across finalize(), partition(), or Distributor::run(). IDs are stable; indices are not.
Why: Indices are reordered by:
- finalize() — assigns continuous 0-based indices
- partition() — reorders by MPI ownership
- Distributor::run() — creates new local ordering
- Graph algorithms (RCM, METIS) — bandwidth/partitioning reordering
Safe pattern:
Vector<id_t> importantNodeIDs = {100, 200, 300};
tMesh->partition(comm_size());
tMesh->finalize();
for (id_t nodeID : importantNodeIDs) {
}
Dangerous pattern:
Cell<index_t> nodeIndices;
nodeIndices.push(node->
index());
}
}
tMesh->partition(comm_size());
for (index_t idx : nodeIndices) {
}
int index
Definition Node.py:15
Connectivity Invalidation Contract
5. Connectivity Invalidation Rules
Contract: Cached connectivity becomes invalid after certain operations. Access after invalidation is undefined behavior.
| Operation | Invalidates Connectivity |
| create_edges() | Node↔Edge, Edge↔Element, Element↔Edge |
| create_faces() | Node↔Face, Face↔Element, Element↔Face |
| scale_mesh() | nothing in the mesh; only node/control-point coordinates change |
| Node duplication | Node↔Element, Node↔Node, Element↔Node |
| partition() | All ownership & adjacency |
| Distributor::run() | All (creates new mesh) |
| unfinalize() | Node*/Edge*/Face* connectivities (selective; ElementToNode kept) |
Safe pattern:
tMesh->finalize();
uint nElems = node->number_of_elements();
tMesh->create_edges();
tMesh->finalize_edges();
uint nEdges = node->number_of_edges();
Dangerous pattern:
uint nElems = node->number_of_elements();
tMesh->create_edges();
uint nElemsNew = node->number_of_elements();
Type Homogeneity Contract
6. Block Homogeneity Assumption
Assumption: Most FEM kernels assume blocks contain a single element type. While BELFEM allows heterogeneous blocks, you must handle element-type dispatch yourself.
Why this matters:
mesh::Block* block = tMesh->block(blockID);
ElementType type = block->element_type();
ShapeFunction* shape = new ShapeFunction(type);
}
Safe handling:
mesh::Block* block = tMesh->block(blockID);
bool isHomogeneous = true;
ElementType firstType = block->element(0)->type();
if (elem->type() != firstType) {
isHomogeneous = false;
break;
}
}
if (!isHomogeneous) {
ElementType type = elem->type();
}
}
Facet Lifetime Contract
7. Facets are Views, Not Standalone Geometry
Invariant: Facets wrap an owned lower-dimensional element and link to the master/slave elements they sit on.
From cl_Facet.hpp:
class Facet : public Vertex {
Element* mElement;
Element* mMaster;
Element* mSlave;
suint mMasterFaceID;
suint mSlaveFaceID;
};
Implications:
- Deleting a master element invalidates all facets referencing it
- Facet nodes are available directly via facet->node(k); the same nodes can be read from the master via master->get_nodes_of_facet(facet->index_on_master(), nodes)
- Facet orientation is relative to master element
Safe pattern:
mesh::SideSet* sideset = tMesh->sideset(sidesetID);
for (mesh::Facet* facet : sideset->facets()) {
uint masterFacetIdx = facet->index_on_master();
Cell<mesh::Node*> facetNodes;
master->get_nodes_of_facet(masterFacetIdx, facetNodes);
}
Both routes give the same nodes:
mesh::Facet* facet = sideset->facets()(0);
uint facetIdx = facet->index_on_master();
Cell<mesh::Node*> nodes;
master->get_nodes_of_facet(facetIdx, nodes);
Edge/Face Creation Order Contract
8. Edges Must Be Created Before Faces (3D Elements)
Rule: In 3D, create_edges() must be called before create_faces().
Why: Face DOFs often reference edge DOFs in Nédélec H(curl) and H(div) elements. Edge containers must exist before face creation.
Correct order:
tMesh->finalize();
tMesh->create_edges();
tMesh->finalize_edges();
tMesh->create_faces();
tMesh->finalize_faces();
Wrong order:
tMesh->finalize();
tMesh->create_faces();
tMesh->create_edges();
Parallel Ghost Layer Guarantees
9. Ghost Layer Completeness After Distribution
partial_mesh() is worker-only. It carries BELFEM_ERROR( mCommRank > 0, ... ) (cl_Mesh_Distributor.cpp:2663), so the root rank must not call it — the root keeps the mesh it started with and takes the communication tables instead (cl_FEM_Kernel.cpp:688-708).
Guarantees after Distributor::run() / partial_mesh():
- All elements have valid node pointers (owned or ghost)
- All ghost elements have complete connectivity
- Nodes are owned by lowest-rank proc containing them
- Sidesets may be incomplete unless explicitly redistributed
Checking ownership:
if (elem->owner() == comm_rank()) {
} else {
}
}
if (node->owner() == comm_rank()) {
} else {
}
}
Performance-Critical Contracts
10. ID Lookup vs. Index Access Performance
Performance note: Access via node(id_t) uses a hash map (O(1) average, O(N) worst case). Prefer index-based iteration in tight loops.
Slow (ID-based access in loop):
for (id_t nodeID : nodeIDs) {
}
Fast (index-based iteration):
Cell<mesh::Node*>& nodes = tMesh->nodes();
for (index_t i = 0; i < nodes.size(); ++i) {
}
Coordinate Modification Invalidation
11. What Breaks When Coordinates Change
Contract: Modifying node coordinates invalidates cached geometric data:
- Jacobians (element transformation matrices)
- Normals (facet orientations)
- Integration weights (quadrature points)
Safe pattern:
tMesh->scale_mesh(0.001);
node->set_coords(newX, newY, newZ);
Summary: Critical Rules for Claude Code
Paste this checklist into Claude Code guidance:
- ✓ Treat finalize() as establishing mesh invariants; do not access topology before it.
- ✓ Do not assume non-master MPI ranks have valid topology before distribution.
- ✓ Treat unfinalize() as cache invalidation, not rollback.
- ✓ Never store indices across finalize(), partition(), or Distributor::run().
- ✓ Assume blocks may be heterogeneous unless explicitly checked.
- ✓ Treat facets as views into elements, never standalone entities.
- ✓ Assume connectivities can be invalidated by topology changes.
- ✓ Prefer index-based iteration for performance-critical loops.
- ✓ Create edges before faces in 3D.
- ✓ Check owner() for all entities in MPI context.
End of Mesh Contracts and Invariants