Date: 2026-03-03 Module: src/mesh Purpose: Documentation of periodic boundary condition infrastructure in BELFEM
⚠️ API references in this document are stale and are being reviewed. Several function names used below do not exist in the tree (searched src/**/*.{hpp,cpp}): create_dofwise_periodicities_master(), collect_nodes_from_flags_12(), flag_periodic_entities_12() and match_faces(). Periodicity's actual surface is master_nodes() / slave_nodes() / master_edges() / … plus update() and set_entity_dependencies() (cl_Mesh_Periodicity.hpp:87-125), and the DOF-side entry point is DofData::create_dofwise_t_matrices_master().
The live path is Periodicity::update() + set_entity_dependencies(), driven from MaxwellFactory (cl_MaxwellFactory.cpp:684-689, cl_Mesh_Periodicity.cpp:49-75) and consumed by create_dofwise_t_matrices_master().
What the described behavior is worth: the shape of the algorithm — how master/slave pairs are matched and how the constraints reach the DOFs — is the reason to read this page, and no part of it has been shown wrong. But it has not been re-derived against that live path either, so treat it as a sketch to check rather than a specification. Do not copy the snippets.
Periodic boundary conditions constrain DOFs on one boundary plane to match their counterparts on an opposite boundary plane. BELFEM implements this via a three-stage pipeline:
After thin shells and cuts are created, the periodicity is rebuilt to include all duplicate nodes, and DOF-level constraints are applied from cl_FEM_DofMgr_DofData.cpp. The real entry point there is DofData::create_dofwise_t_matrices_master() (cl_FEM_DofMgr_DofData.hpp:266, defined at :3478); the mesh-side rebuild is driven from MaxwellFactory (cl_MaxwellFactory.cpp:684-690).
File: cl_Mesh_PeriodicityFactory.{hpp,cpp}
Each periodic boundary is a plane defined by three .geo Point IDs (A, B, C). The factory computes an orthonormal basis [P; Q; N] and plane offset D:
The transform Y = T * X maps 3D coordinates to in-plane coordinates (Y(0), Y(1)) and signed distance from the plane Y(2).
select_sidesets() iterates all mesh sidesets. A sideset is on the plane if all its nodes satisfy |dot(X, N) - D| < epsilon. The matching sidesets are used by map_facets() to pair facets; they are not stored on the Periodicity object.
The three points defining each plane must produce the same in-plane coordinate system (same P and Q directions). This is ensured by choosing corresponding geometry points (e.g., A maps to D, B to E, C to F).
File: cl_Mesh_Periodicity.{hpp,cpp}
The Periodicity class stores the master and slave entities in separate arrays:
These arrays are positionally paired once the factory has run: match_edges() compacts the edge containers to tied pairs, and crosslink() / to_proto() / from_proto() rely on that alignment. The bidirectional periodic() pointer set by match_edges() / match_facets_and_faces() carries the same pairing and is what set_entity_dependencies() follows.
Node pairs are populated externally (by PeriodicityFactory from the deterministic node-pair backup, or collect_nodes_from_flags_12()). Edge and face pairs are derived by PeriodicityFactory::update_periodicity().
Periodicity::update() (cl_Mesh_Periodicity.cpp:38-46) rebuilds a PeriodicityFactory from the stored master/slave planes and calls PeriodicityFactory::update_periodicity( this ); the edge/face matching (collect_edges(), match_edges(), match_facets_and_faces(), fix_face_slaves(), crosslink()) lives in the factory, not on Periodicity.
update_node_indices() assigns periodic indices 0..N-1 to both master and slave nodes:
This shared index space allows edge and face matching: an edge with corner periodic indices (A, B) on the master side matches the edge with the same indices (A, B) on the slave side.
match_edges() uses a Map<key_t, Edge*> with sorted pair keys:
where A, B are periodic indices of the edge's corner nodes and N is the number of periodic nodes. The indices are taken from node->original()->index(), not node->index(): a cohomology cut or thin-shell duplicate on the seam keys to its geometric original, so the master and slave sides resolve to the same shared index. The duplicate's own periodicity is then induced through its sources (hanging chain), so it never needs to be a matched periodic node itself.
Pass 1: Flag master nodes, collect master edges (both corners flagged), insert into map. Pass 2: Flag slave nodes, find slave edges (both corners flagged), look up master via same key.
Slave edge nodes are reordered to match master edge orientation using insert_node().
match_faces() uses a Map<key128_t, Face*> with sorted triple keys:
where sorted(0..2) are the three smallest periodic indices of the face's corner nodes.
fix_face_slaves() reassigns each slave face from master to slave role with the correct orientation. For each slave face:
set_periodic_entities() sets periodic() pointers in both directions:
This applies to nodes, edges, and faces. Self-paired entities (on the rotation axis) point to themselves.
Nodes that lie on both periodic planes (e.g., on the rotation axis of a cylindrical geometry) are paired with themselves: node->periodic() == node. This is necessary for edge and face matching to work correctly, since matching keys are built from periodic indices which require every boundary node to have a valid index.
Self-paired entities are skipped in create_dofwise_periodicities_master() (since constraining a DOF to itself is a no-op).
File: cl_Mesh_Distributor.cpp
The distributor does not track periodic roles with flags and does not transmit a Periodicity object. Periodic slaves are hanging entities whose sources are their masters (Periodicity::set_entity_dependencies()), so select_sources() ghosts the master with the slave (cl_Mesh_Distributor.cpp:326-330), and the constraints themselves travel as the T-matrices (send_t_matrices() / receive_t_matrices()). The partner table is serialized only on the .bfm path, by PeriodicityFactory::to_proto() / from_proto() via ProtoMesh::create_periodicitiy().
On receiving procs, ProtoMesh::create_faces() reconstructs faces from transmitted data. Slave periodic faces (which have no master element, only a slave) are handled by the Face constructor's slave-only branch, using to_master_orientation() with the stored mOrientationOnSlave to reorder nodes correctly.
After the initial PeriodicityFactory::create_periodicity() call, several operations create new nodes that must also be paired periodically:
Each stage propagates the periodic() pointer and flags 1/2 from the original node to the new node's periodic counterpart.
The key insight is that unflag_all_nodes() only clears flag 0, so flags 1 and 2 survive through mesh operations. Each node creation stage checks is_periodic() on the source node and, if true:
In MaxwellFactory::create_magnetic_kernel(), after all node creation stages are complete:
collect_nodes_from_flags_12() walks all mesh nodes, collects those with flag 1 into mMasterNodes and their periodic() partners into mSlaveNodes. This captures the original boundary nodes plus all thin-shell duplicates, cut duplicates, and layer nodes.
| File | Purpose |
|---|---|
| cl_Mesh_PeriodicityFactory.{hpp,cpp} | Plane detection, k-d tree matching, node pairing |
| cl_Mesh_Periodicity.{hpp,cpp} | Entity pair storage, edge/face derivation, update pipeline |
| cl_Mesh_Distributor.cpp | MPI distribution with periodic pairing preservation |
| cl_ProtoMesh.cpp | Face reconstruction on receiving procs |
| fn_to_master_orientation.{hpp,cpp} | Slave-to-master node reordering for faces |
| src/homology/cl_CutFactory.cpp | Periodicity propagation through thin-shell and cut duplicates |
| src/fem/kernel/cl_ThinShellFactory.cpp (namespace mesh) | Periodicity propagation through layer nodes |
| src/fem/maxwell/cl_MaxwellFactory.cpp | Orchestrates rebuild via collect_nodes_from_flags_12() + update() |
| src/fem/kernel/cl_FEM_DofMgr_DofData.cpp | DOF-level periodic constraints (create_dofwise_periodicities_master()) |