Date: 2026-03-02 Purpose: Documents why facet master/slave orientation matters for thin shells, the two-stage algorithm that ensures consistency, and the pitfalls that arise from mesh operations Module: fem/maxwell, homology, mesh
| File | Key Function | Purpose |
|---|---|---|
| cl_MaxwellFactory.cpp | fix_facet_masters() (~line 981) | Corrects master/slave based on DomainType + BFS |
| cl_CutFactory.cpp | relink_slave_elements_with_duplicate_nodes() (~line 1838) | Replaces slave element nodes with thin shell duplicates |
| cl_CutFactory.cpp | duplicate_nodes_on_face_sidesets() (~line 1646) | Creates the duplicate nodes |
| cl_Mesh.cpp | update_facet_nodes() (~line 678) | Copies master element's face nodes into the facet surface element |
| cl_Mesh_ConnectivityCalculator.cpp | connect_facets_to_elements() (~line 171) | Initial master/slave assignment by element ID |
| cl_Facet.cpp | flip() (~line 89) | Swaps master and slave, relinks facet nodes |
| fn_check_facet_orientation.hpp | check_facet_orientation() (~line 39) | Checks if two adjacent facets have consistent outward normals |
| en_DomainType.hpp | DomainType enum (~line 19) | Defines priority ordering: Air(1) < Ferro(2) < Coil(3) < Conductor(4) |
Thin shells in BELFEM are surface-embedded conductors (superconducting tapes, thin films) modeled as sidesets rather than volume blocks. They require topological separation: the mesh must have duplicate nodes on one side of the shell so that DOFs on the two sides are independent. Which side gets the duplicates must be consistent across all facets of a given thin shell sideset.
Three mesh operations interact to make this work:
It skips thin-shell facets. Mesh::update_facet_nodes() returns early for sidesets whose domain_type() is ThinShell or GeometryOnly (cl_Mesh.cpp:687-690), because those are extrusion geometry whose node lists are built by the ThinShellFactory rather than copied from a volume element. The orientation rule below still governs the interface facets between the shell and the bulk, which is where master/slave actually matters.
If master/slave assignment is inconsistent, the wrong side gets duplicates and ThinShellFactory computes incorrect normals (or NaN normals from zero-length vectors). (update_facet_nodes() is not the failure path for the thin-shell sidesets themselves — it skips them, per the note above — but it is for the interface facets between shell and bulk.)
After mesh loading, connect_facets_to_elements() assigns master/slave purely by element ID: the element with the lower ID becomes the master. This is arbitrary and depends on block ordering in the mesh file.
fix_facet_masters() overrides this assignment based on DomainType priority. The DomainType enum values control the ordering (en_DomainType.hpp:19-73):
The higher DomainType value becomes master. For a typical Air/Conductor interface, Conductor (4) becomes master and Air (1) becomes slave.
For each facet where master and slave have different DomainTypes:
These cross-type facets are added to a BFS queue as seeds with known correct orientation.
Facets where both sides have the same DomainType (e.g., Conductor/Conductor) cannot be resolved by DomainType comparison alone. These are flagged for later processing.
BFS propagation from the cross-type seeds resolves them:
check_facet_orientation() compares the traversal direction of the shared edge between two facets. If both facets traverse the shared edge in the same direction, their outward normals point in opposite directions and a flip is needed.
The master/slave assignment set by fix_facet_masters() is preserved across unfinalize()/finalize() cycles because FacetToElement connectivity is not reset during those operations.
This function runs after duplicate_nodes_on_face_sidesets() has created the duplicate nodes. It relinks elements on the slave side to use duplicates instead of originals.
The function collects slave block IDs from all facets of each thin shell sideset, not just the first one:
This is critical for overhanging thin shells where different facets may sit between different block pairs.
All elements in the identified slave blocks are flagged. Then, only the flagged elements that actually touch sideset nodes are collected:
For each collected element, interface nodes that belong to the thin shell sideset are replaced with their duplicates:
Per-sideset node flagging ensures that nodes from different thin shell sidesets are not accidentally replaced in the wrong context.
After both stages complete, the following invariants hold:
When both sides of a thin shell interface have the same DomainType (e.g., two Conductor blocks), DomainType comparison alone cannot determine master/slave. The BFS propagation from cross-type seeds is essential.
Risk: If the entire thin shell sideset has same-type blocks on both sides with no cross-type seeds anywhere in the connected component, the orientation is arbitrary but still consistent (all facets will be oriented the same way by BFS). However, if this leads to the wrong convention relative to other parts of the mesh, results may be incorrect.
A thin shell may overhang a conductor boundary, so that some facets sit between Conductor/Air and others between Conductor/Conductor. The slave block collection must iterate over all facets to capture every slave block ID.
Historical bug: The original code used only the first facet's slave block ID, which failed for overhanging geometries. See todo/orientation_fix_block_ordering_bug.md (Bug 1).
After relink_slave_elements_with_duplicate_nodes() replaces volume element nodes with duplicates, and update_facet_nodes() copies master element nodes into facet surface elements, the original Node::facet() pointers can become stale:
Fix: Build node-to-facet adjacency directly from the facet list rather than relying on tNode->facet(). See todo/orientation_fix_block_ordering_bug.md (Bug 3).
When ThinShellFactory's process_nodes_tri3() / process_nodes_tri6() / process_nodes_line2() compute surface normals, nodes with no valid facet connections produce a zero normal vector tN = [0, 0, 0]. Normalizing this (tN /= norm(tN)) produces NaN, which propagates to node coordinates in create_nodes_on_layers().
Symptom: LAPACK posv error in the MaxwellPostprocessor recovery pass due to NaN entries in the Vandermonde matrix.
If BFS propagation encounters a facet that has already been resolved but with an inconsistent orientation, the surface is non-orientable. This should be detected and reported as an error. In practice, all physically meaningful thin shell surfaces are orientable (they represent real material layers).
Duplicate nodes created by duplicate_nodes_on_face_sidesets() do not have set_original() called on them. This means tDuplicate->original() returns this (the duplicate itself), not the original node.
Facet::compute_orientation() uses original()->id() to match nodes between master and slave faces. Since thin shell duplicates have different IDs from their originals, this matching still works correctly when duplicates appear only on the slave side (the master face has originals). But code that assumes original() tracks the duplication relationship will not work for thin shell duplicates.
The relevant operations occur in this sequence during MaxwellFactory initialization:
The key interaction is between steps 2, 3b, and 3d: fix_facet_masters() determines which side is master, relink_slave_elements_with_duplicate_nodes() gives duplicates to the slave side, and update_facet_nodes() copies from the master (which has originals) into the facet surface elements.