UnstructuredMesh Refactoring Plan¶
Branch: dev/harry
Baseline commit: cd065ad (on dev/harry)
Date: 2026-04-21
1. Current State¶
The Geom mesh module (~11,600 lines across 14 files) centres on two structs:
UnstructuredMesh (Mesh.hpp: ~810 lines header, ~2,585 lines Mesh.cpp)
~50 data members, ~60 methods, 9 impl files
Inherits DeviceTransferable<UnstructuredMesh>
UnstructuredMeshSerialRW (Mesh.hpp: ~250 lines header, shared Mesh.cpp)
16 declared-but-unused adjacency fields
Mixes reading, partitioning, serial output
Implementation files:
File |
Lines |
Responsibility |
|---|---|---|
Mesh.cpp |
2,585 |
Topology building, ghost, face interpolation, reordering, serialization, bnd mesh |
Mesh_Elevation.cpp |
1,120 |
O1→O2 elevation, O2→O1 bisection, boundary smooth |
Mesh_Elevation_SmoothSolver.cpp |
1,782 |
4 smooth solver variants (dispatch, V1Old, V1, V2) |
Mesh_Plts.cpp |
1,685 |
VTK/PLT/CGNS/VTKHDF output |
Mesh_Serial_ReadFromCGNS.cpp |
726 |
CGNS + OpenFOAM readers |
Mesh_Serial_BuildCell2Cell.cpp |
702 |
Cell2Cell, Deduplicate1to1Periodic |
Mesh_ReadSerializeDistributed.cpp |
512 |
Distributed H5 read + ParMetis repartition |
Mesh_Serial_Partition.cpp |
414 |
Metis partitioning |
Mesh_WallDist.cpp |
224 |
Wall distance (CGAL-based) |
Prior Work (Phases 1–3, done on dev/harry_refac1)¶
Phase 1 (Eliminate duplication): Template index conversions (12→4+12 wrappers),
ConvertAdjEntries,PermuteRows— done.Phase 2 (Break up massive methods): Extracted helpers from
ReadFromCGNSSerial,InterpolateFace,ReorderLocalCells,PrintSerialPartVTKDataArray— done.Phase 3 (Coupling/cleanup): Moved
BuildNodeWallDist, broke Geom→Solver dependency, removedauto mesh = this, deleted dead code — done.
Remaining Pain Points¶
Giant functions (>200 lines, hard to understand/test individually):
Function
File
Lines
Issue
ElevatedNodesSolveInternalSmoothV2SmoothSolver.cpp
864
Monolithic: assertions, setup, FEM assembly, limiters, GMRES, apply — all in one
PrintSerialPartVTKDataArrayPlts.cpp
560
Repeated binary/ascii encoding pattern for every data array
BuildO2FromO1ElevationElevation.cpp
450
5 distinct phases jammed together
ElevatedNodesSolveInternalSmoothV1SmoothSolver.cpp
448
Similar structure to V2 but RBF-based
ElevatedNodesGetBoundarySmoothElevation.cpp
427
Extended-face building, normals, Hermite interp
Deduplicate1to1PeriodicBuildCell2Cell.cpp
312
5 distinct phases
RecoverCell2CellAndBnd2CellMesh.cpp
258
Non-periodic (simple) + periodic (complex) fused
Code duplication across smooth solver variants (~80 lines copied 3×):
State assertion + early exit block (identical in V1Old, V1, V2)
nodesBoundInterpolatedidentification loop (identical)boundInterpCoo/boundInterpValinit + populate + ghost pull (identical)KD-tree construction from boundary coords (identical in V1Old, V1)
Interior displacement evaluation via radius search (identical in V1Old, V1)
Adj conversion methods partially unified —
ConvertAdjEntriesused for Primary/PrimaryForBnd/C2CFace but NOT for Facial, C2F, N2CB (which use manual OMP-parallel loops or lambdas).Deprecated smooth solver variant —
V1Old(235 lines) has rawstd::cout << iNdebug prints, serial-only solve, and is superseded by V1.Disabled code blocks in V2 — ~175 lines of
#if 0/ commented-out “fem method” and “spring method” blocks inside theAssembleRHSMatlambda.
2. Refactoring Phases¶
Phase 0: Extend Baseline Tests (DO FIRST)¶
Goal: Ensure elevation/bisection + smooth solver correctness is testable before touching any code.
Tests to add (in test/cpp/Geom/test_MeshPipeline.cpp or new file):
Test |
Assertion |
|---|---|
Elevation + BoundarySmooth: node movement count |
|
Elevation + BoundarySmooth: all O2 nodes valid coords |
No NaN/Inf in coords after smooth |
Elevation + InternalSmooth(V2): Jacobian positive |
All cell Jacobians remain > 0 |
Elevation + InternalSmooth(V2): coords change |
coords differ from pre-smooth state |
Bisection preserves O2 node positions |
Bisected mesh nodes are a subset of O2 coords |
Build and run:
cmake --build build -t geom_test_mesh_pipeline -j8
ctest --test-dir build -R geom_mesh_pipeline --output-on-failure
Phase 2: Decompose ElevatedNodesSolveInternalSmoothV2 (864 → ~150 + helpers)¶
Goal: Break the monolithic function into focused helpers.
# |
Extract |
Current Lines |
Description |
|---|---|---|---|
2.1 |
|
~15 |
Build node-to-node connectivity from cell2node |
2.2 |
|
~40 |
Initialize DOF arrays ( |
2.3 |
|
~200 |
The active “bisect fem method” block from the |
2.4 |
|
~40 |
The “find nDiag, set identity row” pattern (repeated 3× → 1×) |
2.5 |
|
~95 |
The |
2.6 |
|
~40 |
Eigen sparse matrix construction from block matrix A |
2.7 |
Delete disabled code blocks |
~175 |
Remove |
After extraction, ElevatedNodesSolveInternalSmoothV2 becomes a ~150-line
orchestrator calling the helpers in sequence.
Phase 3: Unify Adj Conversion Methods¶
Goal: Reduce boilerplate in the 12 Adj methods without losing OMP parallelism.
# |
Item |
Description |
Lines Saved |
|---|---|---|---|
3.1 |
Add |
Analogous to Cell/Node/Bnd wrappers, using |
0 (enables 3.2-3.3) |
3.2 |
Add OMP variant: |
Same as |
~5 |
3.3 |
Unify C2F pair with |
Replace inline lambda + loops with 2 calls each |
~40 |
3.4 |
Unify N2CB pair with |
Replace manual loops with 2 calls each |
~20 |
Not unified (deliberate):
Facial pair: Fused OMP loop over 3 arrays per face + asymmetric
face2bndconversion + stricter assertions. Splitting would regress cache locality and require assertion wrappers for no readability gain.bnd2cellin Primary: Per-entry assertion depends on row+column index, whichConvertAdjEntries’sindex(index)signature cannot express.
Phase 4: Decompose RecoverCell2CellAndBnd2Cell (258 → ~80 + helpers)¶
Goal: Separate the simple non-periodic logic from the complex periodic filter.
# |
Extract |
Lines |
Description |
|---|---|---|---|
4.1 |
|
~30 |
Node-intersection algorithm to find cells sharing a face |
4.2 |
|
~120 |
Ghost-pull candidate cells, pbi-match filter, donor/receiver classification |
The main function becomes: (1) setup, (2) build cell2cell via node neighbors,
(3) build bnd2cell via FindCellsSharingFace, (4) if periodic, call
FilterPeriodicBnd2Cell.
Phase 5: Cleanup¶
# |
Item |
Lines Removed |
Risk |
|---|---|---|---|
5.1 |
Deprecate |
235 |
Add |
5.2 |
Remove debug prints from V1 ( |
~10 |
Trivial |
5.3 |
Remove debug prints from V1Old ( |
~5 |
Trivial |
5.4 |
Remove commented-out |
~17 |
Dead code |
5.5 |
Remove dead code after |
~22 |
Dead code |
5.6 |
Deduplicate OMP progress reporting in |
~20 |
Extract |
Phase 6 (future): Decompose into Focused Components¶
This is the TODO.md Phase 4, deferred to a future pass because it requires touching the public API (Python bindings, downstream Euler/EulerP/CFV code):
MeshTopology: adjacency arrays + state flags + state transitionsMeshIO: serialization, VTK/PLT/CGNS/VTKHDF outputMeshElevation: O1→O2 elevation, bisection, smooth solversMeshReordering: local cell reordering, partition start trackingUnstructuredMeshbecomes a thin facade
Phase 7 (future): Reduce Periodic Branching¶
if (isPeriodic) appears 58 times across Mesh files. Consider extracting
periodic-specific behavior into a policy so non-periodic paths stay clean.
Deferred because the branching is in hot paths where template specialization
may be needed for performance.
3. Implementation Order and Risk Assessment¶
Phase |
Risk |
Effort |
Prerequisite |
Lines Changed |
|---|---|---|---|---|
Phase 0: Tests |
None |
0.5 day |
— |
+150 test |
Phase 1: Shared Setup |
Low |
0.5 day |
Phase 0 |
~160 removed |
Phase 2: V2 Decomposition |
Medium |
1-2 days |
Phase 0, 1 |
~175 removed (dead), ~400 restructured |
Phase 3: Adj Unification |
Low |
0.5 day |
Phase 0 |
~60 removed |
Phase 4: RecoverC2C Decomposition |
Medium |
0.5 day |
Phase 0 |
~150 restructured |
Phase 5: Cleanup |
Trivial |
0.5 day |
Any time |
~310 removed |
Phase 6: Components |
High |
3-5 days |
All above |
Major restructure |
Phase 7: Periodic Policy |
High |
2-3 days |
Phase 6 |
58 sites |
Each phase must pass the full geom test suite after completion:
cmake --build build -t geom_unit_tests -j8
ctest --test-dir build -R geom_ --output-on-failure
4. Smooth Solver Variant Status¶
Variant |
Lines |
Method |
Status |
Recommendation |
|---|---|---|---|---|
|
139 |
Dispatch to V1Old/V1/V2 |
Active |
Keep as dispatcher |
|
235 |
Serial RBF direct solve |
Deprecated |
|
|
448 |
Distributed RBF GMRES |
Intermediate |
Keep (used by some configs) |
|
864 |
FEM elasticity GMRES |
Active/Default |
Decompose in Phase 2 |
Evidence V1Old is deprecated:
Serial-only solve (unscalable for production meshes)
Has raw
std::cout << iNdebug prints in production codeSuperseded by V1 (parallel GMRES) which was itself superseded by V2 (FEM)
No case file references
nIter=0(V1Old path) in production
5. Adj Conversion Method Summary¶
After Phase 3:
Pair |
Before |
After |
|---|---|---|
Primary |
|
Unchanged (bnd2cell needs positional assertion) |
PrimaryForBnd |
|
Unchanged |
C2F |
Inline lambda + manual loops |
|
N2CB |
Manual OMP loops |
|
Facial |
Fused OMP loop |
Unchanged (fused loop is intentional optimization) |
C2CFace |
|
Unchanged |
6. Test Coverage After Refactoring¶
Existing tests that guard correctness:
Test File |
Tests |
np |
Coverage |
|---|---|---|---|
|
50 test cases |
1,2,4,8 |
Full pipeline, Adj round-trips, elevation, bisection |
|
24 test cases |
1,2,4,8 |
All 12 named wrappers |
|
18 test cases |
1,2,4,8 |
ReadSerializeAndDistribute |
New tests added in Phase 0 specifically guard elevation + smooth solver paths.