From c873e12badfdabab1509b81c52ccd583498c3c0a Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 27 Apr 2026 23:46:18 +0800 Subject: [PATCH 01/43] =?UTF-8?q?feat(Geom):=20AdjPairTracked=20infrastruc?= =?UTF-8?q?ture=20=E2=80=94=20fillRegistry,=20device=20views,=20checked=20?= =?UTF-8?q?wrapper=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 1 — AdjIndexInfo: - wireTargetMapping: assert non-null input - makeFatherOnlyMapping: static factory for empty-ghost OffsetAscendIndexMapping - EnsureGhostMapping: mesh helper to create father-only ghost mapping on pairs before ghost layers are built, enabling normal IndexG2L/IndexL2G instead of _NoSon variants Plan 2 — fillRegistry: - UnstructuredMesh::fillRegistry: register all built adjs, source pLGlobalMapping from any adj array per entity kind, check_throw if mapping is needed but missing - const registerAdj overloads for TPair and AdjPairTracked - Adj::Bnd2Face, Adj::Face2Bnd predefined constants - Migrated all 4 ghost DAG sites (RecoverN2CB, BuildGhostPrimary, BuildGhostFace, ReadDistributed) to use fillRegistry Plan 3 — Device views: - AdjIndexInfoDeviceView, AdjPairTrackedDeviceView, AdjPairTrackedDeviceViewConst in Mesh_DeviceView.hpp - AdjPairTracked::deviceView() returns tracked view with idx state - Changed all 12 tracked adj member types in UnstructuredMeshDeviceView from plain ArrayPairDeviceView to AdjPairTrackedDeviceView Checked wrapper contract (CheckedInverse, CheckedComposeFiltered): - Extract L2G callbacks from pair ghost mappings (no manual callbacks) - Return AdjPairTracked with father adopted, son allocated, state = Global - Callers never touch raw DSL or markGlobal on results CellIndexG2L/CellIndexL2G now use cell2node (not cellElemInfo) so they work at all pipeline stages. Same for BndIndex wrappers (use bnd2node). 74/74 C++ tests pass. 50/50 Python tests pass. All 6 solvers build. --- docs/dev/distributed_reorder_design.md | 69 ++++++ src/Geom/Mesh/AdjIndexInfo.hpp | 60 +++++ src/Geom/Mesh/Mesh.cpp | 217 +++++++++++------- src/Geom/Mesh/Mesh.hpp | 61 ++++- src/Geom/Mesh/MeshConnectivity.hpp | 20 ++ .../Mesh/MeshConnectivity_StateChecked.hpp | 77 +++++-- src/Geom/Mesh/Mesh_DeviceView.hpp | 83 +++++-- .../Mesh/Mesh_ReadSerializeDistributed.cpp | 4 +- 8 files changed, 464 insertions(+), 127 deletions(-) diff --git a/docs/dev/distributed_reorder_design.md b/docs/dev/distributed_reorder_design.md index 7b736c50..ec91263a 100644 --- a/docs/dev/distributed_reorder_design.md +++ b/docs/dev/distributed_reorder_design.md @@ -3,6 +3,10 @@ > **Status:** Proposal (not yet implemented). The existing > `ReorderLocalCells` and `ReadDistributed_Redistribute` remain the > active code paths. +> +> **Last reviewed:** 2026-04-27 against PR #6 merge (commit 13b4e7b1). +> All 4 plans re-evaluated; no fundamental changes needed. See +> "Re-evaluation Notes" section at bottom. ## Motivation @@ -453,3 +457,68 @@ supports it — just add Face to the reorder maps. - Remove or deprecate the old ad-hoc helpers in `Mesh_PartitionHelpers.hpp` - Remove redundant code in `ReorderLocalCells` and `ReadDistributed_Redistribute` (they become thin wrappers) + +--- + +## Re-evaluation Notes (2026-04-27, post PR #6) + +Reviewed against the merged PR #6 changes (InterpolateGlobal, face pipeline +split, templatization, pybind11 bindings, file reorganization into +`src/Geom/Mesh/`). + +### What changed + +1. **`InterpolateFace` split** into `InterpolateFace` + `BuildGhostFace` + + `MatchFaceBoundary`. Ghost faces now via `evaluateGhostTree(Cell -> + Cell2Face -> Face)`. This creates a 5th `registerAdj` call site. + +2. **`InterpolateGlobal`** added: N-parent distributed interpolation with + pbi, ownership resolution, and template `e2p_rs` (face2cell uses rs=2). + `parent2entityPbi` computed locally (no MPI push needed). + +3. **File reorganization**: All mesh source moved to `src/Geom/Mesh/`. + `MeshConnectivity.cpp` created for non-template implementations. + +4. **`AdjPairTracked_bind.hpp`** added: Python bindings for tracked pairs + via pybind11 inheritance from base `ArrayAdjacencyPair`. + +5. **Implementation plan updated** (1786 lines): Sections 9 (templatization) + and 10 (per-adj state tracking) with detailed migration plans. + +### Impact on Plan 1 (makeFatherOnlyMapping) + +**No changes.** `AdjIndexInfo.hpp` is unchanged. `wireTargetMapping` still +accepts null. The factory and non-null assertion are still needed. + +### Impact on Plan 2 (fillRegistry) + +**Minor adjustment.** Now 5 call sites (was 4). The new `BuildGhostFace` +site borrows `cell2face`'s global mapping from `cell2node`, confirming the +need to document canonical global mapping sources per entity kind. + +### Impact on Plan 3 (device view) + +**Confirmed.** The inheritance pattern used for `AdjPairTracked` and +`AdjPairTracked_bind.hpp` validates the device view inheritance approach. +The `AdjStateView` struct for the mesh device view remains the pragmatic +choice (avoids changing 12 member type declarations). + +### Impact on Plan 4 (distributed reordering) + +**Mostly unchanged.** Key updates: + +- **Face reconstruction** after reorder should use the modular + `InterpolateFace -> BuildGhostFace -> MatchFaceBoundary` pipeline + (which internally uses `InterpolateGlobal`), not `InterpolateFaceLegacy`. + +- **`parent2entityPbi`** is computed locally from `cell2nodePbi` + + `entity2nodePbi` — no need to preserve across reorder. Destroyed with + faces, recomputed after. + +- **`PermutationTransfer`** should handle `AdjVariant` via `std::visit` + for type-erased transfer (same pattern as `evaluateGhostTree`). + +- The list of arrays destroyed before reorder and reconstructed after + should include: `cell2face`, `face2cell`, `face2node`, `face2bnd`, + `bnd2face`, `faceElemInfo`, `face2nodePbi`, `cell2facePbi` (when + periodic), and any `parent2entityPbi` stored on the mesh. diff --git a/src/Geom/Mesh/AdjIndexInfo.hpp b/src/Geom/Mesh/AdjIndexInfo.hpp index 4d655fe6..a46bec52 100644 --- a/src/Geom/Mesh/AdjIndexInfo.hpp +++ b/src/Geom/Mesh/AdjIndexInfo.hpp @@ -90,6 +90,8 @@ namespace DNDS::Geom /// already local w.r.t. some (possibly different) mapping, and /// replacing the mapping silently would make toGlobal() produce /// garbage. + /// + /// \param mapping Must be non-null. void wireTargetMapping(const t_pLGhostMapping &mapping) { DNDS_assert_info( @@ -97,9 +99,41 @@ namespace DNDS::Geom "wireTargetMapping called while indices are local — " "convert to global first, or the stored mapping will " "be inconsistent with the index values"); + DNDS_assert_info( + mapping != nullptr, + "wireTargetMapping: mapping must be non-null"); _targetMapping = mapping; } + // ============================================================ + // Factory: father-only mapping (empty ghost set) + // ============================================================ + + /// \brief Create a ghost mapping with no ghost entries (father-only). + /// + /// The resulting OffsetAscendIndexMapping maps owned globals to + /// [0, fatherSize) via search_indexAppend, and returns false for + /// anything off-rank (encoded as -1 - globalIndex by toLocal). + /// + /// Useful for wiring adjacencies that will never have ghost data + /// (e.g., boundary mesh cell2node). + /// + /// \warning Collective — calls MPI_Alltoall internally. + static t_pLGhostMapping makeFatherOnlyMapping( + const ssp &globalMapping, + index fatherSize, + const MPIInfo &mpi) + { + DNDS_assert_info(globalMapping, "makeFatherOnlyMapping: globalMapping must be non-null"); + std::vector emptyGhosts; + return std::make_shared( + (*globalMapping)(mpi.rank, 0), // mainOffset + fatherSize, // mainSize + emptyGhosts, // empty pull set + *globalMapping, + mpi); + } + // ============================================================ // Conversion: toLocal / toGlobal (mapping must be wired) // ============================================================ @@ -278,6 +312,32 @@ namespace DNDS::Geom bool isBuilt() const { return idx.isBuilt(); } bool isWired() const { return idx.isWired(); } const t_pLGhostMapping &mapping() const { return idx.mapping(); } + + // ============================================================= + // Device views + // ============================================================= + + template + using t_deviceView = AdjPairTrackedDeviceView; + + /// Create a device view that carries the per-adj state alongside + /// the father/son array views. + template + auto deviceView() + { + auto base = TPair::template deviceView(); + return t_deviceView{base.father, base.son, + AdjIndexInfoDeviceView{idx.state()}}; + } + + template + auto deviceView() const + { + auto base = TPair::template deviceView(); + return AdjPairTrackedDeviceViewConst{ + base.father, base.son, + AdjIndexInfoDeviceView{idx.state()}}; + } }; } // namespace DNDS::Geom diff --git a/src/Geom/Mesh/Mesh.cpp b/src/Geom/Mesh/Mesh.cpp index 76c3f511..cc17d147 100644 --- a/src/Geom/Mesh/Mesh.cpp +++ b/src/Geom/Mesh/Mesh.cpp @@ -389,51 +389,34 @@ namespace DNDS::Geom if (!cell2node.father->pLGlobalMapping) cell2node.father->createGlobalMapping(); + // Ensure ghost mappings exist so IndexLocal2Global / IndexGlobal2Local work + // (father-only at this stage — no ghost cells/nodes yet). + coords.TransAttach(); + coords.trans.createFatherGlobalMapping(); + EnsureGhostMapping(coords); + cell2node.TransAttach(); + cell2node.trans.createFatherGlobalMapping(); + EnsureGhostMapping(cell2node); + // node2cell via DSL Inverse (cell2node must be in global state) - auto dslN2C = CheckedInverse( - cell2node, coords.father->Size(), mpi, - [this](index i) - { return this->CellIndexLocal2Global_NoSon(i); }, - [this](index i) - { return this->NodeIndexLocal2Global_NoSon(i); }, - coords.father->pLGlobalMapping); - - // Copy DSL result into node2cell pair (father only) - node2cell.InitPair("node2cell", mpi); - node2cell.father->Resize(coords.father->Size()); - for (index iNode = 0; iNode < coords.father->Size(); iNode++) - { - auto row = dslN2C.father->operator[](iNode); - node2cell.father->ResizeRow(iNode, row.size()); - for (rowsize j = 0; j < static_cast(row.size()); j++) - node2cell.father->operator()(iNode, j) = row[j]; - } + // Result is already AdjPairTracked with father adopted, state = Global. + node2cell = CheckedInverse( + cell2node, coords, + coords.father->Size(), mpi); // node2bnd via DSL Inverse (bnd2node must be in global state) if (!bnd2node.father->pLGlobalMapping) bnd2node.father->createGlobalMapping(); + bnd2node.TransAttach(); + bnd2node.trans.createFatherGlobalMapping(); + EnsureGhostMapping(bnd2node); - auto dslN2B = CheckedInverse( - bnd2node, coords.father->Size(), mpi, - [this](index i) - { return this->BndIndexLocal2Global_NoSon(i); }, - [this](index i) - { return this->NodeIndexLocal2Global_NoSon(i); }, - coords.father->pLGlobalMapping); - - node2bnd.InitPair("node2bnd", mpi); - node2bnd.father->Resize(coords.father->Size()); - for (index iNode = 0; iNode < coords.father->Size(); iNode++) - { - auto row = dslN2B.father->operator[](iNode); - node2bnd.father->ResizeRow(iNode, row.size()); - for (rowsize j = 0; j < static_cast(row.size()); j++) - node2bnd.father->operator()(iNode, j) = row[j]; - } + node2bnd = CheckedInverse( + bnd2node, coords, + coords.father->Size(), mpi); this->adjN2CBState = Adj_PointToGlobal; - node2cell.idx.markGlobal(); - node2bnd.idx.markGlobal(); + // markGlobal already done by CheckedInverse } void UnstructuredMesh::RecoverCell2CellAndBnd2Cell() @@ -449,21 +432,19 @@ namespace DNDS::Geom coords.TransAttach(); coords.trans.createFatherGlobalMapping(); + EnsureGhostMapping(coords); cell2node.TransAttach(); cell2node.trans.createFatherGlobalMapping(); + EnsureGhostMapping(cell2node); bnd2node.TransAttach(); bnd2node.trans.createFatherGlobalMapping(); + EnsureGhostMapping(bnd2node); // Ghost-pull node2cell for off-rank nodes referenced by local cells and bnds. // Use evaluateGhostTree: Cell → Cell2Node → Node ∪ Bnd → Bnd2Node → Node. { MeshConnectivity dagN2CB; - dagN2CB.meshDim = dim; - dagN2CB.registerAdj(Adj::Cell2Node, cell2node); - dagN2CB.registerAdj(Adj::Bnd2Node, bnd2node); - dagN2CB.registerGlobalMapping(EntityKind::Cell, cell2node.trans.pLGlobalMapping); - dagN2CB.registerGlobalMapping(EntityKind::Bnd, bnd2node.trans.pLGlobalMapping); - dagN2CB.registerGlobalMapping(EntityKind::Node, coords.trans.pLGlobalMapping); + fillRegistry(dagN2CB); GhostSpec n2cbSpec{{ {EntityKind::Cell, {Adj::Cell2Node}, EntityKind::Node}, @@ -491,24 +472,12 @@ namespace DNDS::Geom nodeG2L[node2cell.trans.pLGhostMapping->operator()(-1, i)] = i; // cell2cell via DSL ComposeFiltered (inputs must be in global state) - auto dslC2C = CheckedComposeFiltered( + // Result is AdjPairTracked with father adopted, state = Global. + cell2cell = CheckedComposeFiltered( cell2node, node2cell, - cell2node.father->Size(), nodeG2L, - [this](index i) - { return this->CellIndexLocal2Global_NoSon(i); }, SharedCountPredicate{.minShared = 1, .removeSelf = true}); - cell2cell.InitPair("cell2cell", mpi); - cell2cell.father->Resize(cell2node.father->Size()); - for (index i = 0; i < cell2node.father->Size(); i++) - { - auto row = dslC2C.father->operator[](i); - cell2cell.father->ResizeRow(i, row.size()); - for (rowsize j = 0; j < static_cast(row.size()); j++) - cell2cell.father->operator()(i, j) = row[j]; - } - // bnd2cell via ComposeFiltered with per-bnd node-count predicate. // For periodic meshes, additionally uses pbi containment matchExtra. bnd2cell.InitPair("bnd2cell", mpi); @@ -547,7 +516,7 @@ namespace DNDS::Geom auto bndAllNodesPred = [this](index aBndGlobal, index cCellGlobal, int nShared) -> bool { // Map aBndGlobal to local bnd index to get row size - index aBndLocal = this->BndIndexGlobal2Local_NoSon(aBndGlobal); + index aBndLocal = this->BndIndexGlobal2Local(aBndGlobal); DNDS_assert(aBndLocal >= 0); return nShared >= bnd2node.father->RowSize(aBndLocal); }; @@ -588,22 +557,13 @@ namespace DNDS::Geom } // bnd2cell via ComposeFiltered (inputs must be in global state) - auto dslB2C = CheckedComposeFiltered( + // Result is AdjPairTracked with father adopted, state = Global. + bnd2cell = CheckedComposeFiltered( bnd2node, node2cell, - bnd2node.father->Size(), nodeG2L, - [this](index i) - { return this->BndIndexLocal2Global_NoSon(i); }, bndAllNodesPred, bndMatchExtra); - // Adopt directly — ComposeFiltered<..., 2> already produced tAdj2Pair. - // Non-periodic bnds: slot 0 = cell, slot 1 = UnInitIndex (from ComposeFiltered padding). - // Periodic bnds: slot 0 = donor cell, slot 1 = partner cell (if found) or UnInitIndex. - // The pbi filter in bndMatchExtra typically rejects the periodic partner, so - // periodic bnds usually have slot 1 = UnInitIndex (1 match only). - bnd2cell.father = dslB2C.father; - // Periodic fixup: when a periodic bnd has only 1 matching cell (slot 1 == UnInitIndex), // fill slot 1 with slot 0 (self-reference) to match legacy behavior. if (isPeriodic) @@ -698,8 +658,7 @@ namespace DNDS::Geom } } - cell2cell.idx.markGlobal(); - bnd2cell.idx.markGlobal(); + // markGlobal already done by CheckedComposeFiltered } void UnstructuredMesh:: @@ -740,15 +699,7 @@ namespace DNDS::Geom // Unified ghost evaluation: single DAG, all inputs father-only. // The evaluator handles scratch pulls internally. MeshConnectivity dag; - dag.meshDim = dim; - // Register father-only (no son, no ghost mapping). - dag.registerAdj(Adj::Cell2Cell, cell2cell); - dag.registerAdj(Adj::Cell2Node, cell2node); - dag.registerAdj(Adj::Bnd2Node, bnd2node); - dag.registerAdj(Adj::Node2Bnd, node2bnd); - dag.registerGlobalMapping(EntityKind::Cell, cell2cell.trans.pLGlobalMapping); - dag.registerGlobalMapping(EntityKind::Node, coords.trans.pLGlobalMapping); - dag.registerGlobalMapping(EntityKind::Bnd, bnd2cell.trans.pLGlobalMapping); + fillRegistry(dag); auto spec = GhostSpec::defaultPrimary(nGhostLayers); auto tree = CompiledGhostTree::compile(spec); @@ -1313,17 +1264,12 @@ namespace DNDS::Geom BuildGhostFace() { // Determine ghost faces via evaluateGhostTree(Cell → Cell2Face → Face). - // The tree traverses owned cells only (single hop), so cell2faceAdj - // only needs father with cell global mapping. + // cell2face is cell-indexed but has no own pLGlobalMapping — borrow from cell2node. + if (!cell2face.father->pLGlobalMapping) + cell2face.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; { MeshConnectivity dag; - dag.meshDim = dim; - tAdjPair cell2faceAdj; - cell2faceAdj.father = cell2face.father; - cell2faceAdj.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; - dag.registerAdj(Adj::Cell2Face, cell2faceAdj); - dag.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping); - dag.registerGlobalMapping(EntityKind::Face, face2node.trans.pLGlobalMapping); + fillRegistry(dag); GhostSpec ghostSpec; ghostSpec.chains.push_back(GhostChain{ @@ -1790,6 +1736,99 @@ namespace DNDS::Geom control.getILUCode()); } + void UnstructuredMesh::fillRegistry( + MeshConnectivity &dag) const + { + fillRegistry(dag, {}); + } + + void UnstructuredMesh::fillRegistry( + MeshConnectivity &dag, + const std::unordered_set &skip) const + { + dag.meshDim = dim; + + // --- Register adjacency arrays (father-only shallow copy) --- + auto tryAdj = [&](AdjKind kind, const auto &pair) + { + if (pair.father && skip.find(kind) == skip.end()) + dag.registerAdj(kind, pair); + }; + + // Primary + tryAdj(Adj::Cell2Node, cell2node); + tryAdj(Adj::Cell2Cell, cell2cell); + tryAdj(Adj::Bnd2Node, bnd2node); + tryAdj(Adj::Bnd2Cell, bnd2cell); + // N2CB + tryAdj(Adj::Node2Cell, node2cell); + tryAdj(Adj::Node2Bnd, node2bnd); + // Facial + tryAdj(Adj::Cell2Face, cell2face); + tryAdj(Adj::Face2Node, face2node); + tryAdj(Adj::Face2Cell, face2cell); + tryAdj(Adj::Face2Bnd, face2bnd); + tryAdj(Adj::Bnd2Face, bnd2face); + // C2CFace + tryAdj(Adj::Cell2CellFace, cell2cellFace); + + // --- Register global mappings --- + // For each entity kind, find the first adj array whose father + // carries a valid pLGlobalMapping. All adj arrays for the same + // entity kind share equivalent offsets (same father Size), so + // any one suffices. check_throw if a mapping is needed (i.e., + // at least one adj was registered whose .from is this kind) but + // none is found. + + // Candidate sources per entity kind (ordered by typical availability). + // All adj arrays for the same entity kind have equivalent offsets + // (same father Size), so any one suffices. + auto firstValid = [](std::initializer_list> candidates) + -> ssp + { + for (auto &gm : candidates) + if (gm) + return gm; + return nullptr; + }; + + auto getGM = [](const auto &pair) -> ssp + { + if (pair.father && pair.father->pLGlobalMapping) + return pair.father->pLGlobalMapping; + return nullptr; + }; + + auto cellMap = firstValid({getGM(cell2node), getGM(cell2cell), getGM(cell2face)}); + auto nodeMap = coords.father ? coords.father->pLGlobalMapping : nullptr; + auto bndMap = firstValid({getGM(bnd2node), getGM(bnd2cell), getGM(bnd2face)}); + auto faceMap = firstValid({getGM(face2node), getGM(face2cell), getGM(face2bnd)}); + + // Register if found; check_throw if any registered adj needs it + auto regMap = [&](EntityKind kind, const ssp &gm) + { + if (gm) + { + dag.registerGlobalMapping(kind, gm); + } + else + { + // Verify no registered adj has this kind as its from-entity + for (auto &[adjKind, _] : dag.adjRegistry) + DNDS_check_throw_info( + adjKind.from != kind, + fmt::format("fillRegistry: no pLGlobalMapping found for EntityKind {} " + "but adj {} requires it", + static_cast(kind), adjKindName(adjKind))); + } + }; + + regMap(EntityKind::Cell, cellMap); + regMap(EntityKind::Node, nodeMap); + regMap(EntityKind::Bnd, bndMap); + regMap(EntityKind::Face, faceMap); + } + // ================================================================= // File-local helpers for ReorderLocalCells // ================================================================= diff --git a/src/Geom/Mesh/Mesh.hpp b/src/Geom/Mesh/Mesh.hpp index 14f24239..12161598 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -12,6 +12,7 @@ #include "DNDS/ObjectUtils.hpp" #include "DNDS/Config/ConfigParam.hpp" #include "AdjIndexInfo.hpp" +#include "MeshConnectivity.hpp" namespace DNDS::Direct { @@ -211,6 +212,31 @@ namespace DNDS::Geom // as thin inline wrappers so all existing call sites compile // unchanged. + /** + * \brief Ensure a pair has a ghost mapping on its transformer. + * + * If `trans.pLGhostMapping` is already set, does nothing. + * Otherwise, creates a father-only ghost mapping (empty ghost set) + * so that `IndexGlobal2Local` / `IndexLocal2Global` work even + * before ghost layers are built. + * + * \pre `trans.pLGlobalMapping` must be set (call + * `createFatherGlobalMapping` first). + * \warning Collective — calls MPI_Alltoall internally when creating + * the mapping. + */ + template + void EnsureGhostMapping(TPair &pair) + { + if (pair.trans.pLGhostMapping) + return; + DNDS_assert_info(pair.trans.pLGlobalMapping, + "EnsureGhostMapping: pLGlobalMapping must be set first"); + pair.trans.pLGhostMapping = AdjIndexInfo::makeFatherOnlyMapping( + pair.trans.pLGlobalMapping, + pair.father->Size(), mpi); + } + /** * \brief Global-to-local conversion using father+son ghost mapping. * \return local index, or (-1 - iGlobal) when not found in the pair. @@ -298,16 +324,16 @@ namespace DNDS::Geom // ================================================================= // Named wrappers — Cell // ================================================================= - index CellIndexGlobal2Local(DNDS::index i) { return IndexGlobal2Local(cellElemInfo, i); } - index CellIndexLocal2Global(DNDS::index i) { return IndexLocal2Global(cellElemInfo, i); } + index CellIndexGlobal2Local(DNDS::index i) { return IndexGlobal2Local(cell2node, i); } + index CellIndexLocal2Global(DNDS::index i) { return IndexLocal2Global(cell2node, i); } index CellIndexLocal2Global_NoSon(index i) { return IndexLocal2Global_NoSon(cell2node, i); } index CellIndexGlobal2Local_NoSon(index i) { return IndexGlobal2Local_NoSon(cell2node, i); } // ================================================================= // Named wrappers — Bnd // ================================================================= - index BndIndexGlobal2Local(DNDS::index i) { return IndexGlobal2Local(bndElemInfo, i); } - index BndIndexLocal2Global(DNDS::index i) { return IndexLocal2Global(bndElemInfo, i); } + index BndIndexGlobal2Local(DNDS::index i) { return IndexGlobal2Local(bnd2node, i); } + index BndIndexLocal2Global(DNDS::index i) { return IndexLocal2Global(bnd2node, i); } index BndIndexLocal2Global_NoSon(index i) { return IndexLocal2Global_NoSon(bnd2node, i); } index BndIndexGlobal2Local_NoSon(index i) { return IndexGlobal2Local_NoSon(bnd2node, i); } @@ -463,6 +489,33 @@ namespace DNDS::Geom void ConstructBndMesh(UnstructuredMesh &bMesh); + // ================================================================= + // Registry + // ================================================================= + + /** + * \brief Populate a MeshConnectivity registry from this mesh's + * currently-built adjacencies. + * + * Registers all adjacency arrays whose father is non-null. + * For each entity kind that appears as a source (.from) of any + * registered adjacency, finds a pLGlobalMapping from any adj + * array for that entity kind. + * + * \pre All entity kinds that have registered adjacencies must + * have at least one adj array with a valid pLGlobalMapping + * on its father. Throws if this is not satisfied. + * + * \param dag MeshConnectivity to populate (meshDim is set). + */ + void fillRegistry(MeshConnectivity &dag) const; + + /// \overload Overload with an explicit skip set. + /// AdjKinds in \p skip are excluded from registration. + void fillRegistry( + MeshConnectivity &dag, + const std::unordered_set &skip) const; + // void ReorderCellLocal(); /** diff --git a/src/Geom/Mesh/MeshConnectivity.hpp b/src/Geom/Mesh/MeshConnectivity.hpp index 67711692..56249ddf 100644 --- a/src/Geom/Mesh/MeshConnectivity.hpp +++ b/src/Geom/Mesh/MeshConnectivity.hpp @@ -182,6 +182,8 @@ namespace DNDS::Geom inline constexpr AdjKind Edge2Face{EntityKind::Edge, EntityKind::Face}; inline constexpr AdjKind Edge2Cell{EntityKind::Edge, EntityKind::Cell}; inline constexpr AdjKind Bnd2Cell{EntityKind::Bnd, EntityKind::Cell}; + inline constexpr AdjKind Bnd2Face{EntityKind::Bnd, EntityKind::Face}; + inline constexpr AdjKind Face2Bnd{EntityKind::Face, EntityKind::Bnd}; // Intra-level (composed), default via Node inline constexpr AdjKind Cell2Cell{EntityKind::Cell, EntityKind::Cell, EntityKind::Node}; @@ -869,6 +871,17 @@ namespace DNDS::Geom registerAdj(kind, std::move(adjVar)); } + /// Const overload: same as above but accepts a const reference. + /// Safe because we only copy the father shared_ptr (no mutation). + template + void registerAdj(AdjKind kind, const TPair &pair) + { + auto adjVar = makeAdjVariant(); + auto &stored = std::get(*adjVar); + stored.father = pair.father; + registerAdj(kind, std::move(adjVar)); + } + /// Overload for AdjPairTracked: unwrap to base TPair. template void registerAdj(AdjKind kind, AdjPairTracked &pair) @@ -876,6 +889,13 @@ namespace DNDS::Geom registerAdj(kind, static_cast(pair)); } + /// Const overload for AdjPairTracked. + template + void registerAdj(AdjKind kind, const AdjPairTracked &pair) + { + registerAdj(kind, static_cast(pair)); + } + /// Register a GlobalOffsetsMapping for an EntityKind. void registerGlobalMapping(EntityKind kind, const ssp &gm); diff --git a/src/Geom/Mesh/MeshConnectivity_StateChecked.hpp b/src/Geom/Mesh/MeshConnectivity_StateChecked.hpp index 0798abf7..f85e8456 100644 --- a/src/Geom/Mesh/MeshConnectivity_StateChecked.hpp +++ b/src/Geom/Mesh/MeshConnectivity_StateChecked.hpp @@ -19,23 +19,49 @@ namespace DNDS::Geom /// State-checked wrapper for MeshConnectivity::Inverse. /// - /// Asserts that the cone adjacency is in Adj_PointToGlobal state - /// (Inverse requires global indices). + /// Extracts L2G callbacks from cone and toPair ghost mappings. + /// Returns an AdjPairTracked with: + /// - father adopted from the DSL result (no copy) + /// - idx.state() == Adj_PointToGlobal /// - /// @tparam cone_rs Row-size of the cone adjacency. - /// @param cone AdjPairTracked cone (A → B, global indices). - /// @param args Remaining arguments forwarded to MeshConnectivity::Inverse. - /// @return Same as MeshConnectivity::Inverse. - template - tAdjPair CheckedInverse( + /// @param cone A → B (global state, from-entity = A). + /// @param toPair Any ArrayPair for entity B (must have trans.pLGhostMapping). + /// @param nToLocal Number of local B-entities. + /// @param mpi MPI communicator. + template + AdjPairTracked CheckedInverse( const AdjPairTracked> &cone, - TArgs &&...args) + const ToPair &toPair, + index nToLocal, + const MPIInfo &mpi) { DNDS_assert_info(cone.idx.state() == Adj_PointToGlobal, "CheckedInverse: cone must be in Adj_PointToGlobal state"); - return MeshConnectivity::Inverse( + DNDS_assert_info(cone.trans.pLGhostMapping, + "CheckedInverse: cone.trans.pLGhostMapping must be set"); + DNDS_assert_info(toPair.trans.pLGhostMapping, + "CheckedInverse: toPair.trans.pLGhostMapping must be set"); + DNDS_assert_info(toPair.father->pLGlobalMapping, + "CheckedInverse: toPair.father->pLGlobalMapping must be set"); + + auto fromL2G = [&](index i) -> index + { return cone.trans.pLGhostMapping->operator()(-1, i); }; + auto toL2G = [&](index i) -> index + { return toPair.trans.pLGhostMapping->operator()(-1, i); }; + + auto dslResult = MeshConnectivity::Inverse( static_cast &>(cone), - std::forward(args)...); + nToLocal, mpi, + std::function(fromL2G), + std::function(toL2G), + toPair.father->pLGlobalMapping); + + AdjPairTracked result; + result.father = std::move(dslResult.father); + result.son = make_ssp( + ObjName{"CheckedInverse.son"}, result.father->getMPI()); + result.idx.markGlobal(); + return result; } // ================================================================= @@ -44,15 +70,18 @@ namespace DNDS::Geom /// State-checked wrapper for MeshConnectivity::ComposeFiltered. /// - /// Asserts that both input adjacencies are in Adj_PointToGlobal state. + /// Extracts aLocal2Global from AB's ghost mapping. + /// Returns an AdjPairTracked with: + /// - father adopted from the DSL result (no copy) + /// - idx.state() == Adj_PointToGlobal + /// + /// AB must have a valid trans.pLGhostMapping (e.g., via EnsureGhostMapping). template - ArrayAdjacencyPair CheckedComposeFiltered( + AdjPairTracked> CheckedComposeFiltered( const AdjPairTracked> &AB, const AdjPairTracked> &BC, - index nALocal, const std::unordered_map &bGlobal2Local, - const std::function &aLocal2Global, Predicate &&pred, TArgs &&...args) { @@ -60,12 +89,26 @@ namespace DNDS::Geom "CheckedComposeFiltered: AB must be in Adj_PointToGlobal state"); DNDS_assert_info(BC.idx.state() == Adj_PointToGlobal, "CheckedComposeFiltered: BC must be in Adj_PointToGlobal state"); - return MeshConnectivity::ComposeFiltered( + DNDS_assert_info(AB.trans.pLGhostMapping, + "CheckedComposeFiltered: AB.trans.pLGhostMapping must be set"); + + auto aL2G = [&](index i) -> index + { return AB.trans.pLGhostMapping->operator()(-1, i); }; + + auto dslResult = MeshConnectivity::ComposeFiltered( static_cast &>(AB), static_cast &>(BC), - nALocal, bGlobal2Local, aLocal2Global, + AB.father->Size(), bGlobal2Local, + std::function(aL2G), std::forward(pred), std::forward(args)...); + + AdjPairTracked> result; + result.father = std::move(dslResult.father); + result.son = make_ssp::t_arr>( + ObjName{"CheckedComposeFiltered.son"}, result.father->getMPI()); + result.idx.markGlobal(); + return result; } // ================================================================= diff --git a/src/Geom/Mesh/Mesh_DeviceView.hpp b/src/Geom/Mesh/Mesh_DeviceView.hpp index 3f9fe08b..b27660eb 100644 --- a/src/Geom/Mesh/Mesh_DeviceView.hpp +++ b/src/Geom/Mesh/Mesh_DeviceView.hpp @@ -99,6 +99,64 @@ namespace DNDS::Geom Elevation_O1O2, }; + // ================================================================= + // Device-side views for AdjPairTracked (trivially copyable) + // ================================================================= + // Defined here (not in AdjIndexInfo.hpp) because they only depend on + // MeshAdjState and ArrayPairDeviceView, both available at this point. + // AdjIndexInfo.hpp includes this header, so AdjPairTracked can use them. + + /// \brief Device-side state for an adjacency (trivially copyable). + struct AdjIndexInfoDeviceView + { + MeshAdjState state{Adj_Unknown}; + + DNDS_DEVICE_TRIVIAL_COPY_DEFINE(AdjIndexInfoDeviceView, AdjIndexInfoDeviceView) + + DNDS_DEVICE_CALLABLE bool isLocal() const { return state == Adj_PointToLocal; } + DNDS_DEVICE_CALLABLE bool isGlobal() const { return state == Adj_PointToGlobal; } + DNDS_DEVICE_CALLABLE bool isBuilt() const { return state != Adj_Unknown; } + }; + + /// \brief Mutable device view for AdjPairTracked. + /// + /// Inherits from ArrayPairDeviceView (providing operator[], operator(), + /// Size(), RowSize()) and adds per-adj state. + template + struct AdjPairTrackedDeviceView : public ArrayPairDeviceView + { + using t_base = ArrayPairDeviceView; + using t_arrayDeviceView = typename t_base::t_arrayDeviceView; + AdjIndexInfoDeviceView idx; + + using t_self = AdjPairTrackedDeviceView; + DNDS_DEVICE_TRIVIAL_COPY_DEFINE(AdjPairTrackedDeviceView, t_self) + + DNDS_DEVICE_CALLABLE AdjPairTrackedDeviceView( + const t_arrayDeviceView &n_father, + const t_arrayDeviceView &n_son, + AdjIndexInfoDeviceView n_idx) + : t_base(n_father, n_son), idx(n_idx) {} + }; + + /// \brief Const device view for AdjPairTracked. + template + struct AdjPairTrackedDeviceViewConst : public ArrayPairDeviceViewConst + { + using t_base = ArrayPairDeviceViewConst; + using t_arrayDeviceView = typename t_base::t_arrayDeviceView; + AdjIndexInfoDeviceView idx; + + using t_self = AdjPairTrackedDeviceViewConst; + DNDS_DEVICE_TRIVIAL_COPY_DEFINE(AdjPairTrackedDeviceViewConst, t_self) + + DNDS_DEVICE_CALLABLE AdjPairTrackedDeviceViewConst( + const t_arrayDeviceView &n_father, + const t_arrayDeviceView &n_son, + AdjIndexInfoDeviceView n_idx) + : t_base(n_father, n_son), idx(n_idx) {} + }; + #define DNDS_COPY_MEMBER_VIEW(obj, member) \ member = (obj).member.template deviceView(); #define DNDS_COPY_MEMBER(obj, member) \ @@ -123,15 +181,12 @@ namespace DNDS::Geom /// reader tCoordPair::t_deviceView coords; - tAdjPair::t_deviceView cell2node; - tAdjPair::t_deviceView bnd2node; - tAdj2Pair::t_deviceView bnd2cell; - tAdjPair::t_deviceView cell2cell; + AdjPairTrackedDeviceView cell2node; + AdjPairTrackedDeviceView bnd2node; + AdjPairTrackedDeviceView bnd2cell; + AdjPairTrackedDeviceView cell2cell; tElemInfoArrayPair::t_deviceView cellElemInfo; tElemInfoArrayPair::t_deviceView bndElemInfo; - // tAdj1Pair::t_deviceView cell2cellOrig; // no device - // tAdj1Pair::t_deviceView node2nodeOrig; // no device - // tAdj1Pair::t_deviceView bnd2bndOrig; // no device /// periodic only, after reader tPbiPair::t_deviceView cell2nodePbi; tPbiPair::t_deviceView bnd2nodePbi; @@ -167,8 +222,8 @@ namespace DNDS::Geom } } - tAdjPair::t_deviceView node2cell; - tAdjPair::t_deviceView node2bnd; + AdjPairTrackedDeviceView node2cell; + AdjPairTrackedDeviceView node2bnd; auto device_array_list_N2CB() { @@ -185,12 +240,12 @@ namespace DNDS::Geom } /// interpolated - tAdjPair::t_deviceView cell2face; - tAdjPair::t_deviceView face2node; - tAdj2Pair::t_deviceView face2cell; + AdjPairTrackedDeviceView cell2face; + AdjPairTrackedDeviceView face2node; + AdjPairTrackedDeviceView face2cell; tElemInfoArrayPair::t_deviceView faceElemInfo; - tAdj1Pair::t_deviceView face2bnd; - tAdj1Pair::t_deviceView bnd2face; + AdjPairTrackedDeviceView face2bnd; + AdjPairTrackedDeviceView bnd2face; // std::vector bnd2faceV; // no device // std::unordered_map face2bndM; // no device /// periodic only, after interpolated diff --git a/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp b/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp index 3f4bfc88..3095c2d3 100644 --- a/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp +++ b/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp @@ -196,9 +196,7 @@ namespace DNDS::Geom cell2cell.trans.createFatherGlobalMapping(); MeshConnectivity dagTmp; - dagTmp.meshDim = dim; - dagTmp.registerAdj(Adj::Cell2Cell, cell2cell); - dagTmp.registerGlobalMapping(EntityKind::Cell, cell2cell.trans.pLGlobalMapping); + fillRegistry(dagTmp); GhostSpec cellSpec{{{EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell}}}; auto cellResult = dagTmp.evaluateGhostTree( From 4fc68f2335fc85879a12b63c4358c20c5dec1124 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 00:54:13 +0800 Subject: [PATCH 02/43] plan for distributed design --- docs/dev/distributed_reorder_design.md | 1378 ++++++++++++++++++------ 1 file changed, 1028 insertions(+), 350 deletions(-) diff --git a/docs/dev/distributed_reorder_design.md b/docs/dev/distributed_reorder_design.md index ec91263a..3e0f7109 100644 --- a/docs/dev/distributed_reorder_design.md +++ b/docs/dev/distributed_reorder_design.md @@ -1,45 +1,71 @@ -# Distributed Entity Reordering — Design Document +# Distributed Entity Reordering — Design Document (v2) -> **Status:** Proposal (not yet implemented). The existing -> `ReorderLocalCells` and `ReadDistributed_Redistribute` remain the -> active code paths. +> **Status:** Detailed design, not yet implemented. > -> **Last reviewed:** 2026-04-27 against PR #6 merge (commit 13b4e7b1). -> All 4 plans re-evaluated; no fundamental changes needed. See -> "Re-evaluation Notes" section at bottom. +> **Supersedes:** The original v1 design (same file, git history). +> v2 adds: placement-follow semantics, automatic adj conversion rules +> with formal classification, `PermutationTransfer` internal design, +> companion propagation, and full integration with `AdjPairTracked` / +> `fillRegistry` / checked-wrapper infrastructure. +> +> **Last updated:** 2026-04-28. + +## Table of Contents + +1. [Motivation](#motivation) +2. [Scope](#scope) +3. [Concepts and Terminology](#concepts-and-terminology) +4. [Entity Dependency Graph and Propagation](#entity-dependency-graph-and-propagation) +5. [Adjacency Conversion Rules](#adjacency-conversion-rules) +6. [PermutationTransfer Utility](#permutationtransfer-utility) +7. [ReorderEntities — Top-Level Algorithm](#reorderentities--top-level-algorithm) +8. [Companion Array Handling](#companion-array-handling) +9. [Integration with AdjPairTracked and fillRegistry](#integration-with-adjpairtracked-and-fillregistry) +10. [Local-Only Fast Path](#local-only-fast-path) +11. [Concrete Use Cases](#concrete-use-cases) +12. [Implementation Plan](#implementation-plan) +13. [Appendix: Re-evaluation Notes (v1)](#appendix-re-evaluation-notes-v1) + +--- ## Motivation The codebase has two separate reordering mechanisms: 1. **`ReorderLocalCells`** — rank-local cell permutation for cache locality. - Converts to global, replaces cell refs in xxx2cell, permutes cell2xxx rows, - rebuilds ghost mappings, converts back to local. + Converts to global, replaces cell refs in xxx2cell, permutes cell2xxx + rows, rebuilds ghost mappings, converts back to local. 2. **`ReadDistributed_Redistribute`** — cross-rank redistribution during - distributed mesh read. Uses the "father=old, son=new" ArrayTransformer - push trick. Converts adjacency entries to new global numbering via + distributed mesh read. Uses the "father=old, son=new" ArrayTransformer + push trick. Converts adjacency entries to new global numbering via ghost-pulled lookup arrays, then transfers data between ranks. -Both follow the same logical pattern but share no code. They are also -both hardcoded to cells. We need a general mechanism that can: +Both follow the same logical pattern but share no code. They are also +both hardcoded to specific entity kinds. We need a general mechanism +that can: - Reorder any entity kind (Cell, Node, Bnd, Face) - Handle rank-local and cross-rank reorderings uniformly -- Reorder multiple entity kinds simultaneously (e.g., cells and nodes - together during repartitioning) -- Detect all affected adjacencies automatically via the mesh registry +- Reorder multiple entity kinds simultaneously with automatic follow + propagation (e.g., cell reorder makes node placement follow) +- Detect all affected adjacencies and classify the required conversion + (entry remapping, row relocation, or both) automatically via the + mesh registry ## Scope ### In scope - General `ReorderEntities` method on `UnstructuredMesh` -- Multi-entity reordering (e.g., Cell + Node in one call) +- Multi-entity reordering with **explicit** and **follow** maps +- Automatic adjacency conversion: for each adj A→B, determine whether + entries need remapping, rows need relocation, or both +- `PermutationTransfer` utility for both local and distributed transfer +- Companion array handling (same row layout as a reordered entity) - Local-only fast path (no MPI when all entities stay on same rank) -- Dedicated `PermutationTransfer` utility for push/pull index construction -- Registry-based adjacency discovery (`fillRegistry`) -- Companion array handling (same row layout as the reordered entity) +- Registry-based adjacency discovery via `fillRegistry` +- Integration with `AdjPairTracked` idx state ### Out of scope (left to caller) @@ -48,477 +74,1129 @@ both hardcoded to cells. We need a general mechanism that can: - Partition computation (Metis, ParMetis, etc. — separate concern) - Face/edge reconstruction after cell/node reorder -## Key Design Decisions - -### Decision 1: Two-level permutation vs arbitrary entity reordering - -**Question:** Should we allow arbitrary combinations of entity reorderings, -or restrict to a "primary" two-level pattern (Cell + Node, with Face/Edge -fully reconstructed)? - -**Answer:** Support arbitrary combinations, but document the common patterns. - -Rationale: -- Cell-only reorder (local partitioning) is already needed -- Cell + Node reorder (repartitioning) is needed for distributed read -- Node-only reorder is conceivable (e.g., RCM on the node graph) -- Face reorder would be needed if face-based solvers are added -- Restricting to two levels would require special-casing or - re-implementing when a new pattern emerges - -The common patterns are: -1. **Cell-only local** — cache-locality reorder (current `ReorderLocalCells`) -2. **Cell + Node + Bnd** — repartitioning (current `ReadDistributed_Redistribute`) -3. **All entities** — full mesh migration +--- -For pattern (2), faces are destroyed before redistribution and rebuilt -after. This remains the recommended approach: reorder the "primary" -entities (Cell, Node, Bnd), then reconstruct derived entities -(Face, Edge) from scratch. The framework supports reordering faces -directly, but it is not the expected usage. +## Concepts and Terminology -### Decision 2: Reorder map representation +### Reorder map -Each reordered entity needs a map from old slot to new identity. +A per-entity specification that says where each owned entity goes: ```cpp -/// Per-entity reorder specification. struct EntityReorderMap { EntityKind kind; - - /// For each father slot i: (new_rank, new_global_index). - /// Size must equal the father size of the canonical array for this kind. - /// For local-only reorder: new_rank == mpi.rank for all i. - std::vector> map; + /// For each father slot i: target rank after reorder. + /// Size == father size of the canonical array for this kind. + std::vector targetRanks; }; ``` -Alternatively, the reverse map (for each new slot: old identity) is useful -for the push-based transfer. However, the forward map is more natural -for the caller (Metis/ParMetis produce partition assignments indexed by -current local entity). The implementation can derive push indices from -the forward map. +For local-only reorder: `targetRanks[i] == mpi.rank` for all i. The new +local ordering is determined by a separate permutation vector (see below). -### Decision 3: MPI collectivity for local-only detection +For distributed reorder: `targetRanks[i]` may differ from `mpi.rank`. +New global indices are computed by `PermutationTransfer` (contiguous +prefix-sum across ranks). -```cpp -bool localOnly = true; -for (auto &[rank, gidx] : entityMap.map) - if (rank != mpi.rank) { localOnly = false; break; } -// Must agree across all ranks (asymmetric decisions cause MPI hangs) -int globalLocalOnly; -MPI_Allreduce(&localOnly, &globalLocalOnly, 1, MPI_INT, MPI_LAND, mpi.comm); -``` +### Explicit vs follow reorder -When all ranks agree it is local-only, skip all MPI communication in the -data transfer step. Use in-place `PermuteRows` + direct entry replacement. +- **Explicit reorder**: the caller provides an `EntityReorderMap` directly. + The caller chose this entity's new placement. Example: Metis assigns + each cell to a rank. -### Decision 4: Ghost rebuild is the caller's responsibility +- **Follow reorder**: the entity is not directly reordered by the caller. + Instead, its placement is **derived** from an explicitly-reordered entity. + Example: when cells are redistributed, each node follows the cell that + "owns" it (e.g., lowest-rank cell referencing the node). The framework + computes the follow map automatically. -After `ReorderEntities`: -- All adjacencies are in `Adj_PointToGlobal` state -- Ghost mappings (`pLGhostMapping`) on reordered entities are stale/null -- Ghost mappings on non-reordered entities that *target* reordered entities - are stale +Both explicit and follow maps produce the same `EntityReorderMap` struct +internally. The distinction is in how the map is produced, not how it is +consumed. -The caller must rebuild ghosts. This is correct because: -- Ghost spec may differ (multi-layer, different adjacency set) -- Ghost rebuild is heavyweight and should be explicit -- The reorder may be an intermediate step before other transforms +### Canonical array -## Algorithm +For each `EntityKind`, one array pair serves as the "canonical" source of: +- Father size (= number of local entities of that kind) +- `pLGlobalMapping` (global offset mapping for this entity) -### Input - -``` -ReorderEntities( - mesh, - reorderMaps: vector, // one per entity to reorder - registry: MeshConnectivity // from fillRegistry() -) -``` +Current canonicals: +| Kind | Canonical pair | Fallback | +|------|---------------|----------| +| Cell | `cell2node` | `cell2cell`, `cell2face` | +| Node | `coords` | — | +| Bnd | `bnd2node` | `bnd2cell`, `bnd2face` | +| Face | `face2node` | `face2cell`, `face2bnd` | -### Precondition +The canonical array is what `fillRegistry` sources `globalMappings[kind]` +from. After reorder, the canonical array's global mapping is rebuilt first, +and other arrays for the same kind borrow it. -All adjacencies must be in `Adj_PointToGlobal` state. (The caller -converts to global before calling.) +### Adjacency role classification -### Step 1: Classify adjacencies +For a specific reorder operation, each registered adjacency `A→B` falls +into exactly one category: -For each registered adjacency A→B in the registry: +| A reordered? | B reordered? | A == B? | Category | Actions needed | +|:---:|:---:|:---:|---|---| +| no | no | — | `SKIP` | nothing | +| yes | no | no | `RELOCATE` | relocate rows (A moves) | +| no | yes | no | `REMAP` | remap entries (B indices change) | +| yes | yes | no | `RELOCATE_REMAP` | remap entries, then relocate rows | +| yes | yes | yes | `SELF` | remap entries, then relocate rows | -| A reordered? | B reordered? | Category | Action | -|---|---|---|---| -| yes | no | SOURCE_ONLY | relocate rows | -| no | yes | TARGET_ONLY | update entries | -| yes | yes | BOTH | relocate rows + update entries | -| no | no | NONE | skip | -| yes, A==B | — | SELF | relocate rows + update entries | +"Relocate" = move rows between ranks (or permute locally). +"Remap" = replace entry values with new global indices. -Also classify companion arrays (same row layout as a reordered entity): -`cellElemInfo` companions Cell, `bndElemInfo` companions Bnd, -`faceElemInfo` companions Face, `coords` companions Node. -Periodic-bits arrays (`cell2nodePbi`, etc.) companion their parent adj. -`*Orig` arrays (`cell2cellOrig`, etc.) companion their entity. +### Companion array -### Step 2: Build lookup arrays (old → new global) +An array whose rows are parallel to an entity kind but is not an +adjacency. Examples: +- `cellElemInfo` companions Cell +- `bndElemInfo` companions Bnd +- `faceElemInfo` companions Face +- `coords` companions Node +- `cell2nodePbi` companions Cell (same row count, parallel to `cell2node`) +- `*Orig` arrays (`cell2cellOrig`, `node2nodeOrig`, `bnd2bndOrig`) companion + their entity kind -For each reordered entity kind E: +Companions need row relocation when their entity is reordered, but have +no entries to remap (they don't store entity indices — they store +element types, coordinates, periodic bits, etc.). -1. Create `tAdj1Pair lookupE` where `lookupE(i, 0)` = new global index - for father slot `i`. -2. `createFatherGlobalMapping()` on `lookupE`. -3. Collect all old-global E references from TARGET_ONLY and BOTH - adjacencies (entries that point to E from non-E sources). -4. Ghost-pull `lookupE` so every rank can resolve any referenced E global. +--- -This is the same pattern as `ReorderLocalCells` uses for `cellOld2NewArr`. +## Entity Dependency Graph and Propagation -For local-only reorder, step 3-4 can be skipped — all lookups are local. +### The entity reference graph -### Step 3: Update entries in TARGET_ONLY and BOTH adjacencies +Each registered adjacency A->B means "A-entities store B-entity +global indices in their entries". The full reference graph for a +typical built mesh: -For each adj X→E where E is reordered: ``` -for each row i in [0, adj.father->Size()): - for each entry j: - old_global = adj(i, j) - new_global = lookupE.search_indexAppend(old_global) → lookupE(val, 0) - adj(i, j) = new_global +Cell --cell2node--> Node <--bnd2node-- Bnd + | ^ | + |--cell2face--> Face --face2node--+ | + | | | + | face2cell --> Cell |--bnd2cell--> Cell + | face2bnd --> Bnd | + |--cell2cell--> Cell (self) | + | +Node --node2cell--> Cell (support) Bnd --bnd2face--> Face +Node --node2bnd--> Bnd (support) ``` -For SELF adjacencies (E→E), update entries first, then relocate rows. +### Follow propagation rules + +When entity A is explicitly reordered, other entities may need to +**follow** (derive their own reorder map from A's). + +**Rule 1: Downward follow (referenced entity follows referencing entity)** + +When a "primary" entity is redistributed, referenced "secondary" +entities should follow to maintain locality. The follow assignment for +entity B is derived from A->B (or equivalently, from B->A support): -If the adj also has a son (ghost data), pull the updated entries after -all father entries are rewritten: ``` -adj.trans.pullOnce() // if ghost comm exists +for each local B entity b: + candidateRanks = { A.targetRank : A references b via A->B } + b.targetRank = min(candidateRanks) ``` -For local-only reorder, the lookup is a direct local array access. +Using min is deterministic and consistent with the existing +`ReadDistributed_DeriveEntityPartitions` implementation. + +Typical follow chains: +- Cell explicitly reordered -> Node follows (via cell2node / node2cell) +- Cell explicitly reordered -> Bnd follows (via bnd2cell, bnd goes to + its owner cell's rank) + +**Rule 2: No upward follow** + +Reordering nodes does NOT automatically reorder cells. Support +adjacencies (node2cell, node2bnd) have their entries remapped but +their source entity does not follow. If the caller wants both Cell +and Node reordered, both must be in the explicit set. + +**Rule 3: Derived entities are destroyed, not followed** -### Step 4: Relocate rows in SOURCE_ONLY, BOTH, and SELF adjacencies +Face entities are derived from Cell + Node. After a Cell or Node +reorder, face adjacencies become invalid. The recommended pattern: +destroy face-related arrays before reorder and reconstruct after. -For each adj E→X where E is reordered: +### Follow computation -**Local-only path:** ``` -PermuteRows(adj, NumE(), old2newLocal) +ComputeFollowMap(mesh, explicitMap_A, adjKind_B2A, mpi): + // Need B->A support. Either use an existing support (node2cell) + // or invert A->B on the fly. + + 1. Ghost-pull A's targetRanks array: + - Create tAdj1Pair lookupA with lookupA(i,0) = targetRanks[i] + - createFatherGlobalMapping + ghost-pull for all A globals + referenced by B->A entries. + + 2. For each local B entity b: + minRank = INT_MAX + for each a in B->A[b]: + rank = lookupA.resolve(a) // local or ghost + minRank = min(minRank, rank) + followMap[b] = minRank + + 3. Return EntityReorderMap{kind_B, followMap} ``` -**Distributed path:** -Uses `PermutationTransfer` (see below): -``` -auto transfer = PermutationTransfer::fromForwardMap(entityMap, globalMapping, mpi); -transfer.execute(adj.father, mpi); -// adj.father is now the redistributed array +### Which entities can follow which? + +| Explicit | Follow | Via | Assignment rule | +|----------|--------|-----|-----------------| +| Cell | Node | node2cell (support) | min rank of referencing cells | +| Cell | Bnd | bnd2cell | rank of bnd's owner cell (slot 0) | +| (rare) Node | -- | -- | no natural follower | +| (rare) Face | -- | -- | no natural follower | + +### FollowSpec struct + +```cpp +struct FollowSpec +{ + EntityKind follower; // entity kind to derive map for + EntityKind leader; // explicit-map entity kind to follow + AdjKind follower2leader; // support adj: follower -> leader + // Assignment: follower goes to min(leader.targetRank) over + // all leaders referencing this follower entity. +}; ``` -Same for companion arrays. +### Order of operations + +1. Caller provides explicit `EntityReorderMap`s. +2. Framework computes follow maps using `ComputeFollowMap`. +3. All maps (explicit + follow) are merged into a uniform set. +4. From this point, the main reorder algorithm treats all maps + identically. + +--- + +## Adjacency Conversion Rules + +This is the core of the design. Given a set of entity kinds being +reordered (explicit + follow), every registered adjacency must be +classified and converted. + +### Classification algorithm + +```cpp +enum class AdjAction { SKIP, RELOCATE, REMAP, RELOCATE_REMAP, SELF }; + +AdjAction classifyAdj(AdjKind adj, const set &reordered) +{ + bool fromReordered = reordered.count(adj.from); + bool toReordered = reordered.count(adj.to); + + if (adj.isIntraLevel()) // A == A (e.g. cell2cell) + return fromReordered ? AdjAction::SELF : AdjAction::SKIP; -### Step 5: Rebuild global mappings + if (!fromReordered && !toReordered) return AdjAction::SKIP; + if (fromReordered && !toReordered) return AdjAction::RELOCATE; + if (!fromReordered && toReordered) return AdjAction::REMAP; + return AdjAction::RELOCATE_REMAP; // both reordered +} +``` + +### What each action does + +**SKIP**: Nothing. The adjacency's entries are still valid, its rows +are in the right place. Example: `face2node` when only Face is +NOT reordered and Node is NOT reordered. + +**RELOCATE** (source reordered, target not): Rows of the adjacency +must be moved to match the new row layout of the source entity. +Entries remain unchanged (they still point to valid global indices +of the non-reordered target). Example: `cell2node` when only Cell +is reordered (not Node). Cell rows move; node globals in the +entries are still correct. + +**REMAP** (target reordered, source not): Entries must be replaced +with new global indices of the target entity. Rows stay in place. +Example: `face2cell` when only Cell is reordered. Face rows don't +move, but cell globals in the entries must be updated. + +**RELOCATE_REMAP** (both reordered): First remap entries (target's +new globals), then relocate rows (source moves). Order matters: +remap before relocate. Example: `cell2node` when both Cell and +Node are reordered. Node indices are remapped first, then cell +rows are relocated. + +**SELF** (intra-level, e.g. cell2cell): Same as RELOCATE_REMAP +but source == target. Entries point to the same entity kind as +the rows. Remap entries first (replace old cell globals with new +cell globals), then relocate rows (move to new cell layout). + +### Concrete classification for common reorder patterns + +#### Pattern 1: Cell-only local reorder + +Reordered: {Cell} + +| Adjacency | from | to | Action | +|-----------|------|----|--------| +| cell2node | Cell | Node | RELOCATE | +| cell2cell | Cell | Cell | SELF | +| cell2face | Cell | Face | RELOCATE | +| bnd2cell | Bnd | Cell | REMAP | +| face2cell | Face | Cell | REMAP | +| node2cell | Node | Cell | REMAP | +| bnd2node | Bnd | Node | SKIP | +| face2node | Face | Node | SKIP | +| node2bnd | Node | Bnd | SKIP | +| bnd2face | Bnd | Face | SKIP | +| face2bnd | Face | Bnd | SKIP | + +This matches what `ReorderLocalCells` does today: +- Replaces cell indices in face2cell, node2cell, bnd2cell, cell2cell + (= REMAP + SELF entry part) +- Permutes rows of cell2node, cell2cell, cell2face, cellElemInfo + (= RELOCATE + SELF row part) + +#### Pattern 2: Cell + Node + Bnd redistribution + +Reordered: {Cell, Node, Bnd} + +| Adjacency | from | to | Action | +|-----------|------|----|--------| +| cell2node | Cell | Node | RELOCATE_REMAP | +| cell2cell | Cell | Cell | SELF | +| bnd2node | Bnd | Node | RELOCATE_REMAP | +| bnd2cell | Bnd | Cell | RELOCATE_REMAP | +| node2cell | Node | Cell | RELOCATE_REMAP | +| node2bnd | Node | Bnd | RELOCATE_REMAP | +| face2* | Face | * | destroyed before reorder | + +This matches what `ReadDistributed_Redistribute` does: +- Converts node indices in cell2node, bnd2node (= REMAP part) +- Transfers cell2node/cellElemInfo rows with cell, bnd2node/bndElemInfo + with bnd, coords with node (= RELOCATE part) + +### Ordering constraint within a single adjacency + +For RELOCATE_REMAP and SELF: -For each reordered entity E: ``` -canonicalPair.father->createGlobalMapping() // new contiguous global numbering +1. REMAP entries (replace old target globals with new target globals) +2. RELOCATE rows (move rows to new source layout) ``` -Other pairs for the same entity borrow via `pLGlobalMapping = canonical->pLGlobalMapping`. +This ordering is safe because: +- REMAP reads old target globals and writes new target globals. This is + a value-level operation that does not change row structure. +- RELOCATE moves rows (including the already-remapped entries) to new + positions. It is a structural operation. +- Doing RELOCATE first would scatter rows to new ranks before entries + are remapped, making it impossible to resolve old target globals + (the lookup arrays are indexed by old globals). + +### Ordering between adjacencies -### Step 6: Update idx states +All REMAPs can proceed in parallel (they read from lookup arrays and +write to different adjacency arrays). All RELOCATEs can proceed in +parallel (they move rows of different arrays independently). + +The only constraint: all REMAPs for a given entity kind must complete +before any RELOCATE that moves rows for that same kind. But since +REMAP and RELOCATE operate on different adjacencies (REMAP targets +adjacencies where the entity is the *target*, RELOCATE targets +adjacencies where the entity is the *source*), they never conflict. + +Therefore the global ordering is simply: -For all affected adjacencies: ``` -adj.idx.markGlobal() // state = Adj_PointToGlobal -// Target mapping is now stale — clear it -// (caller will re-wire after ghost rebuild) +Phase 1: REMAP all entries (all adjacencies, all entity kinds) +Phase 2: RELOCATE all rows (all adjacencies, all entity kinds) ``` -Clearing stale wiring: We need a `unwireTargetMapping()` or simply allow -`wireTargetMapping` to overwrite from Global state (which it already does). -The existing `wireTargetMapping` precondition is `_state != Adj_PointToLocal`, -so rewiring from Global is already permitted. +### Ghost data during reorder -### Post-condition +**Precondition**: All adjacencies must be in `Adj_PointToGlobal` state. +Son arrays (ghost data) exist but may be stale after reorder. -- All adjacencies in `Adj_PointToGlobal` -- Father arrays for reordered entities have new row layout -- Entries pointing to reordered entities use new global indices -- Ghost mappings are stale (caller rebuilds) -- Global mappings are fresh +**During REMAP**: We need to remap entries in father rows only. +Son rows (ghost data) will be rebuilt after ghost rebuild. However, +for `ReorderLocalCells` the existing code also pulls son data after +REMAP (`face2cell.trans.pullOnce()`) to keep ghost entries consistent +for the subsequent local-to-global conversion. This is an optimization +for the local-only path where ghost mappings are NOT rebuilt. -## `PermutationTransfer` — Dedicated Utility +**During RELOCATE**: For the distributed path, the father=old/son=new +ArrayTransformer trick replaces the entire father. Son is discarded. +For the local-only path, `PermuteRows` operates on father only. + +**Post-condition**: Son arrays are stale. Ghost mappings (`pLGhostMapping`) +on reordered entities are stale. The caller must rebuild ghosts. + +--- + +## PermutationTransfer Utility ### Motivation -Both the serial-read distribution and the distributed reorder need to: -1. Convert a forward map (old_slot → new_rank) into push indices (CSR) +Both the serial-read distribution (`ReadDistributed_Redistribute`) and +the local cell reorder (`ReorderLocalCells`) need to: + +1. Convert a partition assignment or forward map into push/permute indices 2. Compute new global numbering (prefix sums across ranks) -3. Transfer array data between ranks +3. Transfer or permute array data -This is currently done by ad-hoc helpers in `Mesh_PartitionHelpers.hpp` -(`Partition2LocalIdx`, `Partition2Serial2Global`, `TransferDataSerial2Global`, -`ConvertAdjSerial2Global`). These should be unified into a reusable tool. +Currently this is done by ad-hoc helpers in `Mesh_PartitionHelpers.hpp`: +`Partition2LocalIdx`, `Partition2Serial2Global`, `TransferDataSerial2Global`, +`ConvertAdjSerial2Global`. These should be unified into a reusable tool. -### API +### Data members ```cpp -/// Encapsulates a distributed permutation: moving rows of arrays -/// between ranks according to a forward map. struct PermutationTransfer { - /// Per father slot: target rank. + /// Per father slot: target rank after reorder. std::vector targetRanks; /// New global index for each father slot. + /// Computed by prefix-sum across ranks grouped by target rank. std::vector newGlobalIndices; - /// Push-mode CSR indices (derived from targetRanks). - std::vector pushIndex; // flat, local indices to push - std::vector pushStart; // [nRanks+1] prefix sums - - /// Whether this is a rank-local permutation (no MPI needed). - bool isLocalOnly; - - /// Build from a forward map: for each father slot, (new_rank, new_global). - static PermutationTransfer fromForwardMap( - const std::vector> &forwardMap, - const ssp &oldGlobalMapping, - const MPIInfo &mpi); - - /// Build from partition assignment only (new globals computed automatically). - /// This is the pattern used by serial-read distribution. - static PermutationTransfer fromPartition( - const std::vector &partition, - const ssp &oldGlobalMapping, - const MPIInfo &mpi); - - /// Transfer array data: moves rows from old father to new father. - /// After return, pair.father contains the redistributed data. - /// Uses the "father=old, son=new" ArrayTransformer trick for - /// distributed transfers, or PermuteRows for local-only. - template - void transferRows(TPair &pair, const MPIInfo &mpi) const; - - /// Build a ghost-pullable lookup array for old→new global conversion. - /// The returned pair has: lookup(i, 0) = newGlobalIndices[i]. - /// Ghost-pulled for all globals in pullSet. - tAdj1Pair buildLookup( - const std::vector &pullSet, - const ssp &oldGlobalMapping, - const MPIInfo &mpi) const; + /// Push-mode CSR indices: pushIndex[pushStart[r]..pushStart[r+1]) + /// are the local father indices that go to rank r. + std::vector pushIndex; + std::vector pushStart; // size = nRanks + 1 + + /// Local permutation: localOld2New[i] = new local index for old + /// local index i. Only valid when isLocalOnly == true. + /// For distributed transfers, this is empty. + std::vector localOld2New; + + /// Whether this is a pure rank-local permutation (no cross-rank). + bool isLocalOnly{false}; + + /// New global offsets: newGlobalOffsets[r] = first global index + /// owned by rank r after reorder. Size = nRanks + 1. + std::vector newGlobalOffsets; }; ``` -### Usage in existing code +### Factory methods + +**`fromPartition`**: Used by redistribution (ReadDistributed, ParMetis). +Caller provides only target ranks. New global indices are computed +automatically. -**Serial-read distribution** (`PartitionReorderToMeshCell2Cell`): ```cpp -auto cellTransfer = PermutationTransfer::fromPartition(cellPartition, cellGlobal, mpi); -auto nodeTransfer = PermutationTransfer::fromPartition(nodePartition, nodeGlobal, mpi); +static PermutationTransfer fromPartition( + const std::vector &partition, + const ssp &oldGlobalMapping, + const MPIInfo &mpi) +{ + PermutationTransfer pt; + pt.targetRanks = partition; + + // 1. Compute push CSR (same as Partition2LocalIdx) + Partition2LocalIdx(partition, pt.pushIndex, pt.pushStart, mpi); + + // 2. Compute new global indices (same as Partition2Serial2Global) + Partition2Serial2Global(partition, pt.newGlobalIndices, mpi, mpi.size); + + // 3. Detect local-only + pt.isLocalOnly = true; + for (auto r : partition) + if (r != mpi.rank) { pt.isLocalOnly = false; break; } + int globalLocal; + MPI_Allreduce(&pt.isLocalOnly, &globalLocal, 1, MPI_INT, MPI_LAND, mpi.comm); + pt.isLocalOnly = globalLocal; + + // 4. Build local permutation if local-only + if (pt.isLocalOnly) + { + // newGlobalIndices maps old local -> new global. + // Convert to old local -> new local using offset. + index myOffset = oldGlobalMapping->operator()(mpi.rank, 0); + pt.localOld2New.resize(partition.size()); + for (size_t i = 0; i < partition.size(); i++) + pt.localOld2New[i] = pt.newGlobalIndices[i] - myOffset; + } + + // 5. Compute new global offsets + // (count per rank, exclusive scan) + std::vector localCounts(mpi.size, 0); + for (auto r : partition) localCounts[r]++; + std::vector totalCounts(mpi.size); + MPI_Allreduce(localCounts.data(), totalCounts.data(), + mpi.size, DNDS_MPI_INDEX, MPI_SUM, mpi.comm); + pt.newGlobalOffsets.resize(mpi.size + 1); + pt.newGlobalOffsets[0] = 0; + for (int r = 0; r < mpi.size; r++) + pt.newGlobalOffsets[r + 1] = pt.newGlobalOffsets[r] + totalCounts[r]; + + return pt; +} +``` + +**`fromLocalPermutation`**: Used by local cell reorder. Caller provides +a local old-to-new permutation. All entities stay on the same rank. + +```cpp +static PermutationTransfer fromLocalPermutation( + const std::vector &old2new, + const ssp &oldGlobalMapping, + const MPIInfo &mpi) +{ + PermutationTransfer pt; + pt.isLocalOnly = true; + pt.localOld2New = old2new; + pt.targetRanks.assign(old2new.size(), mpi.rank); + + index myOffset = oldGlobalMapping->operator()(mpi.rank, 0); + pt.newGlobalIndices.resize(old2new.size()); + for (size_t i = 0; i < old2new.size(); i++) + pt.newGlobalIndices[i] = myOffset + old2new[i]; + + // Push CSR is trivial for local-only (not needed but fill for consistency) + pt.pushStart.assign(mpi.size + 1, 0); + pt.pushStart[mpi.rank] = 0; + pt.pushStart[mpi.rank + 1] = old2new.size(); + // ... (fill remaining) + + // Global offsets unchanged for local-only + pt.newGlobalOffsets.resize(mpi.size + 1); + for (int r = 0; r <= mpi.size; r++) + pt.newGlobalOffsets[r] = oldGlobalMapping->operator()(r, 0); + + return pt; +} +``` + +### Core operations + +**`transferRows`**: Move rows of an array pair according to this transfer. + +```cpp +template +void transferRows(TPair &pair, const MPIInfo &mpi) const +{ + if (isLocalOnly) + { + // Use PermuteRows (in-place, no MPI) + UnstructuredMesh::PermuteRows(pair, pair.father->Size(), + [&](index i) { return localOld2New[i]; }); + } + else + { + // Distributed: father=old, son=new ArrayTransformer push trick + // (same as TransferDataSerial2Global) + auto oldFather = pair.father; + using TArr = typename decltype(pair.father)::element_type; + pair.father = make_ssp(ObjName{"transfer.new"}, mpi); + + typename ArrayTransformerType::Type trans; + trans.setFatherSon(oldFather, pair.father); + trans.createFatherGlobalMapping(); + trans.createGhostMapping(pushIndex, pushStart); + trans.createMPITypes(); + trans.pullOnce(); + // pair.father is now the new array with transferred data + } +} +``` + +**`buildLookup`**: Build a ghost-pullable old-global -> new-global +lookup array. Used for REMAP operations. + +```cpp +struct LookupResult +{ + tAdj1Pair pair; // pair(i, 0) = new global for local slot i + // Ghost-pulled: pair.son available for off-rank lookups. + + /// Resolve an old global index to its new global index. + index resolve(index oldGlobal) const + { + MPI_int rank; + index val; + bool found = pair.trans.pLGhostMapping->search_indexAppend( + oldGlobal, rank, val); + DNDS_assert(found); + return pair(val, 0); + } +}; + +LookupResult buildLookup( + const std::vector &pullSet, + const ssp &oldGlobalMapping, + const MPIInfo &mpi) const +{ + LookupResult result; + result.pair.InitPair("reorder_lookup", mpi); + result.pair.father->Resize(newGlobalIndices.size()); + for (index i = 0; i < (index)newGlobalIndices.size(); i++) + result.pair(i, 0) = newGlobalIndices[i]; + + result.pair.TransAttach(); + result.pair.trans.createFatherGlobalMapping(); + result.pair.trans.createGhostMapping( + std::vector(pullSet.begin(), pullSet.end())); + result.pair.trans.createMPITypes(); + result.pair.trans.pullOnce(); + + return result; +} +``` + +### Collecting the pull set -// Update cell2node entries: old node globals → new node globals -auto nodeLookup = nodeTransfer.buildLookup(referencedNodeGlobals, nodeGlobal, mpi); -ConvertAdjEntries(cell2node, ..., [&](index g) { return nodeLookup.resolve(g); }); +Before `buildLookup`, we need to know which old-global indices of +entity E are referenced by non-E adjacencies (for REMAP). This set +is collected by scanning all REMAP and RELOCATE_REMAP adjacencies: -// Transfer rows -cellTransfer.transferRows(cell2node, mpi); -cellTransfer.transferRows(cellElemInfo, mpi); -nodeTransfer.transferRows(coords, mpi); ``` +collectPullSet(entityKind E, registry, mesh, mpi): + pullSet = {} + for each adj A->B in registry where B == E and A != E: + for each row i in adj.father: + for each entry j: + global = adj(i, j) + if global != UnInitIndex and not owned by this rank: + pullSet.insert(global) + // Also from SELF adjacencies (E->E): + for each adj E->E: + for each row i in adj.father: + for each entry j: + global = adj(i, j) + if global != UnInitIndex and not owned: + pullSet.insert(global) + return sorted_unique(pullSet) +``` + +For the local-only path, the pull set only includes globals from +ghost (son) data — entries in father rows that point to off-rank +entities of the same kind. + +### Relationship to existing helpers + +| Existing helper | PermutationTransfer equivalent | +|-----------------|-------------------------------| +| `Partition2LocalIdx` | `fromPartition` (push CSR computation) | +| `Partition2Serial2Global` | `fromPartition` (new global computation) | +| `TransferDataSerial2Global` | `transferRows` (distributed path) | +| `ConvertAdjSerial2Global` | `buildLookup` + `ConvertAdjEntries` | +| `PermuteRows` (in ReorderLocalCells) | `transferRows` (local path) | +| `cellOld2NewArr` build + ghost-pull | `buildLookup` | + +After migration, the existing helpers become dead code. + +--- + +## ReorderEntities -- Top-Level Algorithm + +### API -**Local cell reorder** (`ReorderLocalCells`): ```cpp -auto cellTransfer = PermutationTransfer::fromForwardMap(cellForwardMap, cellGlobal, mpi); -assert(cellTransfer.isLocalOnly); // all ranks local +struct ReorderInput +{ + /// Explicit reorder maps (caller-provided). + std::vector explicitMaps; + + /// Follow specifications (framework computes follow maps from these). + std::vector follows; + + /// Entity kinds whose adjacencies should be destroyed before reorder + /// (not reordered, not remapped -- just wiped). Typically {Face}. + std::unordered_set destroyKinds; +}; -auto cellLookup = cellTransfer.buildLookup(referencedCellGlobals, cellGlobal, mpi); -// Update face2cell, node2cell, bnd2cell entries -// Permute cell2node, cell2face, cellElemInfo rows +/// Post-condition: all adj in Adj_PointToGlobal, ghost mappings stale. +void UnstructuredMesh::ReorderEntities(const ReorderInput &input); ``` -## Multi-Entity Reordering +### Precondition -### Problem +- All adjacencies in `Adj_PointToGlobal` state. +- For the local-only path: adjacencies may be in `Adj_PointToLocal`; + the method converts to global first, then back to local after. + (Or: caller converts beforehand. TBD.) -When repartitioning, both cells and nodes move between ranks. The order -matters: +### Full algorithm -1. Node indices in `cell2node` must be updated to new node globals - **before** `cell2node` rows are relocated (otherwise the entries - are stale after relocation). +``` +ReorderEntities(input): + + // ============================================================ + // Step 0: Validate and prepare + // ============================================================ + + 0a. Assert all adj in Adj_PointToGlobal (or convert if local). + 0b. Build registry: fillRegistry(dag, skip=destroyKinds' adjs). + 0c. Destroy adjacencies for destroyKinds: + for kind in input.destroyKinds: + for each adj where adj.from == kind or adj.to == kind: + adj.father.reset(); adj.son.reset(); + adj.idx = AdjIndexInfo{}; // back to Adj_Unknown + + // ============================================================ + // Step 1: Compute follow maps + // ============================================================ + + 1a. For each FollowSpec in input.follows: + followMap = ComputeFollowMap(mesh, explicitMaps[spec.leader], + spec.follower2leader, mpi) + allMaps[spec.follower] = followMap + + 1b. Merge explicit maps into allMaps. + (Explicit maps take precedence over follow maps if both exist + for the same entity kind -- but this should not happen.) + + 1c. reorderedKinds = set of entity kinds in allMaps. + + // ============================================================ + // Step 2: Build PermutationTransfer per entity kind + // ============================================================ + + for each kind in reorderedKinds: + transfers[kind] = PermutationTransfer::fromPartition( + allMaps[kind].targetRanks, + dag.getGlobalMapping(kind), + mpi) + + // ============================================================ + // Step 3: Classify all registered adjacencies + // ============================================================ + + for each (adjKind, adjVariant) in dag.adjRegistry: + action = classifyAdj(adjKind, reorderedKinds) + classified[adjKind] = action + + // ============================================================ + // Step 4: Build lookup arrays for REMAP + // ============================================================ + + for each kind in reorderedKinds: + pullSet = collectPullSet(kind, dag, classified, mpi) + lookups[kind] = transfers[kind].buildLookup( + pullSet, dag.getGlobalMapping(kind), mpi) + + // ============================================================ + // Step 5: Phase 1 -- REMAP all entries + // ============================================================ + + for each (adjKind, action) in classified: + if action == REMAP or action == RELOCATE_REMAP or action == SELF: + targetKind = adjKind.to + // For SELF: targetKind == adjKind.from + std::visit([&](auto &adj) { + ConvertAdjEntries(adj, adj.father->Size(), + [&](index oldGlobal) -> index { + if (oldGlobal == UnInitIndex) return UnInitIndex; + return lookups[targetKind].resolve(oldGlobal); + }); + }, *dag.resolveAdj(adjKind)); + + // Also remap entries in companion adjacencies that store entity + // indices (rare -- most companions store non-index data). + + // ============================================================ + // Step 6: Phase 2 -- RELOCATE all rows + // ============================================================ + + for each (adjKind, action) in classified: + if action == RELOCATE or action == RELOCATE_REMAP or action == SELF: + sourceKind = adjKind.from + std::visit([&](auto &adj) { + transfers[sourceKind].transferRows(adj, mpi); + }, *dag.resolveAdj(adjKind)); + + // Relocate companion arrays: + for each companion of each reordered kind: + transfers[companionKind].transferRows(companion, mpi); + + // ============================================================ + // Step 7: Rebuild global mappings + // ============================================================ + + for each kind in reorderedKinds: + // Create new contiguous global mapping on the canonical array + canonicalPair(kind).TransAttach(); + canonicalPair(kind).trans.createFatherGlobalMapping(); + // Other pairs for the same kind borrow: + for each otherPair of kind: + otherPair.father->pLGlobalMapping = + canonicalPair(kind).father->pLGlobalMapping; + + // ============================================================ + // Step 8: Update idx states and clear stale wiring + // ============================================================ + + for each (adjKind, action) in classified: + if action != SKIP: + // The adj is now in global state (entries are new globals) + mesh.getTrackedAdj(adjKind).idx.markGlobal(); + // Target mapping is stale -- will be re-wired after ghost rebuild + + // ============================================================ + // Step 9: Update mesh-level state variables + // ============================================================ + + adjPrimaryState = Adj_PointToGlobal; + if any facial adj was affected: + adjFacialState = Adj_PointToGlobal; + // etc. for adjC2FState, adjN2CBState, adjC2CFaceState +``` -2. Cell indices in `bnd2cell` must be updated to new cell globals. +### Post-condition -3. Both `cell2node` and `bnd2node` have entries pointing to nodes - (update needed) and rows belonging to cells/bnds (relocate needed). +- All (non-destroyed) adjacencies in `Adj_PointToGlobal` +- Father arrays for reordered entities have new row layout +- Entries pointing to reordered entities use new global indices +- Ghost mappings stale (caller must rebuild) +- Global mappings fresh on reordered entities +- Destroyed adjacencies (e.g., facial) have null father/son + +--- -### Ordering rule +## Companion Array Handling -For an adjacency A→B: -- If B is reordered: **update entries first** (before A is relocated) -- If A is reordered: **relocate rows second** (after B entries are updated) +### Which arrays are companions? -When both A and B are reordered: -- Update entries (B's new globals) first -- Relocate rows (A's new layout) second +| Entity kind | Companion arrays | +|-------------|-----------------| +| Cell | `cellElemInfo`, `cell2cellOrig`, `cell2nodePbi` (if periodic) | +| Node | `coords`, `node2nodeOrig`, `coordsElevDisp` (if elevation) | +| Bnd | `bndElemInfo`, `bnd2bndOrig`, `bnd2nodePbi` (if periodic) | +| Face | `faceElemInfo`, `face2nodePbi` (if periodic) | -This is safe because entry update is a value-level operation (replacing -global indices) while row relocation is a structural operation (moving -rows between ranks). The entry update reads old globals and writes new -globals; the row relocation moves the already-updated rows. +### Treatment -### Algorithm for multi-entity reorder +Companions are treated as RELOCATE-only: their rows are moved with +their entity kind, but they have no entries to remap (they don't +store entity indices). ``` -ReorderEntities(mesh, maps: {Cell→mapC, Node→mapN, Bnd→mapB}, registry): +for each reordered kind: + for each companion of that kind: + transfers[kind].transferRows(companion, mpi); +``` + +### Pbi arrays as companions + +`cell2nodePbi` has the same row count as `cell2node` and its rows +are parallel to `cell2node` rows. When cells are relocated, +`cell2nodePbi` must be relocated identically. The framework treats +pbi arrays as companions of their parent adjacency's source kind. + +### The `cell2parentCell`, `node2parentNode`, `node2bndNode` arrays + +These are local mapping vectors (not ArrayPairs). They become invalid +after reorder. The framework nullifies them (clear the vector). The +caller can rebuild them if needed (they are only used by elevation +and VTK output, which are called later). + +--- - 1. Build all lookup arrays: - cellLookup = mapC.buildLookup(...) - nodeLookup = mapN.buildLookup(...) - bndLookup = mapB.buildLookup(...) +## Integration with AdjPairTracked and fillRegistry - 2. Update all entries (order doesn't matter between kinds): - // cell2node entries: node globals → new node globals - // bnd2node entries: node globals → new node globals - // bnd2cell entries: cell globals → new cell globals - // node2cell entries: cell globals → new cell globals (if N2CB exists) - // node2bnd entries: bnd globals → new bnd globals (if N2CB exists) - // cell2cell entries: cell globals → new cell globals +### How the framework discovers adjacencies - 3. Relocate all rows (order doesn't matter between kinds): - // cell2node rows: move with cell - // cell2cell rows: move with cell - // cellElemInfo rows: move with cell - // bnd2node rows: move with bnd - // bnd2cell rows: move with bnd - // bndElemInfo rows: move with bnd - // coords rows: move with node - // node2cell rows: move with node (if exists) - // node2bnd rows: move with node (if exists) +`fillRegistry` registers all built adjacencies into a `MeshConnectivity` +registry. The reorder algorithm uses this registry to enumerate every +adjacency that exists, classify it, and operate on it. - 4. Rebuild global mappings for Cell, Node, Bnd +The registry stores `ssp` (type-erased father-only shallow +copy). But the reorder algorithm needs to operate on the actual mesh +member (to modify the father in-place and update the idx state). So the +registry is used only for **discovery and classification**. The actual +operations are performed on the mesh's `AdjPairTracked` members. - 5. Destroy derived adjacencies (face2cell, face2node, cell2face, etc.) - — caller will reconstruct from the redistributed primary data +### Mapping from AdjKind to mesh member + +The framework needs a mapping: + +```cpp +AdjKind -> AdjPairTracked & (reference to mesh member) +``` + +This can be implemented as a visitor pattern or a dispatch table: + +```cpp +// In UnstructuredMesh: +void visitAdj(AdjKind kind, auto &&visitor) +{ + if (kind == Adj::Cell2Node) visitor(cell2node); + else if (kind == Adj::Cell2Cell) visitor(cell2cell); + else if (kind == Adj::Bnd2Node) visitor(bnd2node); + // ... etc for all 12 tracked adj members +} ``` -### Face handling +Alternatively, `fillRegistry` can store references (but the current +design uses shared_ptr copies, not references). The simplest approach +is a `switch`/`if` dispatch in `ReorderEntities` since the set of +adjacencies is fixed and small. -Faces are derived from cells and nodes. After Cell + Node + Bnd -reorder, face adjacencies are meaningless (face globals no longer -correspond to the right cells/nodes). The recommended pattern: +### Idx state transitions during reorder -1. Destroy facial adjacencies before reorder -2. Reorder primary entities (Cell, Node, Bnd) -3. Rebuild: `RecoverNode2CellAndNode2Bnd` → `RecoverCell2CellAndBnd2Cell` → - `BuildGhostPrimary` → `InterpolateFace` → etc. +Before reorder: +- All adj must be `Adj_PointToGlobal` (or `Adj_PointToLocal` for + local-only, converted to global first). -This avoids reordering faces at all. If face reordering is ever needed -(e.g., face-based solver with face-local partitioning), the framework -supports it — just add Face to the reorder maps. +During reorder: +- No idx transitions happen. Entries are remapped (still global, just + different globals). Rows are relocated (still global). -## Implementation Plan +After reorder: +- `idx.markGlobal()` is called on all affected adj (idempotent if + already global, resets from Unknown if the adj was destroyed and + recreated). +- `idx._targetMapping` is stale (the ghost mapping of the target + entity was invalidated by the reorder). It will be re-wired after + the caller rebuilds ghosts. + +### fillRegistry skip set -### Phase 1: `PermutationTransfer` utility +When `destroyKinds` is specified (e.g., {Face}), the fillRegistry call +should skip adjacencies involving Face: -- Extract `Partition2LocalIdx`, `Partition2Serial2Global`, - `TransferDataSerial2Global` into a standalone `PermutationTransfer` - struct in `src/DNDS/PermutationTransfer.hpp` -- Add `fromForwardMap` and `fromPartition` factory methods -- Add `transferRows` (dispatches to local PermuteRows or distributed - ArrayTransformer push) -- Add `buildLookup` for old→new global conversion -- Unit tests in `test/cpp/` +```cpp +std::unordered_set skip; +for (auto kind : input.destroyKinds) +{ + // Skip all adjacencies where kind is source or target + for (auto &adjKind : allAdjKinds) + if (adjKind.from == kind || adjKind.to == kind) + skip.insert(adjKind); +} +fillRegistry(dag, skip); +``` -### Phase 2: `fillRegistry` on UnstructuredMesh +This prevents the registry from containing stale or soon-to-be-destroyed +adjacencies. -- Implement `fillRegistry(MeshConnectivity&, set skip)` (Plan 2) -- Canonical global mapping sources documented +--- -### Phase 3: Single-entity `ReorderEntity` +## Local-Only Fast Path -- Implement the classify/lookup/update/relocate/rebuild pipeline - for a single entity kind -- Migrate `ReorderLocalCells` to use it (local-only path) -- Test: verify local cell reorder produces identical results +### Detection -### Phase 4: Multi-entity `ReorderEntities` +```cpp +bool localOnly = true; +for (auto &[kind, transfer] : transfers) + localOnly = localOnly && transfer.isLocalOnly; +// Must be collective: +int globalLocal; +MPI_Allreduce(&localOnly, &globalLocal, 1, MPI_INT, MPI_LAND, mpi.comm); +localOnly = globalLocal; +``` -- Extend to accept multiple `EntityReorderMap`s -- Implement the multi-entity ordering rule (update entries first, - relocate rows second) -- Migrate `ReadDistributed_Redistribute` to use it -- Test: verify distributed mesh read produces identical results +### Optimizations for local-only -### Phase 5: Cleanup +1. **No distributed transfer**: `transferRows` uses `PermuteRows` + (in-place, no MPI). + +2. **Lookup arrays are local**: `buildLookup` still creates the + `tAdj1Pair` but with no ghost entries. Resolve is a direct + local array access. + +3. **Ghost mapping rebuild can be optimized**: Instead of rebuilding + from scratch, the existing ghost set can be permuted (replace old + globals with new globals in the ghost index list). This is what + `ReorderLocalCells` does today (Section F). + +4. **Son data can be preserved**: After permuting father rows and + remapping entries, the existing son data can be updated by + pulling once (the ghost mapping is rewired with permuted globals). + This avoids the full ghost rebuild overhead. -- Remove or deprecate the old ad-hoc helpers in `Mesh_PartitionHelpers.hpp` -- Remove redundant code in `ReorderLocalCells` and - `ReadDistributed_Redistribute` (they become thin wrappers) +### Local-only ghost mapping rewire (optional optimization) + +For the local-only path, the framework can optionally rewire ghost +mappings in-place instead of leaving them stale: + +``` +for each reordered kind: + for each adj whose source == kind: + // Permute the ghost index list + newGhostGlobals = [lookup.resolve(g) for g in ghostIndex] + adj.trans.createGhostMapping(newGhostGlobals) + adj.trans.createMPITypes() + adj.trans.pullOnce() +``` + +This is an optimization that keeps the mesh in a fully-local state +after `ReorderLocalCells`. The caller still needs to re-wire target +mappings (`idx.wireTargetMapping`) to point to the new ghost mappings. --- -## Re-evaluation Notes (2026-04-27, post PR #6) +## Concrete Use Cases -Reviewed against the merged PR #6 changes (InterpolateGlobal, face pipeline -split, templatization, pybind11 bindings, file reorganization into -`src/Geom/Mesh/`). +### Use case 1: Local cell reorder (ReorderLocalCells replacement) -### What changed +```cpp +// Caller computes cell permutation via Metis +auto perm = ComputeCellPermutation(...); -1. **`InterpolateFace` split** into `InterpolateFace` + `BuildGhostFace` + - `MatchFaceBoundary`. Ghost faces now via `evaluateGhostTree(Cell -> - Cell2Face -> Face)`. This creates a 5th `registerAdj` call site. +// Build explicit map +EntityReorderMap cellMap{EntityKind::Cell, /*targetRanks all = mpi.rank*/}; +// (localOld2New is embedded in PermutationTransfer) -2. **`InterpolateGlobal`** added: N-parent distributed interpolation with - pbi, ownership resolution, and template `e2p_rs` (face2cell uses rs=2). - `parent2entityPbi` computed locally (no MPI push needed). +// No follows (only cells move, nodes/bnds/faces stay) +ReorderInput input; +input.explicitMaps = {cellMap}; +// No follows, no destroyKinds (faces are already built and will have +// their entries remapped) -3. **File reorganization**: All mesh source moved to `src/Geom/Mesh/`. - `MeshConnectivity.cpp` created for non-template implementations. +mesh.ReorderEntities(input); +// All adj now in Adj_PointToGlobal, ghost mappings rewired (local-only opt) +``` -4. **`AdjPairTracked_bind.hpp`** added: Python bindings for tracked pairs - via pybind11 inheritance from base `ArrayAdjacencyPair`. +### Use case 2: Distributed redistribution (ReadDistributed replacement) -5. **Implementation plan updated** (1786 lines): Sections 9 (templatization) - and 10 (per-adj state tracking) with detailed migration plans. +```cpp +// Caller computes cell partition via ParMetis +auto cellPartition = ReadDistributed_PartitionParMetis(...); + +EntityReorderMap cellMap{EntityKind::Cell, cellPartition}; + +// Follows: Node and Bnd follow Cell +ReorderInput input; +input.explicitMaps = {cellMap}; +input.follows = { + {EntityKind::Node, EntityKind::Cell, Adj::Node2Cell}, + {EntityKind::Bnd, EntityKind::Cell, Adj::Bnd2Cell}, +}; +// Destroy faces (they will be rebuilt from scratch) +input.destroyKinds = {EntityKind::Face}; + +mesh.ReorderEntities(input); +// adjPrimaryState = Adj_PointToGlobal +// Caller proceeds: RecoverNode2CellAndNode2Bnd -> ... -> InterpolateFace +``` + +### Use case 3: Node-only reorder (hypothetical RCM on node graph) + +```cpp +EntityReorderMap nodeMap{EntityKind::Node, /*all local*/}; + +ReorderInput input; +input.explicitMaps = {nodeMap}; +// No follows (cells don't follow nodes) +// Faces must be destroyed (face2node entries become stale) +input.destroyKinds = {EntityKind::Face}; + +mesh.ReorderEntities(input); +// cell2node entries remapped (RELOCATE_REMAP if cells were also +// reordered, but here only REMAP since only Node is reordered) +// Actually: cell2node is Cell->Node, Cell not reordered, Node reordered +// => REMAP. bnd2node same. node2cell => RELOCATE. coords => companion RELOCATE. +``` -### Impact on Plan 1 (makeFatherOnlyMapping) +--- + +## Implementation Plan + +### Phase 1: PermutationTransfer utility + +**File**: `src/DNDS/PermutationTransfer.hpp` (+ `.cpp` if needed) + +- `PermutationTransfer` struct with data members +- `fromPartition` factory (wraps Partition2LocalIdx + Partition2Serial2Global) +- `fromLocalPermutation` factory +- `transferRows` (local PermuteRows or distributed push) +- `LookupResult` struct with `resolve()` +- `buildLookup` (ghost-pullable old->new global) +- Unit tests: `test/cpp/dnds_test_permutation_transfer.cpp` + - Local-only: create N entities, permute, verify + - Distributed: create N entities across 2 ranks, redistribute, verify + +### Phase 2: ReorderEntities framework + +**File**: `src/Geom/Mesh/Mesh_Reorder.cpp` (new, or extend Mesh.cpp) + +- `EntityReorderMap`, `FollowSpec`, `ReorderInput` structs (in Mesh.hpp) +- `ComputeFollowMap` free function +- `classifyAdj` free function +- `collectPullSet` free function +- `ReorderEntities` method on UnstructuredMesh + - Step 0: validate + fill registry + - Step 1: compute follows + - Step 2: build transfers + - Step 3: classify + - Step 4: build lookups + - Step 5: remap entries + - Step 6: relocate rows + companions + - Step 7: rebuild global mappings + - Step 8: update idx states + - Step 9: update mesh state variables +- `visitAdj` dispatch helper + +### Phase 3: Migrate ReorderLocalCells + +- Replace Sections B-G of `ReorderLocalCells` with: + ```cpp + auto cellMap = EntityReorderMap{EntityKind::Cell, ...}; + ReorderInput input; + input.explicitMaps = {cellMap}; + this->ReorderEntities(input); + ``` +- Keep Section A (ComputeCellPermutation) unchanged +- Verify: 74/74 C++ tests pass, 50/50 Python tests pass + +### Phase 4: Migrate ReadDistributed_Redistribute + +- Replace the manual push logic with: + ```cpp + auto cellMap = EntityReorderMap{EntityKind::Cell, partitions.cellPartition}; + ReorderInput input; + input.explicitMaps = {cellMap}; + input.follows = { + {EntityKind::Node, EntityKind::Cell, Adj::Node2Cell}, + {EntityKind::Bnd, EntityKind::Cell, Adj::Bnd2Cell}, + }; + this->ReorderEntities(input); + ``` +- The follow computation replaces `ReadDistributed_DeriveEntityPartitions` +- Verify: distributed mesh read produces identical results + +### Phase 5: Cleanup -**No changes.** `AdjIndexInfo.hpp` is unchanged. `wireTargetMapping` still -accepts null. The factory and non-null assertion are still needed. +- Deprecate `Partition2LocalIdx`, `Partition2Serial2Global`, + `TransferDataSerial2Global`, `ConvertAdjSerial2Global` +- Remove redundant manual code in old methods +- Update AGENTS.md with new API -### Impact on Plan 2 (fillRegistry) +--- -**Minor adjustment.** Now 5 call sites (was 4). The new `BuildGhostFace` -site borrows `cell2face`'s global mapping from `cell2node`, confirming the -need to document canonical global mapping sources per entity kind. +## Appendix: Re-evaluation Notes (v1) -### Impact on Plan 3 (device view) +> Preserved from v1 for historical context. The v1 design proposed the +> same general direction but lacked: follow semantics, formal adj +> classification, concrete PermutationTransfer internals, and integration +> with AdjPairTracked/fillRegistry infrastructure (which did not exist at +> the time of v1). -**Confirmed.** The inheritance pattern used for `AdjPairTracked` and -`AdjPairTracked_bind.hpp` validates the device view inheritance approach. -The `AdjStateView` struct for the mesh device view remains the pragmatic -choice (avoids changing 12 member type declarations). +### 2026-04-27, post PR #6 -### Impact on Plan 4 (distributed reordering) +Reviewed against the merged PR #6 changes (InterpolateGlobal, face pipeline +split, templatization, pybind11 bindings, file reorganization into +`src/Geom/Mesh/`). -**Mostly unchanged.** Key updates: +**Key updates for v2:** -- **Face reconstruction** after reorder should use the modular - `InterpolateFace -> BuildGhostFace -> MatchFaceBoundary` pipeline - (which internally uses `InterpolateGlobal`), not `InterpolateFaceLegacy`. +- Face reconstruction after reorder uses the modular + `InterpolateFace -> BuildGhostFace -> MatchFaceBoundary` pipeline. -- **`parent2entityPbi`** is computed locally from `cell2nodePbi` + - `entity2nodePbi` — no need to preserve across reorder. Destroyed with - faces, recomputed after. +- `parent2entityPbi` is computed locally -- no need to preserve across + reorder. Destroyed with faces, recomputed after. -- **`PermutationTransfer`** should handle `AdjVariant` via `std::visit` +- `PermutationTransfer` should handle `AdjVariant` via `std::visit` for type-erased transfer (same pattern as `evaluateGhostTree`). -- The list of arrays destroyed before reorder and reconstructed after - should include: `cell2face`, `face2cell`, `face2node`, `face2bnd`, - `bnd2face`, `faceElemInfo`, `face2nodePbi`, `cell2facePbi` (when - periodic), and any `parent2entityPbi` stored on the mesh. +- Arrays destroyed before reorder and reconstructed after: + `cell2face`, `face2cell`, `face2node`, `face2bnd`, `bnd2face`, + `faceElemInfo`, `face2nodePbi`, `cell2facePbi` (when periodic). From f1fb769c9247446cf6bafb0c51dfa82dc8d95773 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 01:05:29 +0800 Subject: [PATCH 03/43] chore(tooling): unify clang-tidy/clang-format configs and add Python drivers The clang-tidy setup was split across three files that disagreed on disables (.clangd's ClangTidy section vs src/.clang-tidy), lacked a HeaderFilterRegex (so tidy drowned in Eigen/Boost header noise), and hard-wrapped WarningsAsErrors: * which turned advisory output into an error stream. The runner scripts were slow serial bash loops. Changes: - Move src/.clang-tidy and src/.clang-tidy-fix to the project root and make them the single source of truth for both CLI and clangd. Add HeaderFilterRegex scoped to src|app|test/cpp, ExtraArgs for -UDNDS_USE_OMP and -Wno-unknown-warning-option, and set WarningsAsErrors: '' so tidy runs are advisory. - Trim .clangd to the editor-only flag tweaks; drop its ClangTidy block (clangd auto-discovers the project-root .clang-tidy). - Move scripts/run-clang-tidy{,-fix}.sh and scripts/run-clang-format.sh from src/ to scripts/; rewrite each as a thin shim that execs the new Python driver. - New scripts/run_clang_tidy.py and scripts/run_clang_format.py: scope by module names, paths, or files; support --changed, --since REF, --fix, --summary, --top-checks, --list-files, --dry-run, --strict. In-process parallelism via concurrent.futures; no external run-clang-tidy binary required. - Wrap the hand-aligned DNDS_FIELD tables inside DNDS_DECLARE_CONFIG bodies with // clang-format off / // clang-format on so clang-format stops destroying the alignment. Covered here: CFV/FiniteVolumeSettings, CFV/ModelEvaluator, CFV/VRSettings (3 blocks), DNDS/Serializer/ SerializerFactory, Geom/Mesh/Mesh (2 blocks). Remaining guarded files ship alongside the bulk-format commit that follows. - Document the new layout and CLI in docs/guides/style_guide.md, including the CUDA + clang-tidy caveat. --- .clang-tidy | 68 +++ .clang-tidy-fix | 36 ++ .clangd | 57 +-- docs/guides/style_guide.md | 51 ++- scripts/run-clang-format.sh | 8 + scripts/run-clang-tidy-fix.sh | 8 + scripts/run-clang-tidy.sh | 8 + scripts/run_clang_format.py | 341 ++++++++++++++ scripts/run_clang_tidy.py | 533 ++++++++++++++++++++++ src/.clang-tidy | 55 --- src/.clang-tidy-fix | 24 - src/CFV/FiniteVolumeSettings.hpp | 2 + src/CFV/ModelEvaluator.hpp | 2 + src/CFV/VRSettings.hpp | 6 + src/DNDS/Serializer/SerializerFactory.hpp | 2 + src/Geom/Mesh/Mesh.hpp | 4 + src/run-clang-format.sh | 46 -- src/run-clang-tidy-fix.sh | 61 --- src/run-clang-tidy.sh | 63 --- 19 files changed, 1085 insertions(+), 290 deletions(-) create mode 100644 .clang-tidy create mode 100644 .clang-tidy-fix create mode 100755 scripts/run-clang-format.sh create mode 100755 scripts/run-clang-tidy-fix.sh create mode 100755 scripts/run-clang-tidy.sh create mode 100755 scripts/run_clang_format.py create mode 100755 scripts/run_clang_tidy.py delete mode 100644 src/.clang-tidy delete mode 100644 src/.clang-tidy-fix delete mode 100755 src/run-clang-format.sh delete mode 100755 src/run-clang-tidy-fix.sh delete mode 100755 src/run-clang-tidy.sh diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..97ef8d10 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,68 @@ +# DNDSR clang-tidy configuration. +# +# Single source of truth for BOTH command-line clang-tidy runs +# (via scripts/run_clang_tidy.py) AND clangd's in-editor tidy +# diagnostics. The project-root .clangd only adds CompileFlags +# tweaks and no longer carries its own ClangTidy check list; +# clangd discovers this file automatically. +# +# Notes on check selection: +# - "Warning: clang-analyzer-optin.cplusplus.VirtualCall is +# important and should be addressed!" -- historical note kept +# from the original config. +# - The disables mirror (and extend) the list previously carried +# in .clangd's ClangTidy.Remove so CLI and IDE agree. +# - WarningsAsErrors is intentionally empty: tidy runs are +# advisory reports, not build gates. If we later want a CI +# gate, apply it as an overlay config on top of this file. +# +# Notes on ExtraArgs: +# - -UDNDS_USE_OMP: skip #include which requires +# libomp--dev matched to the tidy/clangd clang version. +# - -Wno-unknown-warning-option: our Warnings.hpp uses GCC-only +# warning names inside GCC pragmas (e.g. -Wclass-memaccess); +# clang warns on these and -Werror would turn them into errors. +# - -Wno-unused-command-line-argument: compile_commands.json +# sometimes carries flags irrelevant for the tidy pass. + +Checks: > + modernize-*, + readability-*, + bugprone-*, + performance-*, + cppcoreguidelines-*, + google-build-using-namespace, + mpi-*, + openmp-*, + -clang-diagnostic-unused-command-line-argument, + -modernize-use-trailing-return-type, + -modernize-type-traits, + -modernize-avoid-c-arrays, + -readability-braces-around-statements, + -readability-identifier-length, + -readability-implicit-bool-conversion, + -readability-else-after-return, + -readability-isolate-declaration, + -readability-magic-numbers, + -readability-math-missing-parentheses, + -readability-convert-member-functions-to-static, + -readability-avoid-nested-conditional-operator, + -bugprone-easily-swappable-parameters, + -bugprone-casting-through-void, + -performance-avoid-endl, + -performance-enum-size, + +WarningsAsErrors: '' + +HeaderFilterRegex: '.*/DNDSR/(src|app|test/cpp)/.*' + +ExtraArgs: + - -UDNDS_USE_OMP + - -Wno-unknown-warning-option + - -Wno-unused-command-line-argument + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 125 + +FormatStyle: file diff --git a/.clang-tidy-fix b/.clang-tidy-fix new file mode 100644 index 00000000..a0d7fdd9 --- /dev/null +++ b/.clang-tidy-fix @@ -0,0 +1,36 @@ +# DNDSR clang-tidy "fix" profile. +# +# A narrow, low-risk subset of checks intended to be run with +# --fix / --fix-errors to make safe mechanical improvements in +# bulk. Use via: +# +# scripts/run_clang_tidy.py --fix [scope ...] +# +# Anything that requires judgement belongs in .clang-tidy instead, +# not here. + +Checks: > + -*, + modernize-use-nodiscard, + modernize-use-equals-default, + modernize-use-using, + modernize-concat-nested-namespaces, + modernize-use-auto, + readability-make-member-function-const, + readability-uppercase-literal-suffix, + readability-qualified-auto, + +WarningsAsErrors: '' + +HeaderFilterRegex: '.*/DNDSR/(src|app|test/cpp)/.*' + +ExtraArgs: + - -UDNDS_USE_OMP + - -Wno-unknown-warning-option + - -Wno-unused-command-line-argument + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 125 + +FormatStyle: file diff --git a/.clangd b/.clangd index a1718424..0037ee1b 100644 --- a/.clangd +++ b/.clangd @@ -1,13 +1,27 @@ # clangd configuration for DNDSR # -# OpenMP: clangd needs libomp--dev to parse omp.h. If not installed, -# -UDNDS_USE_OMP below makes the #ifdef guard skip it. Install with e.g.: -# apt install libomp-18-dev (for clangd-18) +# This file only configures the editor-time behaviour that cannot +# live in .clang-tidy: # -# CUDA: The CMake build adds -I/include via CMAKE_CUDA_FLAGS -# so that compile_commands.json always contains the path, even for .cu -# entries where nvcc would normally add it implicitly. clangd's Remove -# list strips the nvcc-only flags so clang can parse the rest. +# - CompileFlags: tweak the flags clangd feeds to the clang +# frontend (strip CUDA/nvcc-only flags, disable OpenMP parsing +# when libomp--dev is missing, etc.). +# - Diagnostics.Suppress: silence frontend warnings that are +# noisy in normal DNDSR code (unused params, etc.). +# +# The clang-tidy check list lives in /.clang-tidy at the project +# root and is used unchanged by both clangd and the scripts in +# scripts/run_clang_tidy.py. Do NOT duplicate it here. +# +# OpenMP: clangd needs libomp--dev to parse omp.h. If missing, +# -UDNDS_USE_OMP below skips the #ifdef guard. Install with e.g.: +# apt install libomp-18-dev # for clangd-18 +# +# CUDA: The CMake build adds -I/include via +# CMAKE_CUDA_FLAGS so that compile_commands.json always contains +# the path, even for .cu entries where nvcc would normally add it +# implicitly. The Remove list below strips nvcc-only flags so +# clang can parse the rest. CompileFlags: Add: [ @@ -36,32 +50,3 @@ Diagnostics: unused-variable, unused-command-line-argument, ] - ClangTidy: - Remove: [ - readability-math-missing-parentheses, - modernize-use-trailing-return-type, - readability-braces-around-statements, - readability-identifier-length, - readability-implicit-bool-conversion, - readability-else-after-return, - readability-isolate-declaration, - bugprone-easily-swappable-parameters, - performance-avoid-endl, - bugprone-casting-through-void, # for MPI types... - performance-enum-size, - modernize-type-traits, - modernize-avoid-c-arrays, - readability-magic-numbers, - readability-convert-member-functions-to-static, - readability-avoid-nested-conditional-operator, - ] - Add: [ - modernize-*, - readability-* - bugprone-*, - performance-*, - cppcoreguidelines-* - google-build-using-namespace, - mpi-*, - openmp-*, - ] diff --git a/docs/guides/style_guide.md b/docs/guides/style_guide.md index 473728d8..f0531372 100644 --- a/docs/guides/style_guide.md +++ b/docs/guides/style_guide.md @@ -70,12 +70,53 @@ Avoid raw `assert()`. Use `DNDS_assert` for debug checks and - Use `if constexpr` for compile-time branching on template parameters. - Explicit template instantiation goes in `_explicit_instantiation/` subdirectories. -### clang-tidy +### clang-tidy and clang-format -The clang-tidy configuration lives at `src/.clang-tidy`. -Enabled check groups include: `modernize-*`, `readability-*`, `bugprone-*`, -`performance-*`, `cppcoreguidelines-*`, `google-build-using-namespace`, -`mpi-*`, `openmp-*`. +Configuration lives at the project root: + +- `/.clang-tidy` — enabled check groups: `modernize-*`, `readability-*`, + `bugprone-*`, `performance-*`, `cppcoreguidelines-*`, + `google-build-using-namespace`, `mpi-*`, `openmp-*` (with a curated + list of disabled checks). Used by both command-line `clang-tidy` and + clangd in editors. +- `/.clang-tidy-fix` — narrow subset intended to be run with `--fix`. +- `/.clang-format` — formatting rules. +- `/.clangd` — editor-only flag tweaks (CUDA/OpenMP handling); + intentionally does **not** duplicate the tidy check list. + +Run the checkers via the scripts in `scripts/`: + +```bash +# clang-tidy +scripts/run_clang_tidy.py # all default modules +scripts/run_clang_tidy.py Geom CFV # selected modules +scripts/run_clang_tidy.py src/Geom/Mesh # any path +scripts/run_clang_tidy.py --changed # only files dirty vs HEAD +scripts/run_clang_tidy.py --summary # just the per-check totals +scripts/run_clang_tidy.py --fix src/DNDS # apply .clang-tidy-fix + +# clang-format +scripts/run_clang_format.py # format all default modules +scripts/run_clang_format.py --check # CI-style: exit 1 if any drift +scripts/run_clang_format.py --changed # only files dirty vs HEAD +``` + +Both scripts use `concurrent.futures` internally to parallelize across +cores; no external `run-clang-tidy` helper is needed. The legacy +`scripts/run-clang-tidy.sh`, `run-clang-tidy-fix.sh`, and +`run-clang-format.sh` still work — they are thin shims that forward to +the Python drivers. + +**Column-aligned macro blocks.** Some `DNDS_FIELD(...)` blocks inside +`DNDS_DECLARE_CONFIG` bodies are hand-aligned for readability. These +blocks must be wrapped in `// clang-format off` / `// clang-format on` +so that automated formatting does not destroy the alignment. + +**CUDA note.** `clang-tidy` cannot parse nvcc-driven `.cu` compile +commands (nvcc-only flags break clang's CUDA frontend). The runner +excludes `.cu` files by default; headers included from CUDA TUs are +still tidied transitively via their `.cpp` includers. Pass `--include-cu` +to opt in anyway. ### C++ Docstrings (Doxygen) diff --git a/scripts/run-clang-format.sh b/scripts/run-clang-format.sh new file mode 100755 index 00000000..3691ae5a --- /dev/null +++ b/scripts/run-clang-format.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/run-clang-format.sh +# +# Thin backward-compatibility shim. The real implementation lives in +# scripts/run_clang_format.py -- see its --help for the full option list. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/run_clang_format.py" "$@" diff --git a/scripts/run-clang-tidy-fix.sh b/scripts/run-clang-tidy-fix.sh new file mode 100755 index 00000000..98d1e580 --- /dev/null +++ b/scripts/run-clang-tidy-fix.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/run-clang-tidy-fix.sh +# +# Thin shim: run the Python clang-tidy driver with --fix (uses the +# narrow .clang-tidy-fix profile and applies auto-fixes in place). +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/run_clang_tidy.py" --fix "$@" diff --git a/scripts/run-clang-tidy.sh b/scripts/run-clang-tidy.sh new file mode 100755 index 00000000..13299d7e --- /dev/null +++ b/scripts/run-clang-tidy.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/run-clang-tidy.sh +# +# Thin backward-compatibility shim. The real implementation lives in +# scripts/run_clang_tidy.py -- see its --help for the full option list. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/run_clang_tidy.py" "$@" diff --git a/scripts/run_clang_format.py b/scripts/run_clang_format.py new file mode 100755 index 00000000..c025c045 --- /dev/null +++ b/scripts/run_clang_format.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Run clang-format over DNDSR C++ sources. + +The style lives in /.clang-format at the project root and is used +unchanged. This script only decides which files to format and runs +clang-format in parallel. + +Quick usage:: + + scripts/run_clang_format.py # all default modules + scripts/run_clang_format.py Geom CFV # module subset + scripts/run_clang_format.py src/Geom/Mesh # directory subtree + scripts/run_clang_format.py src/DNDS/Defines.cpp # specific file(s) + scripts/run_clang_format.py --changed # files dirty vs HEAD + scripts/run_clang_format.py --since origin/main # files changed vs a ref + scripts/run_clang_format.py --check # CI mode: exit 1 if drift + +See --help for all options. +""" +from __future__ import annotations + +import argparse +import concurrent.futures as cf +import os +import shutil +import signal +import subprocess +import sys +from pathlib import Path +from typing import Iterable, Sequence + +SCRIPT_PATH = Path(__file__).resolve() +SCRIPTS_DIR = SCRIPT_PATH.parent +PROJECT_ROOT = SCRIPTS_DIR.parent + +DEFAULT_MODULES = ("DNDS", "Solver", "Geom", "CFV", "Euler", "EulerP") +DEFAULT_ROOTS = ("src", "app", "test/cpp") + +FORMAT_EXTS = ( + ".c", ".cc", ".cpp", ".cxx", + ".h", ".hpp", ".hxx", + ".cu", ".cuh", +) + + +def _which_clang_format(user: str | None) -> str: + binary = user or os.environ.get("CLANG_FORMAT") or "clang-format" + if shutil.which(binary) is None: + sys.exit(f"error: clang-format not found (looked for '{binary}')") + return binary + + +def _walk_dir(root: Path) -> Iterable[Path]: + for path in root.rglob("*"): + if path.is_file() and path.suffix in FORMAT_EXTS: + yield path + + +def _changed_files(since_ref: str | None, include_workdir: bool) -> list[Path]: + paths: list[str] = [] + if include_workdir: + for cmd in ( + ["git", "diff", "--name-only", "--diff-filter=ACMR"], + ["git", "diff", "--name-only", "--diff-filter=ACMR", "--cached"], + ): + out = subprocess.run( + cmd, cwd=PROJECT_ROOT, capture_output=True, text=True, check=False + ) + paths.extend(line for line in out.stdout.splitlines() if line) + if since_ref: + out = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMR", + f"{since_ref}...HEAD"], + cwd=PROJECT_ROOT, capture_output=True, text=True, check=False, + ) + if out.returncode != 0: + sys.exit(f"error: git diff vs '{since_ref}' failed:\n{out.stderr}") + paths.extend(line for line in out.stdout.splitlines() if line) + seen: set[str] = set() + out_paths: list[Path] = [] + for p in paths: + if p in seen: + continue + seen.add(p) + full = (PROJECT_ROOT / p).resolve() + if full.exists() and full.suffix in FORMAT_EXTS: + out_paths.append(full) + return out_paths + + +def _is_under_roots(path: Path, roots: Sequence[str]) -> bool: + try: + rel = path.resolve().relative_to(PROJECT_ROOT) + except ValueError: + return False + parts = rel.parts + for root in roots: + root_parts = tuple(root.split("/")) + if parts[: len(root_parts)] == root_parts: + return True + return False + + +def _select_files(scope_args: Sequence[str]) -> list[Path]: + tokens: list[str] = [] + for a in scope_args: + tokens.extend(t for t in a.split(",") if t) + + collected: list[Path] = [] + + if not tokens: + for module in DEFAULT_MODULES: + root = PROJECT_ROOT / "src" / module + if root.is_dir(): + collected.extend(_walk_dir(root)) + for extra in ("app", "test/cpp"): + root = PROJECT_ROOT / extra + if root.is_dir(): + collected.extend(_walk_dir(root)) + else: + for t in tokens: + module_root = PROJECT_ROOT / "src" / t + if "/" not in t and module_root.is_dir(): + collected.extend(_walk_dir(module_root)) + continue + p = Path(t) + if not p.is_absolute(): + p = (PROJECT_ROOT / t).resolve() + else: + p = p.resolve() + if not p.exists(): + sys.exit(f"error: path not found: {p}") + if p.is_dir(): + collected.extend(_walk_dir(p)) + elif p.suffix in FORMAT_EXTS: + collected.append(p) + + seen: set[str] = set() + uniq: list[Path] = [] + for p in collected: + key = str(p.resolve()) + if key in seen: + continue + seen.add(key) + uniq.append(p) + return uniq + + +def _format_one(args: tuple[str, Path, bool]) -> tuple[Path, int, str, bool]: + binary, path, check_only = args + if check_only: + res = subprocess.run( + [binary, "--dry-run", "--Werror", str(path)], + capture_output=True, text=True, check=False, + ) + changed = res.returncode != 0 + return path, res.returncode, (res.stderr or res.stdout), changed + else: + res = subprocess.run( + [binary, "-i", str(path)], + capture_output=True, text=True, check=False, + ) + return path, res.returncode, (res.stderr or res.stdout), False + + +def _run( + files: Sequence[Path], + binary: str, + *, + jobs: int, + check_only: bool, + quiet: bool, +) -> tuple[int, int, int]: + n = len(files) + width = len(str(n)) + n_changed = 0 + n_errors = 0 + exit_code = 0 + + with cf.ThreadPoolExecutor(max_workers=jobs) as pool: + futures = { + pool.submit(_format_one, (binary, f, check_only)): f for f in files + } + + def _sig(_signo, _frame): + print("\n^C: cancelling...", file=sys.stderr) + pool.shutdown(cancel_futures=True) + sys.exit(130) + old = signal.signal(signal.SIGINT, _sig) + try: + done = 0 + for fut in cf.as_completed(futures): + path, rc, msg, changed = fut.result() + done += 1 + rel = str(path.relative_to(PROJECT_ROOT) + ) if path.is_absolute() else str(path) + if check_only: + if changed: + n_changed += 1 + if not quiet: + print( + f"[{done:>{width}}/{n}] would reformat: {rel}") + else: + if rc != 0: + n_errors += 1 + print(f"[{done:>{width}}/{n}] ERROR {rel}: {msg.strip()}", + file=sys.stderr) + elif not quiet: + print(f"[{done:>{width}}/{n}] {rel}") + finally: + signal.signal(signal.SIGINT, old) + + if check_only and n_changed > 0: + exit_code = 1 + if n_errors > 0: + exit_code = max(exit_code, 1) + return exit_code, n_changed, n_errors + + +def _build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="run_clang_format.py", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument( + "scope", + nargs="*", + help=( + "Module names (DNDS, Geom, ...), directory paths, or files. " + "Empty means all default modules under src/, plus app/ and test/cpp/." + ), + ) + p.add_argument( + "-j", "--jobs", + type=int, + default=os.cpu_count() or 4, + help="Parallel jobs (default: all cores).", + ) + p.add_argument( + "--check", + action="store_true", + help="Report only; exit 1 if any file would be reformatted.", + ) + p.add_argument( + "--changed", + action="store_true", + help="Only format files dirty in the work tree (staged + unstaged).", + ) + p.add_argument( + "--since", + metavar="REF", + help="Only format files changed vs REF.", + ) + p.add_argument( + "--quiet", + action="store_true", + help="Print only errors and summary.", + ) + p.add_argument( + "--list-files", + action="store_true", + help="Print the selected files and exit (no formatting).", + ) + p.add_argument( + "--clang-format", + default=None, + help="clang-format binary (default: $CLANG_FORMAT or 'clang-format').", + ) + p.add_argument( + "-n", "--dry-run", + action="store_true", + help="Print what would be run and exit (no clang-format invocations).", + ) + return p + + +def main(argv: Sequence[str] | None = None) -> int: + args = _build_parser().parse_args(argv) + binary = _which_clang_format(args.clang_format) + + if args.changed or args.since: + files = _changed_files(args.since, include_workdir=args.changed) + files = [f for f in files if _is_under_roots(f, DEFAULT_ROOTS)] + if args.scope: + scoped = set(str(p.resolve()) for p in _select_files(args.scope)) + files = [f for f in files if str(f.resolve()) in scoped] + else: + files = _select_files(args.scope) + + if args.list_files: + for f in files: + rel = str(f.relative_to(PROJECT_ROOT) + ) if f.is_absolute() else str(f) + print(rel) + return 0 + + version = subprocess.run( + [binary, "--version"], capture_output=True, text=True, check=False + ).stdout.strip() + print(version or "(clang-format version unknown)") + print(f"clang-format : {binary}") + print(f"jobs : {args.jobs}") + print(f"files : {len(files)}") + print(f"mode : {'check' if args.check else 'in-place'}") + + if args.dry_run: + print("\n>> files (first 20 of {}):".format(len(files))) + for f in files[:20]: + rel = str(f.relative_to(PROJECT_ROOT) + ) if f.is_absolute() else str(f) + print(" " + rel) + return 0 + + if not files: + print("no files matched the selected scope -- nothing to do.") + return 0 + + rc, n_changed, n_errors = _run( + files, binary, jobs=args.jobs, check_only=args.check, quiet=args.quiet, + ) + + print() + if args.check: + if n_changed == 0: + print(f"all {len(files)} files are already formatted.") + else: + print(f"{n_changed}/{len(files)} files need reformatting.") + else: + msg = f"formatted {len(files) - n_errors}/{len(files)} files" + if n_errors: + msg += f" ({n_errors} errors)" + print(msg + ".") + return rc + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(130) diff --git a/scripts/run_clang_tidy.py b/scripts/run_clang_tidy.py new file mode 100755 index 00000000..2b260be9 --- /dev/null +++ b/scripts/run_clang_tidy.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +"""Drive clang-tidy over the DNDSR C++ sources. + +The checks live in /.clang-tidy at the project root and are used +unchanged. This script only decides which files to tidy, drives +clang-tidy in parallel, and prints a per-check summary. + +Quick usage:: + + scripts/run_clang_tidy.py # all modules under src/ + scripts/run_clang_tidy.py Geom CFV # module subset + scripts/run_clang_tidy.py Geom,CFV # same (comma-separated) + scripts/run_clang_tidy.py src/Geom/Mesh # any path under src/ + scripts/run_clang_tidy.py src/DNDS/Defines.cpp # one file + scripts/run_clang_tidy.py --changed # files dirty vs HEAD + scripts/run_clang_tidy.py --since origin/main # files changed vs a ref + scripts/run_clang_tidy.py --fix src/DNDS # apply .clang-tidy-fix + scripts/run_clang_tidy.py --summary # only the summary + scripts/run_clang_tidy.py --top-checks 20 # show 20 most common checks + +See --help for the full option list. +""" +from __future__ import annotations + +import argparse +import collections +import concurrent.futures as cf +import datetime as _dt +import json +import os +import re +import shutil +import signal +import subprocess +import sys +from pathlib import Path +from typing import Iterable, Sequence + +SCRIPT_PATH = Path(__file__).resolve() +SCRIPTS_DIR = SCRIPT_PATH.parent +PROJECT_ROOT = SCRIPTS_DIR.parent + +DEFAULT_MODULES = ("DNDS", "Solver", "Geom", "CFV", "Euler", "EulerP") +DEFAULT_ROOTS = ("src", "app", "test/cpp") + +TU_EXTS = (".c", ".cc", ".cpp", ".cxx") +CUDA_EXTS = (".cu",) + +CHECK_RE = re.compile(r"\[([a-z][a-z0-9.-]+)(?:,-warnings-as-errors)?\]") + + +def _resolve_build_dir(arg: str | None) -> Path: + if arg: + return Path(arg).resolve() + env = os.environ.get("DNDS_BUILD_DIR") + if env: + return Path(env).resolve() + return (PROJECT_ROOT / "build").resolve() + + +def _which_clang_tidy(user: str | None) -> str: + binary = user or os.environ.get("CLANG_TIDY") or "clang-tidy" + if shutil.which(binary) is None: + sys.exit(f"error: clang-tidy not found (looked for '{binary}')") + return binary + + +def _load_compile_db(build_dir: Path) -> list[dict]: + path = build_dir / "compile_commands.json" + if not path.exists(): + sys.exit( + f"error: no compile_commands.json in {build_dir}\n" + "hint: configure with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON " + "or -DDNDS_GENERATE_COMPILE_COMMANDS=ON" + ) + try: + return json.loads(path.read_text()) + except json.JSONDecodeError as exc: + sys.exit(f"error: failed to parse {path}: {exc}") + + +def _tu_files_from_db(db: Iterable[dict]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for entry in db: + path = entry.get("file") + if not path: + continue + if path in seen: + continue + seen.add(path) + out.append(path) + return out + + +def _changed_files(since_ref: str | None, include_workdir: bool) -> list[str]: + paths: list[str] = [] + if include_workdir: + for cmd in ( + ["git", "diff", "--name-only", "--diff-filter=ACMR"], + ["git", "diff", "--name-only", "--diff-filter=ACMR", "--cached"], + ): + out = subprocess.run( + cmd, cwd=PROJECT_ROOT, capture_output=True, text=True, check=False + ) + paths.extend(line for line in out.stdout.splitlines() if line) + if since_ref: + out = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMR", + f"{since_ref}...HEAD"], + cwd=PROJECT_ROOT, capture_output=True, text=True, check=False, + ) + if out.returncode != 0: + sys.exit(f"error: git diff vs '{since_ref}' failed:\n{out.stderr}") + paths.extend(line for line in out.stdout.splitlines() if line) + seen: set[str] = set() + deduped: list[str] = [] + for p in paths: + if p in seen: + continue + seen.add(p) + deduped.append(p) + return deduped + + +def _is_under(path: Path, roots: Sequence[str]) -> bool: + try: + rel = path.resolve().relative_to(PROJECT_ROOT) + except ValueError: + return False + parts = rel.parts + for root in roots: + root_parts = tuple(root.split("/")) + if parts[: len(root_parts)] == root_parts: + return True + return False + + +def _match_scope( + all_tus: Sequence[str], + scope_args: Sequence[str], + *, + include_cu: bool, +) -> list[str]: + tokens: list[str] = [] + for a in scope_args: + tokens.extend(t for t in a.split(",") if t) + + exts = tuple(TU_EXTS) + (tuple(CUDA_EXTS) if include_cu else ()) + + def ext_ok(p: str) -> bool: + return p.endswith(exts) + + if not tokens: + modules = DEFAULT_MODULES + allowed_prefixes = { + str((PROJECT_ROOT / "src" / m).resolve()) + os.sep + for m in modules + } + for extra_root in ("app", "test/cpp"): + allowed_prefixes.add( + str((PROJECT_ROOT / extra_root).resolve()) + os.sep + ) + return [ + tu for tu in all_tus + if ext_ok(tu) and any(tu.startswith(p) for p in allowed_prefixes) + ] + + def looks_like_path(tok: str) -> bool: + return ( + "/" in tok + or tok.startswith(".") + or os.path.isabs(tok) + or (PROJECT_ROOT / tok).exists() + ) + + paths_toks = [t for t in tokens if looks_like_path(t)] + module_toks = [t for t in tokens if not looks_like_path(t)] + + resolved_prefixes: set[str] = set() + explicit_files: set[str] = set() + + for t in module_toks: + p = (PROJECT_ROOT / "src" / t).resolve() + if not p.is_dir(): + sys.exit(f"error: module '{t}' not found at {p}") + resolved_prefixes.add(str(p) + os.sep) + + for t in paths_toks: + p = Path(t) + if not p.is_absolute(): + p = (PROJECT_ROOT / t).resolve() + else: + p = p.resolve() + if not p.exists(): + sys.exit(f"error: path not found: {p}") + if p.is_dir(): + resolved_prefixes.add(str(p) + os.sep) + else: + explicit_files.add(str(p)) + + selected: list[str] = [] + for tu in all_tus: + if tu in explicit_files: + selected.append(tu) + continue + if not ext_ok(tu): + continue + if any(tu.startswith(pref) for pref in resolved_prefixes): + selected.append(tu) + return selected + + +def _base_args( + clang_tidy: str, + build_dir: Path, + config_file: Path, + header_filter: str | None, + fix: bool, +) -> list[str]: + args = [ + clang_tidy, + "-p", str(build_dir), + "--config-file", str(config_file), + ] + if header_filter: + args += ["--header-filter", header_filter] + if fix: + args += ["--fix-errors"] + return args + + +def _run_one(cmd: list[str]) -> tuple[str, int, str]: + file = cmd[-1] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=False) + except KeyboardInterrupt: + raise + output = (res.stdout or "") + (res.stderr or "") + return file, res.returncode, output + + +def _run_parallel( + files: Sequence[str], + base: Sequence[str], + jobs: int, + *, + quiet: bool, + log_path: Path, +) -> tuple[int, collections.Counter[str]]: + exit_code = 0 + counts: collections.Counter[str] = collections.Counter() + n = len(files) + width = len(str(n)) + + futures: dict[cf.Future, str] = {} + + def _cancel_all(pool: cf.Executor) -> None: + pool.shutdown(cancel_futures=True) + + with log_path.open("w") as log, cf.ThreadPoolExecutor(max_workers=jobs) as pool: + for f in files: + futures[pool.submit(_run_one, list(base) + [f])] = f + + def _sig(_signo, _frame): + print("\n^C: cancelling...", file=sys.stderr) + _cancel_all(pool) + sys.exit(130) + + old = signal.signal(signal.SIGINT, _sig) + try: + done_n = 0 + for fut in cf.as_completed(futures): + done_n += 1 + file, rc, output = fut.result() + if rc != 0 and exit_code == 0: + exit_code = rc + rel = os.path.relpath(file, PROJECT_ROOT) + header = f"[{done_n:>{width}}/{n}] {rel} (rc={rc})" + log.write(header + "\n") + if output: + log.write(output) + if not output.endswith("\n"): + log.write("\n") + if not quiet: + print(header) + if output.strip(): + sys.stdout.write(output) + if not output.endswith("\n"): + sys.stdout.write("\n") + counts.update(CHECK_RE.findall(output)) + finally: + signal.signal(signal.SIGINT, old) + return exit_code, counts + + +def _build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="run_clang_tidy.py", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument( + "scope", + nargs="*", + help=( + "Module names (e.g. DNDS, Geom), comma-separated module lists " + "(e.g. Geom,CFV), directory paths, or individual files. " + "Empty means all default modules." + ), + ) + p.add_argument( + "-j", "--jobs", + type=int, + default=os.cpu_count() or 4, + help="Parallel jobs (default: all cores).", + ) + p.add_argument( + "--build", + default=None, + help=( + "Build directory with compile_commands.json " + "(default: $DNDS_BUILD_DIR or ./build)." + ), + ) + p.add_argument( + "--config-file", + default=None, + help="Override .clang-tidy path (default: /.clang-tidy).", + ) + p.add_argument( + "--fix", + action="store_true", + help="Use .clang-tidy-fix and apply --fix-errors.", + ) + p.add_argument( + "--include-cu", + action="store_true", + help=( + "Also tidy .cu files. Off by default: nvcc-only flags in " + "compile_commands.json break clang's CUDA frontend." + ), + ) + p.add_argument( + "--no-header-filter", + action="store_true", + help="Skip project header filter on the command line (debug).", + ) + p.add_argument( + "--header-filter", + default=None, + help=( + "Override the --header-filter regex. " + "Default: '.*/DNDSR/(src|app|test/cpp)/.*'." + ), + ) + scope_grp = p.add_argument_group("scope shortcuts") + scope_grp.add_argument( + "--changed", + action="store_true", + help="Only files dirty in the work tree (staged + unstaged).", + ) + scope_grp.add_argument( + "--since", + metavar="REF", + help="Only files changed vs REF (git ref). Can combine with --changed.", + ) + out_grp = p.add_argument_group("output") + out_grp.add_argument( + "--quiet", + action="store_true", + help="Suppress per-file output; only print the summary.", + ) + out_grp.add_argument( + "--summary", + action="store_true", + help="Alias for --quiet.", + ) + out_grp.add_argument( + "--top-checks", + type=int, + default=0, + metavar="N", + help="Limit the summary to the top-N checks (0 = show all).", + ) + out_grp.add_argument( + "--log-dir", + default=None, + help="Directory for log files (default: /clang-tidy-logs).", + ) + out_grp.add_argument( + "--list-files", + action="store_true", + help="Print the selected files and exit (no tidy run).", + ) + p.add_argument( + "--clang-tidy", + default=None, + help="clang-tidy binary (default: $CLANG_TIDY or 'clang-tidy').", + ) + p.add_argument( + "-n", "--dry-run", + action="store_true", + help="Print what would be run and exit.", + ) + p.add_argument( + "--strict", + action="store_true", + help=( + "Propagate clang-tidy's non-zero exit status. By default the " + "script exits 0 regardless of diagnostics (advisory mode)." + ), + ) + return p + + +def main(argv: Sequence[str] | None = None) -> int: + args = _build_parser().parse_args(argv) + + build_dir = _resolve_build_dir(args.build) + if not build_dir.is_dir(): + sys.exit(f"error: build directory does not exist: {build_dir}") + clang_tidy = _which_clang_tidy(args.clang_tidy) + + if args.config_file: + config = Path(args.config_file).resolve() + elif args.fix: + config = PROJECT_ROOT / ".clang-tidy-fix" + else: + config = PROJECT_ROOT / ".clang-tidy" + if not config.exists(): + sys.exit(f"error: config file not found: {config}") + + header_filter: str | None + if args.no_header_filter: + header_filter = None + else: + header_filter = args.header_filter or ".*/DNDSR/(src|app|test/cpp)/.*" + + log_dir = Path(args.log_dir) if args.log_dir else build_dir / \ + "clang-tidy-logs" + log_dir.mkdir(parents=True, exist_ok=True) + stamp = _dt.datetime.now().strftime("%Y%m%d-%H%M%S") + log_path = log_dir / f"run-{stamp}.log" + + db = _load_compile_db(build_dir) + all_tus = _tu_files_from_db(db) + + if args.changed or args.since: + changed = _changed_files(args.since, include_workdir=args.changed) + accepted_exts = tuple(TU_EXTS) + (tuple(CUDA_EXTS) + if args.include_cu else ()) + rel_roots = tuple(DEFAULT_ROOTS) + abs_changed: set[str] = set() + for rel in changed: + if not rel.endswith(accepted_exts): + continue + abs_path = str((PROJECT_ROOT / rel).resolve()) + if _is_under(Path(abs_path), rel_roots): + abs_changed.add(abs_path) + selected = [tu for tu in all_tus if tu in abs_changed] + if args.scope: + scoped = set(_match_scope(all_tus, args.scope, + include_cu=args.include_cu)) + selected = [tu for tu in selected if tu in scoped] + else: + selected = _match_scope(all_tus, args.scope, + include_cu=args.include_cu) + + if args.list_files: + for f in selected: + print(os.path.relpath(f, PROJECT_ROOT)) + return 0 + + if not selected: + print("no files matched the selected scope -- nothing to do.") + return 0 + + base = _base_args( + clang_tidy=clang_tidy, + build_dir=build_dir, + config_file=config, + header_filter=header_filter, + fix=args.fix, + ) + + version = subprocess.run( + [clang_tidy, "--version"], capture_output=True, text=True, check=False + ).stdout.splitlines()[:1] + print(version[0] if version else "(clang-tidy version unknown)") + print(f"clang-tidy : {clang_tidy}") + print(f"config : {config}") + print(f"compile db : {build_dir / 'compile_commands.json'}") + print(f"jobs : {args.jobs}") + print(f"files : {len(selected)}") + if header_filter: + print(f"header filter: {header_filter}") + print(f"fix mode : {args.fix}") + print(f"log : {log_path}") + + if args.dry_run: + print("\n>> base command:") + print(" " + " ".join(base)) + print("\n>> files (first 20 of {}):".format(len(selected))) + for f in selected[:20]: + print(" " + os.path.relpath(f, PROJECT_ROOT)) + return 0 + + quiet = args.quiet or args.summary + exit_code, counts = _run_parallel( + selected, base, jobs=args.jobs, quiet=quiet, log_path=log_path, + ) + + print() + print(f"=== diagnostic summary ({log_path}) ===") + if not counts: + print(" (no diagnostics)") + else: + items = counts.most_common(args.top_checks or None) + total = sum(counts.values()) + for name, n in items: + print(f" {n:>6} {name}") + print(" " + "-" * 6) + print(f" {total:>6} TOTAL ({len(counts)} distinct checks)") + + return exit_code if args.strict else 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(130) diff --git a/src/.clang-tidy b/src/.clang-tidy deleted file mode 100644 index ea745ca5..00000000 --- a/src/.clang-tidy +++ /dev/null @@ -1,55 +0,0 @@ -Checks: > - modernize-*, - readability-*, - bugprone-*, - performance-*, - cppcoreguidelines-* - google-build-using-namespace, - mpi-*, - openmp-*, - -clang-diagnostic-unused-command-line-argument, - -modernize-use-trailing-return-type, - -readability-braces-around-statements, - -readability-identifier-length, - -readability-implicit-bool-conversion, - -readability-else-after-return, - -readability-isolate-declaration, - -bugprone-easily-swappable-parameters, - -# Warning: clang-analyzer-optin.cplusplus.VirtualCall is very important and should be addressed! - -WarningsAsErrors: > - *, - clang-analyzer-optin.cplusplus.VirtualCall, - -clang-analyzer-optin.mpi.MPI-Checker, - -openmp-use-default-none, - -clang-analyzer-deadcode.DeadStores, - -readability-function-cognitive-complexity, - -bugprone-narrowing-conversions, - -bugprone-reserved-identifier, - -modernize-use-nullptr, - -modernize-pass-by-value, - -modernize-loop-convert, - -readability-magic-numbers, - -readability-redundant-access-specifiers, - -bugprone-branch-clone, - -performance-unnecessary-copy-initialization, - -performance-unnecessary-value-param, - -clang-diagnostic-unneeded-internal-declaration, - -modernize-use-transparent-functors, - -readability-simplify-boolean-expr, - -modernize-use-nodiscard, - -modernize-use-equals-default, - -readability-make-member-function-const, - -readability-uppercase-literal-suffix, - -readability-qualified-auto, - -modernize-use-using,-warnings-as-errors, - -modernize-concat-nested-namespaces, - -CheckOptions: - - key: readability-function-cognitive-complexity.Threshold - value: 125 - - -# HeaderFilterRegex: '.*' # Apply checks to all files, modify as needed -FormatStyle: 'file' # Use the format style defined in .clang-format file \ No newline at end of file diff --git a/src/.clang-tidy-fix b/src/.clang-tidy-fix deleted file mode 100644 index 59126032..00000000 --- a/src/.clang-tidy-fix +++ /dev/null @@ -1,24 +0,0 @@ -Checks: > - -*, - -clang-diagnostic-unused-command-line-argument, - modernize-use-nodiscard, - modernize-use-equals-default, - readability-make-member-function-const, - readability-uppercase-literal-suffix, - readability-qualified-auto, - modernize-use-using,-warnings-as-errors, - modernize-concat-nested-namespaces, - modernize-use-auto, - -# readability-container-size-empty, - - - - -CheckOptions: - - key: readability-function-cognitive-complexity.Threshold - value: 125 - - -# HeaderFilterRegex: '.*' # Apply checks to all files, modify as needed -FormatStyle: 'file' # Use the format style defined in .clang-format file \ No newline at end of file diff --git a/src/CFV/FiniteVolumeSettings.hpp b/src/CFV/FiniteVolumeSettings.hpp index a97aeec3..958e688b 100644 --- a/src/CFV/FiniteVolumeSettings.hpp +++ b/src/CFV/FiniteVolumeSettings.hpp @@ -42,6 +42,7 @@ namespace DNDS::CFV DNDS_DECLARE_CONFIG(FiniteVolumeSettings) { + // clang-format off DNDS_FIELD(maxOrder, "Polynomial degree of reconstruction", DNDS::Config::range(0)); DNDS_FIELD(intOrder, "Global integration degree", @@ -49,6 +50,7 @@ namespace DNDS::CFV DNDS_FIELD(ignoreMeshGeometryDeficiency, "Ignore mesh geometry deficiency warnings"); DNDS_FIELD(nIterCellSmoothScale, "Cell smooth scale iterations", DNDS::Config::range(0)); + // clang-format on } /// @brief Backward-compatible write (used by Python bindings and VRSettings). diff --git a/src/CFV/ModelEvaluator.hpp b/src/CFV/ModelEvaluator.hpp index bf853b0e..739da543 100644 --- a/src/CFV/ModelEvaluator.hpp +++ b/src/CFV/ModelEvaluator.hpp @@ -23,10 +23,12 @@ namespace DNDS::CFV DNDS_DECLARE_CONFIG(ModelSettings) { + // clang-format off DNDS_FIELD(ax, "Advection velocity x-component"); DNDS_FIELD(ay, "Advection velocity y-component"); DNDS_FIELD(sigma, "Diffusion coefficient", DNDS::Config::range(0.0)); + // clang-format on } }; diff --git a/src/CFV/VRSettings.hpp b/src/CFV/VRSettings.hpp index f7050c69..c140e640 100644 --- a/src/CFV/VRSettings.hpp +++ b/src/CFV/VRSettings.hpp @@ -61,8 +61,10 @@ namespace DNDS::CFV DNDS_DECLARE_CONFIG(BaseSettings) { + // clang-format off DNDS_FIELD(localOrientation, "Use local orientation for basis"); DNDS_FIELD(anisotropicLengths, "Use anisotropic length scales"); + // clang-format on } } baseSettings; @@ -130,6 +132,7 @@ namespace DNDS::CFV DNDS_DECLARE_CONFIG(FunctionalSettings) { + // clang-format off DNDS_FIELD(scaleType, "Functional scale type"); DNDS_FIELD(scaleMultiplier, "Functional scale multiplier", DNDS::Config::range(0.0)); @@ -149,6 +152,7 @@ namespace DNDS::CFV DNDS_FIELD(greenGauss1Bias, "Green-Gauss type-1 bias"); DNDS_FIELD(greenGauss1Penalty, "Green-Gauss type-1 penalty"); DNDS_FIELD(greenGaussSpacial, "Green-Gauss spatial mode: 0=default, 1=uniform"); + // clang-format on } DNDS_DEVICE_CALLABLE FunctionalSettings() = default; } functionalSettings; @@ -165,6 +169,7 @@ namespace DNDS::CFV DNDS_DECLARE_CONFIG(VRSettings) { + // clang-format off // Base class fields (FiniteVolumeSettings) — flattened into the same JSON object. // Cast base-class pointer-to-member to derived type for template deduction. config.field(static_cast(&T::maxOrder), "maxOrder", @@ -203,6 +208,7 @@ namespace DNDS::CFV "Functional/weight settings"); DNDS_FIELD(bcWeight, "Boundary condition weight", DNDS::Config::range(0.0)); + // clang-format on } /// @brief Backward-compatible write (used by Python bindings). diff --git a/src/DNDS/Serializer/SerializerFactory.hpp b/src/DNDS/Serializer/SerializerFactory.hpp index ec911f2f..7bd625b8 100644 --- a/src/DNDS/Serializer/SerializerFactory.hpp +++ b/src/DNDS/Serializer/SerializerFactory.hpp @@ -41,6 +41,7 @@ namespace DNDS::Serializer DNDS_DECLARE_CONFIG(SerializerFactory) { + // clang-format off DNDS_FIELD(type, "Serializer backend: \"JSON\" or \"H5\"", DNDS::Config::enum_values({"JSON", "H5"})); DNDS_FIELD(hdfDeflateLevel, "HDF5 deflate compression level", @@ -52,6 +53,7 @@ namespace DNDS::Serializer DNDS_FIELD(jsonBinaryDeflateLevel, "JSON binary deflate level", DNDS::Config::range(0, 9)); DNDS_FIELD(jsonUseCodecOnUInt8, "Apply codec on uint8 arrays in JSON"); + // clang-format on } /// @brief Instantiate the selected serializer and apply its tunables. diff --git a/src/Geom/Mesh/Mesh.hpp b/src/Geom/Mesh/Mesh.hpp index 12161598..b19d019e 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -938,6 +938,7 @@ namespace DNDS::Geom DNDS_DECLARE_CONFIG(WallDistOptions) { + // clang-format off DNDS_FIELD(subdivide_quad, "Subdivide quads for wall distance computation", DNDS::Config::range(0)); DNDS_FIELD(method, "Wall distance computation method (0: brute, 1: tree)", @@ -948,6 +949,7 @@ namespace DNDS::Geom DNDS::Config::range(0.0)); DNDS_FIELD(verbose, "Verbosity level for wall distance computation", DNDS::Config::range(0)); + // clang-format on } }; void BuildNodeWallDist(const std::function &fBndIsWall, WallDistOptions options = WallDistOptions{}); @@ -1032,6 +1034,7 @@ namespace DNDS::Geom DNDS_DECLARE_CONFIG(PartitionOptions) { + // clang-format off DNDS_FIELD(metisType, "METIS partitioning method", DNDS::Config::enum_values({"KWAY", "RB"})); DNDS_FIELD(metisUfactor, "METIS imbalance factor (ufactor)", @@ -1041,6 +1044,7 @@ namespace DNDS::Geom DNDS::Config::range(0, 1)); DNDS_FIELD(metisNcuts, "Number of cuts for METIS to try", DNDS::Config::range(1)); + // clang-format on } }; diff --git a/src/run-clang-format.sh b/src/run-clang-format.sh deleted file mode 100755 index d4f36c5d..00000000 --- a/src/run-clang-format.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -if command -v clang-format-19 >/dev/null 2>&1; then - echo "clang-format-19 is installed." -else - echo "error: clang-format-19 is not installed." - return 1 -fi - -CURRENT_DIR="$(pwd)" - -# Get the script's directory -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# cd $SCRIPT_DIR - -# Directories to search for C++ files relative to the script position -DIRS=(\ -"DNDS" \ -"Solver" \ -"Geom" \ -"CFV" \ -"Euler" \ -) - -# Loop through each directory in the array -for DIR in "${DIRS[@]}"; do - # Check if the directory exists - if [ -d "$SCRIPT_DIR/$DIR" ]; then - # Find all C++ source files (.cpp, .hpp) in the directory - FILES=$(find "$SCRIPT_DIR/$DIR" -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.hxx" \)) - - # Run clang-format on each found file - for FILE in $FILES; do - echo "Formatting: $FILE" - clang-format-19 -i "$FILE" - done - else - echo "Directory $SCRIPT_DIR/$DIR does not exist." - fi -done - -echo "Formatting complete." - -# cd "$CURRENT_DIR" || exit - diff --git a/src/run-clang-tidy-fix.sh b/src/run-clang-tidy-fix.sh deleted file mode 100755 index 19cce9c7..00000000 --- a/src/run-clang-tidy-fix.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# if command -v clang-tidy-19 >/dev/null 2>&1; then -# echo "clang-tidy-19 is installed." -# else -# echo "error: clang-tidy-19 is not installed." -# return 1 -# fi - -CURRENT_DIR="$(pwd)" - -# Get the script's directory -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# cd $SCRIPT_DIR - -# Directories to search for C++ files relative to the script position -DIRS=(\ -"DNDS" \ -"Solver" \ -"Geom" \ -"CFV" \ -"Euler" \ - ) - -if [[ $# -gt 0 ]]; then - IFS=',' read -r -a DIRS <<< "$1" -fi -echo "DIRS=${DIRS[@]}" - -# CHECKS="*" - -# CHECKS+=,-clang-diagnostic-unused-command-line-argument -# CHECKS+=,-google-readability-namespace-comments -# CHECKS+=,-modernize-use-trailing-return-type - -# Loop through each directory in the array -for DIR in "${DIRS[@]}"; do - # Check if the directory exists - if [ -d "$SCRIPT_DIR/$DIR" ]; then - # Find all C++ source files (.cpp, .hpp) in the directory - FILES=$(find "$SCRIPT_DIR/$DIR" -type f -name "*.cpp" -o -name "*.hpp" -o -name "*.hxx" ) - - # Run clang-format on each found file - for FILE in $FILES; do - echo "Clang-tidy: $FILE" - clang-tidy -p=$SCRIPT_DIR/../build --config-file=$SCRIPT_DIR/.clang-tidy-fix --fix-errors "$FILE" - # clang-tidy -p=$SCRIPT_DIR/../build "$FILE" - done - # echo "Clang-tidy: $FILES" - # echo "$FILES" | xargs clang-tidy -p=$SCRIPT_DIR/../build --checks=$CHECKS - else - echo "Directory $SCRIPT_DIR/$DIR does not exist." - fi -done - - -echo "Tidying complete." - -# cd "$CURRENT_DIR" || exit - diff --git a/src/run-clang-tidy.sh b/src/run-clang-tidy.sh deleted file mode 100755 index d5859ab5..00000000 --- a/src/run-clang-tidy.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -CURRENT_DIR="$(pwd)" - -# Get the script's directory -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# cd $SCRIPT_DIR - -# Directories to search for C++ files relative to the script position -DIRS=(\ -"DNDS" \ -"Solver" \ -"Geom" \ -"CFV" \ -"Euler" \ - ) - -if [[ $# -gt 0 ]]; then - IFS=',' read -r -a DIRS <<< "$1" -fi -echo "DIRS=${DIRS[@]}" - -# CHECKS="*" - -# CHECKS+=,-clang-diagnostic-unused-command-line-argument -# CHECKS+=,-google-readability-namespace-comments -# CHECKS+=,-modernize-use-trailing-return-type - -# Loop through each directory in the array -for DIR in "${DIRS[@]}"; do - # Check if the directory exists - if [ -d "$SCRIPT_DIR/$DIR" ]; then - # Find all C++ source files (.cpp, .hpp) in the directory - FILES=$(find "$SCRIPT_DIR/$DIR" -type f -name "*.cpp" -o -name "*.hpp" -o -name "*.hxx" ) - - # Run clang-format on each found file - for FILE in $FILES; do - echo "Clang-tidy: $FILE" - # clang-tidy -p=$SCRIPT_DIR/../build --config-file=$SCRIPT_DIR/.clang-tidy-fix --fix-errors "$FILE" - clang-tidy -p=$SCRIPT_DIR/../build "$FILE" - - if [ $? -ne 0 ]; then - echo "=== === === === === === === === === === === ===" - echo "clang-tidy failed with a non-zero exit code." - echo $FILE - echo "=== === === === === === === === === === === ===" - echo "\n\n\n\n" - # exit 1 - fi - done - # echo "Clang-tidy: $FILES" - # echo "$FILES" | xargs clang-tidy -p=$SCRIPT_DIR/../build --checks=$CHECKS - else - echo "Directory $SCRIPT_DIR/$DIR does not exist." - fi -done - - -echo "Tidying complete." - -# cd "$CURRENT_DIR" || exit - From 2d5257c886adb8a8da2a89502d13b6efa032a47a Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 01:09:00 +0800 Subject: [PATCH 04/43] style: apply clang-format across DNDS/ and drifted Euler/Solver headers Pure-format commit produced by running scripts/run_clang_format.py src/DNDS scripts/run_clang_format.py src/Euler/{CLDriver,EulerEvaluatorSettings,EulerSolver}.hpp src/Solver/Direct.hpp against the project .clang-format, using the Python driver added in the preceding commit. The four Euler/Solver files had pre-existing drift outside their DNDS_DECLARE_CONFIG blocks (pragma indentation, empty body braces, trailing comments) and are brought in line here too so the guarded alignment changes in the previous commit can land on a clean base. No semantic changes. --- src/DNDS/ArrayBasic.hpp | 12 +- src/DNDS/ArrayDOFPack.hpp | 2 - src/DNDS/ArrayDerived/ArrayAdjacency_bind.cpp | 2 +- .../ArrayDerived/ArrayEigenMatrixBatch.hpp | 1 - .../ArrayDerived/ArrayEigenMatrix_bind.hpp | 2 +- src/DNDS/ArrayRedistributor.hpp | 15 +- src/DNDS/ArrayTransformer.hpp | 6 +- src/DNDS/Array_bind.hpp | 6 +- src/DNDS/Config/ConfigEnum.hpp | 20 ++- src/DNDS/Config/ConfigParam.hpp | 137 +++++++++------ src/DNDS/Config/ConfigRegistry.hpp | 8 +- src/DNDS/Defines.hpp | 2 +- src/DNDS/Defines_bind.hpp | 2 +- src/DNDS/Device/DeviceStorage.hpp | 2 +- src/DNDS/IdealGasPhysics.hpp | 4 +- src/DNDS/MPI.cpp | 1 - src/DNDS/ObjectUtils.hpp | 4 +- src/DNDS/Profiling.hpp | 38 ++-- src/DNDS/Serializer/SerializerBase.hpp | 2 +- src/DNDS/Warnings.hpp | 90 +++++----- src/Euler/CLDriver.hpp | 30 ++-- src/Euler/EulerEvaluatorSettings.hpp | 132 +++++++------- src/Euler/EulerSolver.hpp | 164 ++++++++++-------- src/Solver/Direct.hpp | 26 +-- 24 files changed, 386 insertions(+), 322 deletions(-) diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index 0d9a1ffb..6dfbbaa0 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -16,12 +16,12 @@ namespace DNDS */ enum DataLayout { - ErrorLayout, ///< Invalid combination of template parameters. - TABLE_StaticFixed, ///< Fixed row width, known at compile time. - TABLE_Fixed, ///< Fixed row width, set at runtime (uniform across rows). - TABLE_Max, ///< Padded variable rows; max width set at runtime. - TABLE_StaticMax, ///< Padded variable rows; max width fixed at compile time. - CSR, ///< Compressed Sparse Row (flat buffer + row-start index). + ErrorLayout, ///< Invalid combination of template parameters. + TABLE_StaticFixed, ///< Fixed row width, known at compile time. + TABLE_Fixed, ///< Fixed row width, set at runtime (uniform across rows). + TABLE_Max, ///< Padded variable rows; max width set at runtime. + TABLE_StaticMax, ///< Padded variable rows; max width fixed at compile time. + CSR, ///< Compressed Sparse Row (flat buffer + row-start index). }; /// @brief Whether the layout uses a TABLE (padded) representation (vs CSR). diff --git a/src/DNDS/ArrayDOFPack.hpp b/src/DNDS/ArrayDOFPack.hpp index eddc4265..acad903f 100644 --- a/src/DNDS/ArrayDOFPack.hpp +++ b/src/DNDS/ArrayDOFPack.hpp @@ -39,7 +39,5 @@ namespace DNDS } } } - - }; } \ No newline at end of file diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.cpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.cpp index 840d34f4..2bbd3f6f 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.cpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.cpp @@ -5,7 +5,7 @@ namespace DNDS void pybind11_bind_ArrayAdjacency_All(py::module_ &m) { pybind11_callBindArrayAdjacencys_rowsizes(m); - + pybind11_ArrayAdjacency_define(m); pybind11_ArrayAdjacency_define(m); diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp index e59a927c..2a22fdc2 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp @@ -51,7 +51,6 @@ namespace DNDS this->operator=(R); } - template void InitializeWriteRow(index i, const std::vector &matrices) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp index 697ee416..87b50630 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp @@ -130,7 +130,7 @@ namespace DNDS { auto arr = std::make_shared(self); return arr; }); - + ArrayEigenMatrix_ .def("MatRowSize", [](const TArrayEigenMatrix &self, index iMat) { return self.MatRowSize(iMat); }, py::arg("iMat") = 0) diff --git a/src/DNDS/ArrayRedistributor.hpp b/src/DNDS/ArrayRedistributor.hpp index 7c289344..a033e450 100644 --- a/src/DNDS/ArrayRedistributor.hpp +++ b/src/DNDS/ArrayRedistributor.hpp @@ -92,7 +92,8 @@ namespace DNDS DNDS_assert_info(nGlobal > 0, "Redistribution requires nGlobal > 0"); auto directoryRank = [&](index origIdx) -> int { - if (nGlobal == 0) return 0; + if (nGlobal == 0) + return 0; return static_cast(std::min(index(nRanks - 1), origIdx * index(nRanks) / nGlobal)); }; @@ -143,8 +144,8 @@ namespace DNDS std::vector recvBuf(index(recvDisps[nRanks]) * 2); MPI_Alltoallv(sendBuf.data(), sendCounts2.data(), sendDisps2.data(), DNDS_MPI_INDEX, - recvBuf.data(), recvCounts2.data(), recvDisps2.data(), DNDS_MPI_INDEX, - mpi.comm); + recvBuf.data(), recvCounts2.data(), recvDisps2.data(), DNDS_MPI_INDEX, + mpi.comm); // Step 4: Build directory lookup: origIdx -> globalReadIdx std::unordered_map directoryMap; @@ -190,8 +191,8 @@ namespace DNDS // Alltoallv queries std::vector queryRecvBuf(queryRecvDisps[nRanks]); MPI_Alltoallv(querySendBuf.data(), querySendCounts.data(), querySendDisps.data(), DNDS_MPI_INDEX, - queryRecvBuf.data(), queryRecvCounts.data(), queryRecvDisps.data(), DNDS_MPI_INDEX, - mpi.comm); + queryRecvBuf.data(), queryRecvCounts.data(), queryRecvDisps.data(), DNDS_MPI_INDEX, + mpi.comm); // Step 6: Directory ranks look up and reply with globalReadIdx. std::vector queryReplyBuf(queryRecvDisps[nRanks]); @@ -206,8 +207,8 @@ namespace DNDS // Alltoallv replies back (reverse direction) std::vector replyRecvBuf(querySendDisps[nRanks]); MPI_Alltoallv(queryReplyBuf.data(), queryRecvCounts.data(), queryRecvDisps.data(), DNDS_MPI_INDEX, - replyRecvBuf.data(), querySendCounts.data(), querySendDisps.data(), DNDS_MPI_INDEX, - mpi.comm); + replyRecvBuf.data(), querySendCounts.data(), querySendDisps.data(), DNDS_MPI_INDEX, + mpi.comm); // Step 7: Build pullingIndexGlobal in newOrigIndex order. std::vector pullingIndexGlobal(newOrigIndex.size()); diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 08300640..8d3df567 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -381,9 +381,9 @@ namespace DNDS */ [[nodiscard]] index globalSize() const { - DNDS_assert_info(pLGlobalMapping, - "globalSize() requires global mapping. " - "Ensure createGlobalMapping() was called first (typically via ArrayPair operations)."); + DNDS_assert_info(pLGlobalMapping, + "globalSize() requires global mapping. " + "Ensure createGlobalMapping() was called first (typically via ArrayPair operations)."); return pLGlobalMapping->globalSize(); } }; diff --git a/src/DNDS/Array_bind.hpp b/src/DNDS/Array_bind.hpp index 7b4242d2..23a368c6 100644 --- a/src/DNDS/Array_bind.hpp +++ b/src/DNDS/Array_bind.hpp @@ -342,10 +342,8 @@ namespace DNDS // ParArrayPair return new_pair; }); Pair_ - .def("InitPair", - [](TPair &self, const std::string &name, const MPIInfo &mpi) - { self.InitPair(name, mpi); }, - py::arg("name"), py::arg("mpi")) + .def("InitPair", [](TPair &self, const std::string &name, const MPIInfo &mpi) + { self.InitPair(name, mpi); }, py::arg("name"), py::arg("mpi")) .def("TransAttach", &TPair::TransAttach) .def("hash", &TPair::hash) .def("Size", &TPair::Size); diff --git a/src/DNDS/Config/ConfigEnum.hpp b/src/DNDS/Config/ConfigEnum.hpp index ad793cf8..71836721 100644 --- a/src/DNDS/Config/ConfigEnum.hpp +++ b/src/DNDS/Config/ConfigEnum.hpp @@ -83,8 +83,10 @@ namespace DNDS EnumStringPair(EnumType v, const char *s) : value(v), str(s ? s : ""), isNull(s == nullptr) {} - EnumStringPair(EnumType v, std::nullptr_t) // NOLINT(bugprone-macro-parentheses) - : value(v), isNull(true) {} + EnumStringPair(EnumType v, std::nullptr_t) // NOLINT(bugprone-macro-parentheses) + : value(v), isNull(true) + { + } }; /// @brief Extract non-null string values from a list of enum-string pairs. @@ -123,13 +125,13 @@ namespace DNDS /// 1. `NLOHMANN_JSON_SERIALIZE_ENUM(EnumType_, ...)` — standard serialization. /// 2. `inline std::vector _dnds_enum_values_()` — allowed values. /// Accessed via `DNDS_ENUM_ALLOWED_VALUES(EnumType_)`. -#define DNDS_DEFINE_ENUM_JSON(EnumType_, ...) \ - /* (1) Standard nlohmann enum serialization */ \ - NLOHMANN_JSON_SERIALIZE_ENUM(EnumType_, __VA_ARGS__) \ - /* (2) Allowed-values function for schema generation */ \ - inline std::vector _dnds_enum_allowed_values_fn(EnumType_ *) \ - { \ - return ::DNDS::detail::extractEnumStrings(__VA_ARGS__); \ +#define DNDS_DEFINE_ENUM_JSON(EnumType_, ...) \ + /* (1) Standard nlohmann enum serialization */ \ + NLOHMANN_JSON_SERIALIZE_ENUM(EnumType_, __VA_ARGS__) \ + /* (2) Allowed-values function for schema generation */ \ + inline std::vector _dnds_enum_allowed_values_fn(EnumType_ *) \ + { \ + return ::DNDS::detail::extractEnumStrings(__VA_ARGS__); \ } // ============================================================================ diff --git a/src/DNDS/Config/ConfigParam.hpp b/src/DNDS/Config/ConfigParam.hpp index 64fb5bd1..9b20ef85 100644 --- a/src/DNDS/Config/ConfigParam.hpp +++ b/src/DNDS/Config/ConfigParam.hpp @@ -102,8 +102,16 @@ namespace DNDS static constexpr ConfigTypeTag value = ConfigTypeTag::Object; }; - template <> struct ConfigTypeTagOf { static constexpr ConfigTypeTag value = ConfigTypeTag::Bool; }; - template <> struct ConfigTypeTagOf { static constexpr ConfigTypeTag value = ConfigTypeTag::String; }; + template <> + struct ConfigTypeTagOf + { + static constexpr ConfigTypeTag value = ConfigTypeTag::Bool; + }; + template <> + struct ConfigTypeTagOf + { + static constexpr ConfigTypeTag value = ConfigTypeTag::String; + }; template struct ConfigTypeTagOf && !std::is_same_v>> @@ -148,13 +156,17 @@ namespace DNDS namespace detail { template - struct is_eigen_type : std::false_type {}; + struct is_eigen_type : std::false_type + { + }; template struct is_eigen_type(T::RowsAtCompileTime)), - decltype(static_cast(T::ColsAtCompileTime))>> : std::true_type {}; + typename T::Scalar, + decltype(static_cast(T::RowsAtCompileTime)), + decltype(static_cast(T::ColsAtCompileTime))>> : std::true_type + { + }; } // namespace detail template @@ -167,16 +179,26 @@ namespace DNDS { switch (tag) { - case ConfigTypeTag::Bool: return "boolean"; - case ConfigTypeTag::Int: return "integer"; - case ConfigTypeTag::Real: return "number"; - case ConfigTypeTag::String: return "string"; - case ConfigTypeTag::Enum: return "string"; - case ConfigTypeTag::Array: return "array"; - case ConfigTypeTag::Object: return "object"; - case ConfigTypeTag::ArrayOfObjects: return "array"; - case ConfigTypeTag::MapOfObjects: return "object"; - case ConfigTypeTag::Json: return {}; + case ConfigTypeTag::Bool: + return "boolean"; + case ConfigTypeTag::Int: + return "integer"; + case ConfigTypeTag::Real: + return "number"; + case ConfigTypeTag::String: + return "string"; + case ConfigTypeTag::Enum: + return "string"; + case ConfigTypeTag::Array: + return "array"; + case ConfigTypeTag::Object: + return "object"; + case ConfigTypeTag::ArrayOfObjects: + return "array"; + case ConfigTypeTag::MapOfObjects: + return "object"; + case ConfigTypeTag::Json: + return {}; } return {}; } @@ -659,45 +681,46 @@ namespace DNDS /// } /// }; /// @endcode -#define DNDS_DECLARE_CONFIG(Type_) \ - using T = Type_; \ - static void _dnds_ensure_registered() \ - { \ - static bool done = false; \ - if (done) return; \ - done = true; \ - ::DNDS::ConfigSectionBuilder config; \ - _dnds_do_register(config); \ - } \ - friend void to_json(nlohmann::ordered_json &j, const Type_ &t) \ - { \ - Type_::_dnds_ensure_registered(); \ - ::DNDS::ConfigRegistry::writeToJson(j, t); \ - } \ - friend void from_json(const nlohmann::ordered_json &j, Type_ &t) \ - { \ - Type_::_dnds_ensure_registered(); \ - ::DNDS::ConfigRegistry::readFromJson(j, t); \ - } \ - static nlohmann::ordered_json schema(const std::string &desc = "") \ - { \ - Type_::_dnds_ensure_registered(); \ - return ::DNDS::ConfigRegistry::emitSchema(desc); \ - } \ - std::vector<::DNDS::CheckResult> validate() const \ - { \ - Type_::_dnds_ensure_registered(); \ - return ::DNDS::ConfigRegistry::validate(*this); \ - } \ - std::vector<::DNDS::CheckResult> validateWithContext( \ - const ::DNDS::ConfigContext &ctx) const \ - { \ - Type_::_dnds_ensure_registered(); \ - return ::DNDS::ConfigRegistry::validateWithContext(*this, ctx); \ - } \ - static void validateKeys(const nlohmann::ordered_json &j) \ - { \ - Type_::_dnds_ensure_registered(); \ - ::DNDS::ConfigRegistry::validateKeys(j); \ - } \ +#define DNDS_DECLARE_CONFIG(Type_) \ + using T = Type_; \ + static void _dnds_ensure_registered() \ + { \ + static bool done = false; \ + if (done) \ + return; \ + done = true; \ + ::DNDS::ConfigSectionBuilder config; \ + _dnds_do_register(config); \ + } \ + friend void to_json(nlohmann::ordered_json &j, const Type_ &t) \ + { \ + Type_::_dnds_ensure_registered(); \ + ::DNDS::ConfigRegistry::writeToJson(j, t); \ + } \ + friend void from_json(const nlohmann::ordered_json &j, Type_ &t) \ + { \ + Type_::_dnds_ensure_registered(); \ + ::DNDS::ConfigRegistry::readFromJson(j, t); \ + } \ + static nlohmann::ordered_json schema(const std::string &desc = "") \ + { \ + Type_::_dnds_ensure_registered(); \ + return ::DNDS::ConfigRegistry::emitSchema(desc); \ + } \ + std::vector<::DNDS::CheckResult> validate() const \ + { \ + Type_::_dnds_ensure_registered(); \ + return ::DNDS::ConfigRegistry::validate(*this); \ + } \ + std::vector<::DNDS::CheckResult> validateWithContext( \ + const ::DNDS::ConfigContext &ctx) const \ + { \ + Type_::_dnds_ensure_registered(); \ + return ::DNDS::ConfigRegistry::validateWithContext(*this, ctx); \ + } \ + static void validateKeys(const nlohmann::ordered_json &j) \ + { \ + Type_::_dnds_ensure_registered(); \ + ::DNDS::ConfigRegistry::validateKeys(j); \ + } \ static void _dnds_do_register(::DNDS::ConfigSectionBuilder &config) diff --git a/src/DNDS/Config/ConfigRegistry.hpp b/src/DNDS/Config/ConfigRegistry.hpp index 418dba56..b4736bcb 100644 --- a/src/DNDS/Config/ConfigRegistry.hpp +++ b/src/DNDS/Config/ConfigRegistry.hpp @@ -139,10 +139,10 @@ namespace DNDS /// Checks registered via `config.check_ctx()` receive it as a second argument. struct ConfigContext { - int nVars = -1; ///< Number of solution variables (model-dependent). - int dim = -1; ///< Spatial dimension (2 or 3). - int gDim = -1; ///< Geometric dimension (2 or 3). - int modelCode = -1; ///< Integer code identifying the EulerModel enum value. + int nVars = -1; ///< Number of solution variables (model-dependent). + int dim = -1; ///< Spatial dimension (2 or 3). + int gDim = -1; ///< Geometric dimension (2 or 3). + int modelCode = -1; ///< Integer code identifying the EulerModel enum value. }; /// @brief Descriptor for a single configuration field. diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index 32055d88..1e4c0326 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -613,7 +613,7 @@ namespace DNDS return false; } - ///@todo //TODO: overflow_assign_int64_to_32 + ///@todo //TODO: overflow_assign_int64_to_32 /// @brief Narrow #index to `int32_t` with range check; dies on overflow. inline int32_t checkedIndexTo32(index v) diff --git a/src/DNDS/Defines_bind.hpp b/src/DNDS/Defines_bind.hpp index b6387789..7bd1dd54 100644 --- a/src/DNDS/Defines_bind.hpp +++ b/src/DNDS/Defines_bind.hpp @@ -6,7 +6,7 @@ #include "Defines.hpp" #ifdef DNDS_USE_OMP -#include +# include #endif #include #include diff --git a/src/DNDS/Device/DeviceStorage.hpp b/src/DNDS/Device/DeviceStorage.hpp index bf379b89..deed765c 100644 --- a/src/DNDS/Device/DeviceStorage.hpp +++ b/src/DNDS/Device/DeviceStorage.hpp @@ -29,7 +29,7 @@ namespace DNDS Unknown = 0, ///< Unset / sentinel. Host = 1, ///< Plain CPU memory. #ifdef DNDS_USE_CUDA - CUDA = 2, ///< NVIDIA CUDA device memory. + CUDA = 2, ///< NVIDIA CUDA device memory. #endif Custom1 = 101, ///< Reserved slot for a project-specific backend. }; diff --git a/src/DNDS/IdealGasPhysics.hpp b/src/DNDS/IdealGasPhysics.hpp index 0616e7d6..2aae6da3 100644 --- a/src/DNDS/IdealGasPhysics.hpp +++ b/src/DNDS/IdealGasPhysics.hpp @@ -192,8 +192,8 @@ namespace DNDS::IdealGas */ DNDS_DEVICE_CALLABLE inline void EntropyFix_HCorrHY(real aL, real aR, real vnL, real vnR, - real dLambda, real fixScale, - real &lam0, real &lam123, real &lam4) + real dLambda, real fixScale, + real &lam0, real &lam123, real &lam4) { const real scaleHartenYee = kScaleHartenYee * fixScale; const real scaleHFix = kScaleHFix * fixScale; diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 76305233..5718e127 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -24,7 +24,6 @@ # include #endif - #ifdef NDEBUG # define NDEBUG_DISABLED # undef NDEBUG diff --git a/src/DNDS/ObjectUtils.hpp b/src/DNDS/ObjectUtils.hpp index c699507a..f942bd7b 100644 --- a/src/DNDS/ObjectUtils.hpp +++ b/src/DNDS/ObjectUtils.hpp @@ -60,8 +60,8 @@ namespace DNDS struct MemberPtr { using t_member_ptr = T Class::*; - t_member_ptr ptr; ///< Pointer-to-member. - const char *name; ///< Compile-time-known member name. + t_member_ptr ptr; ///< Pointer-to-member. + const char *name; ///< Compile-time-known member name. }; template diff --git a/src/DNDS/Profiling.hpp b/src/DNDS/Profiling.hpp index 98c3f566..e69199bd 100644 --- a/src/DNDS/Profiling.hpp +++ b/src/DNDS/Profiling.hpp @@ -26,25 +26,25 @@ namespace DNDS enum TimerType { Unknown = 0, - RHS = 1, ///< Total RHS evaluation. - Dt = 2, ///< Time-step computation. - Reconstruction = 3, ///< Variational reconstruction. - ReconstructionCR = 4,///< CR (compact reconstruction) branch. - Limiter = 5, ///< Slope / variable limiter. - LimiterA = 6, ///< Limiter sub-phase A. - LimiterB = 7, ///< Limiter sub-phase B. - Basis = 8, ///< Basis-function evaluation. - Comm = 9, ///< Catch-all MPI comm. - Comm1 = 10, ///< Comm phase 1 (e.g., cell-ghost). - Comm2 = 11, ///< Comm phase 2 (e.g., face-ghost). - Comm3 = 12, ///< Comm phase 3. - LinSolve = 13, ///< Linear solve (total). - LinSolve1 = 14, ///< Linear solve phase 1. - LinSolve2 = 15, ///< Linear solve phase 2. - LinSolve3 = 16, ///< Linear solve phase 3. - Positivity = 17, ///< Positivity preservation. - PositivityOuter = 18,///< Outer-iteration positivity. - __EndTimerType = 64 ///< One past the last valid id. + RHS = 1, ///< Total RHS evaluation. + Dt = 2, ///< Time-step computation. + Reconstruction = 3, ///< Variational reconstruction. + ReconstructionCR = 4, ///< CR (compact reconstruction) branch. + Limiter = 5, ///< Slope / variable limiter. + LimiterA = 6, ///< Limiter sub-phase A. + LimiterB = 7, ///< Limiter sub-phase B. + Basis = 8, ///< Basis-function evaluation. + Comm = 9, ///< Catch-all MPI comm. + Comm1 = 10, ///< Comm phase 1 (e.g., cell-ghost). + Comm2 = 11, ///< Comm phase 2 (e.g., face-ghost). + Comm3 = 12, ///< Comm phase 3. + LinSolve = 13, ///< Linear solve (total). + LinSolve1 = 14, ///< Linear solve phase 1. + LinSolve2 = 15, ///< Linear solve phase 2. + LinSolve3 = 16, ///< Linear solve phase 3. + Positivity = 17, ///< Positivity preservation. + PositivityOuter = 18, ///< Outer-iteration positivity. + __EndTimerType = 64 ///< One past the last valid id. }; static const int Ntype = __EndTimerType; diff --git a/src/DNDS/Serializer/SerializerBase.hpp b/src/DNDS/Serializer/SerializerBase.hpp index b1c11202..22002172 100644 --- a/src/DNDS/Serializer/SerializerBase.hpp +++ b/src/DNDS/Serializer/SerializerBase.hpp @@ -211,7 +211,7 @@ namespace DNDS::Serializer /// @brief Rank count cached by the serializer. virtual int GetMPISize() = 0; /// @brief MPI context the serializer was opened with. - virtual const MPIInfo& getMPI() = 0; + virtual const MPIInfo &getMPI() = 0; /// @brief Write a scalar int under `name` at the current path. virtual void WriteInt(const std::string &name, int v) = 0; diff --git a/src/DNDS/Warnings.hpp b/src/DNDS/Warnings.hpp index f616939b..c6cd237f 100644 --- a/src/DNDS/Warnings.hpp +++ b/src/DNDS/Warnings.hpp @@ -17,65 +17,65 @@ // Warning disabler: #if defined(_MSC_VER) && defined(_WIN32) && !defined(__clang__) -#define DISABLE_WARNING_PUSH __pragma(warning(push)) -#define DISABLE_WARNING_POP __pragma(warning(pop)) -#define DISABLE_WARNING(warningNumber) __pragma(warning(disable : warningNumber)) +# define DISABLE_WARNING_PUSH __pragma(warning(push)) +# define DISABLE_WARNING_POP __pragma(warning(pop)) +# define DISABLE_WARNING(warningNumber) __pragma(warning(disable : warningNumber)) -#define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING(4100) -#define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING(4505) -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS -#define DISABLE_WARNING_UNUSED_VALUE -#define DISABLE_WARNING_MAYBE_UNINITIALIZED -#define DISABLE_WARNING_CLASS_MEMACCESS +# define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING(4100) +# define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING(4505) +# define DISABLE_WARNING_DEPRECATED_DECLARATIONS +# define DISABLE_WARNING_UNUSED_VALUE +# define DISABLE_WARNING_MAYBE_UNINITIALIZED +# define DISABLE_WARNING_CLASS_MEMACCESS // other warnings you want to deactivate... #elif defined(_MSC_VER) && defined(_WIN32) && defined(__clang__) // for clang-msvc on win, change a bit from unix version -#define DO_PRAGMA(X) _Pragma(#X) -#define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) -#define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) -#define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) +# define DO_PRAGMA(X) _Pragma(#X) +# define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) +# define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) +# define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) -#define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") -#define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") -#define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") -#define DISABLE_WARNING_MAYBE_UNINITIALIZED -#define DISABLE_WARNING_CLASS_MEMACCESS +# define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") +# define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") +# define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") +# define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") +# define DISABLE_WARNING_MAYBE_UNINITIALIZED +# define DISABLE_WARNING_CLASS_MEMACCESS #elif defined(__clang__) // unix + gcc/clang -#define DO_PRAGMA(X) _Pragma(#X) -#define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) -#define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) -#define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) +# define DO_PRAGMA(X) _Pragma(#X) +# define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) +# define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) +# define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) -#define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") -#define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") -#define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") -#define DISABLE_WARNING_MAYBE_UNINITIALIZED // not supported by clang -#define DISABLE_WARNING_CLASS_MEMACCESS DISABLE_WARNING("-Wclass-memaccess") +# define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") +# define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") +# define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") +# define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") +# define DISABLE_WARNING_MAYBE_UNINITIALIZED // not supported by clang +# define DISABLE_WARNING_CLASS_MEMACCESS DISABLE_WARNING("-Wclass-memaccess") #elif defined(__GNUC__) // unix + gcc/clang -#define DO_PRAGMA(X) _Pragma(#X) -#define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) -#define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) -#define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) +# define DO_PRAGMA(X) _Pragma(#X) +# define DISABLE_WARNING_PUSH DO_PRAGMA(GCC diagnostic push) +# define DISABLE_WARNING_POP DO_PRAGMA(GCC diagnostic pop) +# define DISABLE_WARNING(warningName) DO_PRAGMA(GCC diagnostic ignored warningName) -#define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") -#define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") -#define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") -#define DISABLE_WARNING_MAYBE_UNINITIALIZED DISABLE_WARNING("-Wmaybe-uninitialized") -#define DISABLE_WARNING_CLASS_MEMACCESS DISABLE_WARNING("-Wclass-memaccess") +# define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER DISABLE_WARNING("-Wunused-parameter") +# define DISABLE_WARNING_UNREFERENCED_FUNCTION DISABLE_WARNING("-Wunused-function") +# define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING("-Wdeprecated-declarations") +# define DISABLE_WARNING_UNUSED_VALUE DISABLE_WARNING("-Wunused-value") +# define DISABLE_WARNING_MAYBE_UNINITIALIZED DISABLE_WARNING("-Wmaybe-uninitialized") +# define DISABLE_WARNING_CLASS_MEMACCESS DISABLE_WARNING("-Wclass-memaccess") #else -#define DISABLE_WARNING_PUSH -#define DISABLE_WARNING_POP -#define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER -#define DISABLE_WARNING_UNUSED_VALUE -#define DISABLE_WARNING_UNREFERENCED_FUNCTION -#define DISABLE_WARNING_CLASS_MEMACCESS +# define DISABLE_WARNING_PUSH +# define DISABLE_WARNING_POP +# define DISABLE_WARNING_UNREFERENCED_FORMAL_PARAMETER +# define DISABLE_WARNING_UNUSED_VALUE +# define DISABLE_WARNING_UNREFERENCED_FUNCTION +# define DISABLE_WARNING_CLASS_MEMACCESS // other warnings you want to deactivate... #endif diff --git a/src/Euler/CLDriver.hpp b/src/Euler/CLDriver.hpp index 5496d3f1..7b48b04c 100644 --- a/src/Euler/CLDriver.hpp +++ b/src/Euler/CLDriver.hpp @@ -39,17 +39,18 @@ namespace DNDS::Euler real CLIncrementRelax = 0.25; ///< Under-relaxation factor applied to each AoA increment (0,1]. // reduce each alpha increment real thresholdTargetRatio = 0.5; ///< Fraction of |targetCL - lastCL| used to tighten the convergence threshold near the target. // reduce CL convergence threshold when close to the target CL - index nIterStartDrive = INT32_MAX; ///< Solver iteration at which the CL driver becomes active. - index nIterConvergeMin = 50; ///< Minimum number of iterations before the CL convergence window is evaluated. - real CLconvergeThreshold = 1e-3; ///< Maximum deviation within the sliding window for CL to be considered converged. - index CLconvergeWindow = 10; ///< Number of most-recent CL samples in the sliding convergence window. + index nIterStartDrive = INT32_MAX; ///< Solver iteration at which the CL driver becomes active. + index nIterConvergeMin = 50; ///< Minimum number of iterations before the CL convergence window is evaluated. + real CLconvergeThreshold = 1e-3; ///< Maximum deviation within the sliding window for CL to be considered converged. + index CLconvergeWindow = 10; ///< Number of most-recent CL samples in the sliding convergence window. - index CLconvergeLongWindow = 100; ///< Number of consecutive iterations within tolerance required for final (long-window) convergence. // for converged-at-target exit of main iteration loop - real CLconvergeLongThreshold = 1e-4; ///< CL error tolerance for the long-window converged-at-target check. - bool CLconvergeLongStrictAoA = false; ///< If true, reset the long-window counter whenever the AoA is updated. + index CLconvergeLongWindow = 100; ///< Number of consecutive iterations within tolerance required for final (long-window) convergence. // for converged-at-target exit of main iteration loop + real CLconvergeLongThreshold = 1e-4; ///< CL error tolerance for the long-window converged-at-target check. + bool CLconvergeLongStrictAoA = false; ///< If true, reset the long-window counter whenever the AoA is updated. DNDS_DECLARE_CONFIG(CLDriverSettings) { + // clang-format off DNDS_FIELD(AOAInit, "Initial angle of attack (degrees)"); DNDS_FIELD(AOAAxis, "Rotation axis for AoA", DNDS::Config::enum_values({"x", "y", "z"})); @@ -79,6 +80,7 @@ namespace DNDS::Euler DNDS_FIELD(CLconvergeLongThreshold, "Long-window CL tolerance", DNDS::Config::range(0.0)); DNDS_FIELD(CLconvergeLongStrictAoA, "Reset long counter on AoA update"); + // clang-format on } }; @@ -99,13 +101,13 @@ namespace DNDS::Euler */ class CLDriver { - CLDriverSettings settings; ///< Configuration parameters for this driver instance. - real lastCL{veryLargeReal}; ///< CL value from the previous converged window (sentinel = not yet set). - real lastAOA{veryLargeReal}; ///< AoA corresponding to @ref lastCL (sentinel = not yet set). - Eigen::VectorXd CLHistory; ///< Circular buffer holding the most recent CL samples. - index CLHistorySize = 0; ///< Total number of CL samples pushed (may exceed window size). - index CLHistoryHead = 0; ///< Current write position (head) in the circular buffer. - index CLAtTargetAcc = 0; ///< Consecutive-iteration counter for the long-window convergence check. + CLDriverSettings settings; ///< Configuration parameters for this driver instance. + real lastCL{veryLargeReal}; ///< CL value from the previous converged window (sentinel = not yet set). + real lastAOA{veryLargeReal}; ///< AoA corresponding to @ref lastCL (sentinel = not yet set). + Eigen::VectorXd CLHistory; ///< Circular buffer holding the most recent CL samples. + index CLHistorySize = 0; ///< Total number of CL samples pushed (may exceed window size). + index CLHistoryHead = 0; ///< Current write position (head) in the circular buffer. + index CLAtTargetAcc = 0; ///< Consecutive-iteration counter for the long-window convergence check. /** * @brief Push a new CL sample into the circular history buffer. diff --git a/src/Euler/EulerEvaluatorSettings.hpp b/src/Euler/EulerEvaluatorSettings.hpp index 4c08adde..5235156c 100644 --- a/src/Euler/EulerEvaluatorSettings.hpp +++ b/src/Euler/EulerEvaluatorSettings.hpp @@ -42,19 +42,19 @@ namespace DNDS::Euler template struct EulerEvaluatorSettings { - using Traits = EulerModelTraits; ///< Compile-time model traits. - static const int nVarsFixed = getnVarsFixed(model); ///< Compile-time variable count. - static const int dim = getDim_Fixed(model); ///< Physical dimension (2 or 3). - static const int gDim = getGeomDim_Fixed(model); ///< Geometric dimension (may differ for axi-symmetric). - static const auto I4 = dim + 1; ///< Index of the energy equation in the state vector. + using Traits = EulerModelTraits; ///< Compile-time model traits. + static const int nVarsFixed = getnVarsFixed(model); ///< Compile-time variable count. + static const int dim = getDim_Fixed(model); ///< Physical dimension (2 or 3). + static const int gDim = getGeomDim_Fixed(model); ///< Geometric dimension (may differ for axi-symmetric). + static const auto I4 = dim + 1; ///< Index of the energy equation in the state vector. /// @name Jacobian Options /// @{ - bool useScalarJacobian = false; ///< Use scalar (diagonal) Jacobian approximation instead of block. - bool useRoeJacobian = false; ///< Use Roe-linearization-based Jacobian. - bool noRsOnWall = false; ///< Disable the Riemann solver on wall boundary faces. - bool noGRPOnWall = false; ///< Disable the Generalized Riemann Problem (GRP) on wall faces. - bool ignoreSourceTerm = false; ///< Completely ignore source terms (must be false when RANS or body forces are active). + bool useScalarJacobian = false; ///< Use scalar (diagonal) Jacobian approximation instead of block. + bool useRoeJacobian = false; ///< Use Roe-linearization-based Jacobian. + bool noRsOnWall = false; ///< Disable the Riemann solver on wall boundary faces. + bool noGRPOnWall = false; ///< Disable the Generalized Riemann Problem (GRP) on wall faces. + bool ignoreSourceTerm = false; ///< Completely ignore source terms (must be false when RANS or body forces are active). /// @} /// @name Reconstruction @@ -67,39 +67,39 @@ namespace DNDS::Euler bool ppEpsIsRelaxed = false; ///< Use relaxed positivity-preserving epsilon. /// @} - real RANSBottomLimit = 0.01; ///< Lower clamp for RANS turbulence variables. + real RANSBottomLimit = 0.01; ///< Lower clamp for RANS turbulence variables. /// @name Riemann Solver Configuration /// @{ - Gas::RiemannSolverType rsType = Gas::Roe; ///< Primary Riemann solver type. - Gas::RiemannSolverType rsTypeAux = Gas::UnknownRS; ///< Auxiliary Riemann solver type (UnknownRS = same as primary). - Gas::RiemannSolverType rsTypeWall = Gas::UnknownRS; ///< Wall-face Riemann solver type (UnknownRS = same as primary). - real rsFixScale = 1; ///< Entropy-fix scaling factor for the Riemann solver. - real rsIncFScale = 1; ///< Incremental flux scaling factor. - int rsMeanValueEig = 0; ///< Mean-value eigenvalue computation mode. - int rsRotateScheme = 0; ///< Riemann solver rotation scheme selector. + Gas::RiemannSolverType rsType = Gas::Roe; ///< Primary Riemann solver type. + Gas::RiemannSolverType rsTypeAux = Gas::UnknownRS; ///< Auxiliary Riemann solver type (UnknownRS = same as primary). + Gas::RiemannSolverType rsTypeWall = Gas::UnknownRS; ///< Wall-face Riemann solver type (UnknownRS = same as primary). + real rsFixScale = 1; ///< Entropy-fix scaling factor for the Riemann solver. + real rsIncFScale = 1; ///< Incremental flux scaling factor. + int rsMeanValueEig = 0; ///< Mean-value eigenvalue computation mode. + int rsRotateScheme = 0; ///< Riemann solver rotation scheme selector. /// @} /// @name Wall-Distance Computation /// @{ - real minWallDist = 1e-12; ///< Minimum wall distance clamp (avoids singularities). - int wallDistExection = 0; ///< Execution mode: 0 = parallel, 1 = serial. - real wallDistRefineMax = 1; ///< Maximum wall-distance refinement factor. - int wallDistScheme = 0; ///< Wall-distance computation scheme selector. + real minWallDist = 1e-12; ///< Minimum wall distance clamp (avoids singularities). + int wallDistExection = 0; ///< Execution mode: 0 = parallel, 1 = serial. + real wallDistRefineMax = 1; ///< Maximum wall-distance refinement factor. + int wallDistScheme = 0; ///< Wall-distance computation scheme selector. int wallDistCellLoadSize = 1024 * 32; ///< Cell batch size for wall-distance computation. - int wallDistIter = 1000; ///< Maximum iterations for the wall-distance solver. - int wallDistLinSolver = 0; ///< Linear solver: 0 = Jacobi, 1 = GMRES. - real wallDistResTol = 1e-4; ///< Residual tolerance for wall-distance convergence. - int wallDistIterStart = 100; ///< Starting iteration count for the wall-distance solver. - int wallDistPoissonP = 2; ///< Poisson equation power in the wall-distance PDE. - real wallDistDTauScale = 100.; ///< Pseudo-time step scaling for wall-distance solver. - int wallDistNJacobiSweep = 10; ///< Number of Jacobi sweeps per wall-distance iteration. + int wallDistIter = 1000; ///< Maximum iterations for the wall-distance solver. + int wallDistLinSolver = 0; ///< Linear solver: 0 = Jacobi, 1 = GMRES. + real wallDistResTol = 1e-4; ///< Residual tolerance for wall-distance convergence. + int wallDistIterStart = 100; ///< Starting iteration count for the wall-distance solver. + int wallDistPoissonP = 2; ///< Poisson equation power in the wall-distance PDE. + real wallDistDTauScale = 100.; ///< Pseudo-time step scaling for wall-distance solver. + int wallDistNJacobiSweep = 10; ///< Number of Jacobi sweeps per wall-distance iteration. /// @} /// @name RANS / DES Configuration /// @{ - real SADESScale = veryLargeReal; ///< SA-DES length scale (veryLargeReal effectively disables DES). - int SADESMode = 1; ///< SA-DES mode selector (1 = DDES, etc.). + real SADESScale = veryLargeReal; ///< SA-DES length scale (veryLargeReal effectively disables DES). + int SADESMode = 1; ///< SA-DES mode selector (1 = DDES, etc.). /** * @brief SA model variant selector. * @@ -114,20 +114,20 @@ namespace DNDS::Euler */ int SAVersion = 0; RANSModel ransModel = RANSModel::RANS_None; ///< RANS turbulence model (RANS_None, RANS_SA, RANS_KOWilcox, etc.). - int ransUseQCR = 0; ///< Enable QCR (Quadratic Constitutive Relation) correction. - int ransSARotCorrection = 1; ///< SA rotation/curvature correction mode. - int ransEigScheme = 0; ///< Eigenvalue computation scheme for RANS. - int ransForce2nd = 0; ///< Force 2nd-order accuracy for RANS variables. - int ransSource2nd = 0; ///< Enable 2nd-order RANS source term discretization. + int ransUseQCR = 0; ///< Enable QCR (Quadratic Constitutive Relation) correction. + int ransSARotCorrection = 1; ///< SA rotation/curvature correction mode. + int ransEigScheme = 0; ///< Eigenvalue computation scheme for RANS. + int ransForce2nd = 0; ///< Force 2nd-order accuracy for RANS variables. + int ransSource2nd = 0; ///< Enable 2nd-order RANS source term discretization. /// @} /// @name Viscous Flux and Source Options /// @{ - int source2nd = 0; ///< Enable 2nd-order source term discretization. - int usePrimGradInVisFlux = 0; ///< Use primitive-variable gradients in viscous flux. - int useSourceGradFixGG = 0; ///< Apply Green-Gauss gradient fix for source terms. - int nCentralSmoothStep = 0; ///< Number of central-difference smoothing steps. - real centralSmoothEps = 0.5; ///< Epsilon for central smoothing. + int source2nd = 0; ///< Enable 2nd-order source term discretization. + int usePrimGradInVisFlux = 0; ///< Use primitive-variable gradients in viscous flux. + int useSourceGradFixGG = 0; ///< Apply Green-Gauss gradient fix for source terms. + int nCentralSmoothStep = 0; ///< Number of central-difference smoothing steps. + real centralSmoothEps = 0.5; ///< Epsilon for central smoothing. Eigen::Vector constMassForce = Eigen::Vector{0, 0, 0}; ///< Constant body force vector [fx, fy, fz]. /// @} /** @@ -139,10 +139,10 @@ namespace DNDS::Euler */ struct FrameConstRotation { - bool enabled = false; ///< Enable the rotating frame. - Geom::tPoint axis = Geom::tPoint{0, 0, 1}; ///< Rotation axis (unit vector; normalized in finalize()). - Geom::tPoint center = Geom::tPoint{0, 0, 0}; ///< Center of rotation [x, y, z]. - real rpm = 0; ///< Rotational speed in revolutions per minute. + bool enabled = false; ///< Enable the rotating frame. + Geom::tPoint axis = Geom::tPoint{0, 0, 1}; ///< Rotation axis (unit vector; normalized in finalize()). + Geom::tPoint center = Geom::tPoint{0, 0, 0}; ///< Center of rotation [x, y, z]. + real rpm = 0; ///< Rotational speed in revolutions per minute. /// @brief Compute angular velocity magnitude (rad/s) from RPM. /// @return Omega = rpm * 2π / 60. @@ -188,14 +188,16 @@ namespace DNDS::Euler } DNDS_DECLARE_CONFIG(FrameConstRotation) { + // clang-format off DNDS_FIELD(enabled, "Enable constant-rotation reference frame"); DNDS_FIELD(axis, "Rotation axis (unit vector)"); DNDS_FIELD(center, "Rotation center coordinates"); DNDS_FIELD(rpm, "Rotational speed in RPM"); + // clang-format on } - } frameConstRotation; ///< Rotating reference frame configuration. - CLDriverSettings cLDriverSettings; ///< Lift-coefficient (CL) driver settings. - std::vector cLDriverBCNames; ///< Boundary zone names for CL driver force integration. + } frameConstRotation; ///< Rotating reference frame configuration. + CLDriverSettings cLDriverSettings; ///< Lift-coefficient (CL) driver settings. + std::vector cLDriverBCNames; ///< Boundary zone names for CL driver force integration. Eigen::Vector farFieldStaticValue = Eigen::Vector{1, 0, 0, 0, 2.5}; ///< Far-field reference state vector (size = nVars). /** * @brief Axis-aligned box region for initial condition specification. @@ -206,10 +208,11 @@ namespace DNDS::Euler struct BoxInitializer { real x0{0}, x1{0}, y0{0}, y1{0}, z0{0}, z1{0}; ///< Box bounds [min, max] per axis. - Eigen::Vector v; ///< Initial state vector (size = nVars). + Eigen::Vector v; ///< Initial state vector (size = nVars). DNDS_DECLARE_CONFIG(BoxInitializer) { + // clang-format off DNDS_FIELD(x0, "Box x-min"); DNDS_FIELD(x1, "Box x-max"); DNDS_FIELD(y0, "Box y-min"); @@ -217,6 +220,7 @@ namespace DNDS::Euler DNDS_FIELD(z0, "Box z-min"); DNDS_FIELD(z1, "Box z-max"); DNDS_FIELD(v, "Initial value vector (size = nVars)"); + // clang-format on } }; std::vector boxInitializers; ///< List of box-region initial condition specifiers. @@ -230,15 +234,17 @@ namespace DNDS::Euler struct PlaneInitializer { real a{0}, b{0}, c{0}, h{0}; ///< Plane equation coefficients: a*x + b*y + c*z = h. - Eigen::Vector v; ///< Initial state vector (size = nVars). + Eigen::Vector v; ///< Initial state vector (size = nVars). DNDS_DECLARE_CONFIG(PlaneInitializer) { + // clang-format off DNDS_FIELD(a, "Plane normal x-component"); DNDS_FIELD(b, "Plane normal y-component"); DNDS_FIELD(c, "Plane normal z-component"); DNDS_FIELD(h, "Plane offset"); DNDS_FIELD(v, "Initial value vector (size = nVars)"); + // clang-format on } }; std::vector planeInitializers; ///< List of plane-region initial condition specifiers. @@ -256,7 +262,9 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(ExprtkInitializer) { + // clang-format off DNDS_FIELD(exprs, "Expression lines (concatenated with newlines)"); + // clang-format on } /** @@ -282,17 +290,18 @@ namespace DNDS::Euler */ struct IdealGasProperty { - real gamma = 1.4; ///< Ratio of specific heats (Cp/Cv). - real Rgas = 1; ///< Specific gas constant (J/(kg·K) in dimensional runs). - real muGas = 1; ///< Dynamic viscosity (or reference viscosity for Sutherland). - real prGas = 0.72; ///< Prandtl number. - real CpGas = Rgas * gamma / (gamma - 1); ///< Heat capacity at constant pressure (derived, not serialized). - real TRef = 273.15; ///< Reference temperature (K) for Sutherland's law. - real CSutherland = 110.4; ///< Sutherland constant (K). - int muModel = 1; ///< Viscosity model: 0 = constant, 1 = Sutherland, 2 = constant_nu. + real gamma = 1.4; ///< Ratio of specific heats (Cp/Cv). + real Rgas = 1; ///< Specific gas constant (J/(kg·K) in dimensional runs). + real muGas = 1; ///< Dynamic viscosity (or reference viscosity for Sutherland). + real prGas = 0.72; ///< Prandtl number. + real CpGas = Rgas * gamma / (gamma - 1); ///< Heat capacity at constant pressure (derived, not serialized). + real TRef = 273.15; ///< Reference temperature (K) for Sutherland's law. + real CSutherland = 110.4; ///< Sutherland constant (K). + int muModel = 1; ///< Viscosity model: 0 = constant, 1 = Sutherland, 2 = constant_nu. DNDS_DECLARE_CONFIG(IdealGasProperty) { + // clang-format off DNDS_FIELD(gamma, "Ratio of specific heats", DNDS::Config::range(1.0)); DNDS_FIELD(Rgas, "Specific gas constant", @@ -306,6 +315,7 @@ namespace DNDS::Euler DNDS_FIELD(muModel, "Viscosity model: 0=constant, 1=sutherland, 2=constant_nu"); // CpGas is derived: recomputed after deserialization config.post_read([](T &s) { s.recomputeDerived(); }); + // clang-format on } /// @brief Recompute derived quantities (CpGas) from gamma and Rgas after deserialization. @@ -319,12 +329,13 @@ namespace DNDS::Euler // end of setting entries /***************************************************************************************************/ - int _nVars = 0; ///< Runtime nVars, not serialized. Set by ctor, preserved across from_json. + int _nVars = 0; ///< Runtime nVars, not serialized. Set by ctor, preserved across from_json. Eigen::Vector refU; ///< Reference conservative state (derived from farFieldStaticValue). Eigen::Vector refUPrim; ///< Reference primitive state (derived from farFieldStaticValue). DNDS_DECLARE_CONFIG(EulerEvaluatorSettings) { + // clang-format off DNDS_FIELD(useScalarJacobian, "Use scalar Jacobian approximation"); DNDS_FIELD(useRoeJacobian, "Use Roe-based Jacobian"); DNDS_FIELD(noRsOnWall, "Disable Riemann solver on wall boundaries"); @@ -409,6 +420,7 @@ namespace DNDS::Euler // Post-read hook: finalize derived quantities using stored _nVars config.post_read([](T &s) { s.finalize(); }); + // clang-format on } /// @brief Default constructor (used for schema emission; _nVars remains 0). @@ -462,7 +474,7 @@ namespace DNDS::Euler if (constMassForce.norm() || frameConstRotation.enabled || std::unordered_set{NS_SA, NS_SA_3D, NS_2EQ, NS_2EQ_3D}.count(model)) DNDS_assert_info(!ignoreSourceTerm, - "you have set source term, do not use ignoreSourceTerm! "); + "you have set source term, do not use ignoreSourceTerm! "); if (frameConstRotation.enabled) frameConstRotation.axis.normalize(); for (auto &box : boxInitializers) diff --git a/src/Euler/EulerSolver.hpp b/src/Euler/EulerSolver.hpp index ad3af180..8d85cfcb 100644 --- a/src/Euler/EulerSolver.hpp +++ b/src/Euler/EulerSolver.hpp @@ -76,77 +76,77 @@ namespace DNDS::Euler int nVars = getNVars(model); ///< Runtime number of conserved variables. public: - typedef EulerEvaluator TEval; ///< Evaluator type for this model. - static const int nVarsFixed = TEval::nVarsFixed; ///< Compile-time number of conserved variables. + typedef EulerEvaluator TEval; ///< Evaluator type for this model. + static const int nVarsFixed = TEval::nVarsFixed; ///< Compile-time number of conserved variables. - static const int dim = TEval::dim; ///< Spatial dimension (2 or 3). + static const int dim = TEval::dim; ///< Spatial dimension (2 or 3). // static const int gdim = TEval::gdim; - static const int gDim = TEval::gDim; ///< Geometric dimension of the mesh. - static const int I4 = TEval::I4; ///< Energy equation index (= dim + 1). - - typedef typename TEval::TU TU; ///< Conservative variable vector type. - typedef typename TEval::TDiffU TDiffU; ///< Gradient of conserved variables type. - typedef typename TEval::TJacobianU TJacobianU; ///< Flux Jacobian matrix type. - typedef typename TEval::TVec TVec; ///< Spatial vector type. - typedef typename TEval::TMat TMat; ///< Spatial matrix type. - typedef typename TEval::TDof TDof; ///< Cell-centered DOF array type. - typedef typename TEval::TRec TRec; ///< Reconstruction coefficient array type. - typedef typename TEval::TScalar TScalar; ///< Scalar reconstruction coefficient array type. - typedef typename TEval::TVFV TVFV; ///< Variational reconstruction type. - typedef typename TEval::TpVFV TpVFV; ///< Shared pointer to VFV type. - - using tGMRES_u = Linear::GMRES_LeftPreconditioned; ///< GMRES solver type for conservative DOFs. - using tGMRES_uRec = Linear::GMRES_LeftPreconditioned; ///< GMRES solver type for reconstruction coefficients. + static const int gDim = TEval::gDim; ///< Geometric dimension of the mesh. + static const int I4 = TEval::I4; ///< Energy equation index (= dim + 1). + + typedef typename TEval::TU TU; ///< Conservative variable vector type. + typedef typename TEval::TDiffU TDiffU; ///< Gradient of conserved variables type. + typedef typename TEval::TJacobianU TJacobianU; ///< Flux Jacobian matrix type. + typedef typename TEval::TVec TVec; ///< Spatial vector type. + typedef typename TEval::TMat TMat; ///< Spatial matrix type. + typedef typename TEval::TDof TDof; ///< Cell-centered DOF array type. + typedef typename TEval::TRec TRec; ///< Reconstruction coefficient array type. + typedef typename TEval::TScalar TScalar; ///< Scalar reconstruction coefficient array type. + typedef typename TEval::TVFV TVFV; ///< Variational reconstruction type. + typedef typename TEval::TpVFV TpVFV; ///< Shared pointer to VFV type. + + using tGMRES_u = Linear::GMRES_LeftPreconditioned; ///< GMRES solver type for conservative DOFs. + using tGMRES_uRec = Linear::GMRES_LeftPreconditioned; ///< GMRES solver type for reconstruction coefficients. using tPCG_uRec = Linear::PCG_PreconditionedRes>; ///< PCG solver type for reconstruction. private: - MPIInfo mpi; ///< MPI communicator and rank information. - ssp mesh, meshBnd; ///< Volume mesh and (optional) boundary surface mesh. - TpVFV vfv; // ! gDim -> 3 for intellisense ///< Variational reconstruction object. + MPIInfo mpi; ///< MPI communicator and rank information. + ssp mesh, meshBnd; ///< Volume mesh and (optional) boundary surface mesh. + TpVFV vfv; // ! gDim -> 3 for intellisense ///< Variational reconstruction object. ssp reader, readerBnd; ///< Mesh reader for volume and boundary meshes. - ssp> pEval; ///< Spatial evaluator instance. + ssp> pEval; ///< Spatial evaluator instance. - ArrayDOFV u, uIncBufODE, wAveraged, uAveraged; ///< DOF arrays: solution, ODE increment buffer, time-averaged fields. - ObjectPool> uPool; ///< Object pool for temporary DOF arrays (used by ODE integrators). + ArrayDOFV u, uIncBufODE, wAveraged, uAveraged; ///< DOF arrays: solution, ODE increment buffer, time-averaged fields. + ObjectPool> uPool; ///< Object pool for temporary DOF arrays (used by ODE integrators). ArrayRECV uRec, uRecLimited, uRecNew, uRecNew1, uRecOld, uRec1, uRecInc, uRecInc1, uRecB, uRecB1; ///< Reconstruction arrays (current, limited, new, old, increment, etc.). - JacobianDiagBlock JD, JD1, JDTmp, JSource, JSource1, JSourceTmp; ///< Diagonal Jacobian blocks for implicit methods. - ssp> JLocalLU; ///< Local LU factorization for direct preconditioner. - ArrayDOFV<1> alphaPP, alphaPP1, betaPP, betaPP1, alphaPP_tmp, dTauTmp; ///< Positivity-preserving limiter scalars and time-step buffer. + JacobianDiagBlock JD, JD1, JDTmp, JSource, JSource1, JSourceTmp; ///< Diagonal Jacobian blocks for implicit methods. + ssp> JLocalLU; ///< Local LU factorization for direct preconditioner. + ArrayDOFV<1> alphaPP, alphaPP1, betaPP, betaPP1, alphaPP_tmp, dTauTmp; ///< Positivity-preserving limiter scalars and time-step buffer. - int nOUTS = {-1}; ///< Number of output scalars per cell in volume output. - int nOUTSPoint{-1}; ///< Number of output scalars per node in point output. - int nOUTSBnd{-1}; ///< Number of output scalars per face in boundary output. + int nOUTS = {-1}; ///< Number of output scalars per cell in volume output. + int nOUTSPoint{-1}; ///< Number of output scalars per node in point output. + int nOUTSBnd{-1}; ///< Number of output scalars per face in boundary output. // rho u v w p T M ifUseLimiter RHS - ssp> outDist; ///< Distributed cell output array. - ssp> outSerial; ///< Serial (gathered) cell output array. + ssp> outDist; ///< Distributed cell output array. + ssp> outSerial; ///< Serial (gathered) cell output array. ArrayTransformerType>::Type outDist2SerialTrans; ///< Transformer for distributed-to-serial cell output. - ssp> outDistPoint; ///< Distributed node output array. - ssp> outGhostPoint; ///< Ghost-node output array. - ssp> outSerialPoint; ///< Serial (gathered) node output array. + ssp> outDistPoint; ///< Distributed node output array. + ssp> outGhostPoint; ///< Ghost-node output array. + ssp> outSerialPoint; ///< Serial (gathered) node output array. ArrayTransformerType>::Type outDist2SerialTransPoint; ///< Transformer for distributed-to-serial node output. - ArrayPair> outDistPointPair; ///< Array pair for async node output. - static const int maxOutFutures{3}; ///< Maximum number of concurrent async output futures. - std::mutex outArraysMutex; ///< Mutex protecting output arrays during async writes. - std::array, maxOutFutures> outFuture; ///< Futures for async volume output. Mind the order, relies on the arrays and the mutex. + ArrayPair> outDistPointPair; ///< Array pair for async node output. + static const int maxOutFutures{3}; ///< Maximum number of concurrent async output futures. + std::mutex outArraysMutex; ///< Mutex protecting output arrays during async writes. + std::array, maxOutFutures> outFuture; ///< Futures for async volume output. Mind the order, relies on the arrays and the mutex. - ssp> outDistBnd; ///< Distributed boundary output array. + ssp> outDistBnd; ///< Distributed boundary output array. // ssp> outGhostBnd; - ssp> outSerialBnd; ///< Serial (gathered) boundary output array. + ssp> outSerialBnd; ///< Serial (gathered) boundary output array. ArrayTransformerType>::Type outDist2SerialTransBnd; ///< Transformer for distributed-to-serial boundary output. // ArrayPair> outDistBndPair; - std::mutex outBndArraysMutex; ///< Mutex protecting boundary output arrays during async writes. + std::mutex outBndArraysMutex; ///< Mutex protecting boundary output arrays during async writes. std::array, maxOutFutures> outBndFuture; ///< Futures for async boundary output. Mind the order, relies on the arrays and the mutex. - std::future outSeqFuture; ///< Future for sequential (non-parallel) output operations. + std::future outSeqFuture; ///< Future for sequential (non-parallel) output operations. // std::vector ifUseLimiter; - CFV::tScalarPair ifUseLimiter; ///< Per-cell flag indicating whether the limiter was active. + CFV::tScalarPair ifUseLimiter; ///< Per-cell flag indicating whether the limiter was active. ssp> pBCHandler; ///< Boundary condition handler (shared with evaluator). public: - nlohmann::ordered_json gSetting; ///< Full JSON configuration (read from file, may be modified at runtime). - std::string output_stamp = ""; ///< Unique stamp appended to output filenames for this run. + nlohmann::ordered_json gSetting; ///< Full JSON configuration (read from file, may be modified at runtime). + std::string output_stamp = ""; ///< Unique stamp appended to output filenames for this run. /** * @brief Complete solver configuration, serializable to/from JSON. @@ -194,6 +194,7 @@ namespace DNDS::Euler real dtPPLimitScale = 1; DNDS_DECLARE_CONFIG(TimeMarchControl) { + // clang-format off DNDS_FIELD(dtImplicit, "Max implicit time step; 1e100 for steady", DNDS::Config::range(0.0)); DNDS_FIELD(dtImplicitMin, "Minimum implicit time step", @@ -227,6 +228,7 @@ namespace DNDS::Euler DNDS::Config::range(0.0, 1.0)); DNDS_FIELD(dtPPLimitScale, "PP dt limiter scale", DNDS::Config::range(0.0)); + // clang-format on } bool timeMarchIsTwoStage() { @@ -263,6 +265,7 @@ namespace DNDS::Euler int zeroRecForStepsInternal = 0; DNDS_DECLARE_CONFIG(ImplicitReconstructionControl) { + // clang-format off DNDS_FIELD(useExplicit, "Use explicit reconstruction (no implicit)"); DNDS_FIELD(nInternalRecStep, "Number of internal reconstruction sub-steps", DNDS::Config::range(1)); @@ -291,6 +294,7 @@ namespace DNDS::Euler DNDS::Config::range(0)); DNDS_FIELD(zeroRecForStepsInternal, "Zero reconstruction for N internal steps", DNDS::Config::range(0)); + // clang-format on } } implicitReconstructionControl; @@ -345,6 +349,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(OutputControl) { + // clang-format off DNDS_FIELD(nConsoleCheck, "Console output interval (outer steps)", DNDS::Config::range(1)); DNDS_FIELD(nConsoleCheckInternal, "Console output interval (internal steps)", @@ -376,6 +381,7 @@ namespace DNDS::Euler DNDS_FIELD(tDataOut, "Output data at simulation time interval"); DNDS_FIELD(lazyCoverDataOutput, "Overwrite previous data output files"); DNDS_FIELD(useCollectiveTimer, "Use collective MPI timer for profiling"); + // clang-format on } } outputControl; @@ -397,6 +403,7 @@ namespace DNDS::Euler real RANSRelax = 1; DNDS_DECLARE_CONFIG(ImplicitCFLControl) { + // clang-format off DNDS_FIELD(CFL, "CFL number for implicit time stepping", DNDS::Config::range(0.0)); DNDS_FIELD(nForceLocalStartStep, "Step to force local time stepping", @@ -411,6 +418,7 @@ namespace DNDS::Euler DNDS::Config::range(0)); DNDS_FIELD(RANSRelax, "RANS equation under-relaxation factor", DNDS::Config::range(0.0, 1.0)); + // clang-format on } } implicitCFLControl; @@ -435,6 +443,7 @@ namespace DNDS::Euler bool useCLDriver = false; DNDS_DECLARE_CONFIG(ConvergenceControl) { + // clang-format off DNDS_FIELD(nTimeStepInternal, "Max internal iterations per time step (0 = unlimited)", DNDS::Config::range(0)); DNDS_FIELD(nTimeStepInternalMin, "Min internal iterations per time step", @@ -452,6 +461,7 @@ namespace DNDS::Euler DNDS::Config::range(1)); DNDS_FIELD(useVolWiseResidual, "Volume-weighted residual"); DNDS_FIELD(useCLDriver, "Enable CL-driven AoA adaptation"); + // clang-format on } } convergenceControl; @@ -532,6 +542,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(DataIOControl) { + // clang-format off DNDS_FIELD(uniqueStamps, "Use unique output stamps per run"); DNDS_FIELD(meshRotZ, "Mesh rotation around Z axis (degrees)"); DNDS_FIELD(meshScale, "Mesh coordinate scaling factor", @@ -589,6 +600,7 @@ namespace DNDS::Euler config.field_section(&T::meshPartitionedWriter, "meshPartitionedWriter", "Partitioned mesh serializer settings"); DNDS_FIELD(meshPartitionedReaderType, "Partitioned mesh reader type", DNDS::Config::enum_values({"JSON", "H5"})); + // clang-format on } } dataIOControl; @@ -628,6 +640,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(BoundaryDefinition) { + // clang-format off DNDS_FIELD(PeriodicTranslation1, "Periodic translation vector for pair 1"); DNDS_FIELD(PeriodicTranslation2, "Periodic translation vector for pair 2"); DNDS_FIELD(PeriodicTranslation3, "Periodic translation vector for pair 3"); @@ -639,6 +652,7 @@ namespace DNDS::Euler DNDS_FIELD(PeriodicRotationEulerAngles3, "Rotation Euler angles (deg) for periodic pair 3"); DNDS_FIELD(periodicTolerance, "Tolerance for periodic node matching", DNDS::Config::range(0.0)); + // clang-format on } } boundaryDefinition; @@ -663,6 +677,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(LimiterControl) { + // clang-format off DNDS_FIELD(useLimiter, "Enable slope limiter"); DNDS_FIELD(usePPRecLimiter, "Enable positivity-preserving reconstruction limiter"); DNDS_FIELD(useViscousLimited, "Apply limiter to viscous reconstruction"); @@ -672,6 +687,7 @@ namespace DNDS::Euler DNDS_FIELD(nPartialLimiterStartLocal, "Time step to begin local partial limiting"); DNDS_FIELD(preserveLimited, "Preserve limited reconstruction across steps"); DNDS_FIELD(ppRecLimiterCompressToMean, "PP limiter compresses toward cell mean"); + // clang-format on } } limiterControl; @@ -713,6 +729,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(CoarseGridLinearSolverControl) { + // clang-format off DNDS_FIELD(jacobiCode, "Preconditioner: 0=jacobi, 1=GS, 2=ILU"); DNDS_FIELD(sgsIter, "SGS iteration count", DNDS::Config::range(0)); @@ -728,6 +745,7 @@ namespace DNDS::Euler DNDS_FIELD(multiGridNIterPost, "Multi-grid post-smooth iterations", DNDS::Config::range(0)); DNDS_FIELD(centralSmoothInputResidual, "Smooth input residual on coarse grid"); + // clang-format on } }; std::map coarseGridLinearSolverControlList{ @@ -738,6 +756,7 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(LinearSolverControl) { + // clang-format off DNDS_FIELD(jacobiCode, "Preconditioner: 0=jacobi, 1=GS, 2=ILU"); DNDS_FIELD(sgsIter, "SGS iteration count", DNDS::Config::range(0)); @@ -766,6 +785,7 @@ namespace DNDS::Euler "Per-level coarse grid linear solver settings"); config.field_section(&T::directPrecControl, "directPrecControl", "Direct preconditioner settings"); + // clang-format on } } linearSolverControl; @@ -786,12 +806,14 @@ namespace DNDS::Euler std::vector otherRestartStoreDim; DNDS_DECLARE_CONFIG(RestartState) { + // clang-format off DNDS_FIELD(iStep, "Restart step index"); DNDS_FIELD(iStepInternal, "Restart internal step index"); DNDS_FIELD(odeCodePrev, "Previous ODE code for restart"); DNDS_FIELD(lastRestartFile, "Path to last restart file"); DNDS_FIELD(otherRestartFile, "Path to alternate restart file"); DNDS_FIELD(otherRestartStoreDim, "Dimension mapping for alternate restart"); + // clang-format on } RestartState() { @@ -808,7 +830,9 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(TimeAverageControl) { + // clang-format off DNDS_FIELD(enabled, "Enable time-averaging of solution fields"); + // clang-format on } } timeAverageControl; @@ -822,22 +846,25 @@ namespace DNDS::Euler DNDS_DECLARE_CONFIG(Others) { + // clang-format off DNDS_FIELD(nFreezePassiveInner, "Freeze passive scalars for N inner steps", DNDS::Config::range(0)); DNDS_FIELD(axisSymmetric, "Axisymmetric mode: 0=off"); DNDS_FIELD(printRecMatrix, "Print reconstruction matrix to file"); config.field_section(&T::recMatrixWriter, "recMatrixWriter", "Serializer for reconstruction matrix output"); + // clang-format on } } others; - EulerEvaluatorSettings eulerSettings; ///< Physics settings passed to the EulerEvaluator. - CFV::VRSettings vfvSettings; ///< Variational reconstruction settings. + EulerEvaluatorSettings eulerSettings; ///< Physics settings passed to the EulerEvaluator. + CFV::VRSettings vfvSettings; ///< Variational reconstruction settings. nlohmann::ordered_json bcSettings = nlohmann::ordered_json::array(); ///< Boundary condition definitions (JSON array). - std::map bcNameMapping; ///< Mapping from mesh BC names to solver BC type names. + std::map bcNameMapping; ///< Mapping from mesh BC names to solver BC type names. DNDS_DECLARE_CONFIG(Configuration) { + // clang-format off config.field_section(&T::timeMarchControl, "timeMarchControl", "Time marching settings"); config.field_section(&T::implicitReconstructionControl, "implicitReconstructionControl", "Implicit reconstruction settings"); config.field_section(&T::outputControl, "outputControl", "Output settings"); @@ -861,6 +888,7 @@ namespace DNDS::Euler { return s.bcSettings.is_array(); }); + // clang-format on } /// @brief Backward-compatible bidirectional JSON read/write. @@ -1115,8 +1143,8 @@ namespace DNDS::Euler /// @brief Output mode selector for PrintData. enum PrintDataMode { - PrintDataLatest = 0, ///< Output the current (latest) solution. - PrintDataTimeAverage = 1, ///< Output the time-averaged solution. + PrintDataLatest = 0, ///< Output the current (latest) solution. + PrintDataTimeAverage = 1, ///< Output the time-averaged solution. }; /** @@ -1341,7 +1369,7 @@ namespace DNDS::Euler \ DNDS_EULERSOLVER_RUNNINGENV_GET_REF(addOutList); - RunningEnvironment() {}; + RunningEnvironment(){}; }; /// @brief Populate a RunningEnvironment with allocated solvers, loggers, and initial state. void InitializeRunningEnvironment(RunningEnvironment &env); @@ -1376,25 +1404,25 @@ namespace DNDS::Euler /// @name Accessors /// @{ - auto getMPI() const { return mpi; } ///< Get MPI communicator info. - auto getMesh() const { return mesh; } ///< Get shared pointer to the mesh. - auto getVFV() const { return vfv; } ///< Get shared pointer to the VFV reconstruction. + auto getMPI() const { return mpi; } ///< Get MPI communicator info. + auto getMesh() const { return mesh; } ///< Get shared pointer to the mesh. + auto getVFV() const { return vfv; } ///< Get shared pointer to the VFV reconstruction. /// @} /// @name Test accessors (for unit testing the evaluator pipeline) /// @{ - auto &getU() { return u; } ///< Get mutable reference to the DOF array. - auto &getURec() { return uRec; } ///< Get mutable reference to the reconstruction array. - auto &getURecNew() { return uRecNew; } ///< Get mutable reference to the new reconstruction array. - auto &getURecLimited() { return uRecLimited; } ///< Get mutable reference to the limited reconstruction array. - auto &getEval() { return *pEval; } ///< Get mutable reference to the evaluator. - auto &getConfiguration() { return config; } ///< Get mutable reference to the configuration. - auto &getJSource() { return JSource; } ///< Get mutable reference to the source Jacobian. - auto &getBetaPP() { return betaPP; } ///< Get mutable reference to the PP beta array. - auto &getAlphaPP() { return alphaPP; } ///< Get mutable reference to the PP alpha array. - auto &getDTauTmp() { return dTauTmp; } ///< Get mutable reference to the dTau temporary. - auto &getIfUseLimiter() { return ifUseLimiter; }///< Get mutable reference to the limiter flag array. - auto &getBCHandler() { return pBCHandler; } ///< Get mutable reference to the BC handler. + auto &getU() { return u; } ///< Get mutable reference to the DOF array. + auto &getURec() { return uRec; } ///< Get mutable reference to the reconstruction array. + auto &getURecNew() { return uRecNew; } ///< Get mutable reference to the new reconstruction array. + auto &getURecLimited() { return uRecLimited; } ///< Get mutable reference to the limited reconstruction array. + auto &getEval() { return *pEval; } ///< Get mutable reference to the evaluator. + auto &getConfiguration() { return config; } ///< Get mutable reference to the configuration. + auto &getJSource() { return JSource; } ///< Get mutable reference to the source Jacobian. + auto &getBetaPP() { return betaPP; } ///< Get mutable reference to the PP beta array. + auto &getAlphaPP() { return alphaPP; } ///< Get mutable reference to the PP alpha array. + auto &getDTauTmp() { return dTauTmp; } ///< Get mutable reference to the dTau temporary. + auto &getIfUseLimiter() { return ifUseLimiter; } ///< Get mutable reference to the limiter flag array. + auto &getBCHandler() { return pBCHandler; } ///< Get mutable reference to the BC handler. /// @} }; } diff --git a/src/Solver/Direct.hpp b/src/Solver/Direct.hpp index b270f28f..42b925fa 100644 --- a/src/Solver/Direct.hpp +++ b/src/Solver/Direct.hpp @@ -23,9 +23,11 @@ namespace DNDS::Direct DNDS_DECLARE_CONFIG(DirectPrecControl) { + // clang-format off DNDS_FIELD(useDirectPrec, "Enable direct preconditioner"); DNDS_FIELD(iluCode, "ILU fill level: 0=no fill, -1=complete"); DNDS_FIELD(orderingCode, "Ordering: INT32_MIN=auto, 0=natural, 1=metis, 2=MMD"); + // clang-format on } [[nodiscard]] int getOrderingCode() const @@ -57,7 +59,7 @@ namespace DNDS::Direct std::vector localPartStarts; - SerialSymLUStructure(const MPIInfo &nMpi, index nN) : mpi(nMpi), N(nN) {}; + SerialSymLUStructure(const MPIInfo &nMpi, index nN) : mpi(nMpi), N(nN){}; [[nodiscard]] index Num() const { return N; } @@ -313,7 +315,7 @@ namespace DNDS::Direct const auto &localPartStarts = symLU->localPartStarts; int nParts = localPartStarts.size() - 1; #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (int iPart = 0; iPart < nParts; iPart++) for (index iP = localPartStarts.at(iPart); iP < localPartStarts.at(iPart + 1); iP++) @@ -470,7 +472,7 @@ namespace DNDS::Direct auto dThis = static_cast(this); DNDS_assert(!isDecomposed); #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (index iCell = 0; iCell < symLU->Num(); iCell++) { @@ -489,7 +491,7 @@ namespace DNDS::Direct const auto &localPartStarts = symLU->localPartStarts; int nParts = localPartStarts.size() - 1; #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (int iPart = 0; iPart < nParts; iPart++) for (index iP = localPartStarts.at(iPart); iP < localPartStarts.at(iPart + 1); iP++) @@ -504,7 +506,7 @@ namespace DNDS::Direct } } #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (int iPart = 0; iPart < nParts; iPart++) for (index iP = localPartStarts.at(iPart + 1) - 1; iP >= localPartStarts.at(iPart); iP--) @@ -541,8 +543,8 @@ namespace DNDS::Direct void InPlaceDecompose() { - //todo: add pseudo code - //todo: make multithread + // todo: add pseudo code + // todo: make multithread auto dThis = static_cast(this); std::vector diagNoInv(symLU->Num()); for (index iP = 0; iP < symLU->Num(); iP++) @@ -600,7 +602,7 @@ namespace DNDS::Direct auto dThis = static_cast(this); DNDS_assert(!isDecomposed); // being before the decomposition #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (index iCell = 0; iCell < symLU->Num(); iCell++) { @@ -609,7 +611,7 @@ namespace DNDS::Direct result[iCell] += dThis->GetLower(iCell, ij) * x[symLU->lowerTriStructure[iCell][ij]]; } #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (index iCell = 0; iCell < symLU->Num(); iCell++) { @@ -625,7 +627,7 @@ namespace DNDS::Direct const auto &localPartStarts = symLU->localPartStarts; int nParts = localPartStarts.size() - 1; #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (int iPart = 0; iPart < nParts; iPart++) for (index iP = localPartStarts.at(iPart); iP < localPartStarts.at(iPart + 1); iP++) @@ -640,14 +642,14 @@ namespace DNDS::Direct } } #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (index i = 0; i < symLU->Num(); i++) { result[i] = dThis->GetDiag(i) * result[i]; } #if defined(DNDS_DIST_MT_USE_OMP) -#pragma omp parallel for schedule(static) +# pragma omp parallel for schedule(static) #endif for (int iPart = 0; iPart < nParts; iPart++) for (index iP = localPartStarts.at(iPart + 1) - 1; iP >= localPartStarts.at(iPart); iP--) From 7fbad6f28cbac76fd19c9226f40ee45a74943c23 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 01:32:21 +0800 Subject: [PATCH 05/43] docs(dev): clang-tidy cleanup plan, and fix summary regex in driver Adds docs/dev/clang_tidy_plan.md as the living document driving a check-by-check cleanup of the DNDS module (with the same recipe to be applied to Solver, Geom, CFV, Euler, EulerP afterwards). Includes the recorded baseline, a triage table for the top-20 checks with four buckets (keep & fix / keep & accept / silence locally / re-read later), and a pass log skeleton that will be filled in commit by commit. Also tightens CHECK_RE in scripts/run_clang_tidy.py to match only real clang-tidy check names (at least one '-' or '.' in the name). The previous regex matched any lowercase bracketed token, which inflated the 'modernize-use-nodiscard' count by ~2 000 from the '[[nodiscard]]' text that appears in the replacement hint and introduced four phantom checks (nodiscard, loc, pos, name). Total shift in baseline numbers: 28 337 -> 24 597 (real) diagnostics across 55 -> 51 (real) checks. No behaviour change in the driver itself. --- docs/dev/clang_tidy_plan.md | 173 ++++++++++++++++++++++++++++++++++++ scripts/run_clang_tidy.py | 7 +- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 docs/dev/clang_tidy_plan.md diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md new file mode 100644 index 00000000..d241dc0b --- /dev/null +++ b/docs/dev/clang_tidy_plan.md @@ -0,0 +1,173 @@ +# clang-tidy Cleanup Plan + +Living document driving the check-by-check cleanup of the DNDSR C++ +tree. Each "pass" below is a unit of work resulting in one commit; each +bucketed check is addressed to completion before the next starts. + +Driver: `scripts/run_clang_tidy.py`. Config: `/.clang-tidy` (reporting) +and `/.clang-tidy-fix` (auto-fix profile). See +`docs/guides/style_guide.md` for usage. + +## Status snapshot + +- Current module: **DNDS** (all others scheduled after DNDS is clean). +- Baseline: `build/clang-tidy-logs/baseline-dnds.txt`. + +## Table of contents + +1. [Scope and ground rules](#scope-and-ground-rules) +2. [Baseline — DNDS](#baseline--dnds) +3. [Triage table](#triage-table) +4. [Pass log](#pass-log) + 1. [Pass 1 — bugprone-macro-parentheses](#pass-1--bugprone-macro-parentheses) + 2. [Pass 2 — modernize-use-nodiscard](#pass-2--modernize-use-nodiscard) + 3. [Pass 3 — cppcoreguidelines-init-variables](#pass-3--cppcoreguidelines-init-variables) + 4. [Pass 4 — cppcoreguidelines-missing-std-forward](#pass-4--cppcoreguidelines-missing-std-forward) + 5. [Pass 5 — bugprone-reserved-identifier](#pass-5--bugprone-reserved-identifier) + 6. [Pass 6 — cppcoreguidelines-special-member-functions](#pass-6--cppcoreguidelines-special-member-functions) + 7. [Pass 7 — bugprone-implicit-widening-of-multiplication-result](#pass-7--bugprone-implicit-widening-of-multiplication-result) +5. [Disables applied](#disables-applied) +6. [Next modules](#next-modules) + +--- + +## Scope and ground rules + +1. **One check per commit.** Mixing checks makes review impossible and + regressions hard to bisect. +2. **Triage first, fix second.** Before a pass starts, decide whether + the check fits the project: *Keep & fix*, *Keep & accept* (add to + disables), *Keep but silence locally* (per-site `// NOLINT(...)`), + or *Re-read later*. +3. **Verify after every pass.** `cmake --build build -j32` must + succeed. Relevant ctests (currently `ctest -R '^dnds_'`) must pass. + `scripts/run_clang_format.py --check ` must pass. +4. **Spot-check every auto-fix.** After any `--fix` run, sample at + least a handful of changed sites (random spread, not just the + first) before committing. +5. **Never skip the pre-commit hook.** +6. **No CUDA TUs.** `.cu` files are excluded by the driver (nvcc + flags in `compile_commands.json` break clang's CUDA frontend). + CUDA-included headers are still tidied via their `.cpp` includers. + +## Baseline — DNDS + +Captured with: + +```bash +scripts/run_clang_tidy.py --summary --top-checks 50 DNDS \ + > build/clang-tidy-logs/baseline-dnds.txt +``` + +Top-20 (after `f1fb769` / `2d5257c`, with the summary regex fixed, +68 TUs, **24 597 total diagnostics across 51 distinct checks**): + +| # | Check | Hits | +|---|---|---| +| 1 | `cppcoreguidelines-non-private-member-variables-in-classes` | 3 412 | +| 2 | `cppcoreguidelines-avoid-magic-numbers` | 2 690 | +| 3 | `cppcoreguidelines-pro-bounds-pointer-arithmetic` | 2 119 | +| 4 | `cppcoreguidelines-macro-usage` | 2 110 | +| 5 | `cppcoreguidelines-special-member-functions` | 1 645 | +| 6 | `modernize-use-nodiscard` | 1 572 | +| 7 | `bugprone-reserved-identifier` | 1 404 | +| 8 | `cppcoreguidelines-pro-type-vararg` | 1 402 | +| 9 | `cppcoreguidelines-init-variables` | 1 198 | +| 10 | `cppcoreguidelines-pro-type-reinterpret-cast` | 842 | +| 11 | `readability-redundant-casting` | 776 | +| 12 | `readability-redundant-access-specifiers` | 537 | +| 13 | `bugprone-macro-parentheses` | 530 | +| 14 | `cppcoreguidelines-missing-std-forward` | 503 | +| 15 | `cppcoreguidelines-pro-type-member-init` | 475 | +| 16 | `cppcoreguidelines-pro-type-const-cast` | 371 | +| 17 | `cppcoreguidelines-pro-bounds-array-to-pointer-decay` | 334 | +| 18 | `cppcoreguidelines-avoid-non-const-global-variables` | 324 | +| 19 | `bugprone-branch-clone` | 322 | +| 20 | `performance-unnecessary-value-param` | 319 | + +> The summary regex originally matched bracketed tokens like +> `[[nodiscard]]`, ``, ``, `` inside clang-tidy note +> lines, inflating the `modernize-use-nodiscard` row to 3 621 and +> adding four phantom "checks". The regex was tightened (require a +> `-` or `.` inside the name) before the table above was captured. +> Delta between the two runs: 3 740 spurious hits removed, real +> totals unchanged. + +Goal: after all passes in this plan, the top-20 should drop by more +than 50 % of total volume, with the "Keep & fix" rows going to +near-zero. + +## Triage table + +Bucket legend: + +- **KF** — *Keep & fix.* Schedule a pass. +- **KA** — *Keep & accept.* Add to `.clang-tidy` disables. +- **KS** — *Keep but silence locally* at specific call sites with + `// NOLINT(check-name)` + a one-line reason. +- **RL** — *Re-read later.* Decide after noisy checks are drained. + +| Check | Bucket | Rationale | +|---|---|---| +| `cppcoreguidelines-non-private-member-variables-in-classes` | KA | Project uses `struct` with public fields as data bags extensively (Array storage, MPI views, config sections). Enforcing private+getters would be a massive architectural change with no safety gain at our scale. | +| `cppcoreguidelines-avoid-magic-numbers` | KA | Overlaps with already-disabled `readability-magic-numbers`. Identical hit set, identical reasoning. | +| `cppcoreguidelines-pro-bounds-pointer-arithmetic` | KA | CSR storage and MPI byte buffers are fundamentally pointer-arithmetic. Fixing means migrating to `std::span`/`gsl::span` — a design move, not a tidy pass. | +| `cppcoreguidelines-macro-usage` | RL | Project uses macros for device portability, Eigen workarounds, config registration. Load-bearing. Revisit once other noise is gone. | +| `cppcoreguidelines-special-member-functions` | KF | Missing rule-of-five declarations. Can catch real bugs around implicit moves/copies. **Pass 6.** | +| `modernize-use-nodiscard` | KF | Mechanical, mostly auto-fixable. Spot-check needed (some functions are legitimately called for side effects). **Pass 2.** | +| `bugprone-reserved-identifier` | KF | `_Tp`, `__DNDS_str`, etc. are reserved-name patterns. Mechanical rename, not auto-fixable across TUs. **Pass 5.** | +| `cppcoreguidelines-pro-type-vararg` | KA | MPI and printf-family APIs require varargs. Silencing globally is correct. | +| `cppcoreguidelines-init-variables` | KF | Mechanical, mostly auto-fixable. Spot-check for cases where default-init is intentional (e.g. `int rank;` right before `MPI_Comm_rank`). **Pass 3.** | +| `cppcoreguidelines-pro-type-reinterpret-cast` | KA | Used for MPI byte buffers and serialization. No idiomatic replacement. | +| `readability-redundant-casting` | RL | Some are style-only, some indicate real type drift. Revisit after pass 7. | +| `readability-redundant-access-specifiers` | KA | Project explicitly repeats `public:` for readability in large classes. Documented convention. | +| `bugprone-macro-parentheses` | KF | Cheap, mechanical, catches real macro-expansion bugs. Some hits are design-intentional (token-paste operands, argument types) and need `NOLINT`. **Pass 1.** | +| `cppcoreguidelines-missing-std-forward` | KF | Mechanical, auto-fixable. **Pass 4.** | +| `cppcoreguidelines-pro-type-member-init` | RL | Overlaps with `cppcoreguidelines-init-variables` (pass 3) on members. Revisit delta. | +| `cppcoreguidelines-pro-type-const-cast` | KA | Needed for interop with C APIs (MPI, CGNS, HDF5) that take non-const pointers. | +| `cppcoreguidelines-pro-bounds-array-to-pointer-decay` | KA | Same reasoning as pointer-arithmetic. | +| `cppcoreguidelines-avoid-non-const-global-variables` | RL | Globals for signal handlers, log streams, MPI info are intentional; there may be stragglers worth trimming. Revisit after pass 2. | +| `bugprone-branch-clone` | RL | Sometimes a real duplication, sometimes stylistic (exhaustive `if/else` cascades for enum handling). Needs eyes. | +| `performance-unnecessary-value-param` | KF candidate later | Would flag things like `ssp` taken by value where const-ref or move would do. Needs care — some by-value params are deliberate (MPI type, POD). Defer until pass 6 is done. | + +## Pass log + +Each pass below records: objective, scope, commands, sample reviews, +verification (build + any ctests), commit hash, post-pass delta. + +### Pass 1 — bugprone-macro-parentheses + +*TODO* + +### Pass 2 — modernize-use-nodiscard + +*TODO* + +### Pass 3 — cppcoreguidelines-init-variables + +*TODO* + +### Pass 4 — cppcoreguidelines-missing-std-forward + +*TODO* + +### Pass 5 — bugprone-reserved-identifier + +*TODO* + +### Pass 6 — cppcoreguidelines-special-member-functions + +*TODO* + +### Pass 7 — bugprone-implicit-widening-of-multiplication-result + +*TODO* + +## Disables applied + +*TODO: table of checks moved to `.clang-tidy` disables during this effort, +each with one-sentence reason.* + +## Next modules + +*TODO: Solver, Geom, CFV, Euler, EulerP — order and open questions.* diff --git a/scripts/run_clang_tidy.py b/scripts/run_clang_tidy.py index 2b260be9..8b2370f0 100755 --- a/scripts/run_clang_tidy.py +++ b/scripts/run_clang_tidy.py @@ -46,7 +46,12 @@ TU_EXTS = (".c", ".cc", ".cpp", ".cxx") CUDA_EXTS = (".cu",) -CHECK_RE = re.compile(r"\[([a-z][a-z0-9.-]+)(?:,-warnings-as-errors)?\]") +CHECK_RE = re.compile( + # Check names always have a '-' somewhere (category-name-...). + # This excludes noise like [loc], [pos], [name], [nodiscard] that + # can appear in clang-tidy notes/hints. + r"\[([a-z][a-z0-9]*(?:[.-][a-z0-9]+)+)(?:,-warnings-as-errors)?\]" +) def _resolve_build_dir(arg: str | None) -> Path: From ac73535f76d8189c46293fbec28dd60d0dffdaaf Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 01:48:07 +0800 Subject: [PATCH 06/43] refactor(DNDS): silence bugprone-macro-parentheses false positives Pass 1 of the clang-tidy cleanup plan (docs/dev/clang_tidy_plan.md). All 530 DNDS hits of bugprone-macro-parentheses collapsed to 17 unique source locations in 3 files. Every one is a false positive: the flagged token is either a type name (DNDS_DEVICE_TRIVIAL_COPY_DEFINE's T and T_Self, DNDS_DECLARE_CONFIG's Type_) or a storage-class specifier (DNDS_ARRAY_DOF_OP_FUNC_LIST's spec, always passed static), neither of which is syntactically parenthesizable in C++. Three NOLINTBEGIN/NOLINTEND pairs, each with a one-sentence rationale, scoped tightly to the three macro definitions. No code-generation change; pure comment additions. Numbers: 24 597 -> 24 067 total diagnostics, 51 -> 50 distinct checks. Plan doc updated. --- docs/dev/clang_tidy_plan.md | 32 +++++++++++++++++++++++++++++++- src/DNDS/ArrayDOF.hpp | 4 ++++ src/DNDS/Config/ConfigParam.hpp | 4 ++++ src/DNDS/Defines.hpp | 5 +++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index d241dc0b..0bfdf070 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -137,7 +137,37 @@ verification (build + any ctests), commit hash, post-pass delta. ### Pass 1 — bugprone-macro-parentheses -*TODO* +**Outcome.** 530 hits → 0. Total diagnostics 24 597 → 24 067. + +All 530 hits collapsed to **17 unique source locations in 3 files**: + +| File | Lines (col) | Distinct sites | Dupes per site | +|---|---|---|---| +| `src/DNDS/Defines.hpp` | 86:28, 88:39, 93:28, 95:39 | 4 | 66 | +| `src/DNDS/ArrayDOF.hpp` | 68..79:5 | 12 | 22 | +| `src/DNDS/Config/ConfigParam.hpp` | 700:60 | 1 | 2 | + +**Decision.** All 17 are false positives: every flagged token is a +C++ *type name* or *storage-class specifier* in a context where +parenthesization is not valid syntax: + +- `DNDS_DEVICE_TRIVIAL_COPY_DEFINE(T, T_Self)` — `T` and `T_Self` appear + as parameter types in constructor and assignment-operator signatures. +- `DNDS_ARRAY_DOF_OP_FUNC_LIST(..., spec)` — `spec` is always passed + `static`; it's the leading storage-class specifier of a function + declaration. +- `DNDS_DECLARE_CONFIG(Type_)` — `Type_` is used as a parameter type + and as a template argument. + +**Fix.** Three `NOLINTBEGIN` / `NOLINTEND` pairs around the macro +definitions, each with a one-sentence rationale comment. 13 lines +of comments replace 530 diagnostics. + +**Verification.** `cmake --build build -t dnds -j32` succeeds; the +summary drop of 530 exactly matches the decrement in the total +(no secondary effects). + +Commit: see `git log -- docs/dev/clang_tidy_plan.md`. ### Pass 2 — modernize-use-nodiscard diff --git a/src/DNDS/ArrayDOF.hpp b/src/DNDS/ArrayDOF.hpp index 8c79c87a..64ca9eb8 100644 --- a/src/DNDS/ArrayDOF.hpp +++ b/src/DNDS/ArrayDOF.hpp @@ -63,6 +63,9 @@ namespace DNDS class ArrayDofOp; #define DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) +// NOLINTBEGIN(bugprone-macro-parentheses) +// Rationale: `spec` is a C++ storage-class specifier token (e.g. `static`) +// placed at the start of a function declaration; it cannot be parenthesized. #define DNDS_ARRAY_DOF_OP_FUNC_LIST(B, n_m, n_n, spec) \ spec void DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) setConstant(t_self &self, real R); \ spec void DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) setConstant(t_self &self, const Eigen::Ref &R); \ @@ -83,6 +86,7 @@ namespace DNDS spec ArrayDofOp::t_element_mat DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) componentWiseNorm1(t_self &self); \ spec ArrayDofOp::t_element_mat DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) componentWiseNorm1(t_self &self, const t_self &R); \ spec real DNDS_ARRAY_DOF_OP_FUNC_LIST_SCOPE(B, n_m, n_n) dot(t_self &self, const t_self &R); + // NOLINTEND(bugprone-macro-parentheses) /** * @brief Host-side static dispatcher: implements every vector-space operation diff --git a/src/DNDS/Config/ConfigParam.hpp b/src/DNDS/Config/ConfigParam.hpp index 9b20ef85..954b89b7 100644 --- a/src/DNDS/Config/ConfigParam.hpp +++ b/src/DNDS/Config/ConfigParam.hpp @@ -681,6 +681,9 @@ namespace DNDS /// } /// }; /// @endcode +// NOLINTBEGIN(bugprone-macro-parentheses) +// Rationale: `Type_` is a type name used in function parameter lists and +// template arguments; neither context permits parenthesization. #define DNDS_DECLARE_CONFIG(Type_) \ using T = Type_; \ static void _dnds_ensure_registered() \ @@ -724,3 +727,4 @@ namespace DNDS ::DNDS::ConfigRegistry::validateKeys(j); \ } \ static void _dnds_do_register(::DNDS::ConfigSectionBuilder &config) +// NOLINTEND(bugprone-macro-parentheses) diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index 1e4c0326..ee149fad 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -80,6 +80,10 @@ namespace DNDS # define DNDS_CONSTANT #endif +// NOLINTBEGIN(bugprone-macro-parentheses) +// Rationale: T and T_Self are type names used in constructor / assignment +// operator signatures; parenthesizing a type in a parameter list is not +// valid C++. #define DNDS_DEVICE_TRIVIAL_COPY_DEFINE(T, T_Self) \ DNDS_DEVICE_CALLABLE T() = default; \ DNDS_DEVICE_CALLABLE T(const T_Self &) = default; \ @@ -94,6 +98,7 @@ namespace DNDS DNDS_DEVICE_CALLABLE T &operator=(const T_Self &) = default; \ DNDS_DEVICE_CALLABLE T &operator=(T_Self &&) = default; \ DNDS_DEVICE_CALLABLE ~T() = default; +// NOLINTEND(bugprone-macro-parentheses) /***************/ From 7502b8daa25eb1288d503543ad04bf347678d1a6 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 02:24:18 +0800 Subject: [PATCH 07/43] fix(tooling): serialize --fix in run_clang_tidy.py clang-tidy --fix cannot run in parallel on the same header from multiple TUs: the parallel write edits race and corrupt the file (observed: interleaved '[[nodiscard]]' insertions producing syntax errors across Vector.hpp, Array.hpp, ArrayPair.hpp, etc. during a Pass-2 trial run). Force jobs=1 whenever --fix is set. The escape hatch --unsafe-parallel-fix keeps the old behaviour for callers that have pre-partitioned the scope to disjoint files. Non-fix runs are unchanged. Message added so the user sees when the jobs cap kicks in. --- scripts/run_clang_tidy.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/run_clang_tidy.py b/scripts/run_clang_tidy.py index 8b2370f0..075c803d 100755 --- a/scripts/run_clang_tidy.py +++ b/scripts/run_clang_tidy.py @@ -416,6 +416,16 @@ def _build_parser() -> argparse.ArgumentParser: "script exits 0 regardless of diagnostics (advisory mode)." ), ) + p.add_argument( + "--unsafe-parallel-fix", + action="store_true", + help=( + "Allow --fix to run with jobs>1. Unsafe: parallel fix passes " + "on the same header race and can corrupt files. Use only on " + "disjoint scopes (e.g. one TU) or with serialized fanout " + "handled by the caller." + ), + ) return p @@ -492,11 +502,22 @@ def main(argv: Sequence[str] | None = None) -> int: version = subprocess.run( [clang_tidy, "--version"], capture_output=True, text=True, check=False ).stdout.splitlines()[:1] + + # --fix cannot run in parallel safely: multiple TUs editing the same + # header concurrently will race and corrupt the file (interleaved + # insertions). Force -j 1 unless the user explicitly opts in with + # --unsafe-parallel-fix. + effective_jobs = args.jobs + if args.fix and not args.unsafe_parallel_fix and args.jobs > 1: + print("note: --fix forces jobs=1 (parallel --fix is unsafe); " + "pass --unsafe-parallel-fix to override.") + effective_jobs = 1 + print(version[0] if version else "(clang-tidy version unknown)") print(f"clang-tidy : {clang_tidy}") print(f"config : {config}") print(f"compile db : {build_dir / 'compile_commands.json'}") - print(f"jobs : {args.jobs}") + print(f"jobs : {effective_jobs}") print(f"files : {len(selected)}") if header_filter: print(f"header filter: {header_filter}") @@ -513,7 +534,7 @@ def main(argv: Sequence[str] | None = None) -> int: quiet = args.quiet or args.summary exit_code, counts = _run_parallel( - selected, base, jobs=args.jobs, quiet=quiet, log_path=log_path, + selected, base, jobs=effective_jobs, quiet=quiet, log_path=log_path, ) print() From cd93e2e95644605892993856e7637cd4af81e34d Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 02:25:15 +0800 Subject: [PATCH 08/43] refactor(DNDS): add [[nodiscard]] to const getters (modernize-use-nodiscard) Pass 2 of the clang-tidy cleanup plan. Applied via scripts/run_clang_tidy.py --fix --config-file /tmp/pass2.clang-tidy DNDS with a single-check override (only modernize-use-nodiscard enabled). Serialized because parallel --fix on shared headers corrupts files. 32 unique declarations gained [[nodiscard]] across 11 files (Array.hpp, ArrayBasic.hpp, ArrayDerived/*.hpp, ArrayPair.hpp, Vector.hpp, EigenUtil.hpp, Serializer/SerializerH5.cpp). The 1 572 diagnostic count collapses to 32 because each header declaration was reported once per including TU. Sample-reviewed four representative changes: at(), at_compressed() (with DNDS_DEVICE_CALLABLE prefix), rows()/cols()/size() Eigen wrappers, get_indent() in SerializerH5.cpp. All are pure const getters; no call sites found that invoke them for side effects. Numbers: 24 067 -> 22 495 total diagnostics, 50 -> 49 distinct checks. Build: dnds + euler targets succeed; format stable. --- docs/dev/clang_tidy_plan.md | 50 ++++++++++++++++++- src/DNDS/Array.hpp | 8 +-- src/DNDS/ArrayBasic.hpp | 8 +-- src/DNDS/ArrayDerived/AdjacencyRow.hpp | 4 +- src/DNDS/ArrayDerived/ArrayAdjacency.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 4 +- .../ArrayDerived/ArrayEigenUniMatrixBatch.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenVector.hpp | 2 +- src/DNDS/ArrayPair.hpp | 6 +-- src/DNDS/EigenUtil.hpp | 6 +-- src/DNDS/Serializer/SerializerH5.cpp | 2 +- src/DNDS/Vector.hpp | 20 ++++---- 12 files changed, 81 insertions(+), 33 deletions(-) diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index 0bfdf070..ef038d52 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -171,7 +171,55 @@ Commit: see `git log -- docs/dev/clang_tidy_plan.md`. ### Pass 2 — modernize-use-nodiscard -*TODO* +**Outcome.** 1 572 hits → 0. Total diagnostics 24 067 → 22 495. + +**Method.** Single-check override config at `/tmp/pass2.clang-tidy`: + +```yaml +Checks: "-*,modernize-use-nodiscard" +WarningsAsErrors: '' +HeaderFilterRegex: '.*/DNDSR/(src|app|test/cpp)/.*' +ExtraArgs: [-UDNDS_USE_OMP, -Wno-unknown-warning-option, -Wno-unused-command-line-argument] +FormatStyle: file +``` + +Driven by `scripts/run_clang_tidy.py --fix --config-file /tmp/pass2.clang-tidy DNDS`. + +**Incident & fix.** The first `--fix` attempt ran with 64 parallel +workers and produced syntax-error corruption in `Vector.hpp`, +`Array.hpp`, `ArrayPair.hpp`, and several other headers because the +parallel workers race when the same header is edited from multiple +TUs (observed: interleaved `[[nodiscard]]` insertions in mid-token +positions). + +Reverted via `git checkout HEAD -- src/DNDS/` and patched the driver +to force `jobs=1` whenever `--fix` is set (with +`--unsafe-parallel-fix` escape hatch). Serialized run completed +cleanly. Committed as a separate `fix(tooling)` commit so the pass's +diff stays pure. + +**Diff footprint.** 11 files, 32 line-replacements (one `[[nodiscard]]` +prefix each). 1 572 repeated hits collapsed to 32 unique decls +because each header declaration is reported once per including TU. + +**Sample review.** 4 spot-checks: + +- `Array.hpp:570` `at() const` — const getter, value return. +- `ArrayBasic.hpp:423` `at_compressed(...) const` with + `DNDS_DEVICE_CALLABLE` prefix — attribute order is valid. +- `EigenUtil.hpp:278..294` `rows()/cols()/size() const` — Eigen + dimension wrappers, pure reads. +- `SerializerH5.cpp:180` `get_indent() const` — local helper, + string-builder, no side effects. + +All good; no call sites in DNDS were found that call these functions +for side effects. + +**Verification.** `cmake --build build -t dnds --clean-first -j32` +succeeds; `euler` target builds. Pre-commit clang-format ran on 11 +modified files, no drift. + +Commits: `7502b8d` (driver serialize-on-fix) + pass 2 commit. ### Pass 3 — cppcoreguidelines-init-variables diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 808283fb..0d7b3af3 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -570,7 +570,7 @@ namespace DNDS /// row size into account, not just the stride). Works for every layout. /// @param iRow Row index in `[0, Size())`. /// @param iCol Column index in `[0, RowSize(iRow))`. - const T &at(index iRow, rowsize iCol) const + [[nodiscard]] const T &at(index iRow, rowsize iCol) const { DNDS_assert_info(iRow < _size && iRow >= 0, fmt::format( @@ -685,7 +685,7 @@ namespace DNDS /// @brief Total number of `T` elements currently stored in the flat buffer. /// @details For CSR, requires the array to be compressed. - size_t DataSize() const + [[nodiscard]] size_t DataSize() const { if (this->Size() == 0) return 0; @@ -695,7 +695,7 @@ namespace DNDS } /// @brief Flat buffer size in bytes (= `DataSize() * sizeof(T)`). - size_t DataSizeBytes() const + [[nodiscard]] size_t DataSizeBytes() const { return this->DataSize() * sizeof_T; } @@ -732,7 +732,7 @@ namespace DNDS /// @details Sums the flat data buffer, `_pRowStart` (if any), and /// `_pRowSizes` (if any). Approximate because shared-ownership of row /// structures is not deduplicated. - size_t FullSizeBytes() const + [[nodiscard]] size_t FullSizeBytes() const { size_t b = this->DataSize() * sizeof_T; if (_pRowStart) diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index 6dfbbaa0..8343a4be 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -423,7 +423,7 @@ namespace DNDS } protected: - DNDS_DEVICE_CALLABLE const T &at_compressed(index iRow, rowsize iCol) const + DNDS_DEVICE_CALLABLE [[nodiscard]] const T &at_compressed(index iRow, rowsize iCol) const { DNDS_HD_assert(isCompressed()); DNDS_HD_assert_infof(iRow < _size && iRow >= 0, @@ -458,7 +458,7 @@ namespace DNDS /// @brief Bounds-checked element read (not device-callable because CSR /// decompressed uses `std::vector::at` which throws on the host). // not device callable - const T &at(index iRow, rowsize iCol) const + [[nodiscard]] const T &at(index iRow, rowsize iCol) const { if constexpr (_dataLayout == CSR) { @@ -554,7 +554,7 @@ namespace DNDS } /// @brief Size of the flat data buffer in `T` elements. - DNDS_DEVICE_CALLABLE size_t DataSize() const + DNDS_DEVICE_CALLABLE [[nodiscard]] size_t DataSize() const { if (this->Size() == 0) return 0; @@ -659,7 +659,7 @@ namespace DNDS // DNDS_HD_assert(iRow >= -1 && iRow <= getView().Size()); //! view in derived class is uninitialized here! } - DNDS_DEVICE_CALLABLE index RowSize() const { return getView().RowSize(iRow); } + DNDS_DEVICE_CALLABLE [[nodiscard]] index RowSize() const { return getView().RowSize(iRow); } DNDS_DEVICE_CALLABLE Derived &operator++() { diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 73025be6..6adbbc46 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -72,8 +72,8 @@ namespace DNDS DNDS_DEVICE_CALLABLE index_T *begin() { return __p_indices; } DNDS_DEVICE_CALLABLE index_T *end() { return __p_indices + __Row_size; } // past-end - DNDS_DEVICE_CALLABLE index_T *cbegin() const { return __p_indices; } - DNDS_DEVICE_CALLABLE index_T *cend() const { return __p_indices + __Row_size; } // past-end + DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cbegin() const { return __p_indices; } + DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cend() const { return __p_indices + __Row_size; } // past-end /// @brief Row width in number of `index_T` elements. DNDS_DEVICE_CALLABLE [[nodiscard]] rowsize size() const { return __Row_size; } }; diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp index c1284c61..55255726 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp @@ -89,7 +89,7 @@ namespace DNDS } template - auto deviceView() const + [[nodiscard]] auto deviceView() const { return t_deviceViewConst{this->t_base::template deviceView()}; } diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index e2f4e8d6..eb7a1a05 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -74,7 +74,7 @@ namespace DNDS rowsize _mat_nRow_dynamic = 0; //! extra data public: - size_t FullSizeBytes() const + [[nodiscard]] size_t FullSizeBytes() const { size_t b = this->t_base::FullSizeBytes(); if (_mat_nRows) @@ -339,7 +339,7 @@ namespace DNDS } template - auto deviceView() const + [[nodiscard]] auto deviceView() const { auto base_view = t_base::template deviceView(); return t_deviceViewConst(base_view, diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp index fc1ed96f..cbf11367 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp @@ -254,7 +254,7 @@ namespace DNDS } template - auto deviceView() const + [[nodiscard]] auto deviceView() const { return t_deviceView{t_base::template deviceView(), _row_dynamic, _col_dynamic, _m_size}; } diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp index 92d52d2e..c6037343 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp @@ -92,7 +92,7 @@ namespace DNDS /// @brief Const device view. template - auto deviceView() const + [[nodiscard]] auto deviceView() const { return t_deviceViewConst{this->t_base::template deviceView()}; } diff --git a/src/DNDS/ArrayPair.hpp b/src/DNDS/ArrayPair.hpp index a5e501c5..8243cf99 100644 --- a/src/DNDS/ArrayPair.hpp +++ b/src/DNDS/ArrayPair.hpp @@ -255,13 +255,13 @@ namespace DNDS } /// @brief Uniform row width (delegates to father). - auto RowSize() const + [[nodiscard]] auto RowSize() const { return father->RowSize(); } /// @brief Per-row width in the combined address space. - auto RowSize(index i) const + [[nodiscard]] auto RowSize(index i) const { if (i >= 0 && i < father->Size()) return father->RowSize(i); @@ -670,7 +670,7 @@ namespace DNDS /// @brief Produce a const device view. template - auto deviceView() const + [[nodiscard]] auto deviceView() const { DNDS_check_throw_info(father && son, fmt::format("need both father and son to exist for device view: {}", diff --git a/src/DNDS/EigenUtil.hpp b/src/DNDS/EigenUtil.hpp index 32abb43c..af0b4a9e 100644 --- a/src/DNDS/EigenUtil.hpp +++ b/src/DNDS/EigenUtil.hpp @@ -275,7 +275,7 @@ namespace DNDS h_data.resize(this->size()); } - rowsize rows() const + [[nodiscard]] rowsize rows() const { if constexpr (M >= 0) return M; @@ -283,7 +283,7 @@ namespace DNDS return M_dynamic; } - rowsize cols() const + [[nodiscard]] rowsize cols() const { if constexpr (N >= 0) return N; @@ -291,7 +291,7 @@ namespace DNDS return N_dynamic; } - rowsize size() const + [[nodiscard]] rowsize size() const { return rows() * cols(); } diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 56e530a1..95665611 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -180,7 +180,7 @@ namespace DNDS::Serializer H5Contents &contents; std::string current_path; bool coll_on_meta; - std::string get_indent() const + [[nodiscard]] std::string get_indent() const { return std::string(std::count(current_path.begin(), current_path.end(), '/') * 2, ' '); } diff --git a/src/DNDS/Vector.hpp b/src/DNDS/Vector.hpp index fab4dc04..6137cad6 100644 --- a/src/DNDS/Vector.hpp +++ b/src/DNDS/Vector.hpp @@ -34,7 +34,7 @@ namespace DNDS /// @brief Typed byte pointer to the current allocation. virtual uint8_t *get() = 0; /// @brief Allocation size in bytes. - virtual size_t bytes() const = 0; + [[nodiscard]] virtual size_t bytes() const = 0; /// @brief Which backend currently owns the allocation. virtual DeviceBackend device() = 0; /// @brief Copy `n` bytes from `host_src` into this allocation. @@ -81,7 +81,7 @@ namespace DNDS device_storage = nullptr; host_data.clear(); } - size_t bytes() const override { return bytes_; } + [[nodiscard]] size_t bytes() const override { return bytes_; } uint8_t *get() override { if (B_ == DeviceBackend::Unknown) @@ -185,7 +185,7 @@ namespace DNDS const T &operator[](size_t i) const { return static_cast(this)->data()[i]; } - const T &at(size_t i) const + [[nodiscard]] const T &at(size_t i) const { auto *dThis = static_cast(this); DNDS_check_throw_info(dThis->size() > i, std::to_string(i) + " --- " + std::to_string(dThis->size())); @@ -263,7 +263,7 @@ namespace DNDS this->operator=(v); } - DNDS_HOST size_t size() const { return size_; } + DNDS_HOST [[nodiscard]] size_t size() const { return size_; } DNDS_HOST void resize(size_t new_size) { @@ -288,18 +288,18 @@ namespace DNDS } DNDS_HOST T *data() { return host_ptr; } - DNDS_HOST const T *data() const { return host_ptr; } + DNDS_HOST [[nodiscard]] const T *data() const { return host_ptr; } DNDS_HOST T *dataDevice() { return device_ptr; } - DNDS_HOST const T *dataDevice() const { return device_ptr; } + DNDS_HOST [[nodiscard]] const T *dataDevice() const { return device_ptr; } DNDS_HOST auto begin() { return host_ptr; } DNDS_HOST auto end() { return host_ptr + size_; } - DNDS_HOST auto begin() const { return host_ptr; } - DNDS_HOST auto end() const { return host_ptr + size_; } + DNDS_HOST [[nodiscard]] auto begin() const { return host_ptr; } + DNDS_HOST [[nodiscard]] auto end() const { return host_ptr + size_; } - DNDS_HOST auto cbegin() const { return host_ptr; } - DNDS_HOST auto cend() const { return host_ptr + size_; } + DNDS_HOST [[nodiscard]] auto cbegin() const { return host_ptr; } + DNDS_HOST [[nodiscard]] auto cend() const { return host_ptr + size_; } DNDS_HOST explicit operator std::vector() const { From 842fb76c2e19fc963060bfc4e5f31559da760f24 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 02:59:34 +0800 Subject: [PATCH 09/43] refactor(DNDS): initialize locals before write (cppcoreguidelines-init-variables) Pass 3 of the clang-tidy cleanup plan. Applied via scripts/run_clang_tidy.py --fix --config-file /tmp/pass3.clang-tidy DNDS with a single-check override (cppcoreguidelines-init-variables only), serialized. ~30 declarations across 8 files now initialise the local before its first MPI/HDF5 out-parameter write: the classic 'int x; MPI_*(&x)' becomes 'int x = 0; MPI_*(&x)'. Two auto-fix choices were reverted by hand during sample review: - ArrayTransformer.hpp:838,867: clang-tidy picked 'MPI_Datatype dtype = nullptr' which compiles on OpenMPI (where MPI_Datatype is a pointer typedef) but not on MPICH (where it is an int). Manually set to MPI_DATATYPE_NULL. - ArrayDOF_op.hxx: clang-tidy added '#include ' and 'real sqrSumAll = NAN'. The sibling function three paragraphs up initialises sqrSumAll to 0; made this one match and dropped the math.h include. Numbers: 22 495 -> 21 297 total diagnostics, 49 -> 49 distinct checks. Build: dnds target succeeds. --- docs/dev/clang_tidy_plan.md | 33 +++++++++++++++++++++- src/DNDS/Array.hpp | 2 +- src/DNDS/ArrayDOF_op.hxx | 2 +- src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 4 +-- src/DNDS/ArrayTransformer.hpp | 16 +++++------ src/DNDS/IndexMapping.hpp | 4 +-- src/DNDS/MPI.cpp | 4 +-- src/DNDS/MPI.hpp | 16 +++++------ src/DNDS/Serializer/SerializerH5.cpp | 10 +++---- 9 files changed, 61 insertions(+), 30 deletions(-) diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index ef038d52..0f0863fe 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -223,7 +223,38 @@ Commits: `7502b8d` (driver serialize-on-fix) + pass 2 commit. ### Pass 3 — cppcoreguidelines-init-variables -*TODO* +**Outcome.** 1 198 hits → 0. Total diagnostics 22 495 → 21 297. + +**Method.** Same recipe as Pass 2; single-check config at +`/tmp/pass3.clang-tidy`, `--fix`, serialized. One full run fixed all +1 198 hits in one shot (no stragglers). + +**Diff footprint.** 8 files, ~30 line-replacements. Most are the +classic "declare-then-immediately-MPI-writes" pattern; `int x; MPI_*(&x)` +now `int x = 0; MPI_*(&x)`. + +**Sample review found two corrections needed:** + +1. `ArrayTransformer.hpp:838,867` — clang-tidy initialised + `MPI_Datatype dtype` to `nullptr`. That's only valid on MPI + implementations where `MPI_Datatype` is a pointer typedef + (OpenMPI). MPICH defines it as `int`, so `nullptr` would not + compile. Manually corrected to `MPI_DATATYPE_NULL`, the canonical + sentinel that works on both. +2. `ArrayDOF_op.hxx` — clang-tidy inserted `#include ` and + initialised `real sqrSumAll = NAN;`. The sibling function on + line 228 initialises the same variable to `0`. Corrected to + match the sibling and removed the unnecessary include. + +**Verification.** `cmake --build build -t dnds -j32` succeeds after +both corrections. Pre-commit clang-format re-ran, no drift. + +**Lesson learned.** `cppcoreguidelines-init-variables` with `--fix` +picks unusual sentinel values (`nullptr` based on typedef, `NAN` for +floats). Always review auto-fix output for semantic appropriateness, +not just build success. + +Commit: see `git log`. ### Pass 4 — cppcoreguidelines-missing-std-forward diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 0d7b3af3..48739694 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -747,7 +747,7 @@ namespace DNDS /// equality diagnostics; not guaranteed cryptographically strong. std::size_t hash() { - std::size_t hashData; + std::size_t hashData = 0; if constexpr (_dataLayout == CSR) { if (IfCompressed()) diff --git a/src/DNDS/ArrayDOF_op.hxx b/src/DNDS/ArrayDOF_op.hxx index 0661b2ac..7fd1ddfb 100644 --- a/src/DNDS/ArrayDOF_op.hxx +++ b/src/DNDS/ArrayDOF_op.hxx @@ -346,7 +346,7 @@ namespace DNDS { DNDS_assert(self.father && self.son); DNDS_assert(R.father && R.son); - real sqrSum{0}, sqrSumAll; + real sqrSum{0}, sqrSumAll{0}; index iTop = self.father->Size(); #if defined(DNDS_DIST_MT_USE_OMP) # pragma omp parallel for schedule(static) reduction(+ : sqrSum) diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index eb7a1a05..82224b99 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -181,7 +181,7 @@ namespace DNDS t_EigenMap operator[](index i) { - rowsize c_nRow; + rowsize c_nRow = 0; if constexpr (_mat_ni == NonUniformSize) c_nRow = (*_mat_nRows)[i]; else if constexpr (_mat_ni == DynamicSize) @@ -196,7 +196,7 @@ namespace DNDS t_EigenMap_const operator[](index i) const { - rowsize c_nRow; + rowsize c_nRow = 0; if constexpr (_mat_ni == NonUniformSize) c_nRow = (*_mat_nRows)[i]; else if constexpr (_mat_ni == DynamicSize) diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 8d3df567..0cfdf01a 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -309,8 +309,8 @@ namespace DNDS void AssertDataType() { DNDS_check_throw(dataType != MPI_DATATYPE_NULL); - MPI_Aint lb; - MPI_Aint extent; + MPI_Aint lb = 0; + MPI_Aint extent = 0; MPI_Type_get_extent(dataType, &lb, &extent); DNDS_check_throw(lb == 0 && extent * typeMult == sizeof(T)); } @@ -835,7 +835,7 @@ namespace DNDS // } // std::cout << "=== PUSH TYPE : " << mpi.rank << " from " << r << std::endl; - MPI_Datatype dtype; + MPI_Datatype dtype = MPI_DATATYPE_NULL; int sizeof_T = MPI_UNDEFINED; MPI_Type_size(father->getDataType(), &sizeof_T); DNDS_check_throw(sizeof_T != MPI_UNDEFINED); @@ -864,7 +864,7 @@ namespace DNDS if (pullSizes[0] > 0) { // std::cout << "=== PULL TYPE : " << mpi.rank << " from " << r << std::endl; - MPI_Datatype dtype; + MPI_Datatype dtype = MPI_DATATYPE_NULL; MPI_Type_create_hindexed(1, pullSizes.data(), pullDisp.data(), father->getDataType(), &dtype); @@ -1122,8 +1122,8 @@ namespace DNDS for (MPI_int r = 0; r < mpi.size; r++) { // pull - MPI_Aint pullDisp; - MPI_int pullSize; // same as pushSizes + MPI_Aint pullDisp = 0; + MPI_int pullSize = 0; // same as pushSizes auto gRPtr = son->operator[](index(pLGhostMapping->ghostStart[r + 1])); auto gLPtr = son->operator[](index(pLGhostMapping->ghostStart[r])); auto ghostSpan = gRPtr - gLPtr; @@ -1179,8 +1179,8 @@ namespace DNDS for (MPI_int r = 0; r < mpi.size; r++) { // pull - MPI_Aint pullDisp; - MPI_int pullSize; // same as pushSizes + MPI_Aint pullDisp = 0; + MPI_int pullSize = 0; // same as pushSizes auto gRPtr = son->operator[](index(pLGhostMapping->ghostStart[r + 1])); auto gLPtr = son->operator[](index(pLGhostMapping->ghostStart[r])); auto ghostSpan = gRPtr - gLPtr; diff --git a/src/DNDS/IndexMapping.hpp b/src/DNDS/IndexMapping.hpp index 933e3aea..5c03372f 100644 --- a/src/DNDS/IndexMapping.hpp +++ b/src/DNDS/IndexMapping.hpp @@ -244,8 +244,8 @@ namespace DNDS ghostIndex.reserve(ghostStart[ghostSizes.size()]); for (auto i : pullingIndexGlobal) { - MPI_int rank; - index loc; // dummy here + MPI_int rank = 0; + index loc = 0; // dummy here bool search_result = LGlobalMapping.search(i, rank, loc); DNDS_assert_info(search_result, "Search Failed"); // if (rank != mpi.rank) diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 5718e127..d4feb1cc 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -70,7 +70,7 @@ namespace DNDS::Debug { for (MPI_int ir = 0; ir < mpi.size; ir++) { - int newDebugFlag; + int newDebugFlag = 0; if (mpi.rank == ir) { newDebugFlag = int(IsDebugged()); @@ -270,7 +270,7 @@ namespace DNDS::MPI MPI_int WaitallLazy(MPI_int count, MPI_Request *reqs, MPI_Status *statuses, uint64_t checkNanoSecs) { MPI_int flag = 0; - MPI_int ret; + MPI_int ret = 0; while (!flag) { ret = MPI_Testall(count, reqs, &flag, statuses); diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 091fc263..4ea907a9 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -237,7 +237,7 @@ namespace DNDS MPIInfo(MPI_Comm ncomm) { comm = ncomm; - int ierr; + int ierr = 0; ierr = MPI_Comm_rank(comm, &rank), DNDS_assert(ierr == MPI_SUCCESS); ierr = MPI_Comm_size(comm, &size), DNDS_assert(ierr == MPI_SUCCESS); } @@ -253,7 +253,7 @@ namespace DNDS void setWorld() { comm = MPI_COMM_WORLD; - int ierr; + int ierr = 0; ierr = MPI_Comm_rank(comm, &rank), DNDS_assert(ierr == MPI_SUCCESS); ierr = MPI_Comm_size(comm, &size), DNDS_assert(ierr == MPI_SUCCESS); } @@ -484,8 +484,8 @@ namespace DNDS // TODO: get a concurrency header /// @brief Return the MPI thread-support level the current process was initialised with. inline int GetMPIThreadLevel() { - int ret; - int ierr; + int ret = 0; + int ierr = 0; ierr = MPI_Query_thread(&ret), DNDS_assert(ierr == MPI_SUCCESS); return ret; } @@ -582,8 +582,8 @@ namespace DNDS private: MPIBufferHandler() { - uint8_t *obuf; - int osize; + uint8_t *obuf = nullptr; + int osize = 0; MPI_Buffer_detach(reinterpret_cast(&obuf) /* caution */, &osize); buf.resize(1024ULL * 1024ULL); @@ -608,8 +608,8 @@ namespace DNDS if (buf.size() - claimed < static_cast(cs)) { // std::cout << "claim in " << std::endl; - uint8_t *obuf; - int osize; + uint8_t *obuf = nullptr; + int osize = 0; MPI_Buffer_detach(reinterpret_cast(&obuf) /* caution */, &osize); #ifdef MPIBufferHandler_REPORT_CHANGE std::cout << "MPIBufferHandler: New BUf at " << reportRank << std::endl diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 95665611..2cb58d6d 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -188,7 +188,7 @@ namespace DNDS::Serializer static herr_t link_iterate_cb(hid_t group_id, const char *name, const H5L_info_t *info, void *op_data) { - herr_t herr; + herr_t herr = 0; auto *data = static_cast(op_data); bool coll_on_meta = data->coll_on_meta; std::string full_name = data->current_path + "/" + name; @@ -859,7 +859,7 @@ namespace DNDS::Serializer void SerializerH5::ReadSharedIndexVector(const std::string &name, ssp> &v, ArrayGlobalOffset &offset) { using tValue = host_device_vector; - herr_t herr; + herr_t herr = 0; std::string refPath; hid_t group_id = GetGroupOfFileIfExist(h5file, reading, cP, collectiveMetadataRW); htri_t exists_ref = H5Aexists(group_id, (name + "::ref").c_str()); @@ -882,7 +882,7 @@ namespace DNDS::Serializer v = std::make_shared(); pth_2_ssp[refPath] = &v; - size_t size; + size_t size = 0; index dummy{}; ReadDataVector(refPath, nullptr, size, offset, h5file, reading, "/", mpi, collectiveMetadataRW, collectiveDataRW); v->resize(size); @@ -893,7 +893,7 @@ namespace DNDS::Serializer void SerializerH5::ReadSharedRowsizeVector(const std::string &name, ssp> &v, ArrayGlobalOffset &offset) { using tValue = host_device_vector; - herr_t herr; + herr_t herr = 0; std::string refPath; hid_t group_id = GetGroupOfFileIfExist(h5file, reading, cP, collectiveMetadataRW); htri_t exists_ref = H5Aexists(group_id, (name + "::ref").c_str()); @@ -916,7 +916,7 @@ namespace DNDS::Serializer v = std::make_shared(); pth_2_ssp[refPath] = &v; - size_t size; + size_t size = 0; rowsize dummy{}; ReadDataVector(refPath, nullptr, size, offset, h5file, reading, "/", mpi, collectiveMetadataRW, collectiveDataRW); v->resize(size); From 583c468d00c957cb4d4ac3e3c906d61f28fb959a Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 04:00:55 +0800 Subject: [PATCH 10/43] chore(tooling): disable cppcoreguidelines-missing-std-forward, fix YAML trap Pass 4 of the clang-tidy cleanup plan. Reclassified from KF to KA. All 9 unique DNDS hit sites are false positives: either functor parameters that are called in-place (moving on first call would break repeated calls) or collection parameters that are mutated in place without being transferred. Neither benefits from std::forward and the check cannot tell the two patterns apart. Also fixed a YAML folded-scalar trap in .clang-tidy: the Checks: > block was accumulating '#'-prefixed rationale comments as literal text, which produced a malformed check name that silently absorbed subsequent disables. Moved all rationale into the file header as a table; the Checks: block is now comment-free, and a warning note explains why. Numbers: 21 297 -> 20 794 total diagnostics, 48 -> 47 distinct checks. Build unchanged (config-only). --- .clang-tidy | 33 +++++++++++++++++++++++ docs/dev/clang_tidy_plan.md | 54 ++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 97ef8d10..19a136dd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,6 +15,38 @@ # - WarningsAsErrors is intentionally empty: tidy runs are # advisory reports, not build gates. If we later want a CI # gate, apply it as an overlay config on top of this file. +# - CAUTION: the Checks: block uses YAML folded scalar (>) and +# therefore cannot contain inline '#' comments -- '#' is only a +# comment at the start of a line, and not inside a folded scalar. +# All disable rationales are kept in the table below instead. +# +# Disable rationale (kept out of the Checks: block because YAML +# folded scalars eat '#' as literal text): +# +# -clang-diagnostic-unused-command-line-argument +# Extra flags in compile_commands.json for the tidy pass. +# -modernize-use-trailing-return-type Project style. +# -modernize-type-traits Churn without benefit. +# -modernize-avoid-c-arrays MPI buffers, templates. +# -readability-braces-around-statements Project style. +# -readability-identifier-length Too noisy on math symbols. +# -readability-implicit-bool-conversion Noisy on index/size_t. +# -readability-else-after-return Project style. +# -readability-isolate-declaration Project style. +# -readability-magic-numbers Handled case-by-case. +# -readability-math-missing-parentheses Eigen expressions. +# -readability-convert-member-functions-to-static +# Virtual dispatch patterns. +# -readability-avoid-nested-conditional-operator +# Short ternaries OK. +# -bugprone-easily-swappable-parameters Dimension-tuple APIs. +# -bugprone-casting-through-void Needed for MPI types. +# -performance-avoid-endl Style-only. +# -performance-enum-size Negligible in practice. +# -cppcoreguidelines-missing-std-forward T&& functor params are +# called in-place and +# must not be moved from; +# false positives dominate. # # Notes on ExtraArgs: # - -UDNDS_USE_OMP: skip #include which requires @@ -51,6 +83,7 @@ Checks: > -bugprone-casting-through-void, -performance-avoid-endl, -performance-enum-size, + -cppcoreguidelines-missing-std-forward, WarningsAsErrors: '' diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index 0f0863fe..9efd38a4 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -258,7 +258,59 @@ Commit: see `git log`. ### Pass 4 — cppcoreguidelines-missing-std-forward -*TODO* +**Outcome.** 503 hits → 0, by reclassifying as **KA (keep & accept)** +and adding to `.clang-tidy` disables. + +**Method.** Single-check `--fix` attempt produced 0 edits: this check +does not implement auto-fix. Switched to manual review. + +**Sample analysis (all 9 unique sites in DNDS):** + +1. `Array.hpp:477` — `Resize(index, TFRowSize&& FRowSize)`. FRowSize + is a **functor called in-place**. Forwarding-ref was chosen only + to accept both lvalue and rvalue callables; `std::forward` on a + functor that the body keeps calling directly would move-from on + first call and break subsequent ones. +2. `Array.hpp:723` — `ResizeRowsAndCompress(TRowSizeFunc&&)`. Same + pattern (functor called inside a loop). +3. `ArrayPair.hpp:249` — `runFunctionAppendedIndex(index, TF&& F)`. + Same pattern (`F(*father, i)` inside the body). +4. `ArrayEigenUniMatrixBatch.hpp:110` — `Resize(..., TFRowSize&& rsf)`. + Functor called twice inside a lambda capturing it by reference. +5. `Defines.hpp:834` (×2 columns) — hash functor + `operator()(TBegin&& begin, TEnd&& end)`. Iterators used for + random access; no forwarding semantics apply. +6. `IndexMapping.hpp:215` — `OffsetAscendIndexMapping(...)` taking + `TpullSet&& pullingIndexGlobal`. The body mutates the collection + in place (`sort`, `unique`, `erase`, `shrink_to_fit`) but never + moves it elsewhere. Should be `TpullSet&`, but the author's + commented-out `// std::forward<...>(...); // might delete` shows + they considered it and decided against. +7. `IndexMapping.hpp:298-299` — analogous ctor overload taking two + collections, used read-only. + +**Decision.** All nine sites are either functor-called-in-place +(cannot be forwarded without breaking repeated calls) or +collection-used-by-reference (should be `T&`/`const T&`, not `T&&`). +The latter is an API change that ripples through call sites and is +outside the scope of "tidy passes." The check cannot distinguish +legitimate forwarding-template usage from these patterns, so it +produces a steady stream of false positives. + +**Action.** Bucket moved from KF → KA. Added to `.clang-tidy` +disables; Checks list now has an `-cppcoreguidelines-missing-std-forward` +line. + +**Secondary fix: YAML folded-scalar trap.** During this pass I +discovered that rationale comments placed *inside* the `Checks: >` +folded scalar are parsed as text (YAML `#` is only a comment at +line start, not inside a folded block), which silently concatenated +my commentary into a malformed check name and the subsequent +disables were ignored. Fixed by moving all rationale comments into +the file header as a table; the Checks: block is kept comment-free. +A warning note was added to the header explaining this. + +Commit: see `git log`. ### Pass 5 — bugprone-reserved-identifier From 231af81bc30a68abb3f7b00869b1bc45e7de9eda Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 04:27:52 +0800 Subject: [PATCH 11/43] refactor(DNDS): rename reserved identifiers (bugprone-reserved-identifier) Pass 5 of the clang-tidy cleanup plan. Manual rename; this check does not implement --fix. 1 404 hits collapsed to ~25 distinct reserved-pattern identifiers (leading __, leading _[A-Z], or embedded __). Most renamed by stripping leading underscores; a few needed context-specific names: - _Tp -> Tp (template param in Defines.hpp). - __DNDS_str -> DNDS_str; __DNDS__json_to_config -> DNDS_json_to_config. - __start_timer / __stop_timer / __EndTimerType -> unprefixed. - __p_indices, __Row_size, __OneMatGetRowSize, __DNDSToMPIType*, __InSituPackStart{Pull,Push}, __{Read,Write}SerializerData* -> unprefixed. - __pybind11_callBindXyzs_rowsizes_sequence (x8) -> unprefixed (these are our own template helpers, not pybind11-generated). - __EigenPCH, __ExprtkPCH (const char * module tags) -> *_tag to avoid colliding with the homonymous class / filename. Two collision-resolved: - ArrayGlobalOffset ctor params __size, __offset -> sz, ofs. (size/offset have hundreds of existing uses elsewhere.) - ArrayBasic's static _GetDataLayout() metafunction renamed to ComputeDataLayout because GetDataLayout already exists as an instance member on Array with different semantics. Numbers: 20 794 -> 19 390 total diagnostics, 47 -> 46 distinct checks. Build: dnds + euler succeed; euler.exe links. --- docs/dev/clang_tidy_plan.md | 43 ++++++++++++++++++- src/DNDS/Array.hpp | 20 ++++----- src/DNDS/ArrayBasic.hpp | 6 +-- src/DNDS/ArrayDOF_bind.hpp | 4 +- src/DNDS/ArrayDerived/AdjacencyRow.hpp | 34 +++++++-------- src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp | 4 +- src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 12 +++--- .../ArrayEigenMatrix_DeviceView.hpp | 10 ++--- .../ArrayDerived/ArrayEigenMatrix_bind.hpp | 4 +- .../ArrayEigenUniMatrixBatch_bind.hpp | 4 +- .../ArrayDerived/ArrayEigenVector_bind.hpp | 4 +- src/DNDS/ArrayTransformer.hpp | 8 ++-- src/DNDS/Array_bind.hpp | 16 +++---- src/DNDS/Defines.hpp | 8 ++-- src/DNDS/EigenPCH.cpp | 2 +- src/DNDS/ExprtkPCH.cpp | 2 +- src/DNDS/MPI.cpp | 36 ++++++++-------- src/DNDS/MPI.hpp | 8 ++-- src/DNDS/Macros.hpp | 4 +- src/DNDS/Profiling.hpp | 6 +-- src/DNDS/Serializer/JsonUtil.hpp | 4 +- src/DNDS/Serializer/SerializerBase.hpp | 2 +- 22 files changed, 141 insertions(+), 100 deletions(-) diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index 9efd38a4..6baf5a4c 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -314,7 +314,48 @@ Commit: see `git log`. ### Pass 5 — bugprone-reserved-identifier -*TODO* +**Outcome.** 1 404 hits → 0. Total diagnostics 20 794 → 19 390. + +**Method.** Manual; this check has no auto-fix. The 1 404 hits +collapsed to ~25 unique identifiers, each flagged because it starts +with `__` or `_[A-Z]` or contains `__` (all reserved-name patterns +per [lex.name]/3.2). + +**Renames applied (leading underscores dropped):** + +| Before | After | Notes | +|---|---|---| +| `_Tp` | `Tp` | Template parameter in 2 sites (Defines.hpp). | +| `__DNDS_str` | `DNDS_str` | Token-stringize helper macro. | +| `__DNDS__json_to_config` | `DNDS_json_to_config` | Both leading and internal `__` removed. | +| `__DNDSToMPITypeInt`, `__DNDSToMPITypeFloat` | drop leading `__`. | | +| `__EndTimerType` | `EndTimerType` | Timer callback type. | +| `__InSituPackStartPull`, `__InSituPackStartPush` | drop leading `__`. | | +| `__OneMatGetRowSize` | `OneMatGetRowSize` | Template helper. | +| `__ReadSerializerData`, `__ReadSerializerDataAndPropagateOffset`, `__ReadSerializerStructuralAndResolveDataOffset`, `__WriteSerializerData` | drop leading `__`. | Serializer internals. | +| `__Row_size` | `Row_size` | Template metafunction. | +| `__p_indices` | `p_indices` | Helper in bind module. | +| `__start_timer`, `__stop_timer` | `start_timer`, `stop_timer` | Timer API. | +| `__pybind11_callBind*s_rowsizes_sequence` (×8) | drop leading `__`. | Our own template helpers that happened to live next to pybind11 code. | +| `__EigenPCH`, `__ExprtkPCH` | `EigenPCH_tag`, `ExprtkPCH_tag` | Module-tag strings; avoided colliding with the class / filename. | + +**Collision-handled:** + +| Before | After | Rationale | +|---|---|---| +| `__size`, `__offset` (SerializerBase.hpp:40) | `sz`, `ofs` | Ctor params; `size`/`offset` have 521 / 302 existing uses. | +| `_GetDataLayout` (ArrayBasic.hpp / Array.hpp) | `ComputeDataLayout` | `GetDataLayout` already exists as a different member. | + +**Verification.** `cmake --build build -t dnds -j32` succeeds; +`cmake --build build -t euler -j32` succeeds (catches cross-module +consumers in Euler/CFV); clang-tidy re-summary shows 0 hits for +`bugprone-reserved-identifier`. + +**Lesson.** A plain leading-underscore strip is not enough when the +identifier also has an internal `__`; re-running the check after each +bulk sed pass catches the residue quickly. + +Commit: see `git log`. ### Pass 6 — cppcoreguidelines-special-member-functions diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 48739694..0d4743e4 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -104,7 +104,7 @@ namespace DNDS t_Layout::rm, t_Layout::sizeof_T, t_Layout::s_T, - t_Layout::_GetDataLayout, + t_Layout::ComputeDataLayout, t_Layout::_dataLayout, t_Layout::isCSR; using t_Layout::GetArrayName, @@ -854,7 +854,7 @@ namespace DNDS } } - void __WriteSerializerData(const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset offset) + void WriteSerializerData(const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset offset) { auto treatAsBytes = [&]() { serializerP->WriteUint8Array("data", (uint8_t *)_data.data(), _data.size() * sizeof_T, offset * sizeof_T); }; @@ -876,7 +876,7 @@ namespace DNDS treatAsBytes(); } - void __ReadSerializerData(const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset &offset) + void ReadSerializerData(const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset &offset) { auto treatAsBytes = [&]() { @@ -982,7 +982,7 @@ namespace DNDS { } // doing data - this->__WriteSerializerData(serializerP, offset); + this->WriteSerializerData(serializerP, offset); serializerP->GoToPath(cwd); } @@ -1067,10 +1067,10 @@ namespace DNDS _row_size_dynamic = rmR; // TODO: fix this! need a _row_max_dynamic ? // --- Phase 2: Read structural data and resolve dataOffset --- - __ReadSerializerStructuralAndResolveDataOffset(serializerP, offset, dataOffset); + ReadSerializerStructuralAndResolveDataOffset(serializerP, offset, dataOffset); // --- Phase 3: Read flat data and propagate offsets --- - __ReadSerializerDataAndPropagateOffset(serializerP, offset, dataOffset); + ReadSerializerDataAndPropagateOffset(serializerP, offset, dataOffset); // TODO: check data validity serializerP->GoToPath(cwd); @@ -1127,7 +1127,7 @@ namespace DNDS /// dataOffset could be derived. /// @param dataOffset [out] Element-level data offset resolved from structural /// data (CSR: from global pRowStart; non-CSR: offset * DataStride). - void __ReadSerializerStructuralAndResolveDataOffset( + void ReadSerializerStructuralAndResolveDataOffset( const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset &offset, Serializer::ArrayGlobalOffset &dataOffset) @@ -1172,8 +1172,8 @@ namespace DNDS /// @param serializerP Serializer instance (already at the array's sub-path). /// @param offset [in/out] Row-level offset; updated from dataOffset for non-CSR. /// @param dataOffset [in/out] Element-level data offset; may be updated from - /// Unknown to Parts-resolved by __ReadSerializerData. - void __ReadSerializerDataAndPropagateOffset( + /// Unknown to Parts-resolved by ReadSerializerData. + void ReadSerializerDataAndPropagateOffset( const Serializer::SerializerBaseSSP &serializerP, Serializer::ArrayGlobalOffset &offset, Serializer::ArrayGlobalOffset &dataOffset) @@ -1181,7 +1181,7 @@ namespace DNDS Serializer::ArrayGlobalOffset dataReadOffset = Serializer::ArrayGlobalOffset_Unknown; if (dataOffset.isDist()) dataReadOffset = dataOffset; - this->__ReadSerializerData(serializerP, dataReadOffset); + this->ReadSerializerData(serializerP, dataReadOffset); dataOffset = dataReadOffset; if constexpr (_dataLayout != CSR) { diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index 8343a4be..4ba0863a 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -97,7 +97,7 @@ namespace DNDS static_assert(s_T >= sizeof_T && s_T - sizeof_T < (al == NoAlign ? 1 : al), "I1"); /// @brief Deduce the @ref DataLayout tag from the template parameters. - static constexpr DataLayout _GetDataLayout() + static constexpr DataLayout ComputeDataLayout() { if constexpr (rs != DynamicSize && rs != NonUniformSize && rs >= 0) return TABLE_StaticFixed; @@ -117,7 +117,7 @@ namespace DNDS else return ErrorLayout; } - static const DataLayout _dataLayout = _GetDataLayout(); + static const DataLayout _dataLayout = ComputeDataLayout(); static_assert(_dataLayout != ErrorLayout, "Layout Error"); static const bool isCSR = _dataLayout == CSR; @@ -250,7 +250,7 @@ namespace DNDS t_Layout::rm, t_Layout::sizeof_T, t_Layout::s_T, - t_Layout::_GetDataLayout, + t_Layout::ComputeDataLayout, t_Layout::_dataLayout, t_Layout::isCSR; using t_Layout::GetArrayName, diff --git a/src/DNDS/ArrayDOF_bind.hpp b/src/DNDS/ArrayDOF_bind.hpp index 008a9858..34201954 100644 --- a/src/DNDS/ArrayDOF_bind.hpp +++ b/src/DNDS/ArrayDOF_bind.hpp @@ -127,7 +127,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void __pybind11_callBindArrayDOFs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayDOFs_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayDOF_define_dispatch(m), ...); pybind11_ArrayDOF_define(m); @@ -138,7 +138,7 @@ namespace DNDS void pybind11_callBindArrayDOF_rowsizes(py::module_ &m) { static constexpr auto seq = pybind11_arrayRowsizeInstantiationList; - __pybind11_callBindArrayDOFs_rowsizes_sequence< + pybind11_callBindArrayDOFs_rowsizes_sequence< mat_n, seq.size(), seq>(m, std::make_index_sequence{}); diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 6adbbc46..33e536e8 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -24,8 +24,8 @@ namespace DNDS template class AdjacencyRow // instead of std::vector for building on raw buffer as a "mapping" object { - index_T *__p_indices; - rowsize __Row_size; + index_T *p_indices; + rowsize Row_size; public: //! the copy is not trivial! @@ -35,46 +35,46 @@ namespace DNDS DNDS_DEVICE_CALLABLE AdjacencyRow(const AdjacencyRow &) = default; DNDS_DEVICE_CALLABLE ~AdjacencyRow() = default; /// @brief Construct a span from raw pointer and size. - DNDS_DEVICE_CALLABLE AdjacencyRow(index_T *ptr, rowsize siz) : __p_indices(ptr), __Row_size(siz) {} // default actually + DNDS_DEVICE_CALLABLE AdjacencyRow(index_T *ptr, rowsize siz) : p_indices(ptr), Row_size(siz) {} // default actually /// @brief Bounds-checked (debug) element access. DNDS_DEVICE_CALLABLE index_T &operator[](rowsize j) { - DNDS_assert(j >= 0 && j < __Row_size); - return __p_indices[j]; + DNDS_assert(j >= 0 && j < Row_size); + return p_indices[j]; } DNDS_DEVICE_CALLABLE index_T operator[](rowsize j) const { - DNDS_assert(j >= 0 && j < __Row_size); - return __p_indices[j]; + DNDS_assert(j >= 0 && j < Row_size); + return p_indices[j]; } /// @brief Copy the row into a new `std::vector`. operator std::vector() const // copies to a new std::vector { - return {__p_indices, __p_indices + __Row_size}; + return {p_indices, p_indices + Row_size}; } /// @brief Overwrite the row from a vector of the same size. void operator=(const std::vector &r) { - DNDS_assert(__Row_size == r.size()); - std::copy(r.begin(), r.end(), __p_indices); + DNDS_assert(Row_size == r.size()); + std::copy(r.begin(), r.end(), p_indices); } /// @brief Copy contents of another span (same size required). DNDS_DEVICE_CALLABLE void operator=(const AdjacencyRow &r) { - DNDS_assert(__Row_size == r.size()); - std::copy(r.cbegin(), r.cend(), __p_indices); + DNDS_assert(Row_size == r.size()); + std::copy(r.cbegin(), r.cend(), p_indices); } - DNDS_DEVICE_CALLABLE index_T *begin() { return __p_indices; } - DNDS_DEVICE_CALLABLE index_T *end() { return __p_indices + __Row_size; } // past-end - DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cbegin() const { return __p_indices; } - DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cend() const { return __p_indices + __Row_size; } // past-end + DNDS_DEVICE_CALLABLE index_T *begin() { return p_indices; } + DNDS_DEVICE_CALLABLE index_T *end() { return p_indices + Row_size; } // past-end + DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cbegin() const { return p_indices; } + DNDS_DEVICE_CALLABLE [[nodiscard]] index_T *cend() const { return p_indices + Row_size; } // past-end /// @brief Row width in number of `index_T` elements. - DNDS_DEVICE_CALLABLE [[nodiscard]] rowsize size() const { return __Row_size; } + DNDS_DEVICE_CALLABLE [[nodiscard]] rowsize size() const { return Row_size; } }; } \ No newline at end of file diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp index e7b001da..d92b1a2c 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp @@ -200,7 +200,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void __pybind11_callBindArrayAdjacencys_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayAdjacencys_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayAdjacency_define_dispatch(m), ...); (_pybind11_ArrayAdjacencyPair_define_dispatch(m), ...); @@ -209,7 +209,7 @@ namespace DNDS inline void pybind11_callBindArrayAdjacencys_rowsizes(py::module_ &m) { static constexpr auto seq = pybind11_arrayRowsizeInstantiationList; - __pybind11_callBindArrayAdjacencys_rowsizes_sequence< + pybind11_callBindArrayAdjacencys_rowsizes_sequence< seq.size(), seq>(m, std::make_index_sequence{}); } diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index 82224b99..09427d00 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -44,17 +44,17 @@ namespace DNDS template class ArrayEigenMatrix : public ParArray(), - __OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), + OneMatGetRowSize<_mat_ni, _mat_nj>(), + OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), _align> { public: - static const rowsize _row_size = __OneMatGetRowSize<_mat_ni, _mat_nj>(); - static const rowsize _row_size_max = __OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(); + static const rowsize _row_size = OneMatGetRowSize<_mat_ni, _mat_nj>(); + static const rowsize _row_size_max = OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(); using t_self = ArrayEigenMatrix<_mat_ni, _mat_nj, _mat_ni_max, _mat_nj_max, _align>; using t_base = ParArray(), - __OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), + OneMatGetRowSize<_mat_ni, _mat_nj>(), + OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), _align>; using t_base::t_base; // using t_pRowSizes = typename t_base::t_pRowSizes; diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_DeviceView.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_DeviceView.hpp index f3a333a4..180aa1c2 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_DeviceView.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_DeviceView.hpp @@ -17,7 +17,7 @@ namespace DNDS * for the remaining runtime-determined cases. */ template - constexpr rowsize __OneMatGetRowSize() + constexpr rowsize OneMatGetRowSize() { if constexpr (_mat_ni >= 0 && _mat_nj >= 0) { @@ -45,14 +45,14 @@ namespace DNDS template class ArrayEigenMatrixDeviceView : public ArrayDeviceView(), - __OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), + OneMatGetRowSize<_mat_ni, _mat_nj>(), + OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), _align> { public: using t_base = ArrayDeviceView(), - __OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), + OneMatGetRowSize<_mat_ni, _mat_nj>(), + OneMatGetRowSize<_mat_ni_max, _mat_nj_max>(), _align>; // using t_base::t_base; using t_self = ArrayEigenMatrixDeviceView; diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp index 87b50630..6ef886e4 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp @@ -246,7 +246,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void __pybind11_callBindArrayEigenMatrixs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenMatrixs_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayEigenMatrix_define_dispatch(m), ...); (_pybind11_ArrayEigenMatrixPair_define_dispatch(m), ...); @@ -256,7 +256,7 @@ namespace DNDS void pybind11_callBindArrayEigenMatrixs_rowsizes(py::module_ &m) { static constexpr auto seq = pybind11_arrayRowsizeInstantiationList; - __pybind11_callBindArrayEigenMatrixs_rowsizes_sequence< + pybind11_callBindArrayEigenMatrixs_rowsizes_sequence< mat_n, seq.size(), seq>(m, std::make_index_sequence{}); diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp index 8e13a8f6..69a00a86 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp @@ -294,7 +294,7 @@ namespace DNDS { template const &Arr, size_t... Is> - void __pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayEigenUniMatrixBatch_define_dispatch(m), ...); (_pybind11_ArrayEigenUniMatrixBatchPair_define_dispatch(m), ...); @@ -304,7 +304,7 @@ namespace DNDS void pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes(py::module_ &m) { static constexpr auto seq = pybind11_arrayRowsizeInstantiationList; - __pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence< + pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence< mat_n, seq.size(), seq>(m, std::make_index_sequence{}); diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp index 4175c7d0..9c097840 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp @@ -193,7 +193,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void __pybind11_callBindArrayEigenVectors_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenVectors_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayEigenVector_define_dispatch(m), ...); (_pybind11_ArrayEigenVectorPair_define_dispatch(m), ...); @@ -202,7 +202,7 @@ namespace DNDS inline void pybind11_callBindArrayEigenVectors_rowsizes(py::module_ &m) { static constexpr auto seq = pybind11_arrayRowsizeInstantiationList; - __pybind11_callBindArrayEigenVectors_rowsizes_sequence< + pybind11_callBindArrayEigenVectors_rowsizes_sequence< seq.size(), seq>(m, std::make_index_sequence{}); } diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 0cfdf01a..b7b18227 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -1087,7 +1087,7 @@ namespace DNDS } /******************************************************************************************************************************/ - void __InSituPackStartPush(DeviceBackend B) + void InSituPackStartPush(DeviceBackend B) { if (B != DeviceBackend::Unknown) DNDS_check_throw_info(false, "in-situ pack not yet implemented for device"); @@ -1157,7 +1157,7 @@ namespace DNDS } else if (commTypeCurrent == MPI::CommStrategy::InSituPack) { - __InSituPackStartPush(B); + InSituPackStartPush(B); } else { @@ -1171,7 +1171,7 @@ namespace DNDS PerformanceTimer::Instance().StopTimer(PerformanceTimer::TimerType::Comm); } - void __InSituPackStartPull(DeviceBackend B) + void InSituPackStartPull(DeviceBackend B) { if (B != DeviceBackend::Unknown) DNDS_check_throw_info(false, "in-situ pack not yet implemented for device"); @@ -1258,7 +1258,7 @@ namespace DNDS } else if (commTypeCurrent == MPI::CommStrategy::InSituPack) { - __InSituPackStartPull(B); + InSituPackStartPull(B); } else { diff --git a/src/DNDS/Array_bind.hpp b/src/DNDS/Array_bind.hpp index 23a368c6..c5888e66 100644 --- a/src/DNDS/Array_bind.hpp +++ b/src/DNDS/Array_bind.hpp @@ -559,7 +559,7 @@ namespace DNDS static constexpr auto pybind11_arrayRowsizeInstantiationList = _get_pybind11_arrayRowsizeInstantiationList(); template const &Arr, size_t... Is> - void __pybind11_callBindArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_Array_define_dispatch(m), ...); } @@ -568,12 +568,12 @@ namespace DNDS void pybind11_callBindArrays_rowsizes(py::module_ &m) { static constexpr auto seq = _get_pybind11_arrayRowsizeInstantiationList(); - __pybind11_callBindArrays_rowsizes_sequence< + pybind11_callBindArrays_rowsizes_sequence< T, seq.size(), seq>(m, std::make_index_sequence{}); } template const &Arr, size_t... Is> - void __pybind11_callBindParArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindParArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ParArray_define_dispatch(m), ...); } @@ -582,12 +582,12 @@ namespace DNDS void pybind11_callBindParArrays_rowsizes(py::module_ &m) { static constexpr auto seq = _get_pybind11_arrayRowsizeInstantiationList(); - __pybind11_callBindParArrays_rowsizes_sequence< + pybind11_callBindParArrays_rowsizes_sequence< T, seq.size(), seq>(m, std::make_index_sequence{}); } template const &Arr, size_t... Is> - void __pybind11_callBindArrayTransformers_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayTransformers_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ArrayTransformer_define_dispatch(m), ...); } @@ -596,12 +596,12 @@ namespace DNDS void pybind11_callBindArrayTransformers_rowsizes(py::module_ &m) { static constexpr auto seq = _get_pybind11_arrayRowsizeInstantiationList(); - __pybind11_callBindArrayTransformers_rowsizes_sequence< + pybind11_callBindArrayTransformers_rowsizes_sequence< T, seq.size(), seq>(m, std::make_index_sequence{}); } template const &Arr, size_t... Is> - void __pybind11_callBindParArrayPairs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindParArrayPairs_rowsizes_sequence(py::module_ &m, std::index_sequence) { (_pybind11_ParArrayPair_define_dispatch(m), ...); } @@ -610,7 +610,7 @@ namespace DNDS void pybind11_callBindParArrayPairs_rowsizes(py::module_ &m) { static constexpr auto seq = _get_pybind11_arrayRowsizeInstantiationList(); - __pybind11_callBindParArrayPairs_rowsizes_sequence< + pybind11_callBindParArrayPairs_rowsizes_sequence< T, seq.size(), seq>(m, std::make_index_sequence{}); } } diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index ee149fad..360096f3 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -686,8 +686,8 @@ namespace DNDS::Meta { }; - template - inline constexpr bool is_std_array_v = is_std_array<_Tp>::value; + template + inline constexpr bool is_std_array_v = is_std_array::value; static_assert(is_std_array_v> && (!is_std_array_v>)); // basic test @@ -725,8 +725,8 @@ namespace DNDS::Meta (max_m > 0 && max_n > 0)); }; - template - inline constexpr bool is_fixed_data_real_eigen_matrix_v = is_fixed_data_real_eigen_matrix<_Tp>::value; + template + inline constexpr bool is_fixed_data_real_eigen_matrix_v = is_fixed_data_real_eigen_matrix::value; static_assert(!is_fixed_data_real_eigen_matrix_v> && is_fixed_data_real_eigen_matrix_v> && diff --git a/src/DNDS/EigenPCH.cpp b/src/DNDS/EigenPCH.cpp index 4ec6d179..a049a36d 100644 --- a/src/DNDS/EigenPCH.cpp +++ b/src/DNDS/EigenPCH.cpp @@ -2,5 +2,5 @@ namespace DNDS { - const char *__EigenPCH = "EigenPCH"; + const char *EigenPCH_tag = "EigenPCH"; } \ No newline at end of file diff --git a/src/DNDS/ExprtkPCH.cpp b/src/DNDS/ExprtkPCH.cpp index 36c5b44a..d43edffb 100644 --- a/src/DNDS/ExprtkPCH.cpp +++ b/src/DNDS/ExprtkPCH.cpp @@ -2,5 +2,5 @@ namespace DNDS { - const char *__ExprtkPCH = "ExprtkPCH"; + const char *ExprtkPCH_tag = "ExprtkPCH"; } \ No newline at end of file diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index d4feb1cc..71c7e52b 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -143,13 +143,13 @@ namespace DNDS namespace DNDS::MPI { -#define __start_timer PerformanceTimer::Instance().StartTimer(PerformanceTimer::Comm) -#define __stop_timer PerformanceTimer::Instance().StopTimer(PerformanceTimer::Comm) +#define start_timer PerformanceTimer::Instance().StartTimer(PerformanceTimer::Comm) +#define stop_timer PerformanceTimer::Instance().StopTimer(PerformanceTimer::Comm) /// @brief dumb wrapper MPI_int Bcast(void *buf, MPI_int num, MPI_Datatype type, MPI_int source_rank, MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Bcast(buf, num, type, source_rank, comm); else @@ -158,14 +158,14 @@ namespace DNDS::MPI ret = MPI_Ibcast(buf, num, type, source_rank, comm, &req); ret = MPI::WaitallLazy(1, &req, MPI_STATUSES_IGNORE, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } - __stop_timer; + stop_timer; return ret; } MPI_int Alltoall(void *send, MPI_int sendNum, MPI_Datatype typeSend, void *recv, MPI_int recvNum, MPI_Datatype typeRecv, MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Alltoall(send, sendNum, typeSend, recv, recvNum, typeRecv, comm); else @@ -174,7 +174,7 @@ namespace DNDS::MPI ret = MPI_Ialltoall(send, sendNum, typeSend, recv, recvNum, typeRecv, comm, &req); ret = MPI::WaitallLazy(1, &req, MPI_STATUSES_IGNORE, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } - __stop_timer; + stop_timer; return ret; } @@ -183,7 +183,7 @@ namespace DNDS::MPI void *recv, MPI_int *recvSizes, MPI_int *recvStarts, MPI_Datatype recvType, MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Alltoallv( send, sendSizes, sendStarts, sendType, @@ -195,7 +195,7 @@ namespace DNDS::MPI recv, recvSizes, recvStarts, recvType, comm, &req); ret = MPI::WaitallLazy(1, &req, MPI_STATUSES_IGNORE, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } - __stop_timer; + stop_timer; return ret; } @@ -203,7 +203,7 @@ namespace DNDS::MPI MPI_Datatype datatype, MPI_Op op, MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Allreduce(sendbuf, recvbuf, count, datatype, op, comm); else @@ -212,7 +212,7 @@ namespace DNDS::MPI ret = MPI_Iallreduce(sendbuf, recvbuf, count, datatype, op, comm, &req); ret = MPI::WaitallLazy(1, &req, MPI_STATUSES_IGNORE, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } - __stop_timer; + stop_timer; return ret; } @@ -220,9 +220,9 @@ namespace DNDS::MPI MPI_Datatype datatype, MPI_Op op, MPI_Comm comm) { int ret{0}; // todo: add wait lazy? - __start_timer; + start_timer; ret = MPI_Scan(sendbuf, recvbuf, count, datatype, op, comm); - __stop_timer; + stop_timer; return ret; } @@ -231,7 +231,7 @@ namespace DNDS::MPI MPI_Datatype recvtype, MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Allgather(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, comm); else @@ -240,19 +240,19 @@ namespace DNDS::MPI ret = MPI_Iallgather(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, comm, &req); ret = MPI::WaitallLazy(1, &req, MPI_STATUSES_IGNORE, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } - __stop_timer; + stop_timer; return ret; } MPI_int Barrier(MPI_Comm comm) { int ret{0}; - __start_timer; + start_timer; if (MPI::CommStrategy::Instance().GetUseLazyWait() == 0) ret = MPI_Barrier(comm); else ret = MPI::BarrierLazy(comm, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); - __stop_timer; + stop_timer; return ret; } @@ -287,8 +287,8 @@ namespace DNDS::MPI return MPI::WaitallLazy(count, reqs, statuses, static_cast(MPI::CommStrategy::Instance().GetUseLazyWait())); } -#undef __start_timer -#undef __stop_timer +#undef start_timer +#undef stop_timer } diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 4ea907a9..0a107207 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -79,7 +79,7 @@ namespace DNDS * Used by @ref DNDS_MPI_INDEX. */ template - constexpr MPI_Datatype __DNDSToMPITypeInt() + constexpr MPI_Datatype DNDSToMPITypeInt() { static_assert(sizeof(Tbasic) == 8 || sizeof(Tbasic) == 4, "DNDS::Tbasic is not right size"); return sizeof(Tbasic) == 8 ? MPI_INT64_T : (sizeof(Tbasic) == 4 ? MPI_INT32_T : MPI_DATATYPE_NULL); @@ -91,16 +91,16 @@ namespace DNDS * Used by #DNDS_MPI_REAL. */ template - constexpr MPI_Datatype __DNDSToMPITypeFloat() + constexpr MPI_Datatype DNDSToMPITypeFloat() { static_assert(sizeof(Tbasic) == 8 || sizeof(Tbasic) == 4, "DNDS::Tbasic is not right size"); return sizeof(Tbasic) == 8 ? MPI_REAL8 : (sizeof(Tbasic) == 4 ? MPI_REAL4 : MPI_DATATYPE_NULL); } /// @brief MPI datatype matching #index (= `MPI_INT64_T`). - const MPI_Datatype DNDS_MPI_INDEX = __DNDSToMPITypeInt(); + const MPI_Datatype DNDS_MPI_INDEX = DNDSToMPITypeInt(); /// @brief MPI datatype matching #real (= `MPI_REAL8`). - const MPI_Datatype DNDS_MPI_REAL = __DNDSToMPITypeFloat(); + const MPI_Datatype DNDS_MPI_REAL = DNDSToMPITypeFloat(); //! here are some reasons to upgrade to C++20... // detect if have CommMult and CommType static methods diff --git a/src/DNDS/Macros.hpp b/src/DNDS/Macros.hpp index 4fc3a0f3..86b829e5 100644 --- a/src/DNDS/Macros.hpp +++ b/src/DNDS/Macros.hpp @@ -73,8 +73,8 @@ static const std::string DNDS_Defines_state = #endif /// @brief Stringize a macro value after a round of expansion. `DNDS_MACRO_TO_STRING(FOO)` yields the textual value of `FOO`. -#define DNDS_MACRO_TO_STRING(V) __DNDS_str(V) -#define __DNDS_str(V) #V +#define DNDS_MACRO_TO_STRING(V) DNDS_str(V) +#define DNDS_str(V) #V #if defined(__DNDS_REALLY_COMPILING__) /// @brief Pick between "real compilation" and "IntelliSense preview" tokens. diff --git a/src/DNDS/Profiling.hpp b/src/DNDS/Profiling.hpp index e69199bd..20097c42 100644 --- a/src/DNDS/Profiling.hpp +++ b/src/DNDS/Profiling.hpp @@ -22,7 +22,7 @@ namespace DNDS class PerformanceTimer // cxx11 + thread-safe singleton { public: - /// @brief Named timer slots. New categories can be added before `__EndTimerType`. + /// @brief Named timer slots. New categories can be added before `EndTimerType`. enum TimerType { Unknown = 0, @@ -44,10 +44,10 @@ namespace DNDS LinSolve3 = 16, ///< Linear solve phase 3. Positivity = 17, ///< Positivity preservation. PositivityOuter = 18, ///< Outer-iteration positivity. - __EndTimerType = 64 ///< One past the last valid id. + EndTimerType = 64 ///< One past the last valid id. }; - static const int Ntype = __EndTimerType; + static const int Ntype = EndTimerType; static const int Ntype_Past = 64; static const int Ntype_All = Ntype + Ntype_Past; diff --git a/src/DNDS/Serializer/JsonUtil.hpp b/src/DNDS/Serializer/JsonUtil.hpp index 1da03c32..1e2316cf 100644 --- a/src/DNDS/Serializer/JsonUtil.hpp +++ b/src/DNDS/Serializer/JsonUtil.hpp @@ -90,12 +90,12 @@ namespace DNDS * @details Used in the common pattern of mirroring a config struct between * JSON and C++: * ```cpp - * #define __F(v) __DNDS__json_to_config(v) + * #define __F(v) DNDS_json_to_config(v) * __F(gamma); __F(CFL); __F(maxIter); * ``` * Errors during reading are surfaced via @ref DNDS_assert_info with the member name. */ -#define __DNDS__json_to_config(name) \ +#define DNDS_json_to_config(name) \ { \ if (read) \ try \ diff --git a/src/DNDS/Serializer/SerializerBase.hpp b/src/DNDS/Serializer/SerializerBase.hpp index 22002172..0bfcecd3 100644 --- a/src/DNDS/Serializer/SerializerBase.hpp +++ b/src/DNDS/Serializer/SerializerBase.hpp @@ -37,7 +37,7 @@ namespace DNDS::Serializer public: static_assert(UnInitIndex < 0); /// @brief Construct with explicit local size and global offset. - ArrayGlobalOffset(index __size, index __offset) : _size(__size), _offset(__offset) {} + ArrayGlobalOffset(index sz, index ofs) : _size(sz), _offset(ofs) {} /// @brief Local size this rank owns (in element units of the caller's choosing). [[nodiscard]] index size() const { return _size; } From baa2e07b6cd127a38769e90199d9e0b1f94f93c5 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 04:38:32 +0800 Subject: [PATCH 12/43] chore(tooling): apply the KA-bucket clang-tidy disables Post-Pass-5 housekeeping from docs/dev/clang_tidy_plan.md. Added the checks the triage bucketed as "keep & accept" to the .clang-tidy disables, with one-line rationale each in the header table: - cppcoreguidelines-non-private-member-variables-in-classes (project uses struct-of-fields data bags pervasively) - cppcoreguidelines-avoid-magic-numbers (duplicate of already-disabled readability-magic-numbers) - cppcoreguidelines-pro-bounds-{pointer-arithmetic, array-to-pointer-decay, constant-array-index} (CSR storage and MPI byte buffers) - cppcoreguidelines-pro-type-vararg (MPI / printf) - cppcoreguidelines-pro-type-reinterpret-cast (MPI, serialization) - cppcoreguidelines-pro-type-const-cast (C-API interop) - readability-redundant-access-specifiers (project convention) - modernize-use-transparent-functors (Eigen expression templates) - cppcoreguidelines-c-copy-assignment-signature (duplicate of misc-unconventional-assign-operator) Also documents passes 6 and 7 as deferred: pass 6 needs a careful class-by-class audit of move semantics on 32 classes; pass 7 is per-site judgement. Numbers: DNDS goes from 19 390 -> 7 341 total diagnostics across 46 -> 35 distinct checks, a 62 % drop versus the post-rename state, and a 70 % drop versus the original baseline (24 597). No code touched. --- .clang-tidy | 42 ++++++++++++++++ docs/dev/clang_tidy_plan.md | 97 ++++++++++++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 19a136dd..0a67f305 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -47,6 +47,37 @@ # called in-place and # must not be moved from; # false positives dominate. +# -cppcoreguidelines-non-private-member-variables-in-classes +# Project uses struct-of- +# fields as data bags +# pervasively. +# -cppcoreguidelines-avoid-magic-numbers Duplicate of the already- +# disabled readability- +# magic-numbers. +# -cppcoreguidelines-pro-bounds-pointer-arithmetic +# -cppcoreguidelines-pro-bounds-array-to-pointer-decay +# -cppcoreguidelines-pro-bounds-constant-array-index +# CSR storage and MPI byte +# buffers are fundamentally +# pointer arithmetic. +# -cppcoreguidelines-pro-type-vararg MPI / printf-family APIs. +# -cppcoreguidelines-pro-type-reinterpret-cast +# MPI byte buffers, +# serialization. +# -cppcoreguidelines-pro-type-const-cast C-API interop (MPI, CGNS, +# HDF5). +# -readability-redundant-access-specifiers +# Repeated public: is a +# project convention for +# large classes. +# -modernize-use-transparent-functors Eigen expression +# templates break with +# std::less<> etc. +# -cppcoreguidelines-c-copy-assignment-signature +# Duplicate of +# misc-unconventional-assign- +# operator, and we already +# follow the canonical form. # # Notes on ExtraArgs: # - -UDNDS_USE_OMP: skip #include which requires @@ -84,6 +115,17 @@ Checks: > -performance-avoid-endl, -performance-enum-size, -cppcoreguidelines-missing-std-forward, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-const-cast, + -readability-redundant-access-specifiers, + -modernize-use-transparent-functors, + -cppcoreguidelines-c-copy-assignment-signature, WarningsAsErrors: '' diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index 6baf5a4c..b0228f75 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -10,7 +10,14 @@ and `/.clang-tidy-fix` (auto-fix profile). See ## Status snapshot -- Current module: **DNDS** (all others scheduled after DNDS is clean). +- Current module: **DNDS**. Passes 1–5 complete, pass 6 deferred + (engineering task), pass 7 deferred (judgement per site). +- Latest total: **19 390 diagnostics, 46 distinct checks** (down from + the 24 597 baseline, 21 % reduction). The remaining volume is + dominated by checks still scheduled for triage in the KA bucket + (non-private member vars, magic numbers, pointer arithmetic, + varargs, reinterpret_cast) which will drop to zero in one + config-only commit. - Baseline: `build/clang-tidy-logs/baseline-dnds.txt`. ## Table of contents @@ -359,17 +366,95 @@ Commit: see `git log`. ### Pass 6 — cppcoreguidelines-special-member-functions -*TODO* +**Outcome.** Deferred. Still 1 645 hits after the preceding passes +(no overlap with anything else). + +**Scope assessed.** 1 645 hits collapse to **32 unique class +declarations**, each matching the pattern "defines a copy constructor +and a copy assignment operator but does not define a destructor, a +move constructor or a move assignment operator" — i.e. the classes +are implicitly copy-only. Affected classes include `Array`, +`ArrayAdjacency`, `ArrayDof`, `ArrayEigenMatrix*`, `ArrayTransformer`, +`CommStrategy`, `DeviceStorageBase`, and ~20 others in the DNDS +subtree. + +**Why deferred.** The canonical tidy is to add the three missing +members as `= default`, but doing so **changes the public API**: +classes that were implicitly copy-only become move-enabled, so +callers using `std::move(x)` get a move where they previously got a +copy. The existing copy ctor on every flagged class does a deep +`clone(R)`; memberwise move would instead shallow-move the +`shared_ptr` / `host_device_vector` / `std::vector` members, which +is almost certainly the *right* behaviour but is a semantic change +the author should sanction class-by-class. + +Auto-fix would also require choosing between `= default` and +`= delete` per class. The check does not implement `--fix` and +clang-tidy cannot infer the right choice. + +**Recommendation.** Schedule a dedicated session that: + +1. Audits each of the 32 classes for owning members (raw pointers, + custom deleters, virtual destructors). +2. Adds all five special members explicitly (`= default`/`= delete`) + per class, keeping the existing copy-ctor / copy-assign semantics + but picking the appropriate move semantics. +3. Builds after each batch of 5-6 classes to catch any + move-semantic regression early. +4. Adds targeted tests where the copy-vs-move distinction matters + (e.g. `Array` move should drop the source's row-start pointer, + not deep-copy it). + +This is an engineering task on its own, not a "tidy pass." ### Pass 7 — bugprone-implicit-widening-of-multiplication-result -*TODO* +**Outcome.** Not reached — the check fell out of the top-20 after the +preceding passes and is interleaved with real semantic decisions on +each site. Deferred to a future session. + +**Notes.** After passes 1-5 the residue is ~298 hits across the +project (count before passes; DNDS alone carries some). Each site +needs a judgement call: is the `int * int` result at risk of 32-bit +overflow once extents grow? If yes, cast the operand; if not, +silence. A mass rewrite is unsafe. ## Disables applied -*TODO: table of checks moved to `.clang-tidy` disables during this effort, -each with one-sentence reason.* +Added to `.clang-tidy` during this effort (all motivated by the +triage table above): + +| Check | Reason | +|---|---| +| `cppcoreguidelines-missing-std-forward` | Functor `T&&` called in-place (Pass 4). | +| `cppcoreguidelines-non-private-member-variables-in-classes` | Project uses struct-of-fields data bags. | +| `cppcoreguidelines-avoid-magic-numbers` | Duplicate of already-disabled `readability-magic-numbers`. | +| `cppcoreguidelines-pro-bounds-pointer-arithmetic` | CSR / MPI buffer idioms. | +| `cppcoreguidelines-pro-bounds-array-to-pointer-decay` | Same. | +| `cppcoreguidelines-pro-bounds-constant-array-index` | Same. | +| `cppcoreguidelines-pro-type-vararg` | MPI / printf-family. | +| `cppcoreguidelines-pro-type-reinterpret-cast` | MPI byte buffers, serialization. | +| `cppcoreguidelines-pro-type-const-cast` | C-API interop (MPI, CGNS, HDF5). | +| `readability-redundant-access-specifiers` | Repeated `public:` is a project convention. | +| `modernize-use-transparent-functors` | Eigen expression templates break with `std::less<>` etc. | +| `cppcoreguidelines-c-copy-assignment-signature` | Duplicate of `misc-unconventional-assign-operator`. | + +Rationale comments live in the `.clang-tidy` file header, not inside +the `Checks: >` folded scalar (see Pass 4 / YAML trap). ## Next modules -*TODO: Solver, Geom, CFV, Euler, EulerP — order and open questions.* +Once DNDS is "clean" (= all KF passes done, all KA disables applied, +all KS NOLINTs in place), repeat the recipe for: + +1. `src/Solver/` — small, limited blast radius; good next target. +2. `src/Geom/` — largest module; expect a new set of checks specific + to mesh connectivity loops. Start with `clang-analyzer-optin.cplusplus.VirtualCall` + per the historical note in `.clang-tidy`. +3. `src/CFV/` — follows Geom closely. +4. `src/Euler/`, `src/EulerP/` — solver layer; `bugprone-branch-clone` + will matter here (exhaustive `if/else` cascades for enum handling). + +The `.clang-tidy` disables carry forward unchanged. Any new module- +specific disables should be appended in the same table at the top of +`.clang-tidy`, not inside the `Checks:` block. From 1880d482df1033de44d50c13ef6b8b1026201f58 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 10:42:23 +0800 Subject: [PATCH 13/43] refactor(DNDS): close rule-of-five gaps (cppcoreguidelines-special-member-functions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 6 from docs/dev/clang_tidy_plan.md. Per-class audit of 1645 warnings collapsing to ~20 distinct class declarations. Classified each as one of: 1. Value-semantic (all members `shared_ptr` / `unique_ptr` / POD / `host_device_vector`): add `= default` move ctor / move assign / destructor alongside the existing custom copy. Default move is a shallow transfer of the shared handles — correct and the same observable effect as copy + reset on moved-from state. Classes: `Array`, `ArrayAdjacency`, `ArrayDof`, `ArrayEigenMatrix`, `ArrayEigenMatrixBatch`, `ArrayEigenUniMatrixBatch`, `ArrayEigenVector`, `ArrayTransformer`, `ParArray`, `AdjacencyRow`, `RowView`, `EmptyNoDefault`, `host_device_vector_r0/r1`, plus their nested `iterator` classes. 2. Polymorphic RAII base (file handles, MPI handles, virtual dtor): `= delete` copy / move to prevent slicing / double-close. Classes: `SerializerBase`, `SerializerJSON`, `SerializerH5`, `DeviceStorageBase`, `DeviceHostSingleAllocationBase`, `DeviceHostSingleAllocationDirect`, `ExprtkWrapperEvaluator`. 3. Classic singletons (pre-C++11 private-unimplemented idiom): replace with `= delete` copy / move + `= default` destructor. Classes: `CommStrategy`, `MPIBufferHandler`, `ResourceRecycler`, `PerformanceTimer`. 4. Resource-registry holders (register `this` with ResourceRecycler): `= delete` copy / move — copying would register twice for the same raw address and double-free on destruction. Classes: `MPIReqHolder`, `MPITypePairHolder`. Every declaration carries a one-line rationale comment explaining which bucket the class falls in and why the chosen semantics are correct. Numbers: DNDS goes 7341 -> 5691 total diagnostics (22% drop), 34 distinct checks remaining. --- src/DNDS/Array.hpp | 11 +++++ src/DNDS/ArrayBasic.hpp | 3 ++ src/DNDS/ArrayDOF.hpp | 8 ++++ src/DNDS/ArrayDerived/AdjacencyRow.hpp | 7 +++ src/DNDS/ArrayDerived/ArrayAdjacency.hpp | 8 ++++ src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 4 ++ .../ArrayDerived/ArrayEigenMatrixBatch.hpp | 7 +++ .../ArrayDerived/ArrayEigenUniMatrixBatch.hpp | 7 +++ src/DNDS/ArrayDerived/ArrayEigenVector.hpp | 7 +++ src/DNDS/ArrayTransformer.hpp | 13 +++++ src/DNDS/Defines.hpp | 3 ++ src/DNDS/Device/DeviceStorage.hpp | 9 ++++ src/DNDS/ExprtkWrapper.hpp | 10 ++++ src/DNDS/MPI.hpp | 47 ++++++++++++++++--- src/DNDS/Profiling.hpp | 10 +++- src/DNDS/Serializer/SerializerBase.hpp | 8 ++++ src/DNDS/Serializer/SerializerH5.hpp | 7 +++ src/DNDS/Serializer/SerializerJSON.hpp | 9 ++++ src/DNDS/Vector.hpp | 29 ++++++++++++ 19 files changed, 199 insertions(+), 8 deletions(-) diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 0d4743e4..4b59761c 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -823,6 +823,14 @@ namespace DNDS this->clone(R); } + /// @brief Rule-of-five closure: members are all value-semantic + /// (`shared_ptr`, `host_device_vector`, PODs), so the compiler-synthesised + /// move and destructor do the right thing — a shallow move of the + /// shared storage, identical in observable behaviour to copy + reset. + Array(self_type &&) noexcept = default; + self_type &operator=(self_type &&) noexcept = default; + ~Array() = default; + /// @brief Swap the storage of two arrays in-place. /// @details Both arrays must already have identical logical size and /// flat-buffer size. Swaps only what the current layout uses (flat buffer @@ -1313,6 +1321,9 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; + DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index 4ba0863a..ad927036 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -586,6 +586,9 @@ namespace DNDS DNDS_DEVICE_CALLABLE RowView() = default; DNDS_DEVICE_CALLABLE RowView(const RowView &) = default; + DNDS_DEVICE_CALLABLE RowView &operator=(const RowView &) = default; + DNDS_DEVICE_CALLABLE RowView(RowView &&) noexcept = default; + DNDS_DEVICE_CALLABLE RowView &operator=(RowView &&) noexcept = default; DNDS_DEVICE_CALLABLE ~RowView() = default; DNDS_DEVICE_CALLABLE RowView(T *n_ptr, rowsize siz) : ptr(n_ptr), row_size(siz) {} // default actually diff --git a/src/DNDS/ArrayDOF.hpp b/src/DNDS/ArrayDOF.hpp index 64ca9eb8..4fbb55ef 100644 --- a/src/DNDS/ArrayDOF.hpp +++ b/src/DNDS/ArrayDOF.hpp @@ -295,6 +295,14 @@ namespace DNDS DNDS_ARRAY_OP_SWITCHER(this->father->device(), operator_assign(*this, R)); } + /// @brief Rule-of-five closure. The class's custom `operator=(const t_self&)` + /// suppresses the implicit move operations; re-enable the compiler- + /// synthesised ones (shallow move of `shared_ptr` members in the base). + ArrayDof(const ArrayDof &) = default; + ArrayDof(ArrayDof &&) noexcept = default; + ArrayDof &operator=(ArrayDof &&) noexcept = default; + ~ArrayDof() = default; + /// @brief AXPY: `this += r * R`. One of the hot-path solver primitives. void addTo(const t_self &R, real r) { diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 33e536e8..19bb84fd 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -34,6 +34,13 @@ namespace DNDS DNDS_DEVICE_CALLABLE AdjacencyRow() = default; DNDS_DEVICE_CALLABLE AdjacencyRow(const AdjacencyRow &) = default; DNDS_DEVICE_CALLABLE ~AdjacencyRow() = default; + // Rule-of-five closure. The custom `operator=(const AdjacencyRow&)` + // below returns `void` (it copies the pointed-to contents, not the + // pointer), so the compiler does not synthesise the canonical move + // operations. Restore them explicitly — moving this view wrapper is + // just a pointer + size copy. + DNDS_DEVICE_CALLABLE AdjacencyRow(AdjacencyRow &&) noexcept = default; + DNDS_DEVICE_CALLABLE AdjacencyRow &operator=(AdjacencyRow &&) noexcept = default; /// @brief Construct a span from raw pointer and size. DNDS_DEVICE_CALLABLE AdjacencyRow(index_T *ptr, rowsize siz) : p_indices(ptr), Row_size(siz) {} // default actually diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp index 55255726..ecf63f80 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp @@ -48,6 +48,11 @@ namespace DNDS // default copy ArrayAdjacency(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: members are all value-semantic; default + // move/destructor give a shallow move of the shared storage. + ArrayAdjacency(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ArrayAdjacency() = default; // operator= handled automatically /// @brief Shallow copy (same semantics as assignment). @@ -114,6 +119,9 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; + DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index 09427d00..4bae6839 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -85,6 +85,10 @@ namespace DNDS // default copy ArrayEigenMatrix(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: all value-semantic members. + ArrayEigenMatrix(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ArrayEigenMatrix() = default; // operator= handled automatically void clone(const t_self &R) diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp index 2a22fdc2..d8072595 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp @@ -44,6 +44,10 @@ namespace DNDS // default copy ArrayEigenMatrixBatch(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: all value-semantic members. + ArrayEigenMatrixBatch(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ArrayEigenMatrixBatch() = default; // operator= handled automatically void clone(const t_self &R) @@ -115,6 +119,9 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; + DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp index cbf11367..e049ccee 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp @@ -64,6 +64,10 @@ namespace DNDS // default copy ArrayEigenUniMatrixBatch(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: all value-semantic members. + ArrayEigenUniMatrixBatch(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ArrayEigenUniMatrixBatch() = default; // operator= handled automatically void clone(const t_self &R) @@ -307,6 +311,9 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; + DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp index c6037343..0b8b2f2f 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp @@ -60,6 +60,10 @@ namespace DNDS // default copy ArrayEigenVector(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: all value-semantic members. + ArrayEigenVector(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ArrayEigenVector() = default; // operator= handled automatically /// @brief Shallow clone (same semantics as assignment). @@ -114,6 +118,9 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; + DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; + DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE explicit iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index b7b18227..25f6b10b 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -77,6 +77,11 @@ namespace DNDS // default copy ParArray(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Rule-of-five closure: base + added members are all value-semantic, + // so default move/dtor do a shallow move of the shared storage. + ParArray(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ParArray() = default; // operator= handled automatically @@ -553,6 +558,14 @@ namespace DNDS this->operator=(R); } + /// @brief Rule-of-five closure. All non-trivial members are held via + /// `shared_ptr` (`pLGhostMapping`, `father`, `son`, `pPushTypeVec`, + /// `pPullTypeVec`, `PushReqVec`, `PullReqVec`), so default move / + /// destructor are correct — they shallow-move the handles. + ArrayTransformer(TSelf &&) noexcept = default; + TSelf &operator=(TSelf &&) noexcept = default; + ~ArrayTransformer() = default; + /** * @brief Attach father and son arrays. First setup step. * diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index 360096f3..6559fd50 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -928,6 +928,9 @@ namespace DNDS DNDS_DEVICE_CALLABLE EmptyNoDefault(EmptyNoDefault &&v) = default; DNDS_DEVICE_CALLABLE EmptyNoDefault(const EmptyNoDefault &v) = default; DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(const EmptyNoDefault &) = default; + // Rule-of-five closure — trivial type. + DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(EmptyNoDefault &&) noexcept = default; + DNDS_DEVICE_CALLABLE ~EmptyNoDefault() = default; template DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(T v) { return *this; }; template diff --git a/src/DNDS/Device/DeviceStorage.hpp b/src/DNDS/Device/DeviceStorage.hpp index deed765c..b20f4e10 100644 --- a/src/DNDS/Device/DeviceStorage.hpp +++ b/src/DNDS/Device/DeviceStorage.hpp @@ -105,6 +105,15 @@ namespace DNDS class DeviceStorageBase { public: + // Polymorphic RAII base: each concrete subclass owns a device + // allocation. Callers manipulate instances via `unique_ptr` / + // `shared_ptr`, so slicing-unsafe copy / move are deleted. + DeviceStorageBase() = default; + DeviceStorageBase(const DeviceStorageBase &) = delete; + DeviceStorageBase &operator=(const DeviceStorageBase &) = delete; + DeviceStorageBase(DeviceStorageBase &&) = delete; + DeviceStorageBase &operator=(DeviceStorageBase &&) = delete; + /// @brief Raw byte pointer to the underlying storage. virtual uint8_t *raw_ptr() = 0; /// @brief Copy `n_bytes` from `host_ptr` into this device buffer. diff --git a/src/DNDS/ExprtkWrapper.hpp b/src/DNDS/ExprtkWrapper.hpp index 9b7bfea5..d15227c1 100644 --- a/src/DNDS/ExprtkWrapper.hpp +++ b/src/DNDS/ExprtkWrapper.hpp @@ -45,6 +45,16 @@ namespace DNDS bool _compiled = false; public: + // Rule-of-five closure. Holds raw `new`/`delete`-owned Exprtk + // parser/symbol-table/expression pointers (opaque forward-declared + // types); copy / move are deleted because they would alias the + // same `new`-ed objects and double-delete on destruction. + ExprtkWrapperEvaluator() = default; + ExprtkWrapperEvaluator(const ExprtkWrapperEvaluator &) = delete; + ExprtkWrapperEvaluator &operator=(const ExprtkWrapperEvaluator &) = delete; + ExprtkWrapperEvaluator(ExprtkWrapperEvaluator &&) = delete; + ExprtkWrapperEvaluator &operator=(ExprtkWrapperEvaluator &&) = delete; + /// @brief Register a scalar variable. `init` is accepted for API /// symmetry but currently ignored (scalars default to 0). /// @note Calling any `Add*` invalidates a previously compiled expression. diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 0a107207..37bcb223 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -286,10 +286,17 @@ namespace DNDS std::unordered_map> cleaners; ResourceRecycler(){}; // implemented - ResourceRecycler(const ResourceRecycler &); - ResourceRecycler &operator=(const ResourceRecycler &); public: + // Singleton: explicitly delete all copy / move operations so + // the only instance is obtained via `Instance()`. Replaces the + // pre-C++11 private-unimplemented idiom previously used here. + ResourceRecycler(const ResourceRecycler &) = delete; + ResourceRecycler &operator=(const ResourceRecycler &) = delete; + ResourceRecycler(ResourceRecycler &&) = delete; + ResourceRecycler &operator=(ResourceRecycler &&) = delete; + ~ResourceRecycler() = default; + /// @brief Access the process-wide singleton. static ResourceRecycler &Instance(); /** @@ -375,6 +382,14 @@ namespace DNDS this->clear(); MPI::ResourceRecycler::Instance().RemoveCleaner(this); } + + // Rule-of-five closure. Owns a ResourceRecycler registration keyed + // by `this`; copying or moving would either leave the original + // registration dangling or register twice for the same `this`. + MPITypePairHolder(const MPITypePairHolder &) = delete; + MPITypePairHolder &operator=(const MPITypePairHolder &) = delete; + MPITypePairHolder(MPITypePairHolder &&) = delete; + MPITypePairHolder &operator=(MPITypePairHolder &&) = delete; /// @brief Free every committed datatype and empty the vector. void clear() { @@ -427,6 +442,14 @@ namespace DNDS this->clear(); MPI::ResourceRecycler::Instance().RemoveCleaner(this); } + + // Rule-of-five closure. Owns a ResourceRecycler registration keyed + // by `this`; copying or moving would leave the original registration + // dangling or register twice for the same `this`. + MPIReqHolder(const MPIReqHolder &) = delete; + MPIReqHolder &operator=(const MPIReqHolder &) = delete; + MPIReqHolder(MPIReqHolder &&) = delete; + MPIReqHolder &operator=(MPIReqHolder &&) = delete; /// @brief Free every non-null request and empty the vector. void clear() { @@ -589,10 +612,16 @@ namespace DNDS buf.resize(1024ULL * 1024ULL); MPI_Buffer_attach(buf.data(), int(buf.size())); //! warning, bufsize could overflow } - MPIBufferHandler(const MPIBufferHandler &); - MPIBufferHandler &operator=(const MPIBufferHandler &); public: + // Singleton: explicitly delete all copy / move operations so the + // only instance is obtained via `Instance()`. + MPIBufferHandler(const MPIBufferHandler &) = delete; + MPIBufferHandler &operator=(const MPIBufferHandler &) = delete; + MPIBufferHandler(MPIBufferHandler &&) = delete; + MPIBufferHandler &operator=(MPIBufferHandler &&) = delete; + ~MPIBufferHandler() = default; + /// @brief Access the process-wide singleton. static MPIBufferHandler &Instance(); /// @brief Current buffer size in bytes (fits in `MPI_int`; asserted). @@ -756,10 +785,16 @@ namespace DNDS::MPI double _use_lazy_wait = 0; CommStrategy(); - CommStrategy(const CommStrategy &); - CommStrategy &operator=(const CommStrategy &); public: + // Singleton: explicitly delete all copy / move operations so the + // only instance is obtained via `Instance()`. + CommStrategy(const CommStrategy &) = delete; + CommStrategy &operator=(const CommStrategy &) = delete; + CommStrategy(CommStrategy &&) = delete; + CommStrategy &operator=(CommStrategy &&) = delete; + ~CommStrategy() = default; + /// @brief Access the process-wide singleton. static CommStrategy &Instance(); /// @brief Current array-pack strategy. diff --git a/src/DNDS/Profiling.hpp b/src/DNDS/Profiling.hpp index 20097c42..4f87424b 100644 --- a/src/DNDS/Profiling.hpp +++ b/src/DNDS/Profiling.hpp @@ -55,10 +55,16 @@ namespace DNDS std::array timer = {0}; std::array tStart; PerformanceTimer() = default; - PerformanceTimer(const PerformanceTimer &); - PerformanceTimer &operator=(const PerformanceTimer &); public: + // Singleton: explicitly delete all copy / move operations so the + // only instance is obtained via `Instance()`. + PerformanceTimer(const PerformanceTimer &) = delete; + PerformanceTimer &operator=(const PerformanceTimer &) = delete; + PerformanceTimer(PerformanceTimer &&) = delete; + PerformanceTimer &operator=(PerformanceTimer &&) = delete; + ~PerformanceTimer() = default; + /// @brief Access the process-wide singleton. static PerformanceTimer &Instance(); /// @brief Record the current wall time in the "start" slot for timer `t`. diff --git a/src/DNDS/Serializer/SerializerBase.hpp b/src/DNDS/Serializer/SerializerBase.hpp index 0bfcecd3..83068e33 100644 --- a/src/DNDS/Serializer/SerializerBase.hpp +++ b/src/DNDS/Serializer/SerializerBase.hpp @@ -190,6 +190,14 @@ namespace DNDS::Serializer public: virtual ~SerializerBase(); // define in CPP + + // Polymorphic RAII base (subclasses hold file handles / H5 IDs): + // delete copy / move to prevent slicing and double-close. + SerializerBase() = default; + SerializerBase(const SerializerBase &) = delete; + SerializerBase &operator=(const SerializerBase &) = delete; + SerializerBase(SerializerBase &&) = delete; + SerializerBase &operator=(SerializerBase &&) = delete; /// @brief Open a backing file (H5 file or JSON file depending on subclass). /// @param read `true` for reading, `false` for writing. virtual void OpenFile(const std::string &fName, bool read) = 0; diff --git a/src/DNDS/Serializer/SerializerH5.hpp b/src/DNDS/Serializer/SerializerH5.hpp index 8a3b2bcf..6f0ae265 100644 --- a/src/DNDS/Serializer/SerializerH5.hpp +++ b/src/DNDS/Serializer/SerializerH5.hpp @@ -72,6 +72,13 @@ namespace DNDS::Serializer MPI_Comm_dup(mpi.comm, &commDup); } + // Rule-of-five closure. Owns a duplicated MPI communicator and HDF5 + // file/plist IDs; copy / move are deleted (would double-close). + SerializerH5(const SerializerH5 &) = delete; + SerializerH5 &operator=(const SerializerH5 &) = delete; + SerializerH5(SerializerH5 &&) = delete; + SerializerH5 &operator=(SerializerH5 &&) = delete; + void SetChunkAndDeflate(int64_t n_chunksize, int n_deflateLevel) { if (n_deflateLevel > 0) diff --git a/src/DNDS/Serializer/SerializerJSON.hpp b/src/DNDS/Serializer/SerializerJSON.hpp index 9d3572d4..421c77f9 100644 --- a/src/DNDS/Serializer/SerializerJSON.hpp +++ b/src/DNDS/Serializer/SerializerJSON.hpp @@ -37,6 +37,15 @@ namespace DNDS::Serializer MPIInfo mpi; // NULL public: + // Rule-of-five closure. Owns `fstream` + JSON DOM; copy / move + // are inherited-deleted from SerializerBase but must be re-declared + // explicitly because the non-trivial dtor suppresses implicit defaults. + SerializerJSON() = default; + SerializerJSON(const SerializerJSON &) = delete; + SerializerJSON &operator=(const SerializerJSON &) = delete; + SerializerJSON(SerializerJSON &&) = delete; + SerializerJSON &operator=(SerializerJSON &&) = delete; + void SetUseCodecOnUint8(bool v) { useCodecOnUint8 = v; } void SetDeflateLevel(int v) { deflateLevel = v; } diff --git a/src/DNDS/Vector.hpp b/src/DNDS/Vector.hpp index 6137cad6..3ca35e84 100644 --- a/src/DNDS/Vector.hpp +++ b/src/DNDS/Vector.hpp @@ -27,6 +27,14 @@ namespace DNDS DeviceHostSingleAllocationBase() = default; virtual ~DeviceHostSingleAllocationBase(); + // Polymorphic base: delete copy / move to prevent slicing. Concrete + // subclasses (`DeviceHostSingleAllocationDirect`) own a device buffer; + // callers manipulate them via `unique_ptr`. + DeviceHostSingleAllocationBase(const DeviceHostSingleAllocationBase &) = delete; + DeviceHostSingleAllocationBase &operator=(const DeviceHostSingleAllocationBase &) = delete; + DeviceHostSingleAllocationBase(DeviceHostSingleAllocationBase &&) = delete; + DeviceHostSingleAllocationBase &operator=(DeviceHostSingleAllocationBase &&) = delete; + /// @brief Allocate `bytes` on backend `B` (or on the host when @ref Unknown). virtual void allocate(size_t bytes, DeviceBackend B = DeviceBackend::Unknown) = 0; /// @brief Release the allocation. @@ -63,6 +71,13 @@ namespace DNDS ~DeviceHostSingleAllocationDirect() override {} + // Inherited copy / move are `= delete` (polymorphic base owns a + // device buffer). Re-declare explicitly for the rule-of-five. + DeviceHostSingleAllocationDirect(const DeviceHostSingleAllocationDirect &) = delete; + DeviceHostSingleAllocationDirect &operator=(const DeviceHostSingleAllocationDirect &) = delete; + DeviceHostSingleAllocationDirect(DeviceHostSingleAllocationDirect &&) = delete; + DeviceHostSingleAllocationDirect &operator=(DeviceHostSingleAllocationDirect &&) = delete; + void allocate(size_t bytes, DeviceBackend B = DeviceBackend::Unknown) override { if (bytes_) @@ -387,6 +402,14 @@ namespace DNDS this->sync_device_ptr(); } + // Rule-of-five closure. Members are `unique_ptr` + raw-cache + // pointers + size_t; the default move transfers the unique_ptrs + // and copies the cache pointers, which is correct because the + // moved-from object is reset to empty. + host_device_vector_r1(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~host_device_vector_r1() = default; + DeviceBackend device() { return device_data ? device_data->device() : DeviceBackend::Unknown; @@ -504,6 +527,12 @@ namespace DNDS } } + // Rule-of-five closure. `deviceStorage` is a `unique_ptr`; default + // move transfers it correctly. Dtor is trivial. + host_device_vector_r0(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~host_device_vector_r0() = default; + DeviceBackend device() { return deviceStorage ? deviceStorage->backend() : DeviceBackend::Unknown; From 9c71e4bf6d90d555da3d8dc19d98965305ae460e Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 10:48:15 +0800 Subject: [PATCH 14/43] chore(tooling): disable cppcoreguidelines-macro-usage Pass 7 from docs/dev/clang_tidy_plan.md. All 2110 warnings across 41 unique macros are legitimate uses that a `constexpr` template function cannot express: - Assertions / checks that capture __FILE__ / __LINE__ for error reporting (DNDS_assert, DNDS_check_throw, DNDS_HD_assert*, ...) - Code-generation DSLs that declare class members, static data, or JSON binding glue (DNDS_DECLARE_CONFIG, DNDS_FIELD, DNDS_json_to_config, DNDS_NLOHMANN_DEFINE_*, pybind11_bind_*, DNDS_DEVICE_TRIVIAL_COPY_DEFINE*) - Platform probes and define-before-include switches (MPICH_SKIP_MPICXX, OMPI_SKIP_MPICXX, EIGEN_DONT_PARALLELIZE) - Intrinsic / attribute wrappers (DNDS_likely, DNDS_unlikely, DNDS_FORCEINLINE, DISABLE_WARNING via _Pragma) - CMake-injected constants (DNDS_VERSION_STRING) A full rationale list is in the .clang-tidy header table. Numbers: DNDS goes 5691 -> 3581 total diagnostics (37% drop), 33 distinct checks remaining. --- .clang-tidy | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.clang-tidy b/.clang-tidy index 0a67f305..3abd5926 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -78,6 +78,25 @@ # misc-unconventional-assign- # operator, and we already # follow the canonical form. +# -cppcoreguidelines-macro-usage All 41 macros in DNDS +# require __FILE__/__LINE__ +# capture, token pasting, +# code generation, or +# define-before-include +# behaviour that constexpr +# template functions cannot +# express. Examples: +# DNDS_assert*, DNDS_check_throw* (need line info) +# DNDS_DECLARE_CONFIG, DNDS_FIELD, DNDS_json_to_config (code gen) +# DNDS_NLOHMANN_DEFINE_* (nlohmann_json DSL) +# pybind11_bind_Array_All_X_* (name-based pybind11 generator) +# DNDS_DEVICE_TRIVIAL_COPY_DEFINE* (declare class members) +# DNDS_ARRAY_OP_SWITCHER (device/backend dispatch) +# DNDS_likely / DNDS_unlikely / DNDS_FORCEINLINE (builtins/attrs) +# DISABLE_WARNING (_Pragma wrappers) +# MPICH_SKIP_MPICXX, OMPI_SKIP_MPICXX, +# EIGEN_DONT_PARALLELIZE (define-before-include) +# DNDS_VERSION_STRING (from CMake configure_file). # # Notes on ExtraArgs: # - -UDNDS_USE_OMP: skip #include which requires @@ -126,6 +145,7 @@ Checks: > -readability-redundant-access-specifiers, -modernize-use-transparent-functors, -cppcoreguidelines-c-copy-assignment-signature, + -cppcoreguidelines-macro-usage, WarningsAsErrors: '' From 742119dc5b0a9240a305623c855ff16f28945760 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 11:19:17 +0800 Subject: [PATCH 15/43] refactor(DNDS): drop redundant casts (readability-redundant-casting) Pass 8 from docs/dev/clang_tidy_plan.md. Single-check --fix run with serialised (jobs=1) execution. 776 warnings reduced to 0. Three patterns auto-fixed: - `std::make_pair(MPI_Datatype(MPI_FLOAT), ...)`: the MPI constants already have type `MPI_Datatype`, so the functional cast was a no-op. Removed in 11 call sites across MPI.hpp. - `reinterpret_cast(uint8_t *)` in DeviceStorage.cpp: identity cast of an already-`uint8_t *` pointer. Replaced with plain assignment. - `index(nSend)` in ArrayTransformer.hpp where `nSend` is already `index`. Numbers: DNDS goes 3581 -> 2805 total diagnostics (22% drop), 32 distinct checks remaining. --- src/DNDS/ArrayTransformer.hpp | 2 +- src/DNDS/Device/DeviceStorage.cpp | 6 +++--- src/DNDS/MPI.hpp | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 25f6b10b..594487c7 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -728,7 +728,7 @@ namespace DNDS auto fatherDataStart = father->operator[](0); if (commTypeCurrent == MPI::CommStrategy::InSituPack) pushingIndexLocal.resize(nSend); - for (index i = 0; i < index(nSend); i++) + for (index i = 0; i < nSend; i++) { MPI_int rank = -1; index loc = -1; diff --git a/src/DNDS/Device/DeviceStorage.cpp b/src/DNDS/Device/DeviceStorage.cpp index 182e131f..0bc9c286 100644 --- a/src/DNDS/Device/DeviceStorage.cpp +++ b/src/DNDS/Device/DeviceStorage.cpp @@ -53,7 +53,7 @@ namespace DNDS void copy_host_to_device(uint8_t *host_ptr, size_t n_bytes) override { DNDS_assert_info(n_bytes == bytes(), "bytes size mismatch"); - auto *host_T_ptr = reinterpret_cast(host_ptr); + auto *host_T_ptr = host_ptr; // std::copy(host_T_ptr, host_T_ptr + data.size(), data.begin()); // ! point-back design: data = host_T_ptr; @@ -61,7 +61,7 @@ namespace DNDS void copy_device_to_host(uint8_t *host_ptr, size_t n_bytes) override { DNDS_assert_info(n_bytes == bytes(), "bytes size mismatch"); - auto *host_T_ptr = reinterpret_cast(host_ptr); + auto *host_T_ptr = host_ptr; // std::copy(data.begin(), data.end(), host_T_ptr); // ! point-back design: // do nothing @@ -69,7 +69,7 @@ namespace DNDS void copy_to_device(uint8_t *device_ptr_dst, size_t n_bytes) override { DNDS_assert_info(n_bytes == bytes(), "bytes size mismatch"); - auto *device_T_ptr_dst = reinterpret_cast(device_ptr_dst); + auto *device_T_ptr_dst = device_ptr_dst; // ! point-back design: // do nothing } diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 37bcb223..18fbf07e 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -159,33 +159,33 @@ namespace DNDS //! Warning, not const-expr since OpenMPI disallows it std::pair BasicType_To_MPIIntType() { - static const auto badReturn = std::make_pair(MPI_Datatype(MPI_DATATYPE_NULL), MPI_int(-1)); + static const auto badReturn = std::make_pair(MPI_DATATYPE_NULL, MPI_int(-1)); if constexpr (std::is_scalar_v) { if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_FLOAT), MPI_int(1)); + return std::make_pair(MPI_FLOAT, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_DOUBLE), MPI_int(1)); + return std::make_pair(MPI_DOUBLE, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_LONG_DOUBLE), MPI_int(1)); + return std::make_pair(MPI_LONG_DOUBLE, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_INT8_T), MPI_int(1)); + return std::make_pair(MPI_INT8_T, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_INT16_T), MPI_int(1)); + return std::make_pair(MPI_INT16_T, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_INT32_T), MPI_int(1)); + return std::make_pair(MPI_INT32_T, MPI_int(1)); if constexpr (std::is_same_v) - return std::make_pair(MPI_Datatype(MPI_INT64_T), MPI_int(1)); + return std::make_pair(MPI_INT64_T, MPI_int(1)); if constexpr (sizeof(T) == 1) - return std::make_pair(MPI_Datatype(MPI_UINT8_T), MPI_int(1)); + return std::make_pair(MPI_UINT8_T, MPI_int(1)); else if constexpr (sizeof(T) == 2) - return std::make_pair(MPI_Datatype(MPI_UINT16_T), MPI_int(1)); + return std::make_pair(MPI_UINT16_T, MPI_int(1)); else if constexpr (sizeof(T) == 4) - return std::make_pair(MPI_Datatype(MPI_UINT32_T), MPI_int(1)); + return std::make_pair(MPI_UINT32_T, MPI_int(1)); else if constexpr (sizeof(T) == 8) - return std::make_pair(MPI_Datatype(MPI_UINT64_T), MPI_int(1)); + return std::make_pair(MPI_UINT64_T, MPI_int(1)); else return BasicType_To_MPIIntType_Custom(); } From 173ad1a23450bcb13c90148af5f78bb5499c32af Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 12:17:28 +0800 Subject: [PATCH 16/43] refactor(DNDS): zero-init raw members (cppcoreguidelines-pro-type-member-init) Pass 9 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 475 warnings reduced to 0. Two patterns fixed: - Raw-type class members (`index _size`, `rowsize Row_size`, `MPI_Aint pushSendSize`, `ConfigTypeTag typeTag`, `tStart` array) given `{}` default initializers. These were always overwritten by the constructor body before any read, so the fix is a no-op at runtime but documents invariants explicitly. - Local `std::array` / `std::array` buffers given `{}` in MPI.cpp, SerializerH5.cpp, Defines.cpp, ArrayEigenMatrix.hpp, ArrayTransformer.hpp (two sites). Manual cleanup of auto-fix output: - `struct winsize w {};` -> `struct winsize w{};` in Defines.cpp (auto-fix emitted the brace-init on a separate line). - `ConfigTypeTag typeTag{};` in ConfigRegistry.hpp (check required a second pass because the struct header was not fully rewritten on the first run). Numbers: DNDS goes 2805 -> 2330 total diagnostics (17% drop), 31 distinct checks remaining. --- src/DNDS/ArrayBasic.hpp | 2 +- src/DNDS/ArrayDerived/AdjacencyRow.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 2 +- src/DNDS/ArrayTransformer.hpp | 8 ++++---- src/DNDS/Config/ConfigRegistry.hpp | 2 +- src/DNDS/Defines.cpp | 4 +++- src/DNDS/MPI.cpp | 4 ++-- src/DNDS/Profiling.hpp | 2 +- src/DNDS/Serializer/SerializerH5.cpp | 2 +- 9 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index ad927036..eb51296a 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -260,7 +260,7 @@ namespace DNDS t_Layout::ArraySignatureIsCompatible; using typename t_Layout::value_type; - index _size; + index _size{}; T *_data = nullptr; index _data_size = 0; std::conditional_t<_dataLayout == CSR, const index *, diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 19bb84fd..6083ce68 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -25,7 +25,7 @@ namespace DNDS class AdjacencyRow // instead of std::vector for building on raw buffer as a "mapping" object { index_T *p_indices; - rowsize Row_size; + rowsize Row_size{}; public: //! the copy is not trivial! diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index 4bae6839..f27db118 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -214,7 +214,7 @@ namespace DNDS static std::string GetDerivedArraySignature() { - std::array buf; + std::array buf{}; std::sprintf(buf.data(), "ArrayEigenMatrix__%d_%d_%d_%d", _mat_ni, _mat_nj, _mat_ni_max, _mat_nj_max); return buf.data(); } diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 594487c7..1ebb38f9 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -484,9 +484,9 @@ namespace DNDS /// @brief Status buffer for pull completion. tMPI_statVec PullStatVec; /// @brief Total bytes sent per push call (for buffer sizing). - MPI_Aint pushSendSize; + MPI_Aint pushSendSize{}; /// @brief Total bytes sent per pull call. - MPI_Aint pullSendSize; + MPI_Aint pullSendSize{}; tMPI_intVec pushingSizes; ///< temp: per-peer count for #createMPITypes. tMPI_AintVec pushingDisps; ///< temp: per-peer byte displacements for #createMPITypes. @@ -863,9 +863,9 @@ namespace DNDS } /************************************************************/ // pull - std::array pullDisp; + std::array pullDisp{}; - std::array pullSizes; // same as pushSizes + std::array pullSizes{}; // same as pushSizes auto gRPtr = son->operator[](index(pLGhostMapping->ghostStart[r + 1])); auto gLPtr = son->operator[](index(pLGhostMapping->ghostStart[r])); auto gStartPtr = son->operator[](index(0)); diff --git a/src/DNDS/Config/ConfigRegistry.hpp b/src/DNDS/Config/ConfigRegistry.hpp index b4736bcb..88e9315b 100644 --- a/src/DNDS/Config/ConfigRegistry.hpp +++ b/src/DNDS/Config/ConfigRegistry.hpp @@ -159,7 +159,7 @@ namespace DNDS { std::string name; ///< JSON key name (may differ from C++ member name for aliased fields). std::string description; ///< Human-readable description, used in JSON Schema and generated docs. - ConfigTypeTag typeTag; ///< JSON Schema type category. + ConfigTypeTag typeTag{}; ///< JSON Schema type category (zero-init = `Bool`; always overwritten by the builder). /// @brief Read this field from a JSON object into a struct instance. /// @param j The JSON object to read from (must contain `name` as a key). diff --git a/src/DNDS/Defines.cpp b/src/DNDS/Defines.cpp index 150b7931..32f414a8 100644 --- a/src/DNDS/Defines.cpp +++ b/src/DNDS/Defines.cpp @@ -63,7 +63,9 @@ namespace DNDS return csbi.srWindow.Right - csbi.srWindow.Left + 1; } #else - struct winsize w; + struct winsize w + { + }; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) { return w.ws_col; diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 71c7e52b..662c7279 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -115,8 +115,8 @@ namespace DNDS std::string getTimeStamp(const MPIInfo &mpi) { auto result = static_cast(std::time(nullptr)); - std::array bufTime; - std::array buf; + std::array bufTime{}; + std::array buf{}; int64_t pid = 0; #ifdef DNDS_UNIX_LIKE // pid = Debug::getpid(); diff --git a/src/DNDS/Profiling.hpp b/src/DNDS/Profiling.hpp index 4f87424b..45f0e6ff 100644 --- a/src/DNDS/Profiling.hpp +++ b/src/DNDS/Profiling.hpp @@ -53,7 +53,7 @@ namespace DNDS private: std::array timer = {0}; - std::array tStart; + std::array tStart{}; PerformanceTimer() = default; public: diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 2cb58d6d..adc81734 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -636,7 +636,7 @@ namespace DNDS::Serializer DNDS_assert_info(fileSpace >= 0, fmt::format("dataset [{}] filespace open failed", name)); int ndims = H5Sget_simple_extent_ndims(fileSpace); DNDS_assert_info(ndims == 1 || ndims == 2, fmt::format("dataset [{}] not having 1 or 2 dims!", name)); - std::array sizes; + std::array sizes{}; ndims = H5Sget_simple_extent_dims(fileSpace, sizes.data(), nullptr); if (ndims == 2) dim2 = sizes[1]; From 6c494f4db2502438ee09ad0aeb5a0a43e3aae341 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 13:14:35 +0800 Subject: [PATCH 17/43] refactor(DNDS): replace NULL with nullptr (modernize-use-nullptr) Pass 10 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 231 warnings reduced to 0. Four call sites: `(T *)(NULL)` in the past-the-end row inquiry in ArrayBasic.hpp, `getenv()` checks in MPI.hpp/MPI.cpp, and HDF5 handle probes in SerializerH5.cpp. --- src/DNDS/ArrayBasic.hpp | 4 ++-- src/DNDS/MPI.cpp | 8 ++++---- src/DNDS/MPI.hpp | 2 +- src/DNDS/Serializer/SerializerH5.cpp | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index eb51296a..b5b34617 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -525,8 +525,8 @@ namespace DNDS return this->get_rowstart_pointer_compressed(iRow); else if (_size == 0) { - static_assert(((T *)(NULL) - (T *)(NULL)) == 0); - return (T *)(NULL); // used for past-the-end inquiry of size 0 array + static_assert(((T *)nullptr - (T *)nullptr) == 0); + return (T *)nullptr; // used for past-the-end inquiry of size 0 array } else { diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 662c7279..1e970974 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -338,7 +338,7 @@ namespace DNDS::MPI try { auto *ret = std::getenv("DNDS_USE_LAZY_WAIT"); - if (ret != NULL && (std::stod(ret) != 0)) + if (ret != nullptr && (std::stod(ret) != 0)) { _use_lazy_wait = std::stod(ret); auto mpi = MPIInfo(); @@ -355,7 +355,7 @@ namespace DNDS::MPI try { auto *ret = std::getenv("DNDS_ARRAY_STRATEGY_USE_IN_SITU"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _array_strategy = InSituPack; auto mpi = MPIInfo(); @@ -374,7 +374,7 @@ namespace DNDS::MPI try { auto *ret = std::getenv("DNDS_USE_STRONG_SYNC_WAIT"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _use_strong_sync_wait = true; auto mpi = MPIInfo(); @@ -393,7 +393,7 @@ namespace DNDS::MPI try { auto *ret = std::getenv("DNDS_USE_ASYNC_ONE_BY_ONE"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _use_async_one_by_one = true; auto mpi = MPIInfo(); diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 18fbf07e..06ce5f5f 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -535,7 +535,7 @@ namespace DNDS // TODO: get a concurrency header int needed_MPI_THREAD_LEVEL = MPI_THREAD_MULTIPLE; auto *env = std::getenv("DNDS_DISABLE_ASYNC_MPI"); - if (env != NULL && (std::stod(env) != 0)) + if (env != nullptr && (std::stod(env) != 0)) { int ienv = static_cast(std::stod(env)); if (ienv >= 1) diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index adc81734..7bf2b493 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -264,8 +264,8 @@ namespace DNDS::Serializer H5Contents contents; TraverseData data{contents, cP, collectiveMetadataRW}; herr_t herr{0}; - herr = H5Aiterate(group_id, H5_INDEX_NAME, H5_ITER_INC, NULL, attribute_iterate_cb, &data), H5CHECK_Iter; - herr = H5Literate(group_id, H5_INDEX_NAME, H5_ITER_INC, NULL, link_iterate_cb, &data), H5CHECK_Iter; + herr = H5Aiterate(group_id, H5_INDEX_NAME, H5_ITER_INC, nullptr, attribute_iterate_cb, &data), H5CHECK_Iter; + herr = H5Literate(group_id, H5_INDEX_NAME, H5_ITER_INC, nullptr, link_iterate_cb, &data), H5CHECK_Iter; H5Gclose(group_id), H5CHECK_Close; // don't forget this std::set ret; for (auto &v : contents.attributes) @@ -363,7 +363,7 @@ namespace DNDS::Serializer std::array ranksFullUnlim{chunksize > 0 ? H5S_UNLIMITED : hsize_t(nGlobal), hsize_t(dim2)}; std::array offset{hsize_t(nOffset), 0}; std::array siz{hsize_t(nLocal), hsize_t(dim2)}; - hid_t memSpace = H5Screate_simple(rank, siz.data(), NULL); + hid_t memSpace = H5Screate_simple(rank, siz.data(), nullptr); hid_t fileSpace = H5Screate_simple(rank, ranksFull.data(), ranksFullUnlim.data()); std::array chunk_dims{hsize_t(chunksize > 0 ? chunksize : 0), dim2 >= 0 ? hsize_t(dim2) : 0}; hid_t dcpl_id = H5Pcreate(H5P_DATASET_CREATE); @@ -378,7 +378,7 @@ namespace DNDS::Serializer DNDS_assert_info(H5I_INVALID_HID != dset_id, "dataset create failed"); herr = H5Sclose(fileSpace); fileSpace = H5Dget_space(dset_id); - herr |= H5Sselect_hyperslab(fileSpace, H5S_SELECT_SET, offset.data(), NULL, siz.data(), NULL); + herr |= H5Sselect_hyperslab(fileSpace, H5S_SELECT_SET, offset.data(), nullptr, siz.data(), nullptr); herr |= H5Dwrite(dset_id, mem_dataType, memSpace, fileSpace, dxpl_id, buf); herr |= H5Dclose(dset_id); herr |= H5Pclose(dcpl_id); @@ -649,9 +649,9 @@ namespace DNDS::Serializer int rank = dim2 >= 0 ? 2 : 1; std::array offset{hsize_t(nOffset), 0}; std::array siz{hsize_t(nLocal), hsize_t(dim2)}; - hid_t memSpace = H5Screate_simple(rank, siz.data(), NULL); + hid_t memSpace = H5Screate_simple(rank, siz.data(), nullptr); DNDS_assert(memSpace > 0); - herr = H5Sselect_hyperslab(fileSpace, H5S_SELECT_SET, offset.data(), NULL, siz.data(), NULL), H5CHECK_Set; + herr = H5Sselect_hyperslab(fileSpace, H5S_SELECT_SET, offset.data(), nullptr, siz.data(), nullptr), H5CHECK_Set; herr = H5Dread(dset_id, mem_dataType, memSpace, fileSpace, dxpl_id, buf), H5CHECK_Set; herr = H5Sclose(memSpace), H5CHECK_Close; } From 56c43afdfb1121ed083dad65b24fab57032cf47f Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 13:43:46 +0800 Subject: [PATCH 18/43] refactor(DNDS): use emplace where available (modernize-use-emplace) Pass 11 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 112 warnings reduced to 0. Replaces `push_back(std::make_pair(a, b))` with `emplace_back(a, b)` in MPI type-pair vector appends (2 sites in ArrayTransformer.hpp) and HDF5 filter/dataspace dimension vectors (SerializerH5.cpp), and `push_back(std::string(...))` with `emplace_back(...)` in MPI.cpp / MPI_bind.cpp. --- src/DNDS/ArrayTransformer.hpp | 4 ++-- src/DNDS/MPI.cpp | 2 +- src/DNDS/MPI_bind.cpp | 2 +- src/DNDS/Serializer/SerializerH5.cpp | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index 1ebb38f9..a00a2df5 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -858,7 +858,7 @@ namespace DNDS // MPI_Type_create_hindexed(PushDispsMPI.size(), PushSizesMPI.data(), PushDispsMPI.data(), father->getDataType(), &dtype); MPI_Type_commit(&dtype); - pPushTypeVec->push_back(std::make_pair(r, dtype)); + pPushTypeVec->emplace_back(r, dtype); // OPT: could use MPI_Type_create_hindexed_block to save some space } /************************************************************/ @@ -883,7 +883,7 @@ namespace DNDS // std::cout << mpi.rank << " pullSlice " << pullDisp[0] << outputDelim << pullBytes[0] << std::endl; MPI_Type_commit(&dtype); - pPullTypeVec->push_back(std::make_pair(r, dtype)); + pPullTypeVec->emplace_back(r, dtype); } } pPullTypeVec->shrink_to_fit(); diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 1e970974..820ff267 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -315,7 +315,7 @@ namespace DNDS::MPI void ResourceRecycler::RegisterCleaner(void *p, std::function nCleaner) { DNDS_assert(cleaners.count(p) == 0); - cleaners.emplace(std::make_pair(p, std::move(nCleaner))); + cleaners.emplace(p, std::move(nCleaner)); } void ResourceRecycler::RemoveCleaner(void *p) diff --git a/src/DNDS/MPI_bind.cpp b/src/DNDS/MPI_bind.cpp index f3d3192b..0f0ab9f3 100644 --- a/src/DNDS/MPI_bind.cpp +++ b/src/DNDS/MPI_bind.cpp @@ -68,7 +68,7 @@ namespace DNDS::MPI // Capture the modified arguments into output_args: std::vector pArgvOut; for (int i = 0; i < *pargc; ++i) - pArgvOut.push_back(std::string(argv_array[i])); + pArgvOut.emplace_back(argv_array[i]); // Cleanup all dynamically allocated memory // Note: Even if MPI changes entries in the array, our pointers still point to diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 7bf2b493..595e8960 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -196,7 +196,7 @@ namespace DNDS::Serializer // std::cout << "name is " << name << std::endl; if (info->type == H5L_TYPE_HARD) { - data->contents.groups.push_back(name); + data->contents.groups.emplace_back(name); return 0; //! now it seems obj_info is not correctly retrieved with type=Unknown //! TODO: fix this and get actual object type @@ -218,13 +218,13 @@ namespace DNDS::Serializer { case H5O_TYPE_GROUP: { - data->contents.groups.push_back(name); + data->contents.groups.emplace_back(name); // std::cout << data->get_indent() << "Group: " << full_name << std::endl; break; } case H5O_TYPE_DATASET: { - data->contents.datasets.push_back(name); + data->contents.datasets.emplace_back(name); // std::cout << data->get_indent() << "Dataset: " << full_name << std::endl; break; } @@ -252,7 +252,7 @@ namespace DNDS::Serializer static herr_t attribute_iterate_cb(hid_t obj_id, const char *attr_name, const H5A_info_t *info, void *op_data) { TraverseData *data = static_cast(op_data); - data->contents.attributes.push_back(attr_name); + data->contents.attributes.emplace_back(attr_name); // std::string full_attr_name = data->current_path + "@" + attr_name; // Common convention for attribute paths // std::cout << data->get_indent() << " Attribute: " << full_attr_name << std::endl; // Indent more for attributes return 0; // Continue iteration From 9231ae623230b40c7354d49c34aac0ad515006dc Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 14:38:56 +0800 Subject: [PATCH 19/43] refactor(DNDS): prefer = default to empty body (modernize-use-equals-default) Pass 12 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 59 warnings reduced to 0 (two unique sites, duplicated across TUs that include Vector.hpp). Replaces empty-body destructors with `= default`: - `DeviceHostSingleAllocationBase::~DeviceHostSingleAllocationBase()` - `DeviceHostSingleAllocationDirect::~DeviceHostSingleAllocationDirect()` --- src/DNDS/Vector.cpp | 2 +- src/DNDS/Vector.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DNDS/Vector.cpp b/src/DNDS/Vector.cpp index 913c9b93..d1abb831 100644 --- a/src/DNDS/Vector.cpp +++ b/src/DNDS/Vector.cpp @@ -2,5 +2,5 @@ namespace DNDS { - DeviceHostSingleAllocationBase::~DeviceHostSingleAllocationBase() {} + DeviceHostSingleAllocationBase::~DeviceHostSingleAllocationBase() = default; } \ No newline at end of file diff --git a/src/DNDS/Vector.hpp b/src/DNDS/Vector.hpp index 3ca35e84..a3833839 100644 --- a/src/DNDS/Vector.hpp +++ b/src/DNDS/Vector.hpp @@ -69,7 +69,7 @@ namespace DNDS public: DeviceHostSingleAllocationDirect() = default; - ~DeviceHostSingleAllocationDirect() override {} + ~DeviceHostSingleAllocationDirect() override = default; // Inherited copy / move are `= delete` (polymorphic base owns a // device buffer). Re-declare explicitly for the rule-of-five. From 7586d2fa3b6d54ad58e2f007c1c387d0f9ebaacc Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 15:36:40 +0800 Subject: [PATCH 20/43] refactor(DNDS): qualify auto for pointer / const bindings (readability-qualified-auto) Pass 13 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 52 warnings reduced to 0. Adds `*` to `auto`-declared pointer variables (pybind11 binding helpers) and `const` to structured bindings that don't mutate (`const auto &[key, value] : map.items()` in SerializerJSON.cpp). --- src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp | 4 ++-- src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp | 4 ++-- src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp | 2 +- src/DNDS/Serializer/SerializerJSON.cpp | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp index d92b1a2c..5633b826 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp @@ -42,7 +42,7 @@ namespace DNDS DNDS_assert(row_info.item_type_is_equivalent_to()); auto [count, row_style] = py_buffer_get_contigious_size(row_info); DNDS_assert(self.RowSize(index_) == count); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); std::copy(row_start_ptr, row_start_ptr + count, self.rowPtr(index_)); } diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp index 954694da..93e69503 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp @@ -39,7 +39,7 @@ namespace DNDS auto buf_info = buf.request(false); DNDS_assert(buf_info.item_type_is_equivalent_to()); DNDS_assert_info(buf_info.shape.size() == 2, "need to pass a 2-d array"); - auto buf_start_ptr = reinterpret_cast(buf_info.ptr); + auto *buf_start_ptr = reinterpret_cast(buf_info.ptr); DNDS_assert(buf_info.strides.size() == 2); auto c_mat_map = tReadMap( @@ -93,7 +93,7 @@ namespace DNDS DNDS_assert_info(row_info.shape[0] == mat.rows(), "row size not matching"); DNDS_assert_info(row_info.shape[1] == mat.cols(), "col size not matching"); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); DNDS_assert(row_info.strides.size() == 2); auto row_mat_map = Eigen::Map< const Eigen::Matrix, diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp index 6ef886e4..a0a2121f 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp @@ -67,7 +67,7 @@ namespace DNDS DNDS_assert_info(row_info.shape[0] == mat.rows(), "row size not matching"); DNDS_assert_info(row_info.shape[1] == mat.cols(), "col size not matching"); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); DNDS_assert(row_info.strides.size() == 2); diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp index 69a00a86..7988434c 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp @@ -53,7 +53,7 @@ namespace DNDS DNDS_assert_info(row_info.shape.size() == 2, "need to pass a 2-d array"); DNDS_assert_info(row_info.shape[0] == mat.rows(), "row size not matching"); DNDS_assert_info(row_info.shape[1] == mat.cols(), "col size not matching"); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); DNDS_assert(row_info.strides.size() == 2); auto row_mat_map = Eigen::Map< const Eigen::Matrix, @@ -90,7 +90,7 @@ namespace DNDS DNDS_assert_info(row_info.shape[0] == self.BatchSize(index_), "batch size not matching"); DNDS_assert_info(row_info.shape[1] == self.Rows(), "row size not matching"); DNDS_assert_info(row_info.shape[2] == self.Cols(), "col size not matching"); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); DNDS_assert(row_info.strides.size() == 3); for (index iB = 0; iB < row_info.shape[0]; iB++) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp index 9c097840..fa6db13d 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp @@ -54,7 +54,7 @@ namespace DNDS DNDS_assert(row_info.item_type_is_equivalent_to()); auto [count, row_style] = py_buffer_get_contigious_size(row_info); // todo: upgrade to accept any 1D array DNDS_assert(self.RowSize(index_) == count); - auto row_start_ptr = reinterpret_cast(row_info.ptr); + auto *row_start_ptr = reinterpret_cast(row_info.ptr); std::copy(row_start_ptr, row_start_ptr + count, self[index_].data()); } } diff --git a/src/DNDS/Serializer/SerializerJSON.cpp b/src/DNDS/Serializer/SerializerJSON.cpp index 8bed27c4..a8b05d0b 100644 --- a/src/DNDS/Serializer/SerializerJSON.cpp +++ b/src/DNDS/Serializer/SerializerJSON.cpp @@ -93,7 +93,7 @@ namespace DNDS::Serializer auto v = jObj[cPointer]; DNDS_assert_info(v.is_object(), fmt::format("current path is not an object " + cP)); std::set ret; - for (auto &[key, value] : v.items()) + for (const auto &[key, value] : v.items()) ret.insert(key); return ret; } From 0bf9edd164db07a545a0142b2ca91187592f2a43 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 16:06:41 +0800 Subject: [PATCH 21/43] refactor(DNDS): annotate unused parameters (readability-named-parameter) Pass 14 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 222 warnings reduced to 0. Adds `/*unused*/` comment markers on tag-dispatch parameters (mostly `std::index_sequence` used purely for type deduction) in the pybind11 binding machinery. --- src/DNDS/ArrayDOF_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp | 2 +- src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp | 2 +- src/DNDS/Array_bind.hpp | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/DNDS/ArrayDOF_bind.hpp b/src/DNDS/ArrayDOF_bind.hpp index 34201954..a3e72497 100644 --- a/src/DNDS/ArrayDOF_bind.hpp +++ b/src/DNDS/ArrayDOF_bind.hpp @@ -127,7 +127,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void pybind11_callBindArrayDOFs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayDOFs_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayDOF_define_dispatch(m), ...); pybind11_ArrayDOF_define(m); diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp index 5633b826..c1d48379 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp @@ -200,7 +200,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void pybind11_callBindArrayAdjacencys_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayAdjacencys_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayAdjacency_define_dispatch(m), ...); (_pybind11_ArrayAdjacencyPair_define_dispatch(m), ...); diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp index a0a2121f..b9389d7a 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp @@ -246,7 +246,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void pybind11_callBindArrayEigenMatrixs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenMatrixs_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayEigenMatrix_define_dispatch(m), ...); (_pybind11_ArrayEigenMatrixPair_define_dispatch(m), ...); diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp index 7988434c..419bdbe8 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp @@ -294,7 +294,7 @@ namespace DNDS { template const &Arr, size_t... Is> - void pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenUniMatrixBatchs_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayEigenUniMatrixBatch_define_dispatch(m), ...); (_pybind11_ArrayEigenUniMatrixBatchPair_define_dispatch(m), ...); diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp index fa6db13d..e40aa21c 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp @@ -193,7 +193,7 @@ namespace DNDS namespace DNDS { template const &Arr, size_t... Is> - void pybind11_callBindArrayEigenVectors_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayEigenVectors_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayEigenVector_define_dispatch(m), ...); (_pybind11_ArrayEigenVectorPair_define_dispatch(m), ...); diff --git a/src/DNDS/Array_bind.hpp b/src/DNDS/Array_bind.hpp index c5888e66..fd594ec1 100644 --- a/src/DNDS/Array_bind.hpp +++ b/src/DNDS/Array_bind.hpp @@ -559,7 +559,7 @@ namespace DNDS static constexpr auto pybind11_arrayRowsizeInstantiationList = _get_pybind11_arrayRowsizeInstantiationList(); template const &Arr, size_t... Is> - void pybind11_callBindArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrays_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_Array_define_dispatch(m), ...); } @@ -573,7 +573,7 @@ namespace DNDS } template const &Arr, size_t... Is> - void pybind11_callBindParArrays_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindParArrays_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ParArray_define_dispatch(m), ...); } @@ -587,7 +587,7 @@ namespace DNDS } template const &Arr, size_t... Is> - void pybind11_callBindArrayTransformers_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindArrayTransformers_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ArrayTransformer_define_dispatch(m), ...); } @@ -601,7 +601,7 @@ namespace DNDS } template const &Arr, size_t... Is> - void pybind11_callBindParArrayPairs_rowsizes_sequence(py::module_ &m, std::index_sequence) + void pybind11_callBindParArrayPairs_rowsizes_sequence(py::module_ &m, std::index_sequence /*unused*/) { (_pybind11_ParArrayPair_define_dispatch(m), ...); } From e07c3153b623224934a4bd5ab0b7bb37e01cf2ad Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 16:39:59 +0800 Subject: [PATCH 22/43] refactor(DNDS): simplify boolean expressions (readability-simplify-boolean-expr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 15 from docs/dev/clang_tidy_plan.md. Single-check --fix run plus one manual fix in Defines.hpp (auto-fix couldn't rewrite it through the .hpp include graph and left 66 duplicates on one site). 155 warnings reduced to 0. Four patterns fixed: - `!(a && b)` -> `!a || !b` / DeMorgan expansion in ArrayDOF.hpp (SFINAE `enable_if`), ArrayDOF_bind.hpp (`if constexpr`), EigenUtil.hpp (ternary condition). - `!(v > MAX || v < MIN)` -> `v >= MIN && v <= MAX` in Defines.hpp::checkedIndexTo32 (manual — clang-tidy's proposed replacement hit every include of Defines.hpp separately). --- src/DNDS/ArrayDOF.hpp | 2 +- src/DNDS/ArrayDOF_bind.hpp | 2 +- src/DNDS/Defines.hpp | 2 +- src/DNDS/EigenUtil.hpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DNDS/ArrayDOF.hpp b/src/DNDS/ArrayDOF.hpp index 4fbb55ef..1323aaed 100644 --- a/src/DNDS/ArrayDOF.hpp +++ b/src/DNDS/ArrayDOF.hpp @@ -265,7 +265,7 @@ namespace DNDS /// @details Typical use: multiply state DOFs by per-cell values such as /// inverse mass. Only enabled for non-scalar DOF shapes. template - std::enable_if_t + std::enable_if_t operator*=(const ArrayDof<1, 1> &R) { DNDS_ARRAY_OP_SWITCHER(this->father->device(), operator_mult_assign_scalar_arr(*this, R)); diff --git a/src/DNDS/ArrayDOF_bind.hpp b/src/DNDS/ArrayDOF_bind.hpp index a3e72497..8bc66df4 100644 --- a/src/DNDS/ArrayDOF_bind.hpp +++ b/src/DNDS/ArrayDOF_bind.hpp @@ -80,7 +80,7 @@ namespace DNDS .def("__imul__", [](TArr &self, const TArr &R) { self *= R;return self; }, py::arg("R")); - if constexpr (!(n_m == 1 && n_n == 1)) + if constexpr (n_m != 1 || n_n != 1) { Arr_ .def("__imul__", [](TArr &self, ArrayDof<1, 1> &R) diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index 6559fd50..5244a423 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -623,7 +623,7 @@ namespace DNDS /// @brief Narrow #index to `int32_t` with range check; dies on overflow. inline int32_t checkedIndexTo32(index v) { - DNDS_assert_info(!(v > static_cast(INT32_MAX) || v < static_cast(INT32_MIN)), + DNDS_assert_info(v >= static_cast(INT32_MIN) && v <= static_cast(INT32_MAX), fmt::format("Index {} to int32 overflow", v)); return static_cast(v); } diff --git a/src/DNDS/EigenUtil.hpp b/src/DNDS/EigenUtil.hpp index af0b4a9e..5ab912d9 100644 --- a/src/DNDS/EigenUtil.hpp +++ b/src/DNDS/EigenUtil.hpp @@ -55,8 +55,8 @@ namespace Eigen * Use this type (or its @ref VectorFMTSafe / @ref RowVectorFMTSafe aliases) wherever * Eigen objects need to pass through `fmt::format`. */ - template struct MatrixFMTSafe : public Matrix { From b2019a794b31f80685fa11dfe7821a1c11d99378 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 18:23:24 +0800 Subject: [PATCH 23/43] refactor(DNDS): pass by const-ref where copy unused (performance-unnecessary-value-param) Pass 16 from docs/dev/clang_tidy_plan.md. 319 warnings reduced to 0. Combination of single-check --fix for the straightforward cases (auto-fixed across several .cpp TUs) and manual sed across the five pybind11 `_bind.hpp` headers that share the same `py::buffer row` parameter pattern in `setitem` / operator overloads. Two manual sites: - Serializer_bind.hpp: `from_dict` lambda took `py::object options_in` by value; only used via `nlohmann::json j(options_in)`. - MPI_bind.cpp: Allreduce lambda took both `py_sendbuf` and `py_recvbuf` as `py::buffer` by value; only `request()` is called. All 15 instances of `py::buffer row` across ArrayAdjacency, ArrayEigenMatrix, ArrayEigenMatrixBatch, ArrayEigenUniMatrixBatch, ArrayEigenVector binding headers are now `const py::buffer &row`. Each lambda body only calls `row.request()`, which is const. --- src/DNDS/Array.hpp | 2 +- src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp | 6 ++--- .../ArrayEigenMatrixBatch_bind.hpp | 12 +++++---- .../ArrayDerived/ArrayEigenMatrix_bind.hpp | 6 ++--- .../ArrayEigenUniMatrixBatch_bind.hpp | 14 +++++----- .../ArrayDerived/ArrayEigenVector_bind.hpp | 6 ++--- src/DNDS/Array_bind.hpp | 8 +++--- src/DNDS/Defines.cpp | 3 ++- src/DNDS/IndexMapping_bind.hpp | 16 +++++++----- src/DNDS/MPI_bind.cpp | 2 +- src/DNDS/Serializer/SerializerFactory.hpp | 2 +- src/DNDS/Serializer/Serializer_bind.hpp | 26 ++++++++++--------- 12 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 4b59761c..50c670f1 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -1109,7 +1109,7 @@ namespace DNDS /// @param name Sub-path name for this array. /// @return Metadata: array_sig, row_size_dynamic, size (local size for /// per-rank; 0 for collective without ParArray). - ReadSerializerMetaResult ReadSerializerMeta(Serializer::SerializerBaseSSP serializerP, const std::string &name) + ReadSerializerMetaResult ReadSerializerMeta(const Serializer::SerializerBaseSSP &serializerP, const std::string &name) { auto cwd = serializerP->GetCurrentPath(); serializerP->GoToPath(name); diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp index c1d48379..95c26ebe 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp @@ -36,7 +36,7 @@ namespace DNDS namespace DNDS { template > - auto pybind11_ArrayAdjacency_setitem(TArray &self, index index_, py::buffer row) + auto pybind11_ArrayAdjacency_setitem(TArray &self, index index_, const py::buffer &row) { auto row_info = row.request(false); DNDS_assert(row_info.item_type_is_equivalent_to()); @@ -105,7 +105,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayAdjacency &self, index index_, py::buffer row) + [](TArrayAdjacency &self, index index_, const py::buffer &row) { return pybind11_ArrayAdjacency_setitem(self, index_, row); }); @@ -180,7 +180,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, index index_, py::buffer row) + [](TPair &self, index index_, const py::buffer &row) { return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayAdjacency_setitem(ar, iC, row); }); diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp index 93e69503..5e55c903 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "ArrayEigenMatrixBatch.hpp" #include "../Array_bind.hpp" @@ -83,7 +85,7 @@ namespace DNDS false); } - inline auto pybind11_ArrayEigenMatrixBatch_setitem(ArrayEigenMatrixBatch &self, std::tuple index_, py::buffer row) + inline auto pybind11_ArrayEigenMatrixBatch_setitem(ArrayEigenMatrixBatch &self, std::tuple index_, const py::buffer &row) { using tElem = real; auto row_info = row.request(false); @@ -167,9 +169,9 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayEigenMatrixBatch &self, std::tuple index_, py::buffer row) + [](TArrayEigenMatrixBatch &self, std::tuple index_, const py::buffer &row) { - return pybind11_ArrayEigenMatrixBatch_setitem(self, index_, row); + return pybind11_ArrayEigenMatrixBatch_setitem(self, index_, std::move(row)); }); ArrayEigenMatrixBatch_ @@ -215,7 +217,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "InitializeWriteRow", - [](TPair &self, index index_, py::buffer row) + [](TPair &self, index index_, const py::buffer &row) { return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenMatrixBatch_setitem_row(ar, iC, row); }); @@ -232,7 +234,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, std::tuple index_, py::buffer row) + [](TPair &self, std::tuple index_, const py::buffer &row) { return self.runFunctionAppendedIndex(std::get<0>(index_), [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenMatrixBatch_setitem(ar, std::make_tuple(iC, std::get<1>(index_)), row); }); diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp index b9389d7a..b4078f7d 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix_bind.hpp @@ -56,7 +56,7 @@ namespace DNDS } template > - auto pybind11_ArrayEigenMatrix_setitem(TArrayEigenMatrix &self, index index_, py::buffer row) + auto pybind11_ArrayEigenMatrix_setitem(TArrayEigenMatrix &self, index index_, const py::buffer &row) { using tElem = real; auto row_info = row.request(false); @@ -153,7 +153,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayEigenMatrix &self, index index_, py::buffer row) + [](TArrayEigenMatrix &self, index index_, const py::buffer &row) { return pybind11_ArrayEigenMatrix_setitem(self, index_, row); }); @@ -224,7 +224,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, index index_, py::buffer row) + [](TPair &self, index index_, const py::buffer &row) { return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenMatrix_setitem(ar, iC, row); }); diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp index 419bdbe8..e67b55cb 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp @@ -44,7 +44,7 @@ namespace DNDS } template > - auto pybind11_ArrayEigenUniMatrixBatch_setitem(TArrayEigenUniMatrixBatch &self, std::tuple index_, py::buffer row) + auto pybind11_ArrayEigenUniMatrixBatch_setitem(TArrayEigenUniMatrixBatch &self, std::tuple index_, const py::buffer &row) { using tElem = real; auto row_info = row.request(false); @@ -80,7 +80,7 @@ namespace DNDS } template > - auto pybind11_ArrayEigenUniMatrixBatch_setitem_row(TArrayEigenUniMatrixBatch &self, index index_, py::buffer row) + auto pybind11_ArrayEigenUniMatrixBatch_setitem_row(TArrayEigenUniMatrixBatch &self, index index_, const py::buffer &row) { using tElem = real; auto row_info = row.request(false); @@ -145,7 +145,7 @@ namespace DNDS .def("Resize", [](TArrayEigenUniMatrixBatch &self, index size, int r, int c) { return self.Resize(size, r, c); }, py::arg("size"), py::arg("r"), py::arg("c")); ArrayEigenUniMatrixBatch_ // the once for all resize - .def("Resize", [](TArrayEigenUniMatrixBatch &self, index size, int r, int c, py::array_t batchSizes) + .def("Resize", [](TArrayEigenUniMatrixBatch &self, index size, int r, int c, const py::array_t &batchSizes) { return self.Resize(size, r, c, [&](index i) { return batchSizes.at(i); }); }, py::arg("size"), py::arg("r"), py::arg("c"), py::arg("batchSizes")); ArrayEigenUniMatrixBatch_ @@ -178,7 +178,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayEigenUniMatrixBatch &self, std::tuple index_, py::buffer row) + [](TArrayEigenUniMatrixBatch &self, std::tuple index_, const py::buffer &row) { return pybind11_ArrayEigenUniMatrixBatch_setitem(self, index_, row); }) @@ -191,7 +191,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayEigenUniMatrixBatch &self, index index_, py::buffer row) + [](TArrayEigenUniMatrixBatch &self, index index_, const py::buffer &row) { return pybind11_ArrayEigenUniMatrixBatch_setitem_row(self, index_, row); }); @@ -252,7 +252,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, std::tuple index_, py::buffer row) + [](TPair &self, std::tuple index_, const py::buffer &row) { return self.runFunctionAppendedIndex(std::get<0>(index_), [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenUniMatrixBatch_setitem(ar, std::make_tuple(iC, std::get<1>(index_)), row); }); @@ -267,7 +267,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, index index_, py::buffer row) + [](TPair &self, index index_, const py::buffer &row) { return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenUniMatrixBatch_setitem_row(ar, iC, row); }); diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp index e40aa21c..23ee17a7 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp @@ -48,7 +48,7 @@ namespace DNDS } template > - auto pybind11_ArrayEigenVector_setitem(TArrayEigenVector &self, index index_, py::buffer row) + auto pybind11_ArrayEigenVector_setitem(TArrayEigenVector &self, index index_, const py::buffer &row) { auto row_info = row.request(false); DNDS_assert(row_info.item_type_is_equivalent_to()); @@ -105,7 +105,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TArrayEigenVector &self, index index_, py::buffer row) + [](TArrayEigenVector &self, index index_, const py::buffer &row) { return pybind11_ArrayEigenVector_setitem(self, index_, row); }); @@ -173,7 +173,7 @@ namespace DNDS py::keep_alive<0, 1>()) .def( "__setitem__", - [](TPair &self, index index_, py::buffer row) + [](TPair &self, index index_, const py::buffer &row) { return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! { return pybind11_ArrayEigenVector_setitem(ar, iC, row); }); diff --git a/src/DNDS/Array_bind.hpp b/src/DNDS/Array_bind.hpp index fd594ec1..fc1b8f13 100644 --- a/src/DNDS/Array_bind.hpp +++ b/src/DNDS/Array_bind.hpp @@ -187,7 +187,7 @@ namespace DNDS // Array Array_ .def( "Resize", - [](TArray &self, index nRow, py::array_t rowsizes) + [](TArray &self, index nRow, const py::array_t &rowsizes) { DNDS_assert_info(rowsizes.size() >= nRow, fmt::format("rowsizes is of size {}, not enough", rowsizes.size())); self.Resize(nRow, [&](index iRow) @@ -488,7 +488,7 @@ namespace DNDS // ArrayTransformer .def("createFatherGlobalMapping", &TArrayTransformer::createFatherGlobalMapping) .def("createGhostMapping", [](TArrayTransformer &self, std::vector pullIndexGlobal) -> void { self.createGhostMapping(pullIndexGlobal); }, py::arg("pullIndexGlobal")) - .def("createGhostMapping", [](TArrayTransformer &self, py::array_t pullIndexGlobal) + .def("createGhostMapping", [](TArrayTransformer &self, const py::array_t &pullIndexGlobal) { std::vector pullIndexVec; pullIndexVec.reserve(pullIndexGlobal.size()); @@ -502,7 +502,7 @@ namespace DNDS // ArrayTransformer .def("clearMPITypes", &TArrayTransformer::clearMPITypes) .def( "BorrowGGIndexing", - [](TArrayTransformer &self, py::object other) + [](TArrayTransformer &self, const py::object &other) { auto other_father = other.attr("father"); auto other_father_size = other_father.attr("Size")().cast(); @@ -634,7 +634,7 @@ namespace DNDS pybind11_bind_Array_All_X_declare(7); // definitions are offloaded to Array_bind_offset/*.cpp - inline void pybind11_bind_Array_Offsets(py::module_ m) + inline void pybind11_bind_Array_Offsets(const py::module_ &m) { pybind11_bind_Array_All_X_call(1, m); pybind11_bind_Array_All_X_call(2, m); diff --git a/src/DNDS/Defines.cpp b/src/DNDS/Defines.cpp index 32f414a8..4e585ff2 100644 --- a/src/DNDS/Defines.cpp +++ b/src/DNDS/Defines.cpp @@ -10,6 +10,7 @@ // #endif #include #include +#include // #include #ifdef DNDS_UNIX_LIKE @@ -50,7 +51,7 @@ namespace DNDS bool logIsTTY() { return ostreamIsTTY(*logStream); } - void setLogStream(ssp nstream) { useCout = false, logStream = nstream; } + void setLogStream(ssp nstream) { useCout = false, logStream = std::move(nstream); } void setLogStreamCout() { useCout = true, logStream.reset(); } diff --git a/src/DNDS/IndexMapping_bind.hpp b/src/DNDS/IndexMapping_bind.hpp index 3b5f16b3..2097703b 100644 --- a/src/DNDS/IndexMapping_bind.hpp +++ b/src/DNDS/IndexMapping_bind.hpp @@ -8,23 +8,25 @@ #include #include +#include + namespace py = pybind11; namespace DNDS { inline auto pybind11_GlobalOffsetsMapping_declare(py::module_ m) { - return py_class_ssp(m, "GlobalOffsetsMapping"); + return py_class_ssp(std::move(m), "GlobalOffsetsMapping"); } - inline auto pybind11_GlobalOffsetsMapping_get_class(py::module_ m) + inline auto pybind11_GlobalOffsetsMapping_get_class(const py::module_ &m) { return py_class_ssp(m.attr("GlobalOffsetsMapping")); } inline void pybind11_GlobalOffsetsMapping_define(py::module_ m) { - auto Py_GlobalOffsetsMapping = pybind11_GlobalOffsetsMapping_declare(m); + auto Py_GlobalOffsetsMapping = pybind11_GlobalOffsetsMapping_declare(std::move(m)); Py_GlobalOffsetsMapping .def(py::init<>()) @@ -52,17 +54,17 @@ namespace DNDS inline auto pybind11_OffsetAscendIndexMapping_declare(py::module_ m) { - return py_class_ssp(m, "OffsetAscendIndexMapping"); + return py_class_ssp(std::move(m), "OffsetAscendIndexMapping"); } - inline auto pybind11_OffsetAscendIndexMapping_get_class(py::module_ m) + inline auto pybind11_OffsetAscendIndexMapping_get_class(const py::module_ &m) { return py_class_ssp(m.attr("OffsetAscendIndexMapping")); } inline void pybind11_OffsetAscendIndexMapping_define(py::module_ m) { - auto Py_OffsetAscendIndexMapping = pybind11_OffsetAscendIndexMapping_declare(m); + auto Py_OffsetAscendIndexMapping = pybind11_OffsetAscendIndexMapping_declare(std::move(m)); Py_OffsetAscendIndexMapping .def( @@ -146,7 +148,7 @@ namespace DNDS ; } - inline void pybind11_bind_IndexMapping_All(py::module_ m) + inline void pybind11_bind_IndexMapping_All(const py::module_ &m) { pybind11_GlobalOffsetsMapping_define(m); pybind11_OffsetAscendIndexMapping_define(m); diff --git a/src/DNDS/MPI_bind.cpp b/src/DNDS/MPI_bind.cpp index 0f0ab9f3..34e56e1d 100644 --- a/src/DNDS/MPI_bind.cpp +++ b/src/DNDS/MPI_bind.cpp @@ -93,7 +93,7 @@ namespace DNDS::MPI auto m_MPI = m.def_submodule("MPI"); m_MPI.def( "Allreduce", - [](py::buffer py_sendbuf, py::buffer py_recvbuf, const std::string &op, const MPIInfo &mpi) + [](const py::buffer &py_sendbuf, const py::buffer &py_recvbuf, const std::string &op, const MPIInfo &mpi) { auto send_info = py_sendbuf.request(false); auto recv_info = py_recvbuf.request(true); diff --git a/src/DNDS/Serializer/SerializerFactory.hpp b/src/DNDS/Serializer/SerializerFactory.hpp index 7bd625b8..b95648b5 100644 --- a/src/DNDS/Serializer/SerializerFactory.hpp +++ b/src/DNDS/Serializer/SerializerFactory.hpp @@ -91,7 +91,7 @@ namespace DNDS::Serializer * @return Tuple `(finalFilePath, displayPath)` -- the display path is * the JSON dir or the H5 file, depending on backend. */ - std::tuple ModifyFilePath(std::string fname, const MPIInfo &mpi, std::string rank_part_fmt = "%06d", bool read = false) + std::tuple ModifyFilePath(std::string fname, const MPIInfo &mpi, const std::string &rank_part_fmt = "%06d", bool read = false) { if (type == "JSON") { diff --git a/src/DNDS/Serializer/Serializer_bind.hpp b/src/DNDS/Serializer/Serializer_bind.hpp index 5d674652..7500031b 100644 --- a/src/DNDS/Serializer/Serializer_bind.hpp +++ b/src/DNDS/Serializer/Serializer_bind.hpp @@ -15,20 +15,22 @@ namespace py = pybind11; #include "SerializerFactory.hpp" #include +#include + namespace DNDS::Serializer { inline auto pybind11_SerializerBase_declare(py::module_ m) { - return py_class_ssp(m, "SerializerBase"); + return py_class_ssp(std::move(m), "SerializerBase"); } - inline auto pybind11_SerializerBase_get_class(py::module_ m) + inline auto pybind11_SerializerBase_get_class(const py::module_ &m) { return py_class_ssp(m.attr("SerializerBase")); } inline void pybind11_SerializerBase_define(py::module_ m) { - auto Serializer_ = pybind11_SerializerBase_declare(m); + auto Serializer_ = pybind11_SerializerBase_declare(std::move(m)); using tSerializer = SerializerBase; // Serializer_ //! no initializer (ctor) as this is virtual base // .def(py::init<>()); @@ -42,13 +44,13 @@ namespace DNDS::Serializer .def("IsPerRank", &tSerializer::IsPerRank); } - inline auto pybind11_SerializerJSON_declare(py::module_ m) + inline auto pybind11_SerializerJSON_declare(const py::module_ &m) { return py_class_ssp(m, "SerializerJSON", pybind11_SerializerBase_get_class(m)); } - inline void pybind11_SerializerJSON_define(py::module_ m) + inline void pybind11_SerializerJSON_define(const py::module_ &m) { - auto Serializer_ = pybind11_SerializerJSON_declare(m); + auto Serializer_ = pybind11_SerializerJSON_declare(std::move(m)); using tSerializer = SerializerJSON; Serializer_ .def(py::init<>()); @@ -57,13 +59,13 @@ namespace DNDS::Serializer .def("SetUseCodecOnUint8", &tSerializer::SetUseCodecOnUint8); } - inline auto pybind11_SerializerH5_declare(py::module_ m) + inline auto pybind11_SerializerH5_declare(const py::module_ &m) { return py_class_ssp(m, "SerializerH5", pybind11_SerializerBase_get_class(m)); } - inline void pybind11_SerializerH5_define(py::module_ m) + inline void pybind11_SerializerH5_define(const py::module_ &m) { - auto Serializer_ = pybind11_SerializerH5_declare(m); + auto Serializer_ = pybind11_SerializerH5_declare(std::move(m)); using tSerializer = SerializerH5; Serializer_ .def(py::init(), py::arg("mpi")); @@ -74,11 +76,11 @@ namespace DNDS::Serializer inline auto pybind11_SerializerFactory_declare(py::module_ m) { - return py_class_ssp(m, "SerializerFactory"); + return py_class_ssp(std::move(m), "SerializerFactory"); } inline void pybind11_SerializerFactory_define(py::module_ m) { - auto SerializerFactory_ = pybind11_SerializerFactory_declare(m); + auto SerializerFactory_ = pybind11_SerializerFactory_declare(std::move(m)); SerializerFactory_ .def(py::init<>()) // .def(py::init(), py::arg("type")) @@ -96,7 +98,7 @@ namespace DNDS::Serializer }) .def( "from_dict", - [](SerializerFactory &self, py::object options_in) -> void + [](SerializerFactory &self, const py::object &options_in) -> void { nlohmann::json j(options_in); from_json(j, self); From 9a88b361fd931711c8cd8c90a3e6eac73b6222cb Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 19:31:19 +0800 Subject: [PATCH 24/43] refactor(DNDS): range-based for where safe (modernize-loop-convert) Pass 17 from docs/dev/clang_tidy_plan.md. 149 warnings reduced to 0. Auto-fix converted two index-based loops over `std::vector` in ArrayRedistributor.hpp to range-based form (the index was only used to dereference into the same vector). One site in Array_bind.hpp carries a NOLINTBEGIN / NOLINTEND guard: the loop body calls `pullIndexGlobal.at(i)` on a `py::array_t` to invoke the numpy-to-long implicit conversion. Range-based iteration yields `pybind11::handle`, which does not convert to `long` in that context, so the explicit index-based form is required. The first auto-fix attempt broke the build here; the NOLINTBEGIN/NOLINTEND protects it from future runs. A plain NOLINTNEXTLINE is not enough: `--fix` rewrites the `for` line itself, erasing the comment. Block-form NOLINT survives. --- src/DNDS/ArrayRedistributor.hpp | 8 ++++---- src/DNDS/Array_bind.hpp | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/DNDS/ArrayRedistributor.hpp b/src/DNDS/ArrayRedistributor.hpp index a033e450..d17d2bc9 100644 --- a/src/DNDS/ArrayRedistributor.hpp +++ b/src/DNDS/ArrayRedistributor.hpp @@ -102,9 +102,9 @@ namespace DNDS // Count entries per directory rank std::vector sendCounts(nRanks, 0); - for (index i = 0; i < index(readOrigIndex.size()); i++) + for (long i : readOrigIndex) { - int dr = directoryRank(readOrigIndex[i]); + int dr = directoryRank(i); sendCounts[dr]++; } @@ -158,9 +158,9 @@ namespace DNDS // Step 5: Send queries from newOrigIndex to directory, get back globalReadIdx. // Count queries per directory rank std::vector querySendCounts(nRanks, 0); - for (index i = 0; i < index(newOrigIndex.size()); i++) + for (long i : newOrigIndex) { - int dr = directoryRank(newOrigIndex[i]); + int dr = directoryRank(i); querySendCounts[dr]++; } diff --git a/src/DNDS/Array_bind.hpp b/src/DNDS/Array_bind.hpp index fc1b8f13..93122f3d 100644 --- a/src/DNDS/Array_bind.hpp +++ b/src/DNDS/Array_bind.hpp @@ -492,8 +492,14 @@ namespace DNDS // ArrayTransformer { std::vector pullIndexVec; pullIndexVec.reserve(pullIndexGlobal.size()); + // NOLINTBEGIN(modernize-loop-convert) + // Index-based loop is required here: pybind11 `array_t` iterators + // yield `pybind11::handle`, not `long`. The implicit numpy-to-long + // conversion is performed by `pullIndexGlobal.at(i)`, which does + // not have a range-based-for equivalent. for(ssize_t i = 0; i < pullIndexGlobal.size(); i++) pullIndexVec.push_back(pullIndexGlobal.at(i)); // only 1D + // NOLINTEND(modernize-loop-convert) self.createGhostMapping(pullIndexVec); }, py::arg("pullIndexGlobal")) .def("createGhostMapping", [](TArrayTransformer &self, std::vector pushingIndexLocal, std::vector pushingStarts) -> void { self.createGhostMapping(pushingIndexLocal, pushingStarts); }, py::arg("pushingIndexLocal"), py::arg("pushingStarts")); From a3cb4a5ec870567060cbf2a08a207c031ea12016 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 20:26:50 +0800 Subject: [PATCH 25/43] refactor(DNDS): prefer member initializer list (cppcoreguidelines-prefer-member-initializer) Pass 18 from docs/dev/clang_tidy_plan.md. Single-check --fix run. 60 warnings reduced to 0 (one unique site, duplicated across TUs). Moves `comm = ncomm;` in the body of MPIInfo::MPIInfo(MPI_Comm) to the member initializer list. --- src/DNDS/MPI.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 06ce5f5f..04af63ec 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -234,9 +234,9 @@ namespace DNDS MPIInfo() = default; /// @brief Wrap an existing MPI communicator; queries rank and size. - MPIInfo(MPI_Comm ncomm) + MPIInfo(MPI_Comm ncomm) : comm(ncomm) { - comm = ncomm; + int ierr = 0; ierr = MPI_Comm_rank(comm, &rank), DNDS_assert(ierr == MPI_SUCCESS); ierr = MPI_Comm_size(comm, &size), DNDS_assert(ierr == MPI_SUCCESS); From 226ead9a0429847e67bc37468b668d73c445550a Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 20:52:21 +0800 Subject: [PATCH 26/43] refactor(DNDS): replace C-style casts with named casts (cppcoreguidelines-pro-type-cstyle-cast) Pass 19 from docs/dev/clang_tidy_plan.md. Manual (6 unique sites). 124 warnings reduced to 0: - SerializerH5.cpp (lines 878, 912) and SerializerJSON.cpp (lines 223, 252): `(ssp *)(pth_2_ssp[refPath])` -> explicit `reinterpret_cast *>(...)`. The dedup registry stores type-erased `void*`; this matches the author TODO on line 878. Comment added noting the caller-guaranteed type invariant. - MPI.hpp lines 715, 721: `MPI_IN_PLACE` expands to the OpenMPI-defined `((void *)1)` sentinel whose C-style cast is out of project control. Guarded with NOLINTNEXTLINE placed immediately above the offending call (NOLINTNEXTLINE must be on the line directly preceding the warning; a multi-line rationale comment in between breaks the suppression). --- src/DNDS/MPI.hpp | 6 ++++++ src/DNDS/Serializer/SerializerH5.cpp | 8 ++++++-- src/DNDS/Serializer/SerializerJSON.cpp | 8 ++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/DNDS/MPI.hpp b/src/DNDS/MPI.hpp index 04af63ec..b98e2e5b 100644 --- a/src/DNDS/MPI.hpp +++ b/src/DNDS/MPI.hpp @@ -712,12 +712,18 @@ namespace DNDS::MPI /// @brief Single-scalar Allreduce helper for reals (in-place, count = 1). inline void AllreduceOneReal(real &v, MPI_Op op, const MPIInfo &mpi) { + // MPI_IN_PLACE is a library-defined sentinel macro (OpenMPI: `((void *)1)`) + // whose internal C-style cast is outside project control. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) Allreduce(MPI_IN_PLACE, &v, 1, DNDS_MPI_REAL, op, mpi.comm); } /// @brief Single-scalar Allreduce helper for indices (in-place, count = 1). inline void AllreduceOneIndex(index &v, MPI_Op op, const MPIInfo &mpi) { + // MPI_IN_PLACE is a library-defined sentinel macro (OpenMPI: `((void *)1)`) + // whose internal C-style cast is outside project control. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) Allreduce(MPI_IN_PLACE, &v, 1, DNDS_MPI_INDEX, op, mpi.comm); } diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 595e8960..18e51424 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -875,7 +875,9 @@ namespace DNDS::Serializer if (pth_2_ssp.count(refPath)) { - v = *((ssp *)(pth_2_ssp[refPath])); // ! reform this (and in json counterpart) to use reinterpret_cast or use STL's tools + // Dedup registry stores type-erased `ssp *`; caller + // guarantees the stored type matches tValue. + v = *reinterpret_cast *>(pth_2_ssp[refPath]); } else { @@ -909,7 +911,9 @@ namespace DNDS::Serializer if (pth_2_ssp.count(refPath)) { - v = *((ssp *)(pth_2_ssp[refPath])); // ! reform this (and in json counterpart) to use reinterpret_cast or use STL's tools + // Dedup registry stores type-erased `ssp *`; caller + // guarantees the stored type matches tValue. + v = *reinterpret_cast *>(pth_2_ssp[refPath]); } else { diff --git a/src/DNDS/Serializer/SerializerJSON.cpp b/src/DNDS/Serializer/SerializerJSON.cpp index a8b05d0b..da146a9f 100644 --- a/src/DNDS/Serializer/SerializerJSON.cpp +++ b/src/DNDS/Serializer/SerializerJSON.cpp @@ -220,7 +220,9 @@ namespace DNDS::Serializer if (pth_2_ssp.count(refPath)) { - v = *((ssp *)(pth_2_ssp[refPath])); + // Dedup registry stores type-erased `ssp *`; caller + // guarantees the stored type matches tValue. + v = *reinterpret_cast *>(pth_2_ssp[refPath]); } else { @@ -249,7 +251,9 @@ namespace DNDS::Serializer if (pth_2_ssp.count(refPath)) { - v = *((ssp *)(pth_2_ssp[refPath])); + // Dedup registry stores type-erased `ssp *`; caller + // guarantees the stored type matches tValue. + v = *reinterpret_cast *>(pth_2_ssp[refPath]); } else { From c407cffe9121d447c5a7e6d5ea63bcdf19a0f4fb Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:03:19 +0800 Subject: [PATCH 27/43] chore(tooling): disable cppcoreguidelines-avoid-non-const-global-variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 20 from docs/dev/clang_tidy_plan.md. Every global mutable in DNDS is intentional and cannot be made `const`, `thread_local`, or class-scoped without wider redesign: logStream — runtime log redirection (set once at startup, read by every assert/log site) useCout — runtime switch between stdout and file log outputDelim — global print delimiter used by vector-dump helpers HDF_mutex — library-wide HDF5 mutex (must be mutable; HDF5 is not thread-safe) isDebugging — runtime probe set from the debugger to unblock a rank-barrier spin loop EigenPCH_tag, ExprtkPCH_tag — link-time probes indicating whether the PCH TU was compiled into the static library 324 warnings eliminated. Full rationale in the .clang-tidy header table. --- .clang-tidy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.clang-tidy b/.clang-tidy index 3abd5926..199a25df 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -97,6 +97,20 @@ # MPICH_SKIP_MPICXX, OMPI_SKIP_MPICXX, # EIGEN_DONT_PARALLELIZE (define-before-include) # DNDS_VERSION_STRING (from CMake configure_file). +# -cppcoreguidelines-avoid-non-const-global-variables +# Every DNDS global mutable +# is intentional: +# logStream — runtime log redirection +# useCout — runtime switch +# outputDelim — global config +# HDF_mutex — library-wide mutex (must be mutable) +# isDebugging — runtime debugger-attach probe +# EigenPCH_tag, +# ExprtkPCH_tag — PCH presence probes +# Moving any of these to +# thread_local, singletons, +# or class members is out of +# scope for a tidy pass. # # Notes on ExtraArgs: # - -UDNDS_USE_OMP: skip #include which requires @@ -146,6 +160,7 @@ Checks: > -modernize-use-transparent-functors, -cppcoreguidelines-c-copy-assignment-signature, -cppcoreguidelines-macro-usage, + -cppcoreguidelines-avoid-non-const-global-variables, WarningsAsErrors: '' From 1d60a5728e49a911462a046cf13fc47345b90562 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:03:42 +0800 Subject: [PATCH 28/43] refactor(DNDS): replace C-arrays with std::array (cppcoreguidelines-avoid-c-arrays) Pass 21 from docs/dev/clang_tidy_plan.md. 68 warnings reduced to 0 (two unique sites): - Errors.hpp::genFatalErrorMessage: `char format_buf[1024*512]` -> `std::array format_buf{}` with zero-init, then vsnprintf into `format_buf.data()`. Added `#include `. - SerializerFactory.hpp: `char BUF[512]` for `std::sprintf` of rank-tagged filename suffix -> `std::array`. Both sites are stack-allocated scratch buffers for C-style printf family calls; switching to `std::array` keeps the storage class identical while giving a `.size()` that survives array-decay. --- src/DNDS/Errors.hpp | 7 ++++--- src/DNDS/Serializer/SerializerFactory.hpp | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/DNDS/Errors.hpp b/src/DNDS/Errors.hpp index 1ee081a1..34bf01b4 100644 --- a/src/DNDS/Errors.hpp +++ b/src/DNDS/Errors.hpp @@ -28,6 +28,7 @@ #include #include +#include #include namespace DNDS { @@ -61,10 +62,10 @@ namespace DNDS va_start(args, info); std::cerr << getTraceString() << "\n"; std::cerr << "\033[91m DNDS_assertion failed\033[39m: \"" << expr << "\" at [ " << file << ":" << line << " ]\n"; - char format_buf[1024 * 512]; - std::vsnprintf(format_buf, sizeof(format_buf), info, args); + std::array format_buf{}; + std::vsnprintf(format_buf.data(), format_buf.size(), info, args); va_end(args); - std::cerr << format_buf << std::endl; + std::cerr << format_buf.data() << std::endl; std::abort(); } diff --git a/src/DNDS/Serializer/SerializerFactory.hpp b/src/DNDS/Serializer/SerializerFactory.hpp index b95648b5..a58f3e0f 100644 --- a/src/DNDS/Serializer/SerializerFactory.hpp +++ b/src/DNDS/Serializer/SerializerFactory.hpp @@ -99,9 +99,9 @@ namespace DNDS::Serializer outPath = {fname + ".dir"}; if (!read) std::filesystem::create_directories(outPath); - char BUF[512]; - std::sprintf(BUF, rank_part_fmt.c_str(), mpi.rank); - fname = getStringForcePath(outPath / (std::string(BUF) + ".json")); + std::array BUF{}; + std::sprintf(BUF.data(), rank_part_fmt.c_str(), mpi.rank); + fname = getStringForcePath(outPath / (std::string(BUF.data()) + ".json")); return std::make_tuple(fname, getStringForcePath(outPath)); } else if (type == "H5") From dd31508fbbedfae04c2c7243fe3e38fba55a0e02 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:08:16 +0800 Subject: [PATCH 29/43] fix(DNDS): guard AdjacencyRow::operator= against self-assign (bugprone-unhandled-self-assignment) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 22 from docs/dev/clang_tidy_plan.md. 53 warnings reduced to 0 (one unique site, duplicated across TUs). `AdjacencyRow::operator=(const AdjacencyRow &r)` copied `r.cbegin() .. r.cend()` into `p_indices` via `std::copy`. When `&r == this`, source and destination ranges are identical and fully overlapping, which is undefined behaviour for `std::copy`. Added a `this == &r` early-return guard (with doc comment explaining the UB) — matches the standard canonical form used by rule-of-five assignment operators. In practice nothing in the tree self-assigns an `AdjacencyRow` view, but the fix is cheap and closes the last bugprone-* warning outside the deferred bucket. --- src/DNDS/ArrayDerived/AdjacencyRow.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 6083ce68..29fc65dc 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -71,8 +71,13 @@ namespace DNDS } /// @brief Copy contents of another span (same size required). + /// @details Guarded against self-assignment: when `&r == this`, + /// `p_indices` and `r.cbegin()` point into the same buffer, and + /// `std::copy` on overlapping ranges is undefined behaviour. DNDS_DEVICE_CALLABLE void operator=(const AdjacencyRow &r) { + if (this == &r) + return; DNDS_assert(Row_size == r.size()); std::copy(r.cbegin(), r.cend(), p_indices); } From 10f53052c513bb3c5c3836b1101c82e0bfebb498 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:33:32 +0800 Subject: [PATCH 30/43] style(DNDS): document intentional branch clones (bugprone-branch-clone) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 23 from docs/dev/clang_tidy_plan.md. 322 warnings reduced to 0 (7 unique sites). Every flagged branch is intentional — the diagnostic fires where two logically distinct branches happen to produce the same code, which documents the mapping but triggers the check. Sites suppressed with NOLINTBEGIN / NOLINTEND blocks (plain NOLINTNEXTLINE did not work because the diagnostic is reported at the first clone and the fix-region sometimes spans both): - ArrayBasic.hpp:441, 499 and Array.hpp:588 — `if constexpr` cascades over `_dataLayout`. `TABLE_Fixed` and `TABLE_Max` currently both compute `iRow * _row_size_dynamic + iCol`; the two layouts are conceptually distinct (padded rows may diverge). - ArrayEigenUniMatrixBatch.hpp:46 and _DeviceView.hpp:50 and EigenUtil.hpp::MatrixFMTSafe — ternary expression for the Eigen `options` template parameter. Both non-row-vector arms intentionally select `ColMajor`. - Config/ConfigParam.hpp:188 — switch over `ConfigTypeTag` mapping to JSON Schema type strings. Several enum values map to the same built-in Schema type (`Enum`-> "string", `ArrayOfObjects` -> "array", `MapOfObjects` -> "object"); the distinction shows up elsewhere in the schema meta. Each suppression carries a rationale comment explaining the intent. --- src/DNDS/Array.hpp | 3 +++ src/DNDS/ArrayBasic.hpp | 9 +++++++++ src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp | 4 ++++ .../ArrayDerived/ArrayEigenUniMatrixBatch_DeviceView.hpp | 3 +++ src/DNDS/Config/ConfigParam.hpp | 5 +++++ src/DNDS/EigenUtil.hpp | 4 ++++ 6 files changed, 28 insertions(+) diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 50c670f1..2fecf6ee 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -584,10 +584,13 @@ namespace DNDS return _data.at(iRow * rs + iCol); else if constexpr (_dataLayout == TABLE_StaticMax) return _data.at(iRow * rm + iCol); + // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max share + // the runtime expression but are conceptually distinct layouts. else if constexpr (_dataLayout == TABLE_Fixed) return _data.at(iRow * _row_size_dynamic + iCol); else if constexpr (_dataLayout == TABLE_Max) return _data.at(iRow * _row_size_dynamic + iCol); + // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { if (IfCompressed()) diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index b5b34617..c89ac898 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -437,10 +437,15 @@ namespace DNDS pos = iRow * rs + iCol; else if constexpr (_dataLayout == TABLE_StaticMax) pos = iRow * rm + iCol; + // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max happen + // to share the runtime expression (both use `_row_size_dynamic`) but + // are conceptually distinct layout flavours and may diverge in future + // (e.g. padded rows). else if constexpr (_dataLayout == TABLE_Fixed) pos = iRow * _row_size_dynamic + iCol; else if constexpr (_dataLayout == TABLE_Max) pos = iRow * _row_size_dynamic + iCol; + // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { DNDS_HD_assert_infof(0 <= iRow && iRow + 1 < _size + 1, @@ -492,10 +497,14 @@ namespace DNDS return _data + iRow * rs; else if constexpr (_dataLayout == TABLE_StaticMax) return _data + iRow * rm; + // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max share + // the runtime expression but are conceptually distinct layouts + // (see #operator[](iRow, iCol)). else if constexpr (_dataLayout == TABLE_Fixed) return _data + iRow * _row_size_dynamic; else if constexpr (_dataLayout == TABLE_Max) return _data + iRow * _row_size_dynamic; + // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { DNDS_HD_assert_infof(0 <= iRow && iRow < _size + 1, diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp index e049ccee..2d2461e8 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp @@ -41,10 +41,14 @@ namespace DNDS using t_base = ParArray; using t_base::t_base; + // NOLINTBEGIN(bugprone-branch-clone): both non-row-vector arms of + // the options ternary intentionally select ColMajor; mirrors the + // pattern in EigenUtil.hpp::MatrixFMTSafe. using t_EigenMatrix = Eigen::Matrix; + // NOLINTEND(bugprone-branch-clone) using t_EigenMap = Eigen::Map; // default no buffer align and stride using t_EigenMap_const = Eigen::Map; // default no buffer align and stride diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_DeviceView.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_DeviceView.hpp index 35502b60..b19d3857 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_DeviceView.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_DeviceView.hpp @@ -45,10 +45,13 @@ namespace DNDS using t_base_const = const ArrayDeviceView; + // NOLINTBEGIN(bugprone-branch-clone): both non-row-vector arms of + // the options ternary intentionally select ColMajor. using t_EigenMatrix = Eigen::Matrix, _n_row, _n_col, Eigen::AutoAlign | ((_n_row == 1 && _n_col != 1) ? Eigen ::RowMajor : (_n_col == 1 && _n_row != 1) ? Eigen ::ColMajor // ColMajor except for row-vector : Eigen ::ColMajor)>; + // NOLINTEND(bugprone-branch-clone) using t_EigenMap_const = Eigen::Map; // default no buffer align and stride using t_EigenMap = std::conditional_t, diff --git a/src/DNDS/Config/ConfigParam.hpp b/src/DNDS/Config/ConfigParam.hpp index 954b89b7..ef3557ef 100644 --- a/src/DNDS/Config/ConfigParam.hpp +++ b/src/DNDS/Config/ConfigParam.hpp @@ -185,6 +185,10 @@ namespace DNDS return "integer"; case ConfigTypeTag::Real: return "number"; + // NOLINTBEGIN(bugprone-branch-clone): JSON Schema has no distinct + // `enum` / `ArrayOfObjects` / `MapOfObjects` types; each maps to + // the closest built-in kind (string / array / object). Keeping + // the cases explicit documents the mapping at the call site. case ConfigTypeTag::String: return "string"; case ConfigTypeTag::Enum: @@ -197,6 +201,7 @@ namespace DNDS return "array"; case ConfigTypeTag::MapOfObjects: return "object"; + // NOLINTEND(bugprone-branch-clone) case ConfigTypeTag::Json: return {}; } diff --git a/src/DNDS/EigenUtil.hpp b/src/DNDS/EigenUtil.hpp index 5ab912d9..58bec10f 100644 --- a/src/DNDS/EigenUtil.hpp +++ b/src/DNDS/EigenUtil.hpp @@ -55,10 +55,14 @@ namespace Eigen * Use this type (or its @ref VectorFMTSafe / @ref RowVectorFMTSafe aliases) wherever * Eigen objects need to pass through `fmt::format`. */ + // NOLINTBEGIN(bugprone-branch-clone): both non-row-vector arms of the + // options ternary intentionally select ColMajor. Keeping the two arms + // explicit documents the intent at the declaration site. template struct MatrixFMTSafe : public Matrix + // NOLINTEND(bugprone-branch-clone) { using Base = Matrix; using Base::Base; From 583ab45a6ff58e84241552077f58c3e65b7cd02a Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:41:12 +0800 Subject: [PATCH 31/43] style(DNDS): silence spurious widening warning in Errors.hpp (bugprone-implicit-widening-of-multiplication-result) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 24 from docs/dev/clang_tidy_plan.md. 66 warnings reduced to 0 (one source site, duplicated across 7 include paths). `std::array` in `genFatalErrorMessage` — the product 524 288 is a compile-time constant that trivially fits in int32_t. The `int -> size_t` widening happens at compile time when deducing the `std::array` size template; no runtime overflow is possible. NOLINTNEXTLINE with rationale. --- src/DNDS/Errors.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DNDS/Errors.hpp b/src/DNDS/Errors.hpp index 34bf01b4..39a9d42d 100644 --- a/src/DNDS/Errors.hpp +++ b/src/DNDS/Errors.hpp @@ -62,6 +62,8 @@ namespace DNDS va_start(args, info); std::cerr << getTraceString() << "\n"; std::cerr << "\033[91m DNDS_assertion failed\033[39m: \"" << expr << "\" at [ " << file << ":" << line << " ]\n"; + // NOLINTNEXTLINE(bugprone-implicit-widening-of-multiplication-result): compile-time + // constant 1024*512 = 524288 fits in int32_t; no runtime overflow is possible. std::array format_buf{}; std::vsnprintf(format_buf.data(), format_buf.size(), info, args); va_end(args); From 6c90a623c03cca34c4c70664db91ee4f8cfbaa98 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 21:43:53 +0800 Subject: [PATCH 32/43] fix(DNDS): move from rvalue-ref param in ArrayDofDeviceView (cppcoreguidelines-rvalue-reference-param-not-moved) Pass 25 from docs/dev/clang_tidy_plan.md. 44 warnings reduced to 0 (two source sites, duplicated across TUs). Both `ArrayDofDeviceView` and `ArrayDofDeviceViewConst` had an rvalue-ref forwarding ctor that then copy-constructed the base from the named (lvalue) parameter: ArrayDofDeviceView(t_base &&base_view) : t_base(base_view) {} `base_view` inside the function body is an lvalue, so this was silently a copy-from-rvalue (a minor pessimisation; the base is value-semantic `shared_ptr`s so the move is cheap either way). Added `std::move(base_view)` in the base init list to perform the intended move and clear the lint. --- src/DNDS/ArrayDOF.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DNDS/ArrayDOF.hpp b/src/DNDS/ArrayDOF.hpp index 1323aaed..24e8a310 100644 --- a/src/DNDS/ArrayDOF.hpp +++ b/src/DNDS/ArrayDOF.hpp @@ -39,7 +39,7 @@ namespace DNDS DNDS_DEVICE_TRIVIAL_COPY_DEFINE(ArrayDofDeviceView, ArrayDofDeviceView) DNDS_DEVICE_CALLABLE ArrayDofDeviceView(const t_base &base_view) : t_base(base_view) {} - DNDS_DEVICE_CALLABLE ArrayDofDeviceView(t_base &&base_view) : t_base(base_view) {} + DNDS_DEVICE_CALLABLE ArrayDofDeviceView(t_base &&base_view) : t_base(std::move(base_view)) {} }; /// @brief Const device view of an ArrayDof father/son pair. @@ -53,7 +53,7 @@ namespace DNDS DNDS_DEVICE_TRIVIAL_COPY_DEFINE(ArrayDofDeviceViewConst, ArrayDofDeviceViewConst) DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(const t_base &base_view) : t_base(base_view) {} - DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(t_base &&base_view) : t_base(base_view) {} + DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(t_base &&base_view) : t_base(std::move(base_view)) {} }; template From 0d6d0d90b1a81b81f499fa3bd582b6ee0566a2ee Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 23:06:22 +0800 Subject: [PATCH 33/43] refactor(DNDS): clear long-tail clang-tidy warnings (passes 24-26) Drains every remaining check with >=1 instance to zero. The 13 actionable sites span 12 files: Fixes: - Device/DeviceStorage.cpp, ExprtkWrapper.cpp, MPI_bind.cpp: legitimate raw `new`/`delete` behind opaque / smart-pointer boundaries. NOLINTNEXTLINE + NOLINTBEGIN/END with rationale (cppcoreguidelines-owning-memory). - MPI.cpp: four empty `catch (...)` on env-var parse failure in CommStrategy::CommStrategy. NOLINTBEGIN/END with rationale (bugprone-empty-catch). - Errors.hpp: spurious bugprone-implicit-widening on compile-time constant `1024 * 512`. NOLINTNEXTLINE with rationale. - ArrayDerived/ArrayEigenMatrixBatch_bind.hpp: setitem helpers returned `auto` (deduced void) and the wrapper lambdas carried redundant `return ...` of void-exprs; changed helpers to `void`, dropped `return` inside their lambdas. Also removed `std::move` of `const py::buffer &` (no-op). - Serializer_bind.hpp: `std::move(m)` where `m` was `const py::module_ &` (no-op move) -- removed. - SerializerFactory.hpp: `BuildSerializer`, `ModifyFilePath` made `const` and `[[nodiscard]]`; ctor changed to pass-by-value + `std::move` (modernize-pass-by-value). - SerializerH5.hpp: dropped redundant `: SerializerBase()` init. - SerializerH5.cpp: `H5Contents &contents` in TraverseData NOLINT'd; `TraverseData *data = static_cast<...>(op_data)` -> `auto *`; `T vV = v` NOLINT'd (used via `&vV` in the non-string `if constexpr` branch); explicit `&attr_value` cast in `H5Aread(..., char**)` NOLINT'd; `get_indent()` return stays as `std::string(n, ch)` (brace-init triggers -Wnarrowing with the initializer_list overload). - SerializerJSON.hpp: `~SerializerJSON()` now wraps `CloseFileNonVirtual()` in a try/catch (bugprone-exception-escape) -- destructors mustn't throw. - Defines.cpp: `ver.length()` -> `!ver.empty()` (readability-container-size-empty). - MPI_bind.cpp: `pArgvOut.reserve(*pargc)` before the `emplace_back` loop (performance-inefficient-vector-operation); `MPI_Comm(pComm)` int-to-ptr cast NOLINT'd with rationale (performance-no-int-to-ptr). - ArrayDOF.hpp: `t_base(base_view)` -> `t_base(std::move(base_view))` for the rvalue-ref forwarding ctor in ArrayDofDeviceView / ArrayDofDeviceViewConst (cppcoreguidelines-rvalue-reference-param-not-moved). Also documented the `NOLINTNEXTLINE` rule that tripped this pass up repeatedly: the directive applies to the *immediately next* line, so rationale comments must follow (not precede) the NOLINT directive when the fix-region is bigger than one line. Numbers: DNDS goes 976 -> 1 total diagnostic. The sole remaining diagnostic is clang-diagnostic-error 'omp.h' in EigenPCH.cpp which is an Eigen internal include unrelated to DNDS code (documented earlier). 35 -> 18 -> 1 distinct check count. --- .../ArrayEigenMatrixBatch_bind.hpp | 20 ++++++++--------- src/DNDS/Defines.cpp | 2 +- src/DNDS/Device/DeviceStorage.cpp | 4 ++++ src/DNDS/Errors.hpp | 5 +++-- src/DNDS/ExprtkWrapper.cpp | 7 ++++++ src/DNDS/MPI.cpp | 20 +++++++++++++++++ src/DNDS/MPI_bind.cpp | 12 ++++++++++ src/DNDS/Serializer/SerializerFactory.hpp | 8 ++++--- src/DNDS/Serializer/SerializerH5.cpp | 22 ++++++++++++++++++- src/DNDS/Serializer/SerializerH5.hpp | 2 +- src/DNDS/Serializer/SerializerJSON.hpp | 13 ++++++++++- src/DNDS/Serializer/Serializer_bind.hpp | 4 ++-- 12 files changed, 98 insertions(+), 21 deletions(-) diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp index 5e55c903..c25412b0 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp @@ -25,7 +25,7 @@ namespace DNDS namespace DNDS { - inline auto pybind11_ArrayEigenMatrixBatch_setitem_row(ArrayEigenMatrixBatch &self, index i, const py::list &matList) + inline void pybind11_ArrayEigenMatrixBatch_setitem_row(ArrayEigenMatrixBatch &self, index i, const py::list &matList) { using tElem = real; using tReadMap = Eigen::Map< @@ -37,7 +37,7 @@ namespace DNDS { if (!py::isinstance(v)) throw std::runtime_error("All elements must be buffer-compatible objects."); - py::buffer buf = v.cast(); + auto buf = v.cast(); auto buf_info = buf.request(false); DNDS_assert(buf_info.item_type_is_equivalent_to()); DNDS_assert_info(buf_info.shape.size() == 2, "need to pass a 2-d array"); @@ -51,7 +51,7 @@ namespace DNDS Eigen::Stride(buf_info.strides[1] / sizeof(tElem) /*col stride*/, buf_info.strides[0] / sizeof(tElem) /*row stride*/)); mat_maps.push_back(c_mat_map); } - return self.InitializeWriteRow(i, mat_maps); + self.InitializeWriteRow(i, mat_maps); } inline auto pybind11_ArrayEigenMatrixBatch_getitem_row(ArrayEigenMatrixBatch &self, index index_) @@ -85,7 +85,7 @@ namespace DNDS false); } - inline auto pybind11_ArrayEigenMatrixBatch_setitem(ArrayEigenMatrixBatch &self, std::tuple index_, const py::buffer &row) + inline void pybind11_ArrayEigenMatrixBatch_setitem(ArrayEigenMatrixBatch &self, std::tuple index_, const py::buffer &row) { using tElem = real; auto row_info = row.request(false); @@ -144,7 +144,7 @@ namespace DNDS "InitializeWriteRow", [](TArrayEigenMatrixBatch &self, index i, const py::list &matList) { - return pybind11_ArrayEigenMatrixBatch_setitem_row(self, i, matList); + pybind11_ArrayEigenMatrixBatch_setitem_row(self, i, matList); }, py::arg("i"), py::arg("matList")); ArrayEigenMatrixBatch_ @@ -171,7 +171,7 @@ namespace DNDS "__setitem__", [](TArrayEigenMatrixBatch &self, std::tuple index_, const py::buffer &row) { - return pybind11_ArrayEigenMatrixBatch_setitem(self, index_, std::move(row)); + pybind11_ArrayEigenMatrixBatch_setitem(self, index_, row); }); ArrayEigenMatrixBatch_ @@ -219,8 +219,8 @@ namespace DNDS "InitializeWriteRow", [](TPair &self, index index_, const py::buffer &row) { - return self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! - { return pybind11_ArrayEigenMatrixBatch_setitem_row(ar, iC, row); }); + self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! + { pybind11_ArrayEigenMatrixBatch_setitem_row(ar, iC, row); }); }); Pair_ @@ -236,8 +236,8 @@ namespace DNDS "__setitem__", [](TPair &self, std::tuple index_, const py::buffer &row) { - return self.runFunctionAppendedIndex(std::get<0>(index_), [&](auto &ar, index iC) //*note the auto&& reference here!!! - { return pybind11_ArrayEigenMatrixBatch_setitem(ar, std::make_tuple(iC, std::get<1>(index_)), row); }); + self.runFunctionAppendedIndex(std::get<0>(index_), [&](auto &ar, index iC) //*note the auto&& reference here!!! + { pybind11_ArrayEigenMatrixBatch_setitem(ar, std::make_tuple(iC, std::get<1>(index_)), row); }); }); } } diff --git a/src/DNDS/Defines.cpp b/src/DNDS/Defines.cpp index 4e585ff2..7719ce2c 100644 --- a/src/DNDS/Defines.cpp +++ b/src/DNDS/Defines.cpp @@ -175,7 +175,7 @@ namespace DNDS std::string GetSetVersionName(const std::string &ver) { static std::string ver_name = "UNKNOWN"; - if (ver.length()) + if (!ver.empty()) ver_name = ver; return ver_name; } diff --git a/src/DNDS/Device/DeviceStorage.cpp b/src/DNDS/Device/DeviceStorage.cpp index 0bc9c286..86d3cfe4 100644 --- a/src/DNDS/Device/DeviceStorage.cpp +++ b/src/DNDS/Device/DeviceStorage.cpp @@ -11,6 +11,10 @@ namespace DNDS void deviceStorageBase_deleter(DeviceStorageBase *p) { + // Deleter callback passed to `std::shared_ptr`; the caller guarantees + // `p` was allocated with `new`. Smart-pointer ownership is tracked + // upstream. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete p; } diff --git a/src/DNDS/Errors.hpp b/src/DNDS/Errors.hpp index 39a9d42d..ca8d7592 100644 --- a/src/DNDS/Errors.hpp +++ b/src/DNDS/Errors.hpp @@ -62,8 +62,9 @@ namespace DNDS va_start(args, info); std::cerr << getTraceString() << "\n"; std::cerr << "\033[91m DNDS_assertion failed\033[39m: \"" << expr << "\" at [ " << file << ":" << line << " ]\n"; - // NOLINTNEXTLINE(bugprone-implicit-widening-of-multiplication-result): compile-time - // constant 1024*512 = 524288 fits in int32_t; no runtime overflow is possible. + // Compile-time constant 1024 * 512 = 524288 fits in int32_t; + // no runtime overflow is possible. + // NOLINTNEXTLINE(bugprone-implicit-widening-of-multiplication-result) std::array format_buf{}; std::vsnprintf(format_buf.data(), format_buf.size(), info, args); va_end(args); diff --git a/src/DNDS/ExprtkWrapper.cpp b/src/DNDS/ExprtkWrapper.cpp index 8a71195f..7d0a8f49 100644 --- a/src/DNDS/ExprtkWrapper.cpp +++ b/src/DNDS/ExprtkWrapper.cpp @@ -17,6 +17,12 @@ namespace DNDS { this->Clear(); + // NOLINTBEGIN(cppcoreguidelines-owning-memory): the three `new`-ed + // objects (symbol_table, expression, parser) are immediately cast to + // `void*` to type-erase the exprtk dependency from the public header + // (forward-declared `_ptr_*` members). They are freed symmetrically + // in `Clear()` via the same void* handles. A `unique_ptr` would + // require re-exposing the exprtk types in the header. auto *pst = new symbol_table_t; symbol_table_t &st = *pst; _ptr_st = static_cast(pst); @@ -42,6 +48,7 @@ namespace DNDS auto *pparser = new parser_t; parser_t &parser = *pparser; _ptr_parser = static_cast(pparser); + // NOLINTEND(cppcoreguidelines-owning-memory) auto compile_ok = parser.compile(expr, exp); std::string error_info = parser.error() + "\n"; diff --git a/src/DNDS/MPI.cpp b/src/DNDS/MPI.cpp index 820ff267..67b85832 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -349,9 +349,14 @@ namespace DNDS::MPI MPI::BarrierLazy(mpi.comm, static_cast(_use_lazy_wait)); } } + // NOLINTBEGIN(bugprone-empty-catch) + // Empty catch intentional: env var contains a malformed + // number (stod/stoi throws); treat as "unset" and leave the + // default. Logging here would fail inside static-ctor phase. catch (...) { } + // NOLINTEND(bugprone-empty-catch) try { auto *ret = std::getenv("DNDS_ARRAY_STRATEGY_USE_IN_SITU"); @@ -368,9 +373,14 @@ namespace DNDS::MPI MPI_Barrier(mpi.comm); } } + // NOLINTBEGIN(bugprone-empty-catch) + // Empty catch intentional: env var contains a malformed + // number (stod/stoi throws); treat as "unset" and leave the + // default. Logging here would fail inside static-ctor phase. catch (...) { } + // NOLINTEND(bugprone-empty-catch) try { auto *ret = std::getenv("DNDS_USE_STRONG_SYNC_WAIT"); @@ -387,9 +397,14 @@ namespace DNDS::MPI MPI_Barrier(mpi.comm); } } + // NOLINTBEGIN(bugprone-empty-catch) + // Empty catch intentional: env var contains a malformed + // number (stod/stoi throws); treat as "unset" and leave the + // default. Logging here would fail inside static-ctor phase. catch (...) { } + // NOLINTEND(bugprone-empty-catch) try { auto *ret = std::getenv("DNDS_USE_ASYNC_ONE_BY_ONE"); @@ -406,9 +421,14 @@ namespace DNDS::MPI MPI_Barrier(mpi.comm); } } + // NOLINTBEGIN(bugprone-empty-catch) + // Empty catch intentional: env var contains a malformed + // number (stod/stoi throws); treat as "unset" and leave the + // default. Logging here would fail inside static-ctor phase. catch (...) { } + // NOLINTEND(bugprone-empty-catch) } CommStrategy &CommStrategy::Instance() diff --git a/src/DNDS/MPI_bind.cpp b/src/DNDS/MPI_bind.cpp index 34e56e1d..c82266db 100644 --- a/src/DNDS/MPI_bind.cpp +++ b/src/DNDS/MPI_bind.cpp @@ -11,7 +11,11 @@ namespace DNDS { py_class_ssp(m, "MPIInfo") .def(py::init<>()) + // Python side passes an opaque `uintptr_t` handle produced by + // `MPI_Comm_c2f`/ctypes; the reverse conversion back into + // `MPI_Comm` is intentionally an integer-to-pointer reinterpretation. .def(py::init([](uintptr_t pComm) + // NOLINTNEXTLINE(performance-no-int-to-ptr) { return std::make_unique(MPI_Comm(pComm)); })) .def("setWorld", &MPIInfo::setWorld) .def_readonly("rank", &MPIInfo::rank) @@ -45,6 +49,12 @@ namespace DNDS::MPI int initial_argc = static_cast(pArgv.size()); int initial_argc_mine = initial_argc; + // NOLINTBEGIN(cppcoreguidelines-owning-memory): MPI_Init_thread + // requires a mutable `char ***argv` with ownership retained by + // the caller through the lifetime of Init/Finalize. Smart + // pointers would require reshaping the MPI-C API contract. + // Paired `delete[]` at the end of this block. + // // Create an array of pointers to C-style strings: char **argv_array = new char *[initial_argc + 1]; // +1 for NULL terminator char **argv_array_mine = argv_array; @@ -67,6 +77,7 @@ namespace DNDS::MPI // Capture the modified arguments into output_args: std::vector pArgvOut; + pArgvOut.reserve(*pargc); for (int i = 0; i < *pargc; ++i) pArgvOut.emplace_back(argv_array[i]); @@ -76,6 +87,7 @@ namespace DNDS::MPI for (int i = 0; i <= initial_argc_mine; ++i) delete[] argv_array_mine[i]; // Free each string buffer delete[] argv_array_mine; // Free the pointer array + // NOLINTEND(cppcoreguidelines-owning-memory) return std::make_tuple(ret, pArgvOut); }); diff --git a/src/DNDS/Serializer/SerializerFactory.hpp b/src/DNDS/Serializer/SerializerFactory.hpp index a58f3e0f..8d9849e1 100644 --- a/src/DNDS/Serializer/SerializerFactory.hpp +++ b/src/DNDS/Serializer/SerializerFactory.hpp @@ -9,6 +9,8 @@ #include "JsonUtil.hpp" #include "DNDS/Config/ConfigParam.hpp" +#include + namespace DNDS::Serializer { /** @@ -37,7 +39,7 @@ namespace DNDS::Serializer SerializerFactory() = default; /// @brief Construct with a specific backend name; other fields stay at defaults. - SerializerFactory(const std::string &_type) : type(_type) {} + SerializerFactory(std::string _type) : type(std::move(_type)) {} DNDS_DECLARE_CONFIG(SerializerFactory) { @@ -58,7 +60,7 @@ namespace DNDS::Serializer /// @brief Instantiate the selected serializer and apply its tunables. /// @param mpi MPI context (used only by the H5 backend). - SerializerBaseSSP BuildSerializer(const MPIInfo &mpi) + [[nodiscard]] SerializerBaseSSP BuildSerializer(const MPIInfo &mpi) const { SerializerBaseSSP serializerP; if (type == "JSON") @@ -91,7 +93,7 @@ namespace DNDS::Serializer * @return Tuple `(finalFilePath, displayPath)` -- the display path is * the JSON dir or the H5 file, depending on backend. */ - std::tuple ModifyFilePath(std::string fname, const MPIInfo &mpi, const std::string &rank_part_fmt = "%06d", bool read = false) + [[nodiscard]] std::tuple ModifyFilePath(std::string fname, const MPIInfo &mpi, const std::string &rank_part_fmt = "%06d", bool read = false) const { if (type == "JSON") { diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 18e51424..a20b1f09 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -177,11 +177,19 @@ namespace DNDS::Serializer struct TraverseData { + // TraverseData is a short-lived per-call aggregate passed through the + // HDF5 H5Literate callback; the reference is intentional so that the + // callback mutates the caller's H5Contents directly. + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) H5Contents &contents; std::string current_path; bool coll_on_meta; [[nodiscard]] std::string get_indent() const { + // Brace-init `{n, ' '}` is ambiguous with std::string's + // initializer_list ctor and triggers -Wnarrowing; keep + // the explicit std::string(n, ch) form. + // NOLINTNEXTLINE(modernize-return-braced-init-list) return std::string(std::count(current_path.begin(), current_path.end(), '/') * 2, ' '); } }; @@ -251,7 +259,7 @@ namespace DNDS::Serializer // Callback for H5Aiterate (iterating attributes) static herr_t attribute_iterate_cb(hid_t obj_id, const char *attr_name, const H5A_info_t *info, void *op_data) { - TraverseData *data = static_cast(op_data); + auto *data = static_cast(op_data); data->contents.attributes.emplace_back(attr_name); // std::string full_attr_name = data->current_path + "@" + attr_name; // Common convention for attribute paths // std::cout << data->get_indent() << " Attribute: " << full_attr_name << std::endl; // Indent more for attributes @@ -297,6 +305,10 @@ namespace DNDS::Serializer else static_assert(std::is_same_v); + // `vV` is addressed via `&vV` in the non-string `if constexpr` branch + // below; clang-tidy's check misses the template instantiation for + // `T != std::string`. + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) T vV = v; if constexpr (!std::is_same_v) @@ -526,6 +538,10 @@ namespace DNDS::Serializer else static_assert(std::is_same_v); + // `vV` is addressed via `&vV` in the non-string `if constexpr` branch + // below; clang-tidy's check misses the template instantiation for + // `T != std::string`. + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) T vV = v; if constexpr (!std::is_same_v) @@ -568,6 +584,10 @@ namespace DNDS::Serializer { // Variable-length string: HDF5 will allocate memory char *attr_value = nullptr; + // H5Aread wants `void *buf`; the multi-level-implicit-pointer + // check requires an explicit reinterpret to silence the + // `char ** -> void *` conversion inside the argument parens. + // NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion) herr = H5Aread(attr_id, dtype_id, &attr_value), H5CHECK_Set; // std::cout << "Read Attribute (Variable-Length): " << attr_value << "\n"; v = attr_value; // copy as null-terminated string diff --git a/src/DNDS/Serializer/SerializerH5.hpp b/src/DNDS/Serializer/SerializerH5.hpp index 6f0ae265..a4610b3d 100644 --- a/src/DNDS/Serializer/SerializerH5.hpp +++ b/src/DNDS/Serializer/SerializerH5.hpp @@ -67,7 +67,7 @@ namespace DNDS::Serializer bool collectiveDataRW = false; public: - SerializerH5(const MPIInfo &_mpi) : SerializerBase(), mpi(_mpi) + SerializerH5(const MPIInfo &_mpi) : mpi(_mpi) { MPI_Comm_dup(mpi.comm, &commDup); } diff --git a/src/DNDS/Serializer/SerializerJSON.hpp b/src/DNDS/Serializer/SerializerJSON.hpp index 421c77f9..e3f923d3 100644 --- a/src/DNDS/Serializer/SerializerJSON.hpp +++ b/src/DNDS/Serializer/SerializerJSON.hpp @@ -94,7 +94,18 @@ namespace DNDS::Serializer ~SerializerJSON() override { - CloseFileNonVirtual(); + // Destructors must not throw; swallow any exception from the + // close path (fstream flush, bad-path cleanup). A failure here + // is reported when the user invoked CloseFile() explicitly. + // NOLINTBEGIN(bugprone-empty-catch) + try + { + CloseFileNonVirtual(); + } + catch (...) + { + } + // NOLINTEND(bugprone-empty-catch) } }; } \ No newline at end of file diff --git a/src/DNDS/Serializer/Serializer_bind.hpp b/src/DNDS/Serializer/Serializer_bind.hpp index 7500031b..8b5cd8c3 100644 --- a/src/DNDS/Serializer/Serializer_bind.hpp +++ b/src/DNDS/Serializer/Serializer_bind.hpp @@ -50,7 +50,7 @@ namespace DNDS::Serializer } inline void pybind11_SerializerJSON_define(const py::module_ &m) { - auto Serializer_ = pybind11_SerializerJSON_declare(std::move(m)); + auto Serializer_ = pybind11_SerializerJSON_declare(m); using tSerializer = SerializerJSON; Serializer_ .def(py::init<>()); @@ -65,7 +65,7 @@ namespace DNDS::Serializer } inline void pybind11_SerializerH5_define(const py::module_ &m) { - auto Serializer_ = pybind11_SerializerH5_declare(std::move(m)); + auto Serializer_ = pybind11_SerializerH5_declare(m); using tSerializer = SerializerH5; Serializer_ .def(py::init(), py::arg("mpi")); From a0f221943590c203ba4cc8488224b11cf3c23fa5 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Tue, 28 Apr 2026 23:19:07 +0800 Subject: [PATCH 34/43] docs(dev): record full clang-tidy session outcome in plan Updates docs/dev/clang_tidy_plan.md with the complete outcome of passes 6-26: - Status snapshot replaced with per-pass delta table (24 597 baseline -> 1 diagnostic end-state). - Passes 6-26 each get a "Completed" entry with commit hash, source sites, and mechanism (--fix / manual / NOLINT / disable). - TOC expanded to cover all 26 passes. - Disables table gains the two new config-level disables (Pass 7 cppcoreguidelines-macro-usage, Pass 20 cppcoreguidelines-avoid-non-const-global-variables). - New "NOLINT markers in the tree" section tabulates the ~70 targeted NOLINT markers by check. - "Next modules" section updated to reflect that DNDS is clean; the same recipe now applies to Solver, Geom, CFV, Euler, EulerP in that order. - Notes the repeated NOLINT-placement gotcha (NOLINTNEXTLINE applies to the immediately next line; rationale comments must precede the directive, not follow it; use NOLINTBEGIN/NOLINTEND when --fix can rewrite the flagged line). --- docs/dev/clang_tidy_plan.md | 507 +++++++++++++++++++++++++++++++----- 1 file changed, 439 insertions(+), 68 deletions(-) diff --git a/docs/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md index b0228f75..e15ee48b 100644 --- a/docs/dev/clang_tidy_plan.md +++ b/docs/dev/clang_tidy_plan.md @@ -10,15 +10,58 @@ and `/.clang-tidy-fix` (auto-fix profile). See ## Status snapshot -- Current module: **DNDS**. Passes 1–5 complete, pass 6 deferred - (engineering task), pass 7 deferred (judgement per site). -- Latest total: **19 390 diagnostics, 46 distinct checks** (down from - the 24 597 baseline, 21 % reduction). The remaining volume is - dominated by checks still scheduled for triage in the KA bucket - (non-private member vars, magic numbers, pointer arithmetic, - varargs, reinterpret_cast) which will drop to zero in one - config-only commit. -- Baseline: `build/clang-tidy-logs/baseline-dnds.txt`. +- Current module: **DNDS**. **All planned passes complete.** + Pass 6 (special-member-functions), pass 7 (macro-usage), + pass 8 (redundant-casting), pass 9 (member-init), + pass 10 (nullptr), pass 11 (emplace), pass 12 (equals-default), + pass 13 (qualified-auto), pass 14 (named-parameter), + pass 15 (simplify-boolean), pass 16 (unnecessary-value-param), + pass 17 (loop-convert), pass 18 (prefer-member-init), + pass 19 (cstyle-cast), pass 20 (non-const-global — KA disable), + pass 21 (c-arrays), pass 22 (unhandled-self-assign), + pass 23 (branch-clone), pass 24 (implicit-widening), + pass 25 (rvalue-ref-not-moved), pass 26 (long-tail sweep). +- **Latest total: 1 diagnostic** (a `clang-diagnostic-error` from + `omp.h` inside Eigen's PCH — unrelated to DNDS code; cannot be + fixed without installing `libomp--dev` matched to the + clang-tidy / clangd binary version). +- **Original baseline: 24 597 diagnostics across 51 distinct checks** + (captured in `build/clang-tidy-logs/baseline-dnds.txt`). +- **End-state delta: –99.996 %** (or "effectively zero") on total + diagnostics; 51 → 1 distinct checks. + +Summary counts after each pass (DNDS only, all-TU): + +| After | Total | Distinct | Delta | +|---|---:|---:|---:| +| Baseline | 24 597 | 51 | — | +| Pass 1 (macro-parentheses NOLINT) | 24 067 | 50 | −530 | +| Pass 2 (nodiscard --fix) | 22 495 | 49 | −1 572 | +| Pass 3 (init-variables --fix) | 21 297 | 49 | −1 198 | +| Pass 4 (missing-std-forward disable + YAML trap) | 20 794 | 47 | −503 | +| Pass 5 (reserved-identifier rename) | 19 390 | 46 | −1 404 | +| KA bucket disables | 7 341 | 35 | −12 049 | +| Pass 6 (special-member-functions) | 5 691 | 34 | −1 650 | +| Pass 7 (macro-usage disable) | 3 581 | 33 | −2 110 | +| Pass 8 (redundant-casting --fix) | 2 805 | 32 | −776 | +| Pass 9 (member-init --fix) | 2 330 | 31 | −475 | +| Pass 10 (use-nullptr --fix) | 2 099 | 30 | −231 | +| Pass 11 (use-emplace --fix) | 1 987 | 29 | −112 | +| Pass 12 (use-equals-default --fix) | 1 928 | 28 | −59 | +| Pass 13 (qualified-auto --fix) | 1 876 | 27 | −52 | +| Pass 14 (named-parameter --fix) | 1 654 | 26 | −222 | +| Pass 15 (simplify-boolean --fix + manual) | 1 499 | 25 | −155 | +| Pass 16 (unnecessary-value-param) | 1 180 | 24 | −319 | +| Pass 17 (loop-convert --fix + NOLINT) | 1 031 | 23 | −149 | +| Pass 18 (prefer-member-initializer --fix) | 971 | 23 | −60 | +| Pass 19 (cstyle-cast manual) | 847 | 22 | −124 | +| Pass 20 (non-const-global disable) | 523 | 21 | −324 | +| Pass 21 (avoid-c-arrays) | 455 | 20 | −68 | +| Pass 22 (unhandled-self-assign) | 402 | 19 | −53 | +| Pass 23 (branch-clone NOLINTBEGIN/END) | 80 | 18 | −322 | +| Pass 24 (implicit-widening NOLINT) | 14 | 14 | −66 | +| Pass 25 (rvalue-ref move) | 13 | 13 | −1 | +| Pass 26 (long-tail sweep) | 1 | 1 | −12 | ## Table of contents @@ -32,9 +75,29 @@ and `/.clang-tidy-fix` (auto-fix profile). See 4. [Pass 4 — cppcoreguidelines-missing-std-forward](#pass-4--cppcoreguidelines-missing-std-forward) 5. [Pass 5 — bugprone-reserved-identifier](#pass-5--bugprone-reserved-identifier) 6. [Pass 6 — cppcoreguidelines-special-member-functions](#pass-6--cppcoreguidelines-special-member-functions) - 7. [Pass 7 — bugprone-implicit-widening-of-multiplication-result](#pass-7--bugprone-implicit-widening-of-multiplication-result) + 7. [Pass 7 — cppcoreguidelines-macro-usage](#pass-7--cppcoreguidelines-macro-usage) + 8. [Pass 8 — readability-redundant-casting](#pass-8--readability-redundant-casting) + 9. [Pass 9 — cppcoreguidelines-pro-type-member-init](#pass-9--cppcoreguidelines-pro-type-member-init) + 10. [Pass 10 — modernize-use-nullptr](#pass-10--modernize-use-nullptr) + 11. [Pass 11 — modernize-use-emplace](#pass-11--modernize-use-emplace) + 12. [Pass 12 — modernize-use-equals-default](#pass-12--modernize-use-equals-default) + 13. [Pass 13 — readability-qualified-auto](#pass-13--readability-qualified-auto) + 14. [Pass 14 — readability-named-parameter](#pass-14--readability-named-parameter) + 15. [Pass 15 — readability-simplify-boolean-expr](#pass-15--readability-simplify-boolean-expr) + 16. [Pass 16 — performance-unnecessary-value-param](#pass-16--performance-unnecessary-value-param) + 17. [Pass 17 — modernize-loop-convert](#pass-17--modernize-loop-convert) + 18. [Pass 18 — cppcoreguidelines-prefer-member-initializer](#pass-18--cppcoreguidelines-prefer-member-initializer) + 19. [Pass 19 — cppcoreguidelines-pro-type-cstyle-cast](#pass-19--cppcoreguidelines-pro-type-cstyle-cast) + 20. [Pass 20 — cppcoreguidelines-avoid-non-const-global-variables](#pass-20--cppcoreguidelines-avoid-non-const-global-variables) + 21. [Pass 21 — cppcoreguidelines-avoid-c-arrays](#pass-21--cppcoreguidelines-avoid-c-arrays) + 22. [Pass 22 — bugprone-unhandled-self-assignment](#pass-22--bugprone-unhandled-self-assignment) + 23. [Pass 23 — bugprone-branch-clone](#pass-23--bugprone-branch-clone) + 24. [Pass 24 — bugprone-implicit-widening-of-multiplication-result](#pass-24--bugprone-implicit-widening-of-multiplication-result) + 25. [Pass 25 — cppcoreguidelines-rvalue-reference-param-not-moved](#pass-25--cppcoreguidelines-rvalue-reference-param-not-moved) + 26. [Pass 26 — long-tail sweep](#pass-26--long-tail-sweep) 5. [Disables applied](#disables-applied) -6. [Next modules](#next-modules) +6. [NOLINT markers in the tree](#nolint-markers-in-the-tree) +7. [Next modules](#next-modules) --- @@ -366,58 +429,332 @@ Commit: see `git log`. ### Pass 6 — cppcoreguidelines-special-member-functions -**Outcome.** Deferred. Still 1 645 hits after the preceding passes -(no overlap with anything else). - -**Scope assessed.** 1 645 hits collapse to **32 unique class -declarations**, each matching the pattern "defines a copy constructor -and a copy assignment operator but does not define a destructor, a -move constructor or a move assignment operator" — i.e. the classes -are implicitly copy-only. Affected classes include `Array`, -`ArrayAdjacency`, `ArrayDof`, `ArrayEigenMatrix*`, `ArrayTransformer`, -`CommStrategy`, `DeviceStorageBase`, and ~20 others in the DNDS -subtree. - -**Why deferred.** The canonical tidy is to add the three missing -members as `= default`, but doing so **changes the public API**: -classes that were implicitly copy-only become move-enabled, so -callers using `std::move(x)` get a move where they previously got a -copy. The existing copy ctor on every flagged class does a deep -`clone(R)`; memberwise move would instead shallow-move the -`shared_ptr` / `host_device_vector` / `std::vector` members, which -is almost certainly the *right* behaviour but is a semantic change -the author should sanction class-by-class. - -Auto-fix would also require choosing between `= default` and -`= delete` per class. The check does not implement `--fix` and -clang-tidy cannot infer the right choice. - -**Recommendation.** Schedule a dedicated session that: - -1. Audits each of the 32 classes for owning members (raw pointers, - custom deleters, virtual destructors). -2. Adds all five special members explicitly (`= default`/`= delete`) - per class, keeping the existing copy-ctor / copy-assign semantics - but picking the appropriate move semantics. -3. Builds after each batch of 5-6 classes to catch any - move-semantic regression early. -4. Adds targeted tests where the copy-vs-move distinction matters - (e.g. `Array` move should drop the source's row-start pointer, - not deep-copy it). - -This is an engineering task on its own, not a "tidy pass." - -### Pass 7 — bugprone-implicit-widening-of-multiplication-result - -**Outcome.** Not reached — the check fell out of the top-20 after the -preceding passes and is interleaved with real semantic decisions on -each site. Deferred to a future session. - -**Notes.** After passes 1-5 the residue is ~298 hits across the -project (count before passes; DNDS alone carries some). Each site -needs a judgement call: is the `int * int` result at risk of 32-bit -overflow once extents grow? If yes, cast the operand; if not, -silence. A mass rewrite is unsafe. +**Outcome.** 1 645 → 0. Completed as one commit (`1880d48`). + +**Per-class audit.** The 1 645 warnings collapsed to ~20 distinct +class declarations, each bucketed into one of four categories with +an explicit rule-of-five closure: + +1. **Value-semantic classes** (members are all `shared_ptr`, + `host_device_vector`, POD, or `std::vector`): add + `= default` move ctor, move assign, and destructor alongside + the existing custom copy. Default move is a shallow transfer + of the shared handles — correct and observably identical to + "copy source + reset source" on the moved-from side. + Classes: `Array`, `ArrayAdjacency`, `ArrayDof`, `ArrayEigenMatrix`, + `ArrayEigenMatrixBatch`, `ArrayEigenUniMatrixBatch`, + `ArrayEigenVector`, `ArrayTransformer`, `ParArray`, + `AdjacencyRow`, `RowView`, `EmptyNoDefault`, + `host_device_vector_r0`, `host_device_vector_r1`, and their + nested `iterator` classes. +2. **Polymorphic RAII bases** (virtual dtor, owns file handle / + MPI handle / opaque exprtk pointer): `= delete` copy and move + to prevent slicing and double-close. + Classes: `SerializerBase`, `SerializerJSON`, `SerializerH5`, + `DeviceStorageBase`, `DeviceHostSingleAllocationBase`, + `DeviceHostSingleAllocationDirect`, `ExprtkWrapperEvaluator`. +3. **Classic singletons** (old pre-C++11 private-unimplemented + idiom): replace with `= delete` copy/move + `= default` dtor. + Classes: `CommStrategy`, `MPIBufferHandler`, `ResourceRecycler`, + `PerformanceTimer`. +4. **Resource-registry holders** (register `this` with + `ResourceRecycler` by raw pointer): `= delete` copy/move to + prevent registering the same `this` twice. + Classes: `MPIReqHolder`, `MPITypePairHolder`. + +Every declaration carries a one-line rationale comment explaining +which bucket the class falls in and why the chosen semantics are +correct. + +### Pass 7 — cppcoreguidelines-macro-usage + +**Outcome.** 2 110 → 0. Config-only disable commit (`9c71e4b`). + +All 41 distinct macros in DNDS are legitimate uses that a +`constexpr` template function cannot express: + +- Assertions / checks capturing `__FILE__` / `__LINE__` + (`DNDS_assert*`, `DNDS_check_throw*`, `DNDS_HD_assert*`). +- Code-generation DSLs that declare class members, static data, + or JSON binding glue (`DNDS_DECLARE_CONFIG`, `DNDS_FIELD`, + `DNDS_json_to_config`, `DNDS_NLOHMANN_DEFINE_*`, + `pybind11_bind_*`, `DNDS_DEVICE_TRIVIAL_COPY_DEFINE*`). +- Platform probes / define-before-include switches + (`MPICH_SKIP_MPICXX`, `OMPI_SKIP_MPICXX`, + `EIGEN_DONT_PARALLELIZE`). +- Intrinsic / attribute wrappers (`DNDS_likely`, `DNDS_unlikely`, + `DNDS_FORCEINLINE`, `DISABLE_WARNING` via `_Pragma`). +- CMake-injected constants (`DNDS_VERSION_STRING`). + +The full list is in the `.clang-tidy` header disables table. + +### Pass 8 — readability-redundant-casting + +**Outcome.** 776 → 0 via single-check `--fix` run (`742119d`). + +Three patterns auto-fixed: +- `MPI_Datatype(MPI_FLOAT)` etc. (OpenMPI constants are already + `MPI_Datatype`, so the functional cast is a no-op) — 11 sites + in `MPI.hpp`. +- `reinterpret_cast(uint8_t *)` identity casts in + `Device/DeviceStorage.cpp` — 3 sites. +- `index(nSend)` where `nSend` is already `index` in + `ArrayTransformer.hpp`. + +### Pass 9 — cppcoreguidelines-pro-type-member-init + +**Outcome.** 475 → 0 via `--fix` + 1 manual fix (`173ad1a`). + +Raw class members (`index _size`, `rowsize Row_size`, +`MPI_Aint pushSendSize`, `ConfigTypeTag typeTag`, `tStart` +array) gained `{}` default initializers. Local `std::array` scratch +buffers in `MPI.cpp`, `SerializerH5.cpp`, `Defines.cpp`, +`ArrayEigenMatrix.hpp`, `ArrayTransformer.hpp` zero-init'd the same way. + +Manual fix: auto-fix emitted `struct winsize w { };` on two lines; +reformatted inline to `struct winsize w{};`. + +### Pass 10 — modernize-use-nullptr + +**Outcome.** 231 → 0 via `--fix` (`6c494f4`). + +`(T *)(NULL)` → `(T *)nullptr` in the past-the-end row inquiry +(`ArrayBasic.hpp`), `getenv()` comparisons (`MPI.hpp`, `MPI.cpp`), +and HDF5 handle probes (`SerializerH5.cpp`). + +### Pass 11 — modernize-use-emplace + +**Outcome.** 112 → 0 via `--fix` (`56c43af`). + +`push_back(std::make_pair(r, dtype))` → `emplace_back(r, dtype)` +in MPI type-pair vector appends (2 sites in +`ArrayTransformer.hpp`) and HDF5 dimension vectors +(`SerializerH5.cpp`), plus `push_back(std::string(...))` → +`emplace_back(...)` in `MPI.cpp` / `MPI_bind.cpp`. + +### Pass 12 — modernize-use-equals-default + +**Outcome.** 59 → 0 via `--fix` (`9231ae6`). + +Two sites (duplicated across TUs): +`DeviceHostSingleAllocationBase::~DeviceHostSingleAllocationBase() {}` and +`DeviceHostSingleAllocationDirect::~DeviceHostSingleAllocationDirect() +override {}` → `= default`. + +### Pass 13 — readability-qualified-auto + +**Outcome.** 52 → 0 via `--fix` (`7586d2f`). + +`auto ptr = reinterpret_cast(...)` → `auto *ptr = ...` in +pybind11 binding helpers; `for (auto &[k, v] : map.items())` → +`for (const auto &[k, v] : map.items())` in `SerializerJSON.cpp`. + +### Pass 14 — readability-named-parameter + +**Outcome.** 222 → 0 via `--fix` (`0bf9edd`). + +Added `/*unused*/` on tag-dispatch parameters +(`std::index_sequence /*unused*/`) in the pybind11 +binding machinery. + +### Pass 15 — readability-simplify-boolean-expr + +**Outcome.** 155 → 0 via `--fix` + 1 manual (`e07c315`). + +`!(a && b)` / `!(a || b)` DeMorgan expansions in `ArrayDOF.hpp` +(SFINAE `enable_if`), `ArrayDOF_bind.hpp` (`if constexpr`), +`EigenUtil.hpp` (ternary condition), and +`Defines.hpp::checkedIndexTo32` (manual — auto-fix emitted 66 +duplicates into header-include paths). + +### Pass 16 — performance-unnecessary-value-param + +**Outcome.** 319 → 0 via `--fix` + manual sed (`b2019a7`). + +`py::buffer row` by value → `const py::buffer &row` in 15 +pybind11 `setitem` / operator overloads across the 5 +`_bind.hpp` headers. Two additional sites in `Serializer_bind.hpp` +(`py::object options_in`) and `MPI_bind.cpp` (`Allreduce` +`py_sendbuf` / `py_recvbuf`). + +### Pass 17 — modernize-loop-convert + +**Outcome.** 149 → 0 via `--fix` + NOLINTBEGIN/END (`9a88b36`). + +Auto-fix converted two index-based loops over `std::vector` +in `ArrayRedistributor.hpp` to range-based form. One site in +`Array_bind.hpp` (`for (ssize_t i = 0; i < pullIndexGlobal.size(); +i++) pullIndexVec.push_back(pullIndexGlobal.at(i));`) carries +`NOLINTBEGIN / NOLINTEND` — pybind11 `array_t` iterators yield +`pybind11::handle`, not `long`, and the explicit index-based form +is required for the numpy-to-long conversion. + +A plain NOLINTNEXTLINE is insufficient: `--fix` rewrites the +`for` line itself, erasing the preceding comment. Block-form +NOLINT survives. + +### Pass 18 — cppcoreguidelines-prefer-member-initializer + +**Outcome.** 60 → 0 via `--fix` (`a3cb4a5`). + +One unique site: `MPIInfo::MPIInfo(MPI_Comm ncomm)` — moved +`comm = ncomm` from the body into the member-initializer list. + +### Pass 19 — cppcoreguidelines-pro-type-cstyle-cast + +**Outcome.** 124 → 0 via 6 manual edits (`226ead9`). + +Four sites in `SerializerH5.cpp` / `SerializerJSON.cpp`: +`(ssp *)(pth_2_ssp[refPath])` → explicit +`reinterpret_cast *>(...)` on the type-erased dedup +registry (matches the author TODO). + +Two sites in `MPI.hpp`: `MPI_IN_PLACE` expands to the +OpenMPI-defined `((void *)1)` sentinel. `NOLINTNEXTLINE` placed +*immediately* above the offending `Allreduce(...)` call — multi- +line rationale comments between the NOLINT and the offending +line break the suppression (same trap as Pass 17). + +### Pass 20 — cppcoreguidelines-avoid-non-const-global-variables + +**Outcome.** 324 → 0 via config disable (`c407cff`). + +Every global mutable in DNDS is intentional and cannot be made +`const`, `thread_local`, or class-scoped without wider redesign: +`logStream`, `useCout`, `outputDelim`, `HDF_mutex`, `isDebugging`, +`EigenPCH_tag`, `ExprtkPCH_tag`. Full rationale in the +`.clang-tidy` header table. + +### Pass 21 — cppcoreguidelines-avoid-c-arrays + +**Outcome.** 68 → 0 via 2 manual edits (`1d60a57`). + +Two sites, both stack scratch buffers for printf-family calls: +- `Errors.hpp::genFatalErrorMessage`: `char format_buf[1024*512]` + → `std::array format_buf{}`. +- `SerializerFactory.hpp`: `char BUF[512]` → `std::array`. + +### Pass 22 — bugprone-unhandled-self-assignment + +**Outcome.** 53 → 0 via 1 manual edit (`dd31508`). + +`AdjacencyRow::operator=(const AdjacencyRow &r)` called +`std::copy(r.cbegin(), r.cend(), p_indices)`. On self-assign, +source and destination ranges are fully overlapping — UB. +Added `if (this == &r) return;` early-return guard with a +comment explaining the UB. + +### Pass 23 — bugprone-branch-clone + +**Outcome.** 322 → 0 via NOLINTBEGIN / NOLINTEND blocks and +rationale comments (`10f5305`). + +All 7 unique sites are intentional — the diagnostic fires where +two logically distinct branches happen to produce the same code: +- `ArrayBasic.hpp`, `Array.hpp` — `if constexpr` cascades over + `_dataLayout`. `TABLE_Fixed` and `TABLE_Max` currently both + compute `iRow * _row_size_dynamic + iCol`; the layouts are + conceptually distinct (padded rows may diverge). +- `ArrayEigenUniMatrixBatch.hpp`, `ArrayEigenUniMatrixBatch_DeviceView.hpp`, + `EigenUtil.hpp::MatrixFMTSafe` — ternary for the Eigen + `options` template parameter; both non-row-vector arms + intentionally select `ColMajor`. +- `Config/ConfigParam.hpp` — switch over `ConfigTypeTag` → JSON + Schema type strings; several enums collapse to the same built-in + (`Enum`→"string", `ArrayOfObjects`→"array", + `MapOfObjects`→"object"). + +NOLINTBEGIN/END is required because clang-tidy reports the +diagnostic at the first clone of the pair; NOLINTNEXTLINE on one +line of the pair was insufficient. + +### Pass 24 — bugprone-implicit-widening-of-multiplication-result + +**Outcome.** 66 → 0 via 1 NOLINT (`583ab45`). + +One source site: `std::array` in `Errors.hpp`. +Compile-time constant `524 288` trivially fits in `int32_t`; the +widening to `size_t` happens at compile time during template +argument deduction. Spurious diagnostic; NOLINTNEXTLINE with +rationale. + +### Pass 25 — cppcoreguidelines-rvalue-reference-param-not-moved + +**Outcome.** 44 → 0 via 1 edit (`6c90a62`). + +`ArrayDofDeviceView(t_base &&base_view) : t_base(base_view) {}` +and the `Const` variant: `base_view` inside the body is an +lvalue, so `t_base(base_view)` silently copied. Added +`std::move(base_view)` on the base-init to perform the intended +move. + +### Pass 26 — long-tail sweep + +**Outcome.** 12 warnings across 10 distinct checks → 0 via 13 +edits (`0d6d0d9`). Drains every remaining check to zero: + +- `performance-move-const-arg` (3 sites): removed no-op + `std::move` of `const py::buffer &` / `const py::module_ &`. +- `readability-avoid-return-with-void-value` (5 sites): changed + pybind11 setitem helpers from `auto` (deduced void) to + explicit `void`, dropped `return` from the wrapping lambdas. +- `readability-make-member-function-const` (2 sites): made + `SerializerFactory::BuildSerializer` / `::ModifyFilePath` + `const`. +- `bugprone-empty-catch` (4 sites): four env-var parse catches in + `CommStrategy::CommStrategy` guarded with NOLINTBEGIN/END. +- `readability-redundant-member-init`: dropped `: SerializerBase()` + in `SerializerH5.hpp`. +- `modernize-pass-by-value`: `SerializerFactory(const std::string &)` + → `SerializerFactory(std::string _type) : type(std::move(_type)) {}`. +- `modernize-use-auto` (2 sites): `py::buffer buf = v.cast<...>()` + → `auto buf = ...`; `TraverseData *data = static_cast<...>` + → `auto *data`. +- `readability-container-size-empty`: `ver.length()` → + `!ver.empty()` in `Defines.cpp`. +- `bugprone-exception-escape`: `~SerializerJSON()` wraps + `CloseFileNonVirtual()` in `try/catch` — destructors mustn't + throw (NOLINTBEGIN/END guards the empty catch). +- `modernize-return-braced-init-list`: kept `return std::string(n, ch)` + in `SerializerH5.cpp::get_indent` with NOLINTNEXTLINE — brace + init is ambiguous with `initializer_list` and triggers + `-Wnarrowing`. +- `performance-unnecessary-copy-initialization`: NOLINT on + `T vV = v` in `SerializerH5.cpp` (clang-tidy misses that the + variable is addressed via `&vV` in the non-string `if constexpr` + branch). +- `cppcoreguidelines-avoid-const-or-ref-data-members`: NOLINT on + `H5Contents &contents` in the `TraverseData` per-call aggregate. +- `performance-no-int-to-ptr`: NOLINT on `MPI_Comm(pComm)` — + Python side passes an opaque `uintptr_t`. +- `performance-inefficient-vector-operation`: added + `pArgvOut.reserve(*pargc)` before the `emplace_back` loop in + `MPI_bind.cpp`. +- `bugprone-multi-level-implicit-pointer-conversion`: NOLINT on + `H5Aread(attr_id, dtype_id, &attr_value)` where `attr_value` + is `char *` — HDF5 wants `void *buf` and explicit + `static_cast` does not silence the check. +- `cppcoreguidelines-owning-memory` (7 sites): NOLINT on + shared-pointer deleter callback (`DeviceStorage.cpp`), opaque + exprtk pointers (`ExprtkWrapper.cpp`), and MPI_Init argv + allocation (`MPI_bind.cpp`) with rationale. + +### NOLINT placement: a repeated gotcha + +Every auto-fix pass in this session re-taught the same lesson: + +- `NOLINTNEXTLINE(check)` applies to the line *immediately* + following the directive. Rationale comments must come *before* + the directive, not between it and the offending code, otherwise + the NOLINT applies to the rationale line. +- When `--fix` can rewrite the flagged line (e.g. `modernize-loop-convert`, + `modernize-return-braced-init-list`), use + `NOLINTBEGIN(check) ... NOLINTEND(check)` around the + preserved block — the directive line survives the rewrite. +- For `switch` / `?:` / chained `if / else if` with + `bugprone-branch-clone`, the diagnostic is reported at the first + clone of the pair, which may not match the line the edit would + change. NOLINTBEGIN/END is the safe form. ## Disables applied @@ -438,22 +775,56 @@ triage table above): | `readability-redundant-access-specifiers` | Repeated `public:` is a project convention. | | `modernize-use-transparent-functors` | Eigen expression templates break with `std::less<>` etc. | | `cppcoreguidelines-c-copy-assignment-signature` | Duplicate of `misc-unconventional-assign-operator`. | +| `cppcoreguidelines-macro-usage` | All 41 DNDS macros require `__FILE__`/`__LINE__` capture, token pasting, code generation, or define-before-include semantics (Pass 7). | +| `cppcoreguidelines-avoid-non-const-global-variables` | Every DNDS global mutable (`logStream`, `useCout`, `outputDelim`, `HDF_mutex`, `isDebugging`, PCH tags) is intentional; redesign out of scope for a tidy pass (Pass 20). | Rationale comments live in the `.clang-tidy` file header, not inside the `Checks: >` folded scalar (see Pass 4 / YAML trap). +## NOLINT markers in the tree + +The tidy session left ~70 targeted `NOLINT` markers. All of them +pair with a rationale comment. Representative breakdown: + +| Check | Count | Notable sites | +|---|---:|---| +| `bugprone-branch-clone` | 5 | `ArrayBasic.hpp`, `Array.hpp`, Eigen options ternaries, `ConfigParam.hpp` switch | +| `cppcoreguidelines-owning-memory` | 4 | `Device/DeviceStorage.cpp` deleter, `ExprtkWrapper.cpp` opaque new, `MPI_bind.cpp` argv alloc | +| `bugprone-empty-catch` | 5 | 4x `CommStrategy::CommStrategy` env-var parse, 1x `~SerializerJSON` | +| `modernize-loop-convert` | 1 | `Array_bind.hpp` pybind11 array_t iteration | +| `cppcoreguidelines-pro-type-cstyle-cast` | 2 | `MPI_IN_PLACE` (OpenMPI `(void*)1` sentinel) | +| `bugprone-implicit-widening-of-multiplication-result` | 1 | `Errors.hpp` compile-time `1024*512` | +| `modernize-return-braced-init-list` | 1 | `SerializerH5.cpp` `std::string(n, ch)` vs `initializer_list` overload | +| `performance-unnecessary-copy-initialization` | 2 | `SerializerH5.cpp` `T vV = v` used in non-string `if constexpr` branch | +| `cppcoreguidelines-avoid-const-or-ref-data-members` | 1 | `TraverseData` H5Literate callback state | +| `performance-no-int-to-ptr` | 1 | `MPI_Comm(pComm)` in pybind11 ctor | +| `bugprone-multi-level-implicit-pointer-conversion` | 1 | `H5Aread(..., &char_ptr)` | +| `bugprone-macro-parentheses` | 3 blocks | Unparenthesizable type-name / storage-class args | + ## Next modules -Once DNDS is "clean" (= all KF passes done, all KA disables applied, -all KS NOLINTs in place), repeat the recipe for: +**DNDS is now clean.** Every check with >= 1 actionable instance +has been driven to zero, either by fixing the code or by adding a +rationale-commented `NOLINT` / `.clang-tidy` disable. The sole +remaining diagnostic is a `clang-diagnostic-error` on `omp.h` +inside Eigen's PCH, which is an Eigen-internal include path issue +unrelated to DNDS source. + +Repeat the recipe for the other modules in this order: 1. `src/Solver/` — small, limited blast radius; good next target. 2. `src/Geom/` — largest module; expect a new set of checks specific - to mesh connectivity loops. Start with `clang-analyzer-optin.cplusplus.VirtualCall` - per the historical note in `.clang-tidy`. + to mesh connectivity loops. Start with + `clang-analyzer-optin.cplusplus.VirtualCall` per the historical + note in `.clang-tidy`. 3. `src/CFV/` — follows Geom closely. 4. `src/Euler/`, `src/EulerP/` — solver layer; `bugprone-branch-clone` - will matter here (exhaustive `if/else` cascades for enum handling). + will matter here (exhaustive `if / else` cascades for enum handling). + +The `.clang-tidy` disables and NOLINT placement lessons carry +forward unchanged. Any new module-specific disables should be +appended to the header table, not inside the `Checks:` block +(YAML folded scalars eat `#` as literal text). The `.clang-tidy` disables carry forward unchanged. Any new module- specific disables should be appended in the same table at the top of From 983b823e3b642facec2b42e8ffab7539d3ada420 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Wed, 29 Apr 2026 01:49:00 +0800 Subject: [PATCH 35/43] docs: record DNDS clang-tidy sanitation status Surfaces the sanitation outcome in the two places agents and new contributors check first: - docs/guides/style_guide.md gains a "Per-module sanitation status" table (DNDS clean, others not started), a NOLINT-placement rule (NOLINTNEXTLINE applies to the immediately next line; use NOLINTBEGIN/END when --fix can rewrite the flagged line), and a pointer to docs/dev/clang_tidy_plan.md for the 26-pass record. - AGENTS.md gains a "Clang-tidy sanitation" subsection under Code Style noting DNDS is clean, pointing at the plan doc, and listing the module order (Solver, Geom, CFV, Euler, EulerP) for the same recipe. Numbers mentioned: 24 597 -> 1 diagnostics across 26 passes. The remaining diagnostic is the Eigen PCH omp.h include issue, unrelated to DNDS source. --- AGENTS.md | 13 +++++++++++++ docs/guides/style_guide.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a7aefc97..261e8a33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -275,6 +275,19 @@ Quick reference for Python: - `snake_case` functions/variables; C++ wrapper classes match C++ name - Plain `assert`; `@pytest.fixture` for MPI; numpy for array comparisons +### Clang-tidy sanitation + +DNDS is clean as of 2026-04-29 (26-pass cleanup, 24 597 → 1 +diagnostics; the remaining one is an unrelated Eigen PCH +`omp.h` include issue). Full per-pass record, `.clang-tidy` +disable rationale, and NOLINT placement gotchas: +**`docs/dev/clang_tidy_plan.md`**. + +Other modules (`Solver`, `Geom`, `CFV`, `Euler`, `EulerP`) are +not yet sanitised. Apply the same recipe in that order. Run +`scripts/run_clang_tidy.py ` to get the per-check +histogram; the `.clang-tidy` disables carry forward unchanged. + ## Geom Module Architecture Mesh connectivity, ghost management, and the build pipeline are documented diff --git a/docs/guides/style_guide.md b/docs/guides/style_guide.md index f0531372..b1919965 100644 --- a/docs/guides/style_guide.md +++ b/docs/guides/style_guide.md @@ -118,6 +118,37 @@ excludes `.cu` files by default; headers included from CUDA TUs are still tidied transitively via their `.cpp` includers. Pass `--include-cu` to opt in anyway. +**NOLINT placement.** `NOLINTNEXTLINE(check)` applies to the +*immediately following* line. Put any rationale comment *before* +the NOLINT directive, not between it and the offending code. When +`--fix` can rewrite the flagged line, use block-form +`NOLINTBEGIN(check) ... NOLINTEND(check)` so the directive +survives the rewrite. Every NOLINT marker in the tree is paired +with a rationale comment explaining why the check is wrong or +inapplicable at that site. + +**Per-module sanitation status.** + +| Module | Status | Diagnostics | +|---|---|---:| +| `src/DNDS/` | Clean (2026-04-29) | 1 (unrelated Eigen PCH `omp.h`) | +| `src/Solver/` | Not started | — | +| `src/Geom/` | Not started | — | +| `src/CFV/` | Not started | — | +| `src/Euler/` | Not started | — | +| `src/EulerP/` | Not started | — | + +The full cleanup history for DNDS — 26 passes, 24 597 → 1 +diagnostics — is recorded in +[`docs/dev/clang_tidy_plan.md`](../dev/clang_tidy_plan.md). +That document is the reference for the per-pass recipe, the +`.clang-tidy` disable rationales, and the NOLINT placement +gotchas. Use the same recipe for the other modules in the order +Solver → Geom → CFV → Euler → EulerP. The existing `.clang-tidy` +disables carry forward unchanged; any new module-specific disables +go in the file-header table, not inside the `Checks:` folded +scalar. + ### C++ Docstrings (Doxygen) Source comments are processed by **Doxygen** (for the standalone C++ API docs) From 5ed4ad718f9fda1d9f152e44a08934268a41ad9c Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 18:23:54 +0800 Subject: [PATCH 36/43] =?UTF-8?q?docs(dev):=20v2=20distributed=20reorder?= =?UTF-8?q?=20design=20=E2=80=94=20dynamic=20registry,=20callback=20compan?= =?UTF-8?q?ions,=20follow=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete redesign of the distributed entity reordering plan: - Two-layer architecture: ReorderRegistry (callbacks) + ReorderPlan (standalone) - buildReorderRegistry() produces dynamic adj/companion set from mesh members - External code (solver, evaluator) extends registry before plan.apply() - Callback-based companion relocation (type-erased, no variant vectors) - Follow propagation: Node/Bnd follow Cell by default (min-rank rule) - Formal adj classification: SKIP/RELOCATE/REMAP/RELOCATE_REMAP/SELF - PermutationTransfer utility (fromPartition, fromLocalPermutation) - 5 concrete use cases including solver participation and node-only reorder - 5-phase implementation plan --- docs/dev/distributed_reorder_design.md | 606 ++++++++++++++++++++++--- 1 file changed, 540 insertions(+), 66 deletions(-) diff --git a/docs/dev/distributed_reorder_design.md b/docs/dev/distributed_reorder_design.md index 3e0f7109..cfaf2864 100644 --- a/docs/dev/distributed_reorder_design.md +++ b/docs/dev/distributed_reorder_design.md @@ -696,9 +696,85 @@ After migration, the existing helpers become dead code. ## ReorderEntities -- Top-Level Algorithm -### API +### API (two-layer design) ```cpp +// ================================================================= +// Companion callback: type-erased row relocation for non-adj arrays +// ================================================================= + +/// Callback invoked during the RELOCATE phase for a companion array. +/// The framework passes the PermutationTransfer for the entity kind +/// that this companion belongs to. The callback calls transferRows +/// on its own array. +using CompanionRelocateFn = std::function; + +/// One registered companion entry. +struct CompanionEntry +{ + EntityKind kind; // entity kind this array is parallel to + CompanionRelocateFn fn; // callback to relocate the array + std::string name; // diagnostic name (optional) +}; + +// ================================================================= +// Adj entry: type-erased adjacency operation +// ================================================================= + +/// Callback invoked during the REMAP phase for an adjacency array. +/// The framework passes the LookupResult for the target entity kind. +/// The callback calls ConvertAdjEntries on its own array. +using AdjRemapFn = std::function; + +/// Callback invoked during the RELOCATE phase for an adjacency array. +using AdjRelocateFn = std::function; + +/// One registered adjacency entry. +struct AdjEntry +{ + AdjKind kind; // adjacency kind (from -> to) + AdjRemapFn remapFn; // remap entries (may be null if no remap needed) + AdjRelocateFn relocateFn; // relocate rows (may be null if no relocate needed) + std::string name; // diagnostic name (optional) +}; + +// ================================================================= +// ReorderRegistry: the dynamic set of arrays to operate on +// ================================================================= + +/// Collects all adjacencies and companions that participate in a reorder. +/// Built by UnstructuredMesh::buildReorderRegistry() for mesh members, +/// and extended by external code for solver/evaluator arrays. +struct ReorderRegistry +{ + /// All adjacency entries (mesh members + external). + std::vector adjs; + + /// All companion entries (mesh members + external). + std::vector companions; + + /// Global offsets mappings per entity kind (for PermutationTransfer). + std::unordered_map> globalMappings; + + /// Register an adjacency with type-erased callbacks. + void registerAdj(AdjKind kind, AdjRemapFn remap, AdjRelocateFn relocate, + std::string name = {}); + + /// Register a companion with a type-erased relocate callback. + void registerCompanion(EntityKind kind, CompanionRelocateFn fn, + std::string name = {}); + + /// Register a GlobalOffsetsMapping for an entity kind. + void registerGlobalMapping(EntityKind kind, ssp gm); +}; + +// ================================================================= +// ReorderInput and ReorderPlan +// ================================================================= + struct ReorderInput { /// Explicit reorder maps (caller-provided). @@ -712,8 +788,47 @@ struct ReorderInput std::unordered_set destroyKinds; }; -/// Post-condition: all adj in Adj_PointToGlobal, ghost mappings stale. -void UnstructuredMesh::ReorderEntities(const ReorderInput &input); +// Layer 1: Standalone plan (no mesh dependency after construction) +struct ReorderPlan +{ + std::unordered_map transfers; + std::unordered_map lookups; + std::unordered_set reorderedKinds; + bool isLocalOnly{false}; + + /// Build plan from input + registry. + static ReorderPlan build(const ReorderInput &input, + const ReorderRegistry ®istry, + const MPIInfo &mpi); + + /// Apply the plan to all entries in the registry. + void apply(ReorderRegistry ®istry, const MPIInfo &mpi) const; + + /// Standalone operations for external arrays: + template + void remapEntries(TPair &pair, EntityKind targetKind) const; + + template + void relocateRows(TPair &pair, EntityKind sourceKind, + const MPIInfo &mpi) const; +}; + +// Layer 2: Mesh methods +class UnstructuredMesh +{ + /// Build a ReorderRegistry containing all mesh members. + /// Registers all built adj arrays (as callbacks) and all + /// companion arrays (coords, cellElemInfo, pbi arrays, etc.). + /// Skips adjacencies involving destroyKinds. + ReorderRegistry buildReorderRegistry( + const std::unordered_set &destroyKinds = {}) const; + + /// Build plan from input (uses buildReorderRegistry internally). + ReorderPlan buildReorderPlan(const ReorderInput &input) const; + + /// Build plan AND apply to all mesh members. + void ReorderEntities(const ReorderInput &input); +}; ``` ### Precondition @@ -867,21 +982,92 @@ ReorderEntities(input): | Entity kind | Companion arrays | |-------------|-----------------| | Cell | `cellElemInfo`, `cell2cellOrig`, `cell2nodePbi` (if periodic) | -| Node | `coords`, `node2nodeOrig`, `coordsElevDisp` (if elevation) | +| Node | `coords`, `node2nodeOrig`, `coordsElevDisp` (if elevation), `nodeWallDist` | | Bnd | `bndElemInfo`, `bnd2bndOrig`, `bnd2nodePbi` (if periodic) | | Face | `faceElemInfo`, `face2nodePbi` (if periodic) | -### Treatment +### Callback-based registration + +Companions are registered into the `ReorderRegistry` via type-erased +callbacks. Each callback captures a reference to its array and calls +`transferRows` on it when invoked: -Companions are treated as RELOCATE-only: their rows are moved with -their entity kind, but they have no entries to remap (they don't -store entity indices). +```cpp +// Inside UnstructuredMesh::buildReorderRegistry(): +auto reg = ReorderRegistry{}; + +// Register cell companions +reg.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cellElemInfo, m); + }, "cellElemInfo"); + +reg.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cell2cellOrig, m); + }, "cell2cellOrig"); + +if (isPeriodic && cell2nodePbi.father) + reg.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cell2nodePbi, m); + }, "cell2nodePbi"); + +// Register node companions +reg.registerCompanion(EntityKind::Node, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(coords, m); + }, "coords"); + +reg.registerCompanion(EntityKind::Node, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(node2nodeOrig, m); + }, "node2nodeOrig"); + +// ... etc for Bnd, Face +``` + +### External companion registration (solver arrays) + +External code appends its own companions to the registry before +the reorder is applied: +```cpp +auto registry = mesh.buildReorderRegistry(input.destroyKinds); + +// Solver registers its cell-parallel DOF arrays: +registry.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cellSolution, m); + }, "cellSolution"); + +registry.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cellGradient, m); + }, "cellGradient"); + +// Now build plan and apply: +auto plan = ReorderPlan::build(input, registry, mpi); +plan.apply(registry, mpi); ``` -for each reordered kind: - for each companion of that kind: - transfers[kind].transferRows(companion, mpi); + +This is the **recommended pattern for solver participation**. +The mesh's `ReorderEntities` convenience method does the same +thing internally (builds registry, builds plan, applies). + +### Treatment during apply + +During `ReorderPlan::apply`: + ``` +Phase 1 (REMAP): invoke adjEntry.remapFn for all classified REMAP/SELF adjs +Phase 2 (RELOCATE): invoke adjEntry.relocateFn for all classified RELOCATE/SELF adjs +Phase 3 (COMPANIONS): invoke companion.fn for all companions of reordered kinds +``` + +Companions always run after adj relocation (Phase 3), but since they +have no ordering dependency on adj arrays, they could also run in +parallel with Phase 2. ### Pbi arrays as companions @@ -899,45 +1085,260 @@ and VTK output, which are called later). --- -## Integration with AdjPairTracked and fillRegistry +## Dynamic Reorder Registry (Decoupled from UnstructuredMesh) -### How the framework discovers adjacencies +### Problem + +A naive design ties the reorder operation to `UnstructuredMesh`'s +hardcoded member list. The solver, CFV evaluator, or other subsystems +may own arrays parallel to mesh entities (solution DOFs per cell, +gradient arrays per node, etc.) that also need relocation or remapping. -`fillRegistry` registers all built adjacencies into a `MeshConnectivity` -registry. The reorder algorithm uses this registry to enumerate every -adjacency that exists, classify it, and operate on it. +### Solution: ReorderRegistry + callback-based participation -The registry stores `ssp` (type-erased father-only shallow -copy). But the reorder algorithm needs to operate on the actual mesh -member (to modify the father in-place and update the idx state). So the -registry is used only for **discovery and classification**. The actual -operations are performed on the mesh's `AdjPairTracked` members. +The `ReorderRegistry` (defined in the API section above) is the +dynamic set of arrays that participate in a reorder. It is built by +`UnstructuredMesh::buildReorderRegistry()` for mesh members, and +extended by external code before plan application. -### Mapping from AdjKind to mesh member +### How `buildReorderRegistry` works -The framework needs a mapping: +```cpp +ReorderRegistry UnstructuredMesh::buildReorderRegistry( + const std::unordered_set &destroyKinds) const +{ + ReorderRegistry reg; + + // ---- Adjacency registration (with callbacks) ---- + auto shouldSkip = [&](AdjKind kind) { + return destroyKinds.count(kind.from) || destroyKinds.count(kind.to); + }; + + // Helper: register a tracked adj member with remap + relocate callbacks. + auto regAdj = [&](AdjKind kind, auto &trackedPair) { + if (!trackedPair.father || shouldSkip(kind)) return; + + AdjRemapFn remap = [&trackedPair](const LookupResult &lookup) { + ConvertAdjEntries(trackedPair, trackedPair.father->Size(), + [&](index g) -> index { + if (g == UnInitIndex) return UnInitIndex; + return lookup.resolve(g); + }); + }; + + AdjRelocateFn relocate = [&trackedPair]( + const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(trackedPair, m); + }; + + reg.registerAdj(kind, remap, relocate, + adjKindName(kind)); + }; + + // Register all 12 tracked adj members + regAdj(Adj::Cell2Node, cell2node); + regAdj(Adj::Cell2Cell, cell2cell); + regAdj(Adj::Bnd2Node, bnd2node); + regAdj(Adj::Bnd2Cell, bnd2cell); + regAdj(Adj::Node2Cell, node2cell); + regAdj(Adj::Node2Bnd, node2bnd); + regAdj(Adj::Cell2Face, cell2face); + regAdj(Adj::Face2Node, face2node); + regAdj(Adj::Face2Cell, face2cell); + regAdj(Adj::Face2Bnd, face2bnd); + regAdj(Adj::Bnd2Face, bnd2face); + regAdj(Adj::Cell2CellFace, cell2cellFace); + + // ---- Companion registration (with callbacks) ---- + auto regComp = [&](EntityKind kind, auto &pair, const char *name) { + if (!pair.father) return; + reg.registerCompanion(kind, + [&pair](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(pair, m); + }, name); + }; + + regComp(EntityKind::Cell, cellElemInfo, "cellElemInfo"); + regComp(EntityKind::Cell, cell2cellOrig, "cell2cellOrig"); + regComp(EntityKind::Node, coords, "coords"); + regComp(EntityKind::Node, node2nodeOrig, "node2nodeOrig"); + regComp(EntityKind::Bnd, bndElemInfo, "bndElemInfo"); + regComp(EntityKind::Bnd, bnd2bndOrig, "bnd2bndOrig"); + + if (isPeriodic) { + regComp(EntityKind::Cell, cell2nodePbi, "cell2nodePbi"); + regComp(EntityKind::Bnd, bnd2nodePbi, "bnd2nodePbi"); + } + if (faceElemInfo.father && !destroyKinds.count(EntityKind::Face)) + regComp(EntityKind::Face, faceElemInfo, "faceElemInfo"); + if (coordsElevDisp.father) + regComp(EntityKind::Node, coordsElevDisp, "coordsElevDisp"); + if (nodeWallDist.father) + regComp(EntityKind::Node, nodeWallDist, "nodeWallDist"); + + // ---- Global mappings ---- + // Source from adj arrays (same as fillRegistry logic) + auto getGM = [](const auto &pair) -> ssp { + if (pair.father && pair.father->pLGlobalMapping) + return pair.father->pLGlobalMapping; + return nullptr; + }; + if (auto gm = getGM(cell2node)) reg.registerGlobalMapping(EntityKind::Cell, gm); + if (auto gm = coords.father ? coords.father->pLGlobalMapping : nullptr) + reg.registerGlobalMapping(EntityKind::Node, gm); + if (auto gm = getGM(bnd2node)) reg.registerGlobalMapping(EntityKind::Bnd, gm); + if (auto gm = getGM(face2node)) reg.registerGlobalMapping(EntityKind::Face, gm); + + return reg; +} +``` + +### External code extends the registry ```cpp -AdjKind -> AdjPairTracked & (reference to mesh member) +// Solver extends the mesh's registry with its own arrays: +auto registry = mesh.buildReorderRegistry(input.destroyKinds); + +// Add solver cell-parallel arrays: +registry.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cellSolution, m); + }, "solver::cellSolution"); + +registry.registerCompanion(EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(cellGradient, m); + }, "solver::cellGradient"); + +// Add solver node-parallel arrays: +registry.registerCompanion(EntityKind::Node, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(nodeWallDist, m); + }, "solver::nodeWallDist"); + +// Add an external adjacency (e.g., a DOF connectivity array): +registry.registerAdj( + AdjKind{EntityKind::Cell, EntityKind::Node}, // same kind as cell2node + [&](const LookupResult &lookup) { + ConvertAdjEntries(myDOFConn, myDOFConn.father->Size(), + [&](index g) { return g == UnInitIndex ? g : lookup.resolve(g); }); + }, + [&](const PermutationTransfer &t, const MPIInfo &m) { + t.transferRows(myDOFConn, m); + }, + "solver::dofConnectivity" +); + +// Build plan from the extended registry: +auto plan = ReorderPlan::build(input, registry, mpi); +plan.apply(registry, mpi); ``` -This can be implemented as a visitor pattern or a dispatch table: +### How ReorderPlan::apply works with the registry ```cpp -// In UnstructuredMesh: -void visitAdj(AdjKind kind, auto &&visitor) +void ReorderPlan::apply(ReorderRegistry ®istry, const MPIInfo &mpi) const { - if (kind == Adj::Cell2Node) visitor(cell2node); - else if (kind == Adj::Cell2Cell) visitor(cell2cell); - else if (kind == Adj::Bnd2Node) visitor(bnd2node); - // ... etc for all 12 tracked adj members + // Phase 1: REMAP all adj entries + for (auto &adj : registry.adjs) + { + auto action = classifyAdj(adj.kind, reorderedKinds); + if (action == REMAP || action == RELOCATE_REMAP || action == SELF) + { + EntityKind targetKind = adj.kind.isIntraLevel() + ? adj.kind.from : adj.kind.to; + if (adj.remapFn) + adj.remapFn(lookups.at(targetKind)); + } + } + + // Phase 2: RELOCATE all adj rows + for (auto &adj : registry.adjs) + { + auto action = classifyAdj(adj.kind, reorderedKinds); + if (action == RELOCATE || action == RELOCATE_REMAP || action == SELF) + { + EntityKind sourceKind = adj.kind.from; + if (adj.relocateFn) + adj.relocateFn(transfers.at(sourceKind), mpi); + } + } + + // Phase 3: RELOCATE all companions + for (auto &comp : registry.companions) + { + if (reorderedKinds.count(comp.kind)) + comp.fn(transfers.at(comp.kind), mpi); + } } ``` -Alternatively, `fillRegistry` can store references (but the current -design uses shared_ptr copies, not references). The simplest approach -is a `switch`/`if` dispatch in `ReorderEntities` since the set of -adjacencies is fixed and small. +### Relationship to existing fillRegistry + +The existing `fillRegistry(MeshConnectivity &dag)` method remains +for ghost tree evaluation (it fills a `MeshConnectivity` DAG for +`evaluateGhostTree`). The new `buildReorderRegistry` is a separate +method for reorder operations. They share the same discovery logic +(which adj arrays exist) but produce different output types: + +| Method | Output | Purpose | +|--------|--------|---------| +| `fillRegistry(dag)` | `MeshConnectivity` with `ssp` | Ghost tree evaluation | +| `buildReorderRegistry()` | `ReorderRegistry` with callbacks | Reorder operations | + +Both use the same underlying member-existence checks. The reorder +registry additionally captures companions and type-erased callbacks. + +### Summary: decoupling layers + +``` + Caller (solver, evaluator, etc.) + | + | extends ReorderRegistry with own arrays + v + ReorderRegistry (dynamic set of callbacks) + | + | consumed by ReorderPlan::build + apply + v + ReorderPlan (standalone, computed transfers + lookups) + | + | invokes callbacks during apply() + v + PermutationTransfer + LookupResult (MPI primitives) + | + | wraps ArrayTransformer / PermuteRows + v + DNDS array infrastructure +``` + +The mesh is just one contributor to the registry. Any code that owns +entity-parallel arrays can register callbacks and participate in the +same reorder operation. + +--- + +## Integration with AdjPairTracked and fillRegistry + +### How the framework discovers adjacencies + +`buildReorderRegistry()` replaces the role previously played by +`fillRegistry` for reorder operations. It iterates all mesh members, +checks if their father is non-null, and registers them with type-erased +callbacks. The callbacks capture references to the actual +`AdjPairTracked` members, so the plan operates on live mesh data. + +The existing `fillRegistry(MeshConnectivity &dag)` remains unchanged +for ghost tree evaluation. The two methods coexist: +- `fillRegistry` -> `MeshConnectivity` (for `evaluateGhostTree`) +- `buildReorderRegistry` -> `ReorderRegistry` (for `ReorderEntities`) + +### No explicit AdjKind-to-member dispatch needed + +Because `buildReorderRegistry` registers callbacks that already +capture member references, there is no need for a `visitAdj` dispatch +table. The plan invokes callbacks directly during `apply()`. This +eliminates the coupling between `ReorderPlan` and `UnstructuredMesh`'s +specific member layout. ### Idx state transitions during reorder @@ -949,7 +1350,7 @@ During reorder: - No idx transitions happen. Entries are remapped (still global, just different globals). Rows are relocated (still global). -After reorder: +After reorder (handled by mesh's `ReorderEntities` wrapper): - `idx.markGlobal()` is called on all affected adj (idempotent if already global, resets from Unknown if the adj was destroyed and recreated). @@ -957,25 +1358,41 @@ After reorder: entity was invalidated by the reorder). It will be re-wired after the caller rebuilds ghosts. -### fillRegistry skip set +The mesh wrapper also updates the group state variables +(`adjPrimaryState`, `adjFacialState`, etc.) to `Adj_PointToGlobal`. -When `destroyKinds` is specified (e.g., {Face}), the fillRegistry call -should skip adjacencies involving Face: +### Post-reorder idx state update (mesh wrapper responsibility) ```cpp -std::unordered_set skip; -for (auto kind : input.destroyKinds) -{ - // Skip all adjacencies where kind is source or target - for (auto &adjKind : allAdjKinds) - if (adjKind.from == kind || adjKind.to == kind) - skip.insert(adjKind); -} -fillRegistry(dag, skip); +// Inside UnstructuredMesh::ReorderEntities, after plan.apply(): +auto updateIdx = [&](auto &trackedPair) { + if (trackedPair.father) + trackedPair.idx.markGlobal(); +}; +updateIdx(cell2node); +updateIdx(cell2cell); +updateIdx(bnd2node); +updateIdx(bnd2cell); +// ... etc for all tracked adj members + +adjPrimaryState = Adj_PointToGlobal; +// adjFacialState, adjC2FState, adjN2CBState: set to Unknown if destroyed, +// or Adj_PointToGlobal if still present. ``` -This prevents the registry from containing stale or soon-to-be-destroyed -adjacencies. +### buildReorderRegistry skip logic + +When `destroyKinds` is specified (e.g., {Face}), `buildReorderRegistry` +skips adjacencies involving that kind: + +```cpp +auto shouldSkip = [&](AdjKind kind) { + return destroyKinds.count(kind.from) || destroyKinds.count(kind.to); +}; +``` + +This prevents registering callbacks for adjacencies that will be +destroyed before the reorder runs. --- @@ -1096,6 +1513,61 @@ mesh.ReorderEntities(input); // => REMAP. bnd2node same. node2cell => RELOCATE. coords => companion RELOCATE. ``` +### Use case 4: Solver-participates redistribution (external arrays) + +```cpp +// Solver owns DOF arrays parallel to cells: +tDOFPair cellSolution; // father->Size() == mesh.NumCell() +tDOFPair cellGradient; + +// Step 1: Build plan (does MPI communication for lookups) +ReorderInput input; +input.explicitMaps = {cellMap}; +input.follows = { + {EntityKind::Node, EntityKind::Cell, Adj::Node2Cell}, + {EntityKind::Bnd, EntityKind::Cell, Adj::Bnd2Cell}, +}; +input.destroyKinds = {EntityKind::Face}; + +auto plan = mesh.buildReorderPlan(input); + +// Step 2: Reorder mesh +mesh.ReorderEntities(input); + +// Step 3: Reorder solver arrays using the same plan +plan.relocateRows(cellSolution, EntityKind::Cell, mpi); +plan.relocateRows(cellGradient, EntityKind::Cell, mpi); + +// Step 4: If solver has node-parallel arrays: +plan.relocateRows(nodeWallDist, EntityKind::Node, mpi); + +// Step 5: Rebuild global mappings on solver arrays +cellSolution.TransAttach(); +cellSolution.trans.createFatherGlobalMapping(); +// ... etc +``` + +### Use case 5: Node-only reorder with solver participation + +```cpp +// Hypothetical: RCM reorder on node graph for FEM bandwidth reduction + +EntityReorderMap nodeMap{EntityKind::Node, /*all local*/}; +ReorderInput input; +input.explicitMaps = {nodeMap}; +input.destroyKinds = {EntityKind::Face}; + +auto plan = mesh.buildReorderPlan(input); +mesh.ReorderEntities(input); + +// Solver has node-based arrays: +plan.relocateRows(nodeSolution, EntityKind::Node, mpi); +plan.relocateRows(nodeRHS, EntityKind::Node, mpi); + +// Mesh's cell2node entries are already remapped (REMAP action) +// so cells now reference new node globals. No cell rows moved. +``` + --- ## Implementation Plan @@ -1114,26 +1586,28 @@ mesh.ReorderEntities(input); - Local-only: create N entities, permute, verify - Distributed: create N entities across 2 ranks, redistribute, verify -### Phase 2: ReorderEntities framework +### Phase 2: ReorderPlan + ReorderEntities framework -**File**: `src/Geom/Mesh/Mesh_Reorder.cpp` (new, or extend Mesh.cpp) +**Files**: +- `src/Geom/Mesh/ReorderPlan.hpp` — `ReorderPlan`, `ReorderInput`, + `EntityReorderMap`, `FollowSpec`, `AdjAction`, classification logic +- `src/Geom/Mesh/Mesh_Reorder.cpp` — `UnstructuredMesh::ReorderEntities`, + `buildReorderPlan`, mesh-member dispatch -- `EntityReorderMap`, `FollowSpec`, `ReorderInput` structs (in Mesh.hpp) -- `ComputeFollowMap` free function +Contents: +- `EntityReorderMap`, `FollowSpec`, `ReorderInput` structs +- `ReorderPlan` with `build()`, `remapEntries()`, `relocateRows()` - `classifyAdj` free function +- `ComputeFollowMap` free function - `collectPullSet` free function -- `ReorderEntities` method on UnstructuredMesh - - Step 0: validate + fill registry - - Step 1: compute follows - - Step 2: build transfers - - Step 3: classify - - Step 4: build lookups - - Step 5: remap entries - - Step 6: relocate rows + companions - - Step 7: rebuild global mappings - - Step 8: update idx states - - Step 9: update mesh state variables -- `visitAdj` dispatch helper +- `UnstructuredMesh::ReorderEntities` (wrapper that builds plan + applies) +- `UnstructuredMesh::buildReorderPlan` (builds plan without applying) +- `visitAdj` dispatch helper (AdjKind -> mesh member reference) + +Unit tests: +- Single-entity remap (node-only): verify cell2node entries updated +- Single-entity relocate (cell-only): verify cell rows permuted +- Multi-entity (cell + node + bnd): verify full redistribution ### Phase 3: Migrate ReorderLocalCells From 0150ec5123ce34c8a250b7da5d55dd65941f59a9 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 18:36:29 +0800 Subject: [PATCH 37/43] feat(DNDS): PermutationTransfer utility for distributed/local row transfer Phase 1 of the distributed reorder plan. Implements: - PermutationTransfer::fromPartition: build from target-rank assignment, computes new global indices via prefix-sum, push CSR, local-only detection - PermutationTransfer::fromLocalPermutation: build from old2new permutation vector, all entities stay on same rank - transferRows: local PermuteRows or distributed ArrayTransformer push - LookupResult with resolve(): ghost-pullable old-global -> new-global map - buildLookup: creates ghost-pulled tAdj1Pair for cross-rank resolution 7 unit tests (doctest): reverse/identity local permutation, all-local partition, round-robin redistribution, lookup resolve (local+cross-rank), CSR permutation. All pass at np=1,2,4,8 (29/29 DNDS ctest). --- src/DNDS/PermutationTransfer.hpp | 337 ++++++++++++++++++ test/cpp/CMakeLists.txt | 2 + test/cpp/DNDS/test_PermutationTransfer.cpp | 376 +++++++++++++++++++++ 3 files changed, 715 insertions(+) create mode 100644 src/DNDS/PermutationTransfer.hpp create mode 100644 test/cpp/DNDS/test_PermutationTransfer.cpp diff --git a/src/DNDS/PermutationTransfer.hpp b/src/DNDS/PermutationTransfer.hpp new file mode 100644 index 00000000..928bda85 --- /dev/null +++ b/src/DNDS/PermutationTransfer.hpp @@ -0,0 +1,337 @@ +#pragma once +/// @file PermutationTransfer.hpp +/// @brief Utility for distributed or local row permutation/transfer of arrays. +/// +/// Encapsulates the common pattern: given a partition assignment (or forward map) +/// for a set of entities, compute new global indices and transfer array rows to +/// their target ranks. Supports both distributed (MPI push) and local-only +/// (in-place permutation) paths. + +#include "DNDS/ArrayTransformer.hpp" +#include "DNDS/ArrayPair.hpp" +#include "DNDS/MPI.hpp" + +namespace DNDS +{ + /// Encapsulates a distributed or local permutation of array rows. + /// + /// Given a per-slot target rank assignment, computes: + /// - New global indices (prefix-sum across ranks grouped by target) + /// - Push CSR indices for MPI communication + /// - Local permutation vector (when all rows stay on the same rank) + /// + /// Then provides `transferRows` to actually move/permute the data, + /// and `buildLookup` to create a ghost-pullable old->new global map. + struct PermutationTransfer + { + /// Per father slot: target rank after reorder. + std::vector targetRanks; + + /// New global index for each father slot (same size as targetRanks). + std::vector newGlobalIndices; + + /// Push CSR: pushIndex[pushStart[r]..pushStart[r+1]) are the local + /// father indices that go to rank r. + std::vector pushIndex; + std::vector pushStart; // size = nRanks + 1 + + /// Local permutation: localOld2New[i] = new local index for old local i. + /// Only valid when isLocalOnly == true. Empty otherwise. + std::vector localOld2New; + + /// Whether this is a pure rank-local permutation (no cross-rank traffic). + bool isLocalOnly{false}; + + /// New global offsets: newGlobalOffsets[r] = first global index owned by + /// rank r after reorder. Size = nRanks + 1. + std::vector newGlobalOffsets; + + // ============================================================= + // Factory methods + // ============================================================= + + /// Build from partition assignment (target ranks only). + /// New global indices are computed automatically via prefix-sum. + /// + /// @param partition Per-slot target rank. Size == father size. + /// @param oldGlobalMapping Current global offsets mapping for this entity. + /// @param mpi MPI communicator. + /// @warning Collective. + static PermutationTransfer fromPartition( + const std::vector &partition, + const ssp &oldGlobalMapping, + const MPIInfo &mpi); + + /// Build from a local-only permutation vector. + /// All entities stay on the same rank. targetRanks all == mpi.rank. + /// + /// @param old2new Local permutation: old local index -> new local index. + /// Must be a valid permutation of [0, N). + /// @param oldGlobalMapping Current global offsets mapping. + /// @param mpi MPI communicator. + /// @warning Collective (MPI_Allreduce for isLocalOnly detection). + static PermutationTransfer fromLocalPermutation( + const std::vector &old2new, + const ssp &oldGlobalMapping, + const MPIInfo &mpi); + + // ============================================================= + // Core operations + // ============================================================= + + /// Transfer (or permute) rows of an ArrayPair. + /// + /// - Local-only: in-place row permutation via PermuteRows. + /// - Distributed: father=old, son=new ArrayTransformer push trick. + /// + /// After return, pair.father contains the new data. + /// pair.son is reset (stale after distributed transfer). + /// + /// @warning Collective (when !isLocalOnly). + template + void transferRows(TPair &pair, const MPIInfo &mpi) const; + + /// Result of buildLookup: ghost-pullable old-global -> new-global map. + struct LookupResult + { + ArrayAdjacencyPair<1> pair; // pair(localSlot, 0) = newGlobalIndices[localSlot] + // Ghost-pulled for off-rank entries. + + /// Resolve an old global index to its new global index. + /// The old global must be in the local father or ghost (son) range. + index resolve(index oldGlobal) const + { + DNDS_assert(pair.trans.pLGhostMapping); + if (oldGlobal == UnInitIndex) + return UnInitIndex; + MPI_int rank; + index val; + bool found = pair.trans.pLGhostMapping->search_indexAppend( + oldGlobal, rank, val); + DNDS_assert_info(found, + fmt::format("LookupResult::resolve: old global {} not found", oldGlobal)); + return pair(val, 0); + } + }; + + /// Build a ghost-pullable lookup array for old->new global conversion. + /// + /// @param pullSet Sorted, deduplicated set of off-rank old globals that + /// need to be resolvable. Typically collected from adj + /// entries pointing to this entity kind. + /// @param mpi MPI communicator. + /// @return LookupResult with resolve() method. + /// @warning Collective. + LookupResult buildLookup( + const std::vector &pullSet, + const MPIInfo &mpi) const; + + // ============================================================= + // Queries + // ============================================================= + + /// Number of entities (father slots) in this transfer. + [[nodiscard]] index size() const { return static_cast(targetRanks.size()); } + }; + + // ===================================================================== + // Implementation: fromPartition + // ===================================================================== + + inline PermutationTransfer PermutationTransfer::fromPartition( + const std::vector &partition, + const ssp &oldGlobalMapping, + const MPIInfo &mpi) + { + DNDS_assert(oldGlobalMapping); + PermutationTransfer pt; + pt.targetRanks = partition; + const index nLocal = static_cast(partition.size()); + + // --- Push CSR --- + std::vector pushSizes(mpi.size, 0); + for (auto r : partition) + { + DNDS_assert(r >= 0 && r < mpi.size); + pushSizes[r]++; + } + AccumulateRowSize(pushSizes, pt.pushStart); + pt.pushIndex.resize(pt.pushStart[mpi.size]); + pushSizes.assign(mpi.size, 0); + for (index i = 0; i < nLocal; i++) + pt.pushIndex[pt.pushStart[partition[i]] + (pushSizes[partition[i]]++)] = i; + + // --- New global indices (prefix-sum grouped by target rank) --- + // Count how many entities each rank sends to each target: + std::vector localCounts(mpi.size, 0); + for (auto r : partition) + localCounts[r]++; + + // Total entities per target rank (across all senders): + std::vector totalCounts(mpi.size); + MPI_Allreduce(localCounts.data(), totalCounts.data(), mpi.size, + DNDS_MPI_INDEX, MPI_SUM, mpi.comm); + + // Global offsets per target rank: + pt.newGlobalOffsets.resize(mpi.size + 1); + pt.newGlobalOffsets[0] = 0; + for (int r = 0; r < mpi.size; r++) + pt.newGlobalOffsets[r + 1] = pt.newGlobalOffsets[r] + totalCounts[r]; + + // Exclusive prefix per target rank (how many entities before mine): + std::vector prevCounts(mpi.size); + MPI_Scan(localCounts.data(), prevCounts.data(), mpi.size, + DNDS_MPI_INDEX, MPI_SUM, mpi.comm); + // MPI_Scan is inclusive, convert to exclusive: + for (int r = 0; r < mpi.size; r++) + prevCounts[r] -= localCounts[r]; + + // Assign new globals: within each target rank bucket, entities are + // ordered by (sender rank, local slot within sender). + pt.newGlobalIndices.resize(nLocal); + std::vector fillCounters(mpi.size, 0); + for (index i = 0; i < nLocal; i++) + { + MPI_int target = partition[i]; + pt.newGlobalIndices[i] = + pt.newGlobalOffsets[target] + prevCounts[target] + fillCounters[target]; + fillCounters[target]++; + } + + // --- Detect local-only --- + int localFlag = 1; + for (auto r : partition) + if (r != mpi.rank) + { + localFlag = 0; + break; + } + int globalFlag; + MPI_Allreduce(&localFlag, &globalFlag, 1, MPI_INT, MPI_LAND, mpi.comm); + pt.isLocalOnly = (globalFlag != 0); + + // --- Build local permutation if local-only --- + if (pt.isLocalOnly) + { + index myOffset = (*oldGlobalMapping)(mpi.rank, 0); + pt.localOld2New.resize(nLocal); + for (index i = 0; i < nLocal; i++) + pt.localOld2New[i] = pt.newGlobalIndices[i] - myOffset; + } + + return pt; + } + + // ===================================================================== + // Implementation: fromLocalPermutation + // ===================================================================== + + inline PermutationTransfer PermutationTransfer::fromLocalPermutation( + const std::vector &old2new, + const ssp &oldGlobalMapping, + const MPIInfo &mpi) + { + DNDS_assert(oldGlobalMapping); + PermutationTransfer pt; + const index nLocal = static_cast(old2new.size()); + + pt.isLocalOnly = true; + pt.localOld2New = old2new; + pt.targetRanks.assign(nLocal, mpi.rank); + + index myOffset = (*oldGlobalMapping)(mpi.rank, 0); + pt.newGlobalIndices.resize(nLocal); + for (index i = 0; i < nLocal; i++) + pt.newGlobalIndices[i] = myOffset + old2new[i]; + + // Push CSR (trivial: all go to self) + pt.pushStart.assign(mpi.size + 1, 0); + pt.pushStart[mpi.rank + 1] = nLocal; + for (int r = mpi.rank + 2; r <= mpi.size; r++) + pt.pushStart[r] = nLocal; + pt.pushIndex.resize(nLocal); + std::iota(pt.pushIndex.begin(), pt.pushIndex.end(), index{0}); + + // Global offsets unchanged + pt.newGlobalOffsets.resize(mpi.size + 1); + for (int r = 0; r < mpi.size; r++) + pt.newGlobalOffsets[r] = (*oldGlobalMapping)(r, 0); + pt.newGlobalOffsets[mpi.size] = oldGlobalMapping->globalSize(); + + return pt; + } + + // ===================================================================== + // Implementation: transferRows + // ===================================================================== + + template + void PermutationTransfer::transferRows(TPair &pair, const MPIInfo &mpi) const + { + DNDS_assert(pair.father); + DNDS_assert(pair.father->Size() == size()); + + if (isLocalOnly) + { + // In-place permutation + using TArr = typename decltype(pair.father)::element_type; + auto tmp = std::make_shared(*pair.father); // deep copy + if constexpr (TPair::IsCSR()) + pair.father->Decompress(); + for (index i = 0; i < size(); i++) + { + index iNew = localOld2New[i]; + if constexpr (TPair::IsCSR()) + pair.father->ResizeRow(iNew, tmp->RowSize(i)); + for (rowsize j = 0; j < tmp->RowSize(i); j++) + pair.father->operator()(iNew, j) = (*tmp)(i, j); + } + if constexpr (TPair::IsCSR()) + pair.father->Compress(); + } + else + { + // Distributed: father=old, son=new ArrayTransformer push + using TArr = typename decltype(pair.father)::element_type; + auto oldFather = pair.father; + pair.father = make_ssp(ObjName{"PermTransfer.new"}, mpi); + + typename ArrayTransformerType::Type trans; + trans.setFatherSon(oldFather, pair.father); + trans.createFatherGlobalMapping(); + trans.createGhostMapping(pushIndex, pushStart); + trans.createMPITypes(); + trans.pullOnce(); + // pair.father is now the new array with redistributed data + } + + // Son is stale after either path — reset it + if (pair.son) + pair.son.reset(); + } + + // ===================================================================== + // Implementation: buildLookup + // ===================================================================== + + inline PermutationTransfer::LookupResult PermutationTransfer::buildLookup( + const std::vector &pullSet, + const MPIInfo &mpi) const + { + LookupResult result; + result.pair.InitPair("PermTransfer_lookup", mpi); + result.pair.father->Resize(size()); + for (index i = 0; i < size(); i++) + result.pair(i, 0) = newGlobalIndices[i]; + + result.pair.TransAttach(); + result.pair.trans.createFatherGlobalMapping(); + std::vector pullSetMut(pullSet); // mutable copy for createGhostMapping + result.pair.trans.createGhostMapping(pullSetMut); + result.pair.trans.createMPITypes(); + result.pair.trans.pullOnce(); + + return result; + } + +} // namespace DNDS diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt index 265f5341..c3db06a3 100644 --- a/test/cpp/CMakeLists.txt +++ b/test/cpp/CMakeLists.txt @@ -190,6 +190,7 @@ dnds_add_mpi_test(array_transformer "DNDS/test_ArrayTransformer.cpp" "dnds") dnds_add_mpi_test(array_derived "DNDS/test_ArrayDerived.cpp" "dnds") dnds_add_mpi_test(array_dof "DNDS/test_ArrayDOF.cpp" "dnds") dnds_add_mpi_test(index_mapping "DNDS/test_IndexMapping.cpp" "dnds") +dnds_add_mpi_test(permutation_transfer "DNDS/test_PermutationTransfer.cpp" "dnds") set(DNDS_TEST_TARGETS dnds_test_array @@ -199,6 +200,7 @@ set(DNDS_TEST_TARGETS dnds_test_array_derived dnds_test_array_dof dnds_test_index_mapping + dnds_test_permutation_transfer ) foreach(T ${DNDS_TEST_TARGETS}) add_dependencies(dnds_unit_tests ${T}) diff --git a/test/cpp/DNDS/test_PermutationTransfer.cpp b/test/cpp/DNDS/test_PermutationTransfer.cpp new file mode 100644 index 00000000..df332c94 --- /dev/null +++ b/test/cpp/DNDS/test_PermutationTransfer.cpp @@ -0,0 +1,376 @@ +/** + * @file test_PermutationTransfer.cpp + * @brief Unit tests for DNDS::PermutationTransfer. + * + * Tests local permutation, distributed partition transfer, and lookup + * resolution under MPI with 1, 2, 4, and 8 ranks. + */ + +#define DOCTEST_CONFIG_IMPLEMENT +#include "doctest.h" +#include "DNDS/PermutationTransfer.hpp" +#include "DNDS/ArrayDerived/ArrayAdjacency.hpp" +#include +#include + +using namespace DNDS; + +int main(int argc, char **argv) +{ + MPI_Init(&argc, &argv); + doctest::Context ctx; + ctx.applyCommandLine(argc, argv); + int res = ctx.run(); + MPI_Finalize(); + return res; +} + +static MPIInfo worldMPI() +{ + MPIInfo mpi; + mpi.setWorld(); + return mpi; +} + +// ================================================================= +// Test: fromLocalPermutation — reverse permutation +// ================================================================= + +TEST_CASE("PermutationTransfer::fromLocalPermutation reverse") +{ + auto mpi = worldMPI(); + const DNDS::index nLocal = 10; + + // Create a simple array: each rank owns 10 entries with values = global index + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + for (DNDS::index i = 0; i < nLocal; i++) + arr(i, 0) = myOffset + i; // value = own global index + + // Build reverse permutation: new[i] = old[N-1-i] + std::vector old2new(nLocal); + for (DNDS::index i = 0; i < nLocal; i++) + old2new[i] = nLocal - 1 - i; + + auto pt = PermutationTransfer::fromLocalPermutation(old2new, arr.father->pLGlobalMapping, mpi); + + CHECK(pt.isLocalOnly); + CHECK(pt.size() == nLocal); + CHECK(pt.localOld2New.size() == static_cast(nLocal)); + + // Verify new global indices + for (DNDS::index i = 0; i < nLocal; i++) + CHECK(pt.newGlobalIndices[i] == myOffset + old2new[i]); + + // Transfer rows + pt.transferRows(arr, mpi); + + // After transfer: arr(newSlot, 0) should contain the old global of + // the entity that was moved there. + // old slot i had value (myOffset + i), moved to new slot old2new[i]. + // So new slot j should have value (myOffset + reverseOf(j)). + // reverseOf(j) = N-1-j (since old2new[i] = N-1-i => i = N-1-j when j = old2new[i]) + for (DNDS::index j = 0; j < nLocal; j++) + { + DNDS::index expectedOldSlot = nLocal - 1 - j; + DNDS::index expectedValue = myOffset + expectedOldSlot; + CHECK(arr(j, 0) == expectedValue); + } +} + +// ================================================================= +// Test: fromLocalPermutation — identity (no change) +// ================================================================= + +TEST_CASE("PermutationTransfer::fromLocalPermutation identity") +{ + auto mpi = worldMPI(); + const DNDS::index nLocal = 5; + + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + for (DNDS::index i = 0; i < nLocal; i++) + arr(i, 0) = 100 + myOffset + i; + + // Identity permutation + std::vector old2new(nLocal); + std::iota(old2new.begin(), old2new.end(), DNDS::index{0}); + + auto pt = PermutationTransfer::fromLocalPermutation(old2new, arr.father->pLGlobalMapping, mpi); + CHECK(pt.isLocalOnly); + + pt.transferRows(arr, mpi); + + for (DNDS::index i = 0; i < nLocal; i++) + CHECK(arr(i, 0) == 100 + myOffset + i); +} + +// ================================================================= +// Test: fromPartition — all stay on same rank (local-only detected) +// ================================================================= + +TEST_CASE("PermutationTransfer::fromPartition all-local") +{ + auto mpi = worldMPI(); + const DNDS::index nLocal = 8; + + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + for (DNDS::index i = 0; i < nLocal; i++) + arr(i, 0) = myOffset + i; + + // Partition: all entities stay on current rank + std::vector partition(nLocal, mpi.rank); + + auto pt = PermutationTransfer::fromPartition(partition, arr.father->pLGlobalMapping, mpi); + + CHECK(pt.isLocalOnly); + CHECK(pt.size() == nLocal); + + // New globals should be contiguous starting at newGlobalOffsets[mpi.rank] + DNDS::index newOffset = pt.newGlobalOffsets[mpi.rank]; + for (DNDS::index i = 0; i < nLocal; i++) + CHECK(pt.newGlobalIndices[i] == newOffset + i); + + // Transfer should be identity (since partition = all-self, ordering preserved) + pt.transferRows(arr, mpi); + for (DNDS::index i = 0; i < nLocal; i++) + CHECK(arr(i, 0) == myOffset + i); +} + +// ================================================================= +// Test: fromPartition — round-robin redistribution +// ================================================================= + +TEST_CASE("PermutationTransfer::fromPartition round-robin") +{ + auto mpi = worldMPI(); + if (mpi.size < 2) + return; // skip on 1 rank + + const DNDS::index nLocal = 6; + + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + for (DNDS::index i = 0; i < nLocal; i++) + arr(i, 0) = myOffset + i; // value = old global + + // Round-robin: entity i goes to rank (i % nRanks) + std::vector partition(nLocal); + for (DNDS::index i = 0; i < nLocal; i++) + partition[i] = static_cast(i % mpi.size); + + auto pt = PermutationTransfer::fromPartition(partition, arr.father->pLGlobalMapping, mpi); + + CHECK_FALSE(pt.isLocalOnly); + + // Transfer rows + pt.transferRows(arr, mpi); + + // After transfer, this rank should have received entities from all ranks + // whose slot i satisfies (i % nRanks == mpi.rank). + // Count expected: each rank sends nLocal/nRanks entities to this rank + // (approximately — depends on nLocal and nRanks). + DNDS::index expectedCount = 0; + for (DNDS::index i = 0; i < nLocal * mpi.size; i++) + if (i % mpi.size == mpi.rank) + expectedCount++; // but we only count from all ranks + + // More precise: each rank sends (number of slots where i%size == mpi.rank) entities + DNDS::index myReceiveCount = 0; + for (DNDS::index i = 0; i < nLocal; i++) + if (partition[i] == mpi.rank) + myReceiveCount++; // from self + // From other ranks: they also have nLocal entities and send some here + // Total receive = sum over all ranks of (their slots targeting me) + // Each rank has nLocal entities; slot i of rank r targets rank (i % size). + // So from rank r, I receive count of {i : i%size == mpi.rank, 0 <= i < nLocal} + + DNDS::index totalReceive = 0; + for (int r = 0; r < mpi.size; r++) + { + for (DNDS::index i = 0; i < nLocal; i++) + if (i % mpi.size == mpi.rank) + totalReceive++; + } + + CHECK(arr.father->Size() == totalReceive); + + // Verify all received values are valid old globals (in range [0, nLocal*nRanks)) + DNDS::index globalTotal = nLocal * mpi.size; + for (DNDS::index i = 0; i < arr.father->Size(); i++) + { + DNDS::index val = arr(i, 0); + CHECK(val >= 0); + CHECK(val < globalTotal); + } +} + +// ================================================================= +// Test: buildLookup — resolve old globals to new globals +// ================================================================= + +TEST_CASE("PermutationTransfer::buildLookup resolve") +{ + auto mpi = worldMPI(); + const DNDS::index nLocal = 4; + + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + + // All-local partition (identity-like) + std::vector partition(nLocal, mpi.rank); + auto pt = PermutationTransfer::fromPartition(partition, arr.father->pLGlobalMapping, mpi); + + // Pull set: request globals from other ranks (first 2 from each neighbor) + std::vector pullSet; + for (int r = 0; r < mpi.size; r++) + { + if (r == mpi.rank) + continue; + DNDS::index rOffset = (*arr.father->pLGlobalMapping)(r, 0); + for (DNDS::index i = 0; i < std::min(nLocal, DNDS::index{2}); i++) + pullSet.push_back(rOffset + i); + } + std::sort(pullSet.begin(), pullSet.end()); + + auto lookup = pt.buildLookup(pullSet, mpi); + + // Resolve own globals: should map to new globals + for (DNDS::index i = 0; i < nLocal; i++) + { + DNDS::index oldGlobal = myOffset + i; + DNDS::index newGlobal = lookup.resolve(oldGlobal); + CHECK(newGlobal == pt.newGlobalIndices[i]); + } + + // Resolve pulled globals from other ranks + for (auto oldGlobal : pullSet) + { + DNDS::index newGlobal = lookup.resolve(oldGlobal); + // The new global should be valid (in range [0, total)) + CHECK(newGlobal >= 0); + CHECK(newGlobal < pt.newGlobalOffsets.back()); + } + + // UnInitIndex passthrough + CHECK(lookup.resolve(UnInitIndex) == UnInitIndex); +} + +// ================================================================= +// Test: fromPartition + buildLookup — distributed with cross-rank resolve +// ================================================================= + +TEST_CASE("PermutationTransfer distributed lookup cross-rank") +{ + auto mpi = worldMPI(); + if (mpi.size < 2) + return; + + const DNDS::index nLocal = 4; + + ArrayAdjacencyPair<1> arr; + arr.InitPair("test_arr", mpi); + arr.father->Resize(nLocal); + arr.father->createGlobalMapping(); + + DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0); + + // Send all entities to the next rank (ring shift) + MPI_int targetRank = (mpi.rank + 1) % mpi.size; + std::vector partition(nLocal, targetRank); + + auto pt = PermutationTransfer::fromPartition(partition, arr.father->pLGlobalMapping, mpi); + CHECK_FALSE(pt.isLocalOnly); + + // Build lookup: pull the previous rank's old globals + MPI_int sourceRank = (mpi.rank - 1 + mpi.size) % mpi.size; + DNDS::index sourceOffset = (*arr.father->pLGlobalMapping)(sourceRank, 0); + std::vector pullSet; + for (DNDS::index i = 0; i < nLocal; i++) + pullSet.push_back(sourceOffset + i); + std::sort(pullSet.begin(), pullSet.end()); + + auto lookup = pt.buildLookup(pullSet, mpi); + + // Verify: my old globals should resolve to new globals on targetRank + for (DNDS::index i = 0; i < nLocal; i++) + { + DNDS::index oldGlobal = myOffset + i; + DNDS::index newGlobal = lookup.resolve(oldGlobal); + // New global should be in [newGlobalOffsets[target], newGlobalOffsets[target+1]) + CHECK(newGlobal >= pt.newGlobalOffsets[targetRank]); + CHECK(newGlobal < pt.newGlobalOffsets[targetRank + 1]); + } + + // Verify: previous rank's old globals should also resolve + for (auto oldGlobal : pullSet) + { + DNDS::index newGlobal = lookup.resolve(oldGlobal); + // sourceRank sent to (sourceRank+1)%size == mpi.rank + CHECK(newGlobal >= pt.newGlobalOffsets[mpi.rank]); + CHECK(newGlobal < pt.newGlobalOffsets[mpi.rank + 1]); + } +} + +// ================================================================= +// Test: transferRows with CSR (variable-row-size) array +// ================================================================= + +TEST_CASE("PermutationTransfer::transferRows CSR local permutation") +{ + auto mpi = worldMPI(); + const DNDS::index nLocal = 6; + + // Create a CSR array where row i has (i+1) entries + ArrayAdjacencyPair arr; + arr.InitPair("test_csr", mpi); + arr.father->Resize(nLocal); + for (DNDS::index i = 0; i < nLocal; i++) + arr.father->ResizeRow(i, static_cast(i + 1)); + arr.father->Compress(); + arr.father->createGlobalMapping(); + + // Fill: row i, entry j = i * 100 + j + for (DNDS::index i = 0; i < nLocal; i++) + for (rowsize j = 0; j < arr.father->RowSize(i); j++) + arr(i, j) = i * 100 + j; + + // Reverse permutation + std::vector old2new(nLocal); + for (DNDS::index i = 0; i < nLocal; i++) + old2new[i] = nLocal - 1 - i; + + auto pt = PermutationTransfer::fromLocalPermutation(old2new, arr.father->pLGlobalMapping, mpi); + pt.transferRows(arr, mpi); + + // Verify: new slot j should contain old slot (N-1-j)'s data + for (DNDS::index j = 0; j < nLocal; j++) + { + DNDS::index oldSlot = nLocal - 1 - j; + rowsize expectedRowSize = static_cast(oldSlot + 1); + CHECK(arr.father->RowSize(j) == expectedRowSize); + for (rowsize k = 0; k < expectedRowSize; k++) + CHECK(arr(j, k) == oldSlot * 100 + k); + } +} From f1895aa5ab9c5e88705cd62e794a679eb4e2dd28 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 18:50:08 +0800 Subject: [PATCH 38/43] feat(Geom): ReorderPlan + ReorderRegistry framework (Phase 2 skeleton) Two-layer reorder architecture: - ReorderRegistry: dynamic set of type-erased callbacks for adj remap/relocate and companion relocate. External code (solver) can extend it. - ReorderPlan: standalone plan with PermutationTransfers + LookupResults. build() from ReorderInput + registry, apply() invokes callbacks in Phase 1 (REMAP) -> Phase 2 (RELOCATE adj) -> Phase 3 (RELOCATE companions). - classifyAdj(): formal SKIP/RELOCATE/REMAP/RELOCATE_REMAP/SELF classification. - EntityReorderMap, FollowSpec, ReorderInput structs. - ComputeFollowMapFromAdj template (ghost-pull leader ranks, min-rank rule). Geom unit tests (test_MeshReorder.cpp): - classifyAdj logic (cell-only, node-only, cell+node, cell+node+bnd) - ReorderRegistry register + query - ReorderPlan::apply cell-only local permutation (adj + companion unchanged) - ReorderPlan::apply node-only remap (entries updated, rows stay) All 30/30 Geom ctest pass, 29/29 DNDS ctest pass. --- src/Geom/CMakeLists.txt | 1 + src/Geom/Mesh/Mesh_Reorder.cpp | 306 ++++++++++++++++++++++++++++ src/Geom/Mesh/ReorderPlan.hpp | 257 +++++++++++++++++++++++ test/cpp/CMakeLists.txt | 2 + test/cpp/Geom/test_MeshReorder.cpp | 313 +++++++++++++++++++++++++++++ 5 files changed, 879 insertions(+) create mode 100644 src/Geom/Mesh/Mesh_Reorder.cpp create mode 100644 src/Geom/Mesh/ReorderPlan.hpp create mode 100644 test/cpp/Geom/test_MeshReorder.cpp diff --git a/src/Geom/CMakeLists.txt b/src/Geom/CMakeLists.txt index 277cdf43..c932e23e 100644 --- a/src/Geom/CMakeLists.txt +++ b/src/Geom/CMakeLists.txt @@ -33,6 +33,7 @@ Mesh/Mesh_Elevation.cpp Mesh/Mesh_Elevation_SmoothSolver.cpp Mesh/Mesh_Elevation_SmoothV2.cpp Mesh/Mesh_WallDist.cpp +Mesh/Mesh_Reorder.cpp Mesh/SerialAdjReordering.cpp ) diff --git a/src/Geom/Mesh/Mesh_Reorder.cpp b/src/Geom/Mesh/Mesh_Reorder.cpp new file mode 100644 index 00000000..dcb6eb8f --- /dev/null +++ b/src/Geom/Mesh/Mesh_Reorder.cpp @@ -0,0 +1,306 @@ +/// @file Mesh_Reorder.cpp +/// @brief Implementation of ReorderPlan::build, ReorderPlan::apply, and +/// UnstructuredMesh::buildReorderRegistry / ReorderEntities. + +#include "ReorderPlan.hpp" +#include "Mesh.hpp" + +#include +#include + +namespace DNDS::Geom +{ + // ================================================================= + // ComputeFollowMap: derive follower placement from leader + // ================================================================= + + /// Compute a follow map for `follower` based on `leader`'s explicit map. + /// + /// Uses the support adjacency follower->leader (e.g., node2cell) to + /// determine: for each follower entity, go to min(leader.targetRank) + /// over all leaders referencing it. + /// + /// @param followerGM Global mapping for the follower entity kind. + /// @param leaderGM Global mapping for the leader entity kind. + /// @param follower2leader Support adj: follower -> leader (father-only, + /// global entries). Must cover all follower entities. + /// @param leaderTargetRanks Per-leader-slot target rank (the explicit map). + /// @param mpi MPI communicator. + /// @return Per-follower-slot target rank. + /// @warning Collective. + static std::vector ComputeFollowMap( + const ssp &followerGM, + const ssp &leaderGM, + const ReorderRegistry ®istry, + AdjKind follower2leaderKind, + const std::vector &leaderTargetRanks, + const MPIInfo &mpi) + { + DNDS_assert(followerGM); + DNDS_assert(leaderGM); + + index nFollower = followerGM->RLengths()[mpi.rank]; + + // Step 1: Build a ghost-pullable lookup of leader's targetRanks. + // lookup(leaderLocalSlot, 0) = leaderTargetRanks[leaderLocalSlot] + ArrayAdjacencyPair<1> leaderLookup; + leaderLookup.InitPair("followMap_leaderLookup", mpi); + leaderLookup.father->Resize(static_cast(leaderTargetRanks.size())); + for (index i = 0; i < static_cast(leaderTargetRanks.size()); i++) + leaderLookup(i, 0) = static_cast(leaderTargetRanks[i]); + leaderLookup.TransAttach(); + leaderLookup.trans.createFatherGlobalMapping(); + + // Step 2: Collect leader globals referenced by follower2leader + // (need ghost-pull for off-rank leaders) + // Find the adj in the registry + const AdjEntry *f2lEntry = nullptr; + for (auto &adj : registry.adjs) + if (adj.kind == follower2leaderKind) + { + f2lEntry = &adj; + break; + } + + // If follower2leader is not in registry, we need to look at the mesh data. + // For now, we require it to be registered. The caller should ensure node2cell + // or bnd2cell is built and registered before calling. + DNDS_assert_info(f2lEntry != nullptr, + fmt::format("ComputeFollowMap: follower2leader adj {} not in registry", + adjKindName(follower2leaderKind))); + + // We cannot directly iterate the adj via the registry (callbacks are type-erased). + // Instead, we use a different approach: the registry has globalMappings for the + // leader, and the follower2leader adj stores leader globals. We ghost-pull the + // leaderLookup for all off-rank leader globals referenced. + // + // Problem: we can't access the adj data through callbacks. We need the raw data. + // Solution: the caller passes the adj data directly to ComputeFollowMap. + // For now, return empty — this will be connected in buildReorderRegistry. + // + // Actually, we redesign: ComputeFollowMap takes a direct reference to the + // follower2leader pair data. + + // This function should not be called from here — see the overload below. + DNDS_assert_info(false, "ComputeFollowMap: internal error — use the pair-based overload"); + return {}; + } + + /// Overload that takes raw follower2leader data (father-only, global entries). + template + static std::vector ComputeFollowMapFromAdj( + const ArrayAdjacencyPair &follower2leader, + index nFollower, + const ssp &leaderGM, + const std::vector &leaderTargetRanks, + const MPIInfo &mpi) + { + DNDS_assert(leaderGM); + DNDS_assert(follower2leader.father); + + // Step 1: Build a ghost-pullable lookup of leader's targetRanks. + ArrayAdjacencyPair<1> leaderLookup; + leaderLookup.InitPair("followMap_leaderLookup", mpi); + index nLeader = static_cast(leaderTargetRanks.size()); + leaderLookup.father->Resize(nLeader); + for (index i = 0; i < nLeader; i++) + leaderLookup(i, 0) = static_cast(leaderTargetRanks[i]); + leaderLookup.TransAttach(); + leaderLookup.trans.createFatherGlobalMapping(); + + // Step 2: Collect off-rank leader globals from follower2leader entries. + std::set offRankLeaderGlobals; + for (index i = 0; i < nFollower; i++) + for (rowsize j = 0; j < follower2leader.RowSize(i); j++) + { + index leaderGlobal = follower2leader(i, j); + if (leaderGlobal == UnInitIndex) + continue; + auto [found, rank, val] = leaderGM->search(leaderGlobal); + if (found && rank != mpi.rank) + offRankLeaderGlobals.insert(leaderGlobal); + } + + // Step 3: Ghost-pull leaderLookup for off-rank leaders. + std::vector pullSet(offRankLeaderGlobals.begin(), offRankLeaderGlobals.end()); + leaderLookup.trans.createGhostMapping(pullSet); + leaderLookup.trans.createMPITypes(); + leaderLookup.trans.pullOnce(); + + // Step 4: For each follower, find min leader targetRank. + std::vector followMap(nFollower, mpi.size); // init to max + for (index i = 0; i < nFollower; i++) + { + MPI_int minRank = mpi.size; + for (rowsize j = 0; j < follower2leader.RowSize(i); j++) + { + index leaderGlobal = follower2leader(i, j); + if (leaderGlobal == UnInitIndex) + continue; + // Resolve to local-appended index in leaderLookup + MPI_int rank; + index val; + bool found = leaderLookup.trans.pLGhostMapping->search_indexAppend( + leaderGlobal, rank, val); + DNDS_assert(found); + MPI_int leaderTarget = static_cast(leaderLookup(val, 0)); + minRank = std::min(minRank, leaderTarget); + } + // If no leaders found (should not happen for valid mesh), stay put + followMap[i] = (minRank < mpi.size) ? minRank : mpi.rank; + } + + return followMap; + } + + // ================================================================= + // ReorderPlan::build + // ================================================================= + + ReorderPlan ReorderPlan::build( + const ReorderInput &input, + const ReorderRegistry ®istry, + const MPIInfo &mpi) + { + ReorderPlan plan; + + // --- Step 1: Merge explicit maps into allMaps --- + std::unordered_map> allMaps; + for (auto &em : input.explicitMaps) + allMaps[em.kind] = em.targetRanks; + + // --- Step 2: Compute follow maps --- + // (Follow computation requires raw adj data. This is handled by + // the mesh's buildReorderRegistry which populates follow maps + // before calling build. For now, we accept pre-computed follows + // in the input.follows and note that the mesh wrapper handles + // the actual computation.) + // + // Note: follow maps that are already in allMaps (explicit) take + // precedence. Follows are skipped for kinds already explicit. + + // Follow maps will be inserted by the mesh wrapper before calling build. + // (See UnstructuredMesh::ReorderEntities in the mesh wrapper section.) + + // --- Step 3: Build PermutationTransfer per entity kind --- + plan.reorderedKinds.clear(); + for (auto &[kind, ranks] : allMaps) + { + plan.reorderedKinds.insert(kind); + auto gm = registry.getGlobalMapping(kind); + DNDS_assert_info(gm, fmt::format("ReorderPlan::build: no global mapping for kind {}", + entityKindName(kind))); + plan.transfers[kind] = PermutationTransfer::fromPartition(ranks, gm, mpi); + } + + // --- Step 4: Detect global local-only --- + plan.isLocalOnly = true; + for (auto &[kind, transfer] : plan.transfers) + if (!transfer.isLocalOnly) + { + plan.isLocalOnly = false; + break; + } + int globalFlag; + int localFlag = plan.isLocalOnly ? 1 : 0; + MPI_Allreduce(&localFlag, &globalFlag, 1, MPI_INT, MPI_LAND, mpi.comm); + plan.isLocalOnly = (globalFlag != 0); + + // --- Step 5: Collect pull sets and build lookups --- + for (auto kind : plan.reorderedKinds) + { + std::set pullSetCollector; + auto gm = registry.getGlobalMapping(kind); + + // Scan all registered adjs for entries pointing to this kind + for (auto &adjEntry : registry.adjs) + { + EntityKind targetKind = adjEntry.kind.isIntraLevel() + ? adjEntry.kind.from + : adjEntry.kind.to; + if (targetKind != kind) + continue; + // We cannot iterate raw adj data here (type-erased callbacks). + // The pull set must be pre-collected by the mesh wrapper and + // passed via an extended mechanism. + // + // Alternative: the mesh wrapper collects pull sets and stores + // them in the registry. For now, we use a simpler approach: + // pull ALL off-rank globals that this rank's adjs reference. + // This is done by the mesh wrapper before calling build. + } + + // For simplicity in the initial implementation: if the transfer + // is local-only, no ghost-pull is needed (all lookups are local). + // For distributed, the mesh wrapper must pre-collect the pull set + // and store it. We accept a pull set from the registry. + // + // Store empty pull set for now — the mesh wrapper will populate + // lookups directly. + + std::vector pullSet(pullSetCollector.begin(), pullSetCollector.end()); + plan.lookups[kind] = plan.transfers.at(kind).buildLookup(pullSet, mpi); + } + + return plan; + } + + // ================================================================= + // ReorderPlan::apply + // ================================================================= + + void ReorderPlan::apply(ReorderRegistry ®istry, const MPIInfo &mpi) const + { + // Phase 1: REMAP all adj entries + for (auto &adj : registry.adjs) + { + auto action = classifyAdj(adj.kind, reorderedKinds); + if (action == AdjAction::REMAP || + action == AdjAction::RELOCATE_REMAP || + action == AdjAction::SELF) + { + EntityKind targetKind = adj.kind.isIntraLevel() + ? adj.kind.from + : adj.kind.to; + auto it = lookups.find(targetKind); + DNDS_assert_info(it != lookups.end(), + fmt::format("ReorderPlan::apply REMAP: no lookup for target kind {}", + entityKindName(targetKind))); + if (adj.remapFn) + adj.remapFn(it->second); + } + } + + // Phase 2: RELOCATE all adj rows + for (auto &adj : registry.adjs) + { + auto action = classifyAdj(adj.kind, reorderedKinds); + if (action == AdjAction::RELOCATE || + action == AdjAction::RELOCATE_REMAP || + action == AdjAction::SELF) + { + EntityKind sourceKind = adj.kind.from; + auto it = transfers.find(sourceKind); + DNDS_assert_info(it != transfers.end(), + fmt::format("ReorderPlan::apply RELOCATE: no transfer for source kind {}", + entityKindName(sourceKind))); + if (adj.relocateFn) + adj.relocateFn(it->second, mpi); + } + } + + // Phase 3: RELOCATE all companions of reordered kinds + for (auto &comp : registry.companions) + { + if (reorderedKinds.count(comp.kind)) + { + auto it = transfers.find(comp.kind); + DNDS_assert_info(it != transfers.end(), + fmt::format("ReorderPlan::apply COMPANION: no transfer for kind {}", + entityKindName(comp.kind))); + comp.fn(it->second, mpi); + } + } + } + +} // namespace DNDS::Geom diff --git a/src/Geom/Mesh/ReorderPlan.hpp b/src/Geom/Mesh/ReorderPlan.hpp new file mode 100644 index 00000000..da781599 --- /dev/null +++ b/src/Geom/Mesh/ReorderPlan.hpp @@ -0,0 +1,257 @@ +#pragma once +/// @file ReorderPlan.hpp +/// @brief Distributed entity reordering framework: ReorderRegistry, ReorderPlan, ReorderInput. +/// +/// Two-layer architecture: +/// - ReorderRegistry: dynamic set of callbacks (adj remap/relocate + companion relocate) +/// - ReorderPlan: standalone computed transfers + lookups, applies via callbacks +/// +/// UnstructuredMesh provides buildReorderRegistry() and ReorderEntities() convenience methods. + +#include "DNDS/PermutationTransfer.hpp" +#include "MeshConnectivity.hpp" + +#include +#include +#include +#include + +namespace DNDS::Geom +{ + // ================================================================= + // EntityReorderMap: per-entity target rank assignment + // ================================================================= + + /// Per-entity reorder specification: where each owned entity goes. + struct EntityReorderMap + { + EntityKind kind; + /// Per father slot: target rank after reorder. Size == father size. + std::vector targetRanks; + }; + + // ================================================================= + // FollowSpec: how one entity kind derives its placement from another + // ================================================================= + + /// Specification for follow-placement: entity `follower` derives its + /// target rank from entity `leader` via the support adjacency + /// `follower2leader` (e.g., Node follows Cell via node2cell). + /// + /// Assignment rule: follower entity goes to min(leader.targetRank) + /// over all leaders referencing it. + struct FollowSpec + { + EntityKind follower; ///< Entity kind to derive map for. + EntityKind leader; ///< Explicit-map entity kind to follow. + AdjKind follower2leader; ///< Support adj: follower -> leader. + }; + + // ================================================================= + // ReorderInput: what the caller provides + // ================================================================= + + /// Input to the reorder framework. + struct ReorderInput + { + /// Explicit reorder maps (caller-provided). + std::vector explicitMaps; + + /// Follow specifications (framework computes follow maps from these). + /// Default follows (Node, Bnd follow Cell) are added automatically + /// when Cell is in explicitMaps and Node/Bnd are not. + std::vector follows; + + /// Entity kinds whose adjacencies should be destroyed before reorder + /// (not reordered, not remapped -- just wiped). Typically {Face}. + std::unordered_set destroyKinds; + }; + + // ================================================================= + // Adjacency action classification + // ================================================================= + + /// Action to take on an adjacency during reorder. + enum class AdjAction + { + SKIP, ///< Neither source nor target reordered. + RELOCATE, ///< Source reordered, target not: move rows. + REMAP, ///< Target reordered, source not: update entries. + RELOCATE_REMAP, ///< Both reordered: update entries then move rows. + SELF, ///< Intra-level (A==A): update entries then move rows. + }; + + /// Classify an adjacency given the set of reordered entity kinds. + inline AdjAction classifyAdj(AdjKind adj, const std::unordered_set &reorderedKinds) + { + if (adj.isIntraLevel()) + return reorderedKinds.count(adj.from) ? AdjAction::SELF : AdjAction::SKIP; + + bool fromReordered = reorderedKinds.count(adj.from) > 0; + bool toReordered = reorderedKinds.count(adj.to) > 0; + + if (!fromReordered && !toReordered) + return AdjAction::SKIP; + if (fromReordered && !toReordered) + return AdjAction::RELOCATE; + if (!fromReordered && toReordered) + return AdjAction::REMAP; + return AdjAction::RELOCATE_REMAP; + } + + // ================================================================= + // ReorderRegistry: dynamic set of arrays participating in reorder + // ================================================================= + + /// Callback invoked during REMAP phase for an adjacency array. + using AdjRemapFn = std::function; + + /// Callback invoked during RELOCATE phase for an adjacency or companion array. + using AdjRelocateFn = std::function; + + /// One registered adjacency entry. + struct AdjEntry + { + AdjKind kind; + AdjRemapFn remapFn; ///< Remap entries (null if not needed). + AdjRelocateFn relocateFn; ///< Relocate rows (null if not needed). + std::string name; + }; + + /// One registered companion entry. + struct CompanionEntry + { + EntityKind kind; ///< Entity kind this array is parallel to. + AdjRelocateFn fn; ///< Callback to relocate the array. + std::string name; + }; + + /// Dynamic set of arrays that participate in a reorder operation. + /// Built by UnstructuredMesh::buildReorderRegistry() for mesh members, + /// extended by external code (solver, evaluator) before plan application. + struct ReorderRegistry + { + /// All adjacency entries (mesh members + external). + std::vector adjs; + + /// All companion entries (mesh members + external). + std::vector companions; + + /// Global offsets mappings per entity kind (for PermutationTransfer). + std::unordered_map> globalMappings; + + /// Register an adjacency with type-erased callbacks. + void registerAdj(AdjKind kind, AdjRemapFn remap, AdjRelocateFn relocate, + std::string name = {}) + { + adjs.push_back(AdjEntry{kind, std::move(remap), std::move(relocate), std::move(name)}); + } + + /// Register a companion with a type-erased relocate callback. + void registerCompanion(EntityKind kind, AdjRelocateFn fn, std::string name = {}) + { + companions.push_back(CompanionEntry{kind, std::move(fn), std::move(name)}); + } + + /// Register a GlobalOffsetsMapping for an entity kind. + void registerGlobalMapping(EntityKind kind, ssp gm) + { + globalMappings[kind] = std::move(gm); + } + + /// Get a registered global mapping (nullptr if not registered). + ssp getGlobalMapping(EntityKind kind) const + { + auto it = globalMappings.find(kind); + return (it != globalMappings.end()) ? it->second : nullptr; + } + }; + + // ================================================================= + // ReorderPlan: computed transfers + lookups, applies via callbacks + // ================================================================= + + /// Standalone plan object containing all computed PermutationTransfers + /// and LookupResults for a set of entity kinds. + /// + /// After construction (via `build`), this object has no dependency on + /// UnstructuredMesh. It can apply to any ReorderRegistry. + struct ReorderPlan + { + /// Per reordered entity kind: the computed transfer. + std::unordered_map transfers; + + /// Per reordered entity kind: the old->new global lookup. + std::unordered_map lookups; + + /// Set of entity kinds being reordered. + std::unordered_set reorderedKinds; + + /// Whether all transfers are local-only (collective agreement). + bool isLocalOnly{false}; + + // ------------------------------------------------------------- + // Factory + // ------------------------------------------------------------- + + /// Build a ReorderPlan from input + registry + MPI. + /// + /// Steps: + /// 1. Compute follow maps (ghost-pull leader targetRanks, min-rank rule). + /// 2. Build PermutationTransfer per entity kind. + /// 3. Collect pull sets per entity kind from registry adj entries. + /// 4. Build lookups (ghost-pullable old->new global). + /// + /// @warning Collective. + static ReorderPlan build( + const ReorderInput &input, + const ReorderRegistry ®istry, + const MPIInfo &mpi); + + // ------------------------------------------------------------- + // Application + // ------------------------------------------------------------- + + /// Apply the plan to all entries in a registry. + /// + /// Phase 1: REMAP all adj entries (target kind's lookup). + /// Phase 2: RELOCATE all adj rows (source kind's transfer). + /// Phase 3: RELOCATE all companions of reordered kinds. + /// + /// @warning Collective (when !isLocalOnly). + void apply(ReorderRegistry ®istry, const MPIInfo &mpi) const; + + // ------------------------------------------------------------- + // Standalone operations for external arrays + // ------------------------------------------------------------- + + /// Remap entries of an adjacency array whose target kind is `targetKind`. + template + void remapEntries(TPair &pair, EntityKind targetKind) const + { + auto it = lookups.find(targetKind); + DNDS_assert_info(it != lookups.end(), + "remapEntries: no lookup for target kind"); + const auto &lookup = it->second; + for (index i = 0; i < pair.father->Size(); i++) + for (rowsize j = 0; j < pair.RowSize(i); j++) + { + index &v = pair(i, j); + if (v == UnInitIndex) + continue; + v = lookup.resolve(v); + } + } + + /// Relocate rows of an array pair whose source kind is `sourceKind`. + template + void relocateRows(TPair &pair, EntityKind sourceKind, const MPIInfo &mpi) const + { + auto it = transfers.find(sourceKind); + DNDS_assert_info(it != transfers.end(), + "relocateRows: no transfer for source kind"); + it->second.transferRows(pair, mpi); + } + }; + +} // namespace DNDS::Geom diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt index c3db06a3..f307547e 100644 --- a/test/cpp/CMakeLists.txt +++ b/test/cpp/CMakeLists.txt @@ -217,6 +217,7 @@ geom_add_mpi_test(mesh_distributed_read "Geom/test_MeshDistributedRead.cpp" "geo geom_add_mpi_test(mesh_connectivity "Geom/test_MeshConnectivity.cpp" "geom;dnds") geom_add_mpi_test(mesh_connectivity_ghost "Geom/test_MeshConnectivity_Ghost.cpp" "geom;dnds") geom_add_mpi_test(mesh_connectivity_interpolate "Geom/test_MeshConnectivity_Interpolate.cpp" "geom;dnds") +geom_add_mpi_test(mesh_reorder "Geom/test_MeshReorder.cpp" "geom;dnds") set(GEOM_TEST_TARGETS geom_test_elements @@ -227,6 +228,7 @@ set(GEOM_TEST_TARGETS geom_test_mesh_connectivity geom_test_mesh_connectivity_ghost geom_test_mesh_connectivity_interpolate + geom_test_mesh_reorder ) foreach(T ${GEOM_TEST_TARGETS}) add_dependencies(geom_unit_tests ${T}) diff --git a/test/cpp/Geom/test_MeshReorder.cpp b/test/cpp/Geom/test_MeshReorder.cpp new file mode 100644 index 00000000..655ce81d --- /dev/null +++ b/test/cpp/Geom/test_MeshReorder.cpp @@ -0,0 +1,313 @@ +/** + * @file test_MeshReorder.cpp + * @brief Unit tests for ReorderPlan, ReorderRegistry, and classification. + * + * Tests: + * - AdjAction classification logic + * - ReorderRegistry registration and lookup + * - ReorderPlan::build + apply on synthetic data (no real mesh) + * - Full mesh reorder via UnstructuredMesh (later phases) + */ + +#define DOCTEST_CONFIG_IMPLEMENT +#include "doctest.h" +#include "Geom/Mesh/ReorderPlan.hpp" +#include + +using namespace DNDS; +using namespace DNDS::Geom; + +int main(int argc, char **argv) +{ + MPI_Init(&argc, &argv); + doctest::Context ctx; + ctx.applyCommandLine(argc, argv); + int res = ctx.run(); + MPI_Finalize(); + return res; +} + +static MPIInfo worldMPI() +{ + MPIInfo mpi; + mpi.setWorld(); + return mpi; +} + +// ================================================================= +// Test: classifyAdj logic +// ================================================================= + +TEST_CASE("classifyAdj basic classification") +{ + std::unordered_set reordered; + + SUBCASE("empty reordered set") + { + CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::SKIP); + CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SKIP); + } + + SUBCASE("cell only reordered") + { + reordered = {EntityKind::Cell}; + CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE); + CHECK(classifyAdj(Adj::Cell2Face, reordered) == AdjAction::RELOCATE); + CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF); + CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Face2Cell, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::SKIP); + CHECK(classifyAdj(Adj::Face2Node, reordered) == AdjAction::SKIP); + } + + SUBCASE("node only reordered") + { + reordered = {EntityKind::Node}; + CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE); + CHECK(classifyAdj(Adj::Node2Bnd, reordered) == AdjAction::RELOCATE); + CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SKIP); + } + + SUBCASE("cell + node reordered") + { + reordered = {EntityKind::Cell, EntityKind::Node}; + CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF); + CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::REMAP); + CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::REMAP); + } + + SUBCASE("cell + node + bnd reordered") + { + reordered = {EntityKind::Cell, EntityKind::Node, EntityKind::Bnd}; + CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Node2Bnd, reordered) == AdjAction::RELOCATE_REMAP); + CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF); + } +} + +// ================================================================= +// Test: ReorderRegistry basic operations +// ================================================================= + +TEST_CASE("ReorderRegistry register and query") +{ + auto mpi = worldMPI(); + ReorderRegistry reg; + + // Register a global mapping + auto gm = make_ssp(); + gm->setMPIAlignBcast(mpi, 10); + reg.registerGlobalMapping(EntityKind::Cell, gm); + + CHECK(reg.getGlobalMapping(EntityKind::Cell) == gm); + CHECK(reg.getGlobalMapping(EntityKind::Node) == nullptr); + + // Register an adj + bool remapCalled = false; + bool relocateCalled = false; + reg.registerAdj( + Adj::Cell2Node, + [&](const PermutationTransfer::LookupResult &) + { remapCalled = true; }, + [&](const PermutationTransfer &, const MPIInfo &) + { relocateCalled = true; }, + "cell2node"); + + CHECK(reg.adjs.size() == 1); + CHECK(reg.adjs[0].kind == Adj::Cell2Node); + CHECK(reg.adjs[0].name == "cell2node"); + + // Register a companion + bool compCalled = false; + reg.registerCompanion( + EntityKind::Cell, + [&](const PermutationTransfer &, const MPIInfo &) + { compCalled = true; }, + "cellElemInfo"); + + CHECK(reg.companions.size() == 1); + CHECK(reg.companions[0].kind == EntityKind::Cell); +} + +// ================================================================= +// Test: ReorderPlan::apply with synthetic data +// ================================================================= + +TEST_CASE("ReorderPlan::apply cell-only local permutation") +{ + auto mpi = worldMPI(); + const DNDS::index nCell = 8; + const DNDS::index nNode = 4; + + // Create synthetic cell2node: each cell references 2 nodes (global) + ArrayAdjacencyPair<2> cell2node; + cell2node.InitPair("cell2node", mpi); + cell2node.father->Resize(nCell); + cell2node.father->createGlobalMapping(); + + DNDS::index cellOffset = (*cell2node.father->pLGlobalMapping)(mpi.rank, 0); + + // Create synthetic node array + ArrayAdjacencyPair<1> nodeArr; + nodeArr.InitPair("nodeArr", mpi); + nodeArr.father->Resize(nNode); + nodeArr.father->createGlobalMapping(); + + DNDS::index nodeOffset = (*nodeArr.father->pLGlobalMapping)(mpi.rank, 0); + + // Fill cell2node: cell i references nodes (i%nNode) and ((i+1)%nNode) + for (DNDS::index i = 0; i < nCell; i++) + { + cell2node(i, 0) = nodeOffset + (i % nNode); + cell2node(i, 1) = nodeOffset + ((i + 1) % nNode); + } + + // Create a companion array (cellElemInfo analog) + ArrayAdjacencyPair<1> cellInfo; + cellInfo.InitPair("cellInfo", mpi); + cellInfo.father->Resize(nCell); + for (DNDS::index i = 0; i < nCell; i++) + cellInfo(i, 0) = 1000 + cellOffset + i; // tag = 1000 + global + + // Build registry + ReorderRegistry reg; + reg.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping); + reg.registerGlobalMapping(EntityKind::Node, nodeArr.father->pLGlobalMapping); + + reg.registerAdj( + Adj::Cell2Node, + nullptr, // no remap needed (only Cell reordered, not Node) + [&](const PermutationTransfer &t, const MPIInfo &m) + { t.transferRows(cell2node, m); }, + "cell2node"); + + reg.registerCompanion( + EntityKind::Cell, + [&](const PermutationTransfer &t, const MPIInfo &m) + { t.transferRows(cellInfo, m); }, + "cellInfo"); + + // Build plan: reverse cell permutation (all local) + std::vector cellPartition(nCell, mpi.rank); + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition}); + + auto plan = ReorderPlan::build(input, reg, mpi); + CHECK(plan.isLocalOnly); + CHECK(plan.reorderedKinds.count(EntityKind::Cell)); + CHECK_FALSE(plan.reorderedKinds.count(EntityKind::Node)); + + // Apply + plan.apply(reg, mpi); + + // Since partition = all-self and ordering is preserved within rank, + // the data should be unchanged (identity permutation via fromPartition). + for (DNDS::index i = 0; i < nCell; i++) + { + CHECK(cell2node(i, 0) == nodeOffset + (i % nNode)); + CHECK(cell2node(i, 1) == nodeOffset + ((i + 1) % nNode)); + CHECK(cellInfo(i, 0) == 1000 + cellOffset + i); + } +} + +// ================================================================= +// Test: ReorderPlan::apply with remap (node reorder, cells stay) +// ================================================================= + +TEST_CASE("ReorderPlan::apply node-only remap") +{ + auto mpi = worldMPI(); + const DNDS::index nCell = 4; + const DNDS::index nNode = 6; + + // cell2node: Cell->Node (cell rows fixed, node entries need remapping) + ArrayAdjacencyPair<2> cell2node; + cell2node.InitPair("cell2node", mpi); + cell2node.father->Resize(nCell); + cell2node.father->createGlobalMapping(); + + ArrayAdjacencyPair<1> nodeArr; + nodeArr.InitPair("nodeArr", mpi); + nodeArr.father->Resize(nNode); + nodeArr.father->createGlobalMapping(); + + DNDS::index nodeOffset = (*nodeArr.father->pLGlobalMapping)(mpi.rank, 0); + + // Fill: cell i refs nodes i and i+1 + for (DNDS::index i = 0; i < nCell; i++) + { + cell2node(i, 0) = nodeOffset + i; + cell2node(i, 1) = nodeOffset + i + 1; + } + + // Node companion: coords analog + ArrayAdjacencyPair<1> coords; + coords.InitPair("coords", mpi); + coords.father->Resize(nNode); + for (DNDS::index i = 0; i < nNode; i++) + coords(i, 0) = 500 + nodeOffset + i; // value = 500 + globalNode + + // Build registry + ReorderRegistry reg; + reg.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping); + reg.registerGlobalMapping(EntityKind::Node, nodeArr.father->pLGlobalMapping); + + reg.registerAdj( + Adj::Cell2Node, + [&](const PermutationTransfer::LookupResult &lookup) + { + for (DNDS::index i = 0; i < nCell; i++) + for (rowsize j = 0; j < 2; j++) + { + DNDS::index &v = cell2node(i, j); + if (v != UnInitIndex) + v = lookup.resolve(v); + } + }, + nullptr, // no relocate (Cell not reordered) + "cell2node"); + + reg.registerCompanion( + EntityKind::Node, + [&](const PermutationTransfer &t, const MPIInfo &m) + { t.transferRows(coords, m); }, + "coords"); + + // Reorder nodes: all stay local (identity partition) + std::vector nodePartition(nNode, mpi.rank); + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, nodePartition}); + + auto plan = ReorderPlan::build(input, reg, mpi); + CHECK(plan.isLocalOnly); + CHECK(plan.reorderedKinds.count(EntityKind::Node)); + CHECK_FALSE(plan.reorderedKinds.count(EntityKind::Cell)); + + // Apply + plan.apply(reg, mpi); + + // With identity partition (all stay on same rank), new globals are + // contiguous starting at newGlobalOffsets[mpi.rank]. + // For identity: newGlobalIndices[i] = nodeOffset + i (unchanged). + // So remap should be identity, coords should be unchanged. + for (DNDS::index i = 0; i < nCell; i++) + { + // Since it's identity partition, old globals map to same new globals + auto &transfer = plan.transfers.at(EntityKind::Node); + DNDS::index expectedNode0 = transfer.newGlobalIndices[i]; + DNDS::index expectedNode1 = transfer.newGlobalIndices[i + 1]; + CHECK(cell2node(i, 0) == expectedNode0); + CHECK(cell2node(i, 1) == expectedNode1); + } + + for (DNDS::index i = 0; i < nNode; i++) + CHECK(coords(i, 0) == 500 + nodeOffset + i); +} From a341ba26cd24838861f44561cfd3a4d8162c4838 Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 20:46:49 +0800 Subject: [PATCH 39/43] feat(Geom): UnstructuredMesh::buildReorderRegistry + ReorderEntities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2b implementation: - buildReorderRegistry(): registers all 12 tracked adj members and all companion arrays (cellElemInfo, coords, pbi, etc.) as type-erased callbacks. Registers global mappings per entity kind. - buildReorderPlan(): augments input with default follows (Node/Bnd follow Cell), computes follow maps via ComputeFollowMapFromAdj, builds the plan. - ReorderEntities(): full pipeline — validate state, build registry, compute follows, build plan, destroy face adjs if requested, apply plan (REMAP -> RELOCATE -> COMPANIONS), rebuild global mappings, update idx states, clear stale local vectors. Default follow: when Cell is explicitly reordered and Node/Bnd are not in the explicit set, they automatically follow Cell via node2cell and bnd2cell (min-rank rule). --- src/Geom/Mesh/Mesh.hpp | 42 ++++ src/Geom/Mesh/Mesh_Reorder.cpp | 400 +++++++++++++++++++++++++++++++++ 2 files changed, 442 insertions(+) diff --git a/src/Geom/Mesh/Mesh.hpp b/src/Geom/Mesh/Mesh.hpp index b19d019e..a230c678 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -13,6 +13,7 @@ #include "DNDS/Config/ConfigParam.hpp" #include "AdjIndexInfo.hpp" #include "MeshConnectivity.hpp" +#include "ReorderPlan.hpp" namespace DNDS::Direct { @@ -516,6 +517,47 @@ namespace DNDS::Geom MeshConnectivity &dag, const std::unordered_set &skip) const; + // ================================================================= + // Reorder (distributed entity reordering framework) + // ================================================================= + + /** + * \brief Build a ReorderRegistry containing all mesh members. + * + * Registers all built adj arrays (as type-erased callbacks) and all + * companion arrays (coords, cellElemInfo, pbi, etc.). + * Skips adjacencies involving destroyKinds. + * + * External code may extend the returned registry with its own arrays + * before passing to ReorderPlan::build. + */ + ReorderRegistry buildReorderRegistry( + const std::unordered_set &destroyKinds = {}); + + /** + * \brief Reorder entities using the general framework. + * + * Builds a ReorderRegistry, computes follow maps, builds a + * ReorderPlan, applies it, rebuilds global mappings, and updates + * idx states. + * + * \pre All adjacencies in Adj_PointToGlobal state. + * \post All (non-destroyed) adjacencies in Adj_PointToGlobal. + * Ghost mappings stale (caller must rebuild ghosts). + * Global mappings fresh on reordered entities. + * + * \warning Collective. + */ + void ReorderEntities(const ReorderInput &input); + + /** + * \brief Build a ReorderPlan without applying it. + * + * Useful for external code to obtain the plan and apply it to + * its own arrays after the mesh reorder. + */ + ReorderPlan buildReorderPlan(const ReorderInput &input); + // void ReorderCellLocal(); /** diff --git a/src/Geom/Mesh/Mesh_Reorder.cpp b/src/Geom/Mesh/Mesh_Reorder.cpp index dcb6eb8f..d6331acb 100644 --- a/src/Geom/Mesh/Mesh_Reorder.cpp +++ b/src/Geom/Mesh/Mesh_Reorder.cpp @@ -303,4 +303,404 @@ namespace DNDS::Geom } } + // ================================================================= + // UnstructuredMesh::buildReorderRegistry + // ================================================================= + + ReorderRegistry UnstructuredMesh::buildReorderRegistry( + const std::unordered_set &destroyKinds) + { + ReorderRegistry reg; + + auto shouldSkip = [&](AdjKind kind) + { + return destroyKinds.count(kind.from) || destroyKinds.count(kind.to); + }; + + // --- Helper: register a tracked adj member --- + auto regAdj = [&](AdjKind kind, auto &trackedPair) + { + if (!trackedPair.father || shouldSkip(kind)) + return; + + AdjRemapFn remap = [&trackedPair](const PermutationTransfer::LookupResult &lookup) + { + index nRows = trackedPair.father->Size(); + for (index i = 0; i < nRows; i++) + for (rowsize j = 0; j < trackedPair.RowSize(i); j++) + { + index &v = trackedPair(i, j); + if (v != UnInitIndex) + v = lookup.resolve(v); + } + }; + + AdjRelocateFn relocate = [&trackedPair]( + const PermutationTransfer &t, const MPIInfo &m) + { + t.transferRows(trackedPair, m); + }; + + reg.registerAdj(kind, std::move(remap), std::move(relocate), + adjKindName(kind)); + }; + + // Register tracked adj members + regAdj(Adj::Cell2Node, cell2node); + regAdj(Adj::Cell2Cell, cell2cell); + regAdj(Adj::Bnd2Node, bnd2node); + regAdj(Adj::Bnd2Cell, bnd2cell); + regAdj(Adj::Node2Cell, node2cell); + regAdj(Adj::Node2Bnd, node2bnd); + regAdj(Adj::Cell2Face, cell2face); + regAdj(Adj::Face2Node, face2node); + regAdj(Adj::Face2Cell, face2cell); + regAdj(Adj::Face2Bnd, face2bnd); + regAdj(Adj::Bnd2Face, bnd2face); + regAdj(Adj::Cell2CellFace, cell2cellFace); + + // --- Helper: register a companion --- + auto regComp = [&](EntityKind kind, auto &pair, const char *name) + { + if (!pair.father || destroyKinds.count(kind)) + return; + reg.registerCompanion(kind, [&pair](const PermutationTransfer &t, const MPIInfo &m) + { t.transferRows(pair, m); }, name); + }; + + // Register companions + regComp(EntityKind::Cell, cellElemInfo, "cellElemInfo"); + regComp(EntityKind::Cell, cell2cellOrig, "cell2cellOrig"); + regComp(EntityKind::Node, coords, "coords"); + regComp(EntityKind::Node, node2nodeOrig, "node2nodeOrig"); + regComp(EntityKind::Bnd, bndElemInfo, "bndElemInfo"); + regComp(EntityKind::Bnd, bnd2bndOrig, "bnd2bndOrig"); + + if (isPeriodic) + { + regComp(EntityKind::Cell, cell2nodePbi, "cell2nodePbi"); + regComp(EntityKind::Bnd, bnd2nodePbi, "bnd2nodePbi"); + if (!destroyKinds.count(EntityKind::Face)) + regComp(EntityKind::Face, face2nodePbi, "face2nodePbi"); + } + if (!destroyKinds.count(EntityKind::Face)) + regComp(EntityKind::Face, faceElemInfo, "faceElemInfo"); + if (coordsElevDisp.father) + regComp(EntityKind::Node, coordsElevDisp, "coordsElevDisp"); + if (nodeWallDist.father) + regComp(EntityKind::Node, nodeWallDist, "nodeWallDist"); + + // --- Register global mappings --- + auto getGM = [](const auto &pair) -> ssp + { + if (pair.father && pair.father->pLGlobalMapping) + return pair.father->pLGlobalMapping; + return nullptr; + }; + + auto firstValid = [](std::initializer_list> candidates) + -> ssp + { + for (auto &gm : candidates) + if (gm) + return gm; + return nullptr; + }; + + if (auto gm = firstValid({getGM(cell2node), getGM(cell2cell), getGM(cell2face)})) + reg.registerGlobalMapping(EntityKind::Cell, gm); + if (auto gm = coords.father ? coords.father->pLGlobalMapping : nullptr) + reg.registerGlobalMapping(EntityKind::Node, gm); + if (auto gm = firstValid({getGM(bnd2node), getGM(bnd2cell), getGM(bnd2face)})) + reg.registerGlobalMapping(EntityKind::Bnd, gm); + if (auto gm = firstValid({getGM(face2node), getGM(face2cell), getGM(face2bnd)})) + reg.registerGlobalMapping(EntityKind::Face, gm); + + return reg; + } + + // ================================================================= + // UnstructuredMesh::buildReorderPlan + // ================================================================= + + ReorderPlan UnstructuredMesh::buildReorderPlan(const ReorderInput &input) + { + auto reg = buildReorderRegistry(input.destroyKinds); + + // Augment input with default follows: Node, Bnd follow Cell + // if Cell is explicit and Node/Bnd are not. + ReorderInput augmented = input; + std::unordered_set explicitKinds; + for (auto &em : augmented.explicitMaps) + explicitKinds.insert(em.kind); + + // Add default follows + if (explicitKinds.count(EntityKind::Cell)) + { + if (!explicitKinds.count(EntityKind::Node) && node2cell.father) + { + augmented.follows.push_back( + FollowSpec{EntityKind::Node, EntityKind::Cell, Adj::Node2Cell}); + } + if (!explicitKinds.count(EntityKind::Bnd) && bnd2cell.father) + { + augmented.follows.push_back( + FollowSpec{EntityKind::Bnd, EntityKind::Cell, Adj::Bnd2Cell}); + } + } + + // Compute follow maps and merge into the input + std::unordered_map> allMaps; + for (auto &em : augmented.explicitMaps) + allMaps[em.kind] = em.targetRanks; + + for (auto &spec : augmented.follows) + { + if (allMaps.count(spec.follower)) + continue; // explicit takes precedence + + auto leaderIt = allMaps.find(spec.leader); + DNDS_assert_info(leaderIt != allMaps.end(), + fmt::format("buildReorderPlan: follow spec references leader {} " + "which has no map", + entityKindName(spec.leader))); + + // Use the raw adj data for follow computation + // We need the follower->leader adj data. Dispatch by known kinds: + std::vector followMap; + auto followerGM = reg.getGlobalMapping(spec.follower); + DNDS_assert_info(followerGM, + fmt::format("buildReorderPlan: no global mapping for follower {}", + entityKindName(spec.follower))); + index nFollower = followerGM->RLengths()[mpi.rank]; + + if (spec.follower2leader == Adj::Node2Cell && node2cell.father) + { + followMap = ComputeFollowMapFromAdj( + static_cast(node2cell), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + } + else if (spec.follower2leader == Adj::Bnd2Cell && bnd2cell.father) + { + followMap = ComputeFollowMapFromAdj( + static_cast(bnd2cell), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + } + else if (spec.follower2leader == Adj::Node2Bnd && node2bnd.father) + { + followMap = ComputeFollowMapFromAdj( + static_cast(node2bnd), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + } + else + { + DNDS_assert_info(false, + fmt::format("buildReorderPlan: unsupported follow adj {}", + adjKindName(spec.follower2leader))); + } + + allMaps[spec.follower] = std::move(followMap); + } + + // Now build the plan with all maps (explicit + follow) + // We need to pass allMaps into ReorderPlan::build. + // Convert allMaps to explicitMaps format for build: + ReorderInput finalInput; + for (auto &[kind, ranks] : allMaps) + finalInput.explicitMaps.push_back(EntityReorderMap{kind, ranks}); + finalInput.destroyKinds = input.destroyKinds; + + return ReorderPlan::build(finalInput, reg, mpi); + } + + // ================================================================= + // UnstructuredMesh::ReorderEntities + // ================================================================= + + void UnstructuredMesh::ReorderEntities(const ReorderInput &input) + { + // Step 0: Validate precondition + DNDS_assert_info(adjPrimaryState == Adj_PointToGlobal, + "ReorderEntities: adjPrimaryState must be Adj_PointToGlobal"); + + // Step 1: Build registry (with destroy skip) + auto reg = buildReorderRegistry(input.destroyKinds); + + // Step 2: Build plan (with follows computed) + // We replicate the logic from buildReorderPlan but use the mutable registry + ReorderInput augmented = input; + std::unordered_set explicitKinds; + for (auto &em : augmented.explicitMaps) + explicitKinds.insert(em.kind); + + if (explicitKinds.count(EntityKind::Cell)) + { + if (!explicitKinds.count(EntityKind::Node) && node2cell.father) + augmented.follows.push_back( + FollowSpec{EntityKind::Node, EntityKind::Cell, Adj::Node2Cell}); + if (!explicitKinds.count(EntityKind::Bnd) && bnd2cell.father) + augmented.follows.push_back( + FollowSpec{EntityKind::Bnd, EntityKind::Cell, Adj::Bnd2Cell}); + } + + // Compute follows + std::unordered_map> allMaps; + for (auto &em : augmented.explicitMaps) + allMaps[em.kind] = em.targetRanks; + + for (auto &spec : augmented.follows) + { + if (allMaps.count(spec.follower)) + continue; + auto leaderIt = allMaps.find(spec.leader); + DNDS_assert(leaderIt != allMaps.end()); + + auto followerGM = reg.getGlobalMapping(spec.follower); + DNDS_assert(followerGM); + index nFollower = followerGM->RLengths()[mpi.rank]; + + std::vector followMap; + if (spec.follower2leader == Adj::Node2Cell && node2cell.father) + followMap = ComputeFollowMapFromAdj( + static_cast(node2cell), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + else if (spec.follower2leader == Adj::Bnd2Cell && bnd2cell.father) + followMap = ComputeFollowMapFromAdj( + static_cast(bnd2cell), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + else if (spec.follower2leader == Adj::Node2Bnd && node2bnd.father) + followMap = ComputeFollowMapFromAdj( + static_cast(node2bnd), + nFollower, reg.getGlobalMapping(spec.leader), + leaderIt->second, mpi); + else + DNDS_assert(false); + + allMaps[spec.follower] = std::move(followMap); + } + + // Build plan + ReorderInput finalInput; + for (auto &[kind, ranks] : allMaps) + finalInput.explicitMaps.push_back(EntityReorderMap{kind, ranks}); + finalInput.destroyKinds = input.destroyKinds; + + auto plan = ReorderPlan::build(finalInput, reg, mpi); + + // Step 3: Destroy adjacencies for destroyKinds + auto destroyAdj = [&](auto &trackedPair) + { + trackedPair.father.reset(); + trackedPair.son.reset(); + trackedPair.idx = AdjIndexInfo{}; + }; + for (auto kind : input.destroyKinds) + { + if (kind == EntityKind::Face) + { + destroyAdj(cell2face); + destroyAdj(face2node); + destroyAdj(face2cell); + destroyAdj(face2bnd); + destroyAdj(bnd2face); + destroyAdj(cell2cellFace); + faceElemInfo.father.reset(); + faceElemInfo.son.reset(); + if (isPeriodic) + { + face2nodePbi.father.reset(); + face2nodePbi.son.reset(); + } + adjFacialState = Adj_Unknown; + adjC2FState = Adj_Unknown; + adjC2CFaceState = Adj_Unknown; + } + } + + // Step 4: Apply plan (REMAP entries, RELOCATE rows + companions) + plan.apply(reg, mpi); + + // Step 5: Rebuild global mappings for reordered entities + for (auto kind : plan.reorderedKinds) + { + if (kind == EntityKind::Cell && cell2node.father) + { + cell2node.TransAttach(); + cell2node.trans.createFatherGlobalMapping(); + // Borrow to other cell-parallel arrays + if (cell2cell.father) + cell2cell.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + if (cellElemInfo.father) + cellElemInfo.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + if (cell2cellOrig.father) + cell2cellOrig.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + } + else if (kind == EntityKind::Node && coords.father) + { + coords.TransAttach(); + coords.trans.createFatherGlobalMapping(); + if (node2nodeOrig.father) + node2nodeOrig.father->pLGlobalMapping = coords.father->pLGlobalMapping; + } + else if (kind == EntityKind::Bnd && bnd2node.father) + { + bnd2node.TransAttach(); + bnd2node.trans.createFatherGlobalMapping(); + if (bndElemInfo.father) + bndElemInfo.father->pLGlobalMapping = bnd2node.father->pLGlobalMapping; + if (bnd2bndOrig.father) + bnd2bndOrig.father->pLGlobalMapping = bnd2node.father->pLGlobalMapping; + } + else if (kind == EntityKind::Face && face2node.father) + { + face2node.TransAttach(); + face2node.trans.createFatherGlobalMapping(); + if (faceElemInfo.father) + faceElemInfo.father->pLGlobalMapping = face2node.father->pLGlobalMapping; + } + } + + // Step 6: Update idx states + auto markGlobalIfBuilt = [](auto &trackedPair) + { + if (trackedPair.father && trackedPair.idx.state() != Adj_Unknown) + trackedPair.idx.markGlobal(); + }; + // For all tracked adj that were affected (non-SKIP), mark global. + // Simplification: mark all existing adj as global since we're in + // Adj_PointToGlobal state overall. + if (cell2node.father) + cell2node.idx.markGlobal(); + if (cell2cell.father) + cell2cell.idx.markGlobal(); + if (bnd2node.father) + bnd2node.idx.markGlobal(); + if (bnd2cell.father) + bnd2cell.idx.markGlobal(); + if (node2cell.father) + node2cell.idx.markGlobal(); + if (node2bnd.father) + node2bnd.idx.markGlobal(); + + // Step 7: Update mesh-level state + adjPrimaryState = Adj_PointToGlobal; + if (node2cell.father) + adjN2CBState = Adj_PointToGlobal; + + // Invalidate local vectors + cell2parentCell.clear(); + node2parentNode.clear(); + node2bndNode.clear(); + vtkCell2nodeOffsets.clear(); + vtkCellType.clear(); + vtkCell2node.clear(); + nodeRecreated2nodeLocal.clear(); + localPartitionStarts.clear(); + } + } // namespace DNDS::Geom From 8270bf31041bb6cbc3168b6bb897eed4219639df Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 21:04:09 +0800 Subject: [PATCH 40/43] =?UTF-8?q?feat(Geom):=20ReorderEntities=20on=20real?= =?UTF-8?q?=20mesh=20=E2=80=94=20pull=20set=20collection,=20son=20reattach?= =?UTF-8?q?,=208=20tests=20passing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete Phase 2b with real-mesh testing: - buildReorderRegistry: collects pull sets (off-rank globals from adj entries) per entity kind, deduplicates/sorts them for buildLookup - ReorderEntities: allocates empty sons + TransAttach after plan.apply (required for subsequent RecoverNode2CellAndNode2Bnd pipeline) - ReorderPlan::build: uses pre-collected pullSets from registry - father->createGlobalMapping() instead of TransAttach (no son needed) Real-mesh tests on UniformSquare_10.cgns (100 cells, 2D): - Cell-only local reorder: global counts conserved, adj entries valid, full rebuild (ghost + local) succeeds, cell2node entries valid local - Cell-only with face destruction: faces destroyed, state reset, rebuild from scratch succeeds (InterpolateFace + AssertOnFaces) - Cell distributed round-robin (np>=2): cross-rank redistribution with Node/Bnd follow, global counts conserved, adj entries valid, full rebuild succeeds - Node-only local: coords unchanged (identity), cell2node remapped, full rebuild + face interpolation succeeds All 8/8 tests pass at np=1,2,4. --- src/Geom/Mesh/Mesh_Reorder.cpp | 136 +++++++++--- src/Geom/Mesh/ReorderPlan.hpp | 5 + test/cpp/Geom/test_MeshReorder.cpp | 336 ++++++++++++++++++++++++++++- 3 files changed, 441 insertions(+), 36 deletions(-) diff --git a/src/Geom/Mesh/Mesh_Reorder.cpp b/src/Geom/Mesh/Mesh_Reorder.cpp index d6331acb..795ec6b5 100644 --- a/src/Geom/Mesh/Mesh_Reorder.cpp +++ b/src/Geom/Mesh/Mesh_Reorder.cpp @@ -212,33 +212,12 @@ namespace DNDS::Geom std::set pullSetCollector; auto gm = registry.getGlobalMapping(kind); - // Scan all registered adjs for entries pointing to this kind - for (auto &adjEntry : registry.adjs) - { - EntityKind targetKind = adjEntry.kind.isIntraLevel() - ? adjEntry.kind.from - : adjEntry.kind.to; - if (targetKind != kind) - continue; - // We cannot iterate raw adj data here (type-erased callbacks). - // The pull set must be pre-collected by the mesh wrapper and - // passed via an extended mechanism. - // - // Alternative: the mesh wrapper collects pull sets and stores - // them in the registry. For now, we use a simpler approach: - // pull ALL off-rank globals that this rank's adjs reference. - // This is done by the mesh wrapper before calling build. - } - - // For simplicity in the initial implementation: if the transfer - // is local-only, no ghost-pull is needed (all lookups are local). - // For distributed, the mesh wrapper must pre-collect the pull set - // and store it. We accept a pull set from the registry. - // - // Store empty pull set for now — the mesh wrapper will populate - // lookups directly. + // Use pre-collected pull sets from the registry + std::vector pullSet; + auto psIt = registry.pullSets.find(kind); + if (psIt != registry.pullSets.end()) + pullSet = psIt->second; - std::vector pullSet(pullSetCollector.begin(), pullSetCollector.end()); plan.lookups[kind] = plan.transfers.at(kind).buildLookup(pullSet, mpi); } @@ -416,6 +395,54 @@ namespace DNDS::Geom if (auto gm = firstValid({getGM(face2node), getGM(face2cell), getGM(face2bnd)})) reg.registerGlobalMapping(EntityKind::Face, gm); + // --- Pre-collect pull sets --- + // For each entity kind, collect off-rank globals from adj entries targeting it. + auto collectPS = [&](EntityKind targetKind, const auto &adjPair, auto targetGM) + { + if (!adjPair.father || !targetGM) + return; + auto &ps = reg.pullSets[targetKind]; + DNDS::index nRows = adjPair.father->Size(); + for (DNDS::index i = 0; i < nRows; i++) + for (rowsize j = 0; j < adjPair.RowSize(i); j++) + { + DNDS::index v = adjPair(i, j); + if (v == UnInitIndex) + continue; + auto [found, rank, val] = targetGM->search(v); + if (found && rank != mpi.rank) + ps.push_back(v); + } + }; + + // Cell as target (from: bnd2cell, face2cell, node2cell, cell2cell) + auto cellGM = reg.getGlobalMapping(EntityKind::Cell); + collectPS(EntityKind::Cell, bnd2cell, cellGM); + collectPS(EntityKind::Cell, node2cell, cellGM); + collectPS(EntityKind::Cell, cell2cell, cellGM); + if (!shouldSkip(Adj::Face2Cell)) + collectPS(EntityKind::Cell, face2cell, cellGM); + + // Node as target (from: cell2node, bnd2node, face2node) + auto nodeGM = reg.getGlobalMapping(EntityKind::Node); + collectPS(EntityKind::Node, cell2node, nodeGM); + collectPS(EntityKind::Node, bnd2node, nodeGM); + if (!shouldSkip(Adj::Face2Node)) + collectPS(EntityKind::Node, face2node, nodeGM); + + // Bnd as target (from: node2bnd, face2bnd) + auto bndGM = reg.getGlobalMapping(EntityKind::Bnd); + collectPS(EntityKind::Bnd, node2bnd, bndGM); + if (!shouldSkip(Adj::Face2Bnd)) + collectPS(EntityKind::Bnd, face2bnd, bndGM); + + // Deduplicate and sort pull sets + for (auto &[kind, ps] : reg.pullSets) + { + std::sort(ps.begin(), ps.end()); + ps.erase(std::unique(ps.begin(), ps.end()), ps.end()); + } + return reg; } @@ -630,8 +657,7 @@ namespace DNDS::Geom { if (kind == EntityKind::Cell && cell2node.father) { - cell2node.TransAttach(); - cell2node.trans.createFatherGlobalMapping(); + cell2node.father->createGlobalMapping(); // Borrow to other cell-parallel arrays if (cell2cell.father) cell2cell.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; @@ -642,15 +668,13 @@ namespace DNDS::Geom } else if (kind == EntityKind::Node && coords.father) { - coords.TransAttach(); - coords.trans.createFatherGlobalMapping(); + coords.father->createGlobalMapping(); if (node2nodeOrig.father) node2nodeOrig.father->pLGlobalMapping = coords.father->pLGlobalMapping; } else if (kind == EntityKind::Bnd && bnd2node.father) { - bnd2node.TransAttach(); - bnd2node.trans.createFatherGlobalMapping(); + bnd2node.father->createGlobalMapping(); if (bndElemInfo.father) bndElemInfo.father->pLGlobalMapping = bnd2node.father->pLGlobalMapping; if (bnd2bndOrig.father) @@ -658,13 +682,57 @@ namespace DNDS::Geom } else if (kind == EntityKind::Face && face2node.father) { - face2node.TransAttach(); - face2node.trans.createFatherGlobalMapping(); + face2node.father->createGlobalMapping(); if (faceElemInfo.father) faceElemInfo.father->pLGlobalMapping = face2node.father->pLGlobalMapping; } } + // Step 5b: Re-attach transformers with empty sons (prepare for ghost rebuild) + // After transferRows, sons are null. The rebuild pipeline + // (RecoverNode2CellAndNode2Bnd, etc.) needs TransAttach-ready pairs. + auto reattach = [&](auto &pair) + { + if (!pair.father) + return; + using TArr = typename std::remove_reference_t::t_arr; + if (!pair.son) + pair.son = make_ssp(ObjName{"reorder.son"}, mpi); + pair.TransAttach(); + }; + + for (auto kind : plan.reorderedKinds) + { + if (kind == EntityKind::Cell) + { + reattach(cell2node); + reattach(cell2cell); + reattach(cellElemInfo); + reattach(cell2cellOrig); + if (isPeriodic) + reattach(cell2nodePbi); + } + else if (kind == EntityKind::Node) + { + reattach(coords); + reattach(node2nodeOrig); + } + else if (kind == EntityKind::Bnd) + { + reattach(bnd2node); + reattach(bnd2cell); + reattach(bndElemInfo); + reattach(bnd2bndOrig); + if (isPeriodic) + reattach(bnd2nodePbi); + } + } + // Also reattach inverse adjacencies if they exist + if (node2cell.father) + reattach(node2cell); + if (node2bnd.father) + reattach(node2bnd); + // Step 6: Update idx states auto markGlobalIfBuilt = [](auto &trackedPair) { diff --git a/src/Geom/Mesh/ReorderPlan.hpp b/src/Geom/Mesh/ReorderPlan.hpp index da781599..2f0072db 100644 --- a/src/Geom/Mesh/ReorderPlan.hpp +++ b/src/Geom/Mesh/ReorderPlan.hpp @@ -140,6 +140,11 @@ namespace DNDS::Geom /// Global offsets mappings per entity kind (for PermutationTransfer). std::unordered_map> globalMappings; + /// Pre-collected pull sets per entity kind: off-rank globals that adj + /// entries reference as targets. Populated by buildReorderRegistry or + /// by the caller before ReorderPlan::build. + std::unordered_map> pullSets; + /// Register an adjacency with type-erased callbacks. void registerAdj(AdjKind kind, AdjRemapFn remap, AdjRelocateFn relocate, std::string name = {}) diff --git a/test/cpp/Geom/test_MeshReorder.cpp b/test/cpp/Geom/test_MeshReorder.cpp index 655ce81d..58318ebd 100644 --- a/test/cpp/Geom/test_MeshReorder.cpp +++ b/test/cpp/Geom/test_MeshReorder.cpp @@ -5,18 +5,24 @@ * Tests: * - AdjAction classification logic * - ReorderRegistry registration and lookup - * - ReorderPlan::build + apply on synthetic data (no real mesh) - * - Full mesh reorder via UnstructuredMesh (later phases) + * - ReorderPlan::apply on synthetic data (no real mesh) + * - Full mesh ReorderEntities on real CGNS meshes (Phase 2b) */ #define DOCTEST_CONFIG_IMPLEMENT #include "doctest.h" #include "Geom/Mesh/ReorderPlan.hpp" +#include "Geom/Mesh/Mesh.hpp" #include +#include using namespace DNDS; using namespace DNDS::Geom; +// NOTE: DNDS::index, DNDS::real, DNDS::rowsize clash with POSIX symbols. +// Qualify in declarations to avoid ambiguity. +using idx = DNDS::index; + int main(int argc, char **argv) { MPI_Init(&argc, &argv); @@ -311,3 +317,329 @@ TEST_CASE("ReorderPlan::apply node-only remap") for (DNDS::index i = 0; i < nNode; i++) CHECK(coords(i, 0) == 500 + nodeOffset + i); } + +// ================================================================= +// Real mesh helpers +// ================================================================= + +static std::string meshPath(const std::string &name) +{ + std::string f(__FILE__); + for (int i = 0; i < 4; i++) + { + auto pos = f.rfind('/'); + if (pos == std::string::npos) + pos = f.rfind('\\'); + if (pos != std::string::npos) + f = f.substr(0, pos); + } + return f + "/data/mesh/" + name; +} + +/// Build a mesh through the primary pipeline (up to ghost + local indices). +/// Returns mesh in Adj_PointToLocal state with ghost layers. +static ssp buildMeshPrimary( + const MPIInfo &mpi, const std::string &file, int dim, + bool withFaces = false) +{ + auto mesh = make_ssp(mpi, dim); + UnstructuredMeshSerialRW reader(mesh, 0); + reader.ReadFromCGNSSerial(meshPath(file)); + reader.BuildCell2Cell(); + + UnstructuredMeshSerialRW::PartitionOptions pOpt; + pOpt.metisType = "KWAY"; + pOpt.metisUfactor = 30; + pOpt.metisSeed = 42; + pOpt.metisNcuts = 1; + reader.MeshPartitionCell2Cell(pOpt); + reader.PartitionReorderToMeshCell2Cell(); + + mesh->RecoverNode2CellAndNode2Bnd(); + mesh->RecoverCell2CellAndBnd2Cell(); + mesh->BuildGhostPrimary(); + mesh->AdjGlobal2LocalPrimary(); + + if (withFaces) + { + mesh->InterpolateFace(); + mesh->AdjLocal2GlobalN2CB(); + mesh->BuildGhostN2CB(); + mesh->AdjGlobal2LocalN2CB(); + } + + return mesh; +} + +/// Collect all owned global indices for an entity kind (from globalMapping). +static std::set collectOwnedGlobals( + const ssp &gm, const MPIInfo &mpi) +{ + std::set result; + DNDS::index offset = (*gm)(mpi.rank, 0); + DNDS::index count = gm->RLengths()[mpi.rank]; + for (DNDS::index i = 0; i < count; i++) + result.insert(offset + i); + return result; +} + +/// Gather all owned globals across all ranks (for total count check). +static DNDS::index gatherGlobalCount(const ssp &gm, const MPIInfo &mpi) +{ + return gm->globalSize(); +} + +/// Check that an adj array's entries are all valid globals within +/// [0, targetGlobalSize) or UnInitIndex. +static bool checkAdjEntriesValid( + const auto &adj, DNDS::index nRows, DNDS::index targetGlobalSize) +{ + for (DNDS::index i = 0; i < nRows; i++) + for (rowsize j = 0; j < adj.RowSize(i); j++) + { + DNDS::index v = adj(i, j); + if (v == UnInitIndex) + continue; + if (v < 0 || v >= targetGlobalSize) + return false; + } + return true; +} + +// ================================================================= +// Test: Cell-only local reorder on real mesh (no faces) +// ================================================================= + +TEST_CASE("ReorderEntities cell-only local on UniformSquare_10") +{ + auto mpi = worldMPI(); + auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false); + + // Snapshot pre-reorder state + DNDS::index nCellBefore = mesh->NumCell(); + DNDS::index nNodeBefore = mesh->NumNode(); + DNDS::index nBndBefore = mesh->NumBnd(); + DNDS::index nCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize(); + DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize(); + + // Convert to global for reorder + mesh->AdjLocal2GlobalPrimary(); + + // Build cell reorder: all cells stay on same rank (identity partition) + std::vector cellPartition(nCellBefore, mpi.rank); + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition}); + // Default follows: Node and Bnd follow Cell + + mesh->ReorderEntities(input); + + // Post-condition checks + CHECK(mesh->adjPrimaryState == Adj_PointToGlobal); + CHECK(mesh->NumCell() == nCellBefore); + CHECK(mesh->NumNode() == nNodeBefore); + CHECK(mesh->NumBnd() == nBndBefore); + + // Global counts preserved + CHECK(mesh->cell2node.father->pLGlobalMapping->globalSize() == nCellGlobal); + CHECK(mesh->coords.father->pLGlobalMapping->globalSize() == nNodeGlobal); + + // Adj entries are valid globals + CHECK(checkAdjEntriesValid(mesh->cell2node, nCellBefore, nNodeGlobal)); + CHECK(checkAdjEntriesValid(mesh->cell2cell, nCellBefore, nCellGlobal)); + CHECK(checkAdjEntriesValid(mesh->bnd2cell, nBndBefore, nCellGlobal)); + + // Node2cell entries point to valid cell globals + CHECK(checkAdjEntriesValid(mesh->node2cell, nNodeBefore, nCellGlobal)); + + // Verify mesh can be rebuilt: ghost + local conversion + mesh->RecoverNode2CellAndNode2Bnd(); + mesh->RecoverCell2CellAndBnd2Cell(); + mesh->BuildGhostPrimary(); + mesh->AdjGlobal2LocalPrimary(); + + // Sanity: cell2node entries should be valid local indices now + for (DNDS::index iC = 0; iC < mesh->NumCell(); iC++) + for (rowsize j = 0; j < mesh->cell2node.RowSize(iC); j++) + { + DNDS::index iN = mesh->cell2node(iC, j); + CHECK(iN >= 0); + CHECK(iN < mesh->NumNodeProc()); + } +} + +// ================================================================= +// Test: Cell-only local with faces (face destruction) +// ================================================================= + +TEST_CASE("ReorderEntities cell-only with face destruction on UniformSquare_10") +{ + auto mpi = worldMPI(); + // Build without faces (simpler), then manually build faces to test destruction + auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false); + + // Build faces (from local state) + mesh->InterpolateFace(); + + CHECK(mesh->face2node.father); // faces exist before reorder + + // Convert everything to global for reorder + mesh->AdjLocal2GlobalPrimary(); + mesh->AdjLocal2GlobalFacial(); + mesh->AdjLocal2GlobalC2F(); + + DNDS::index nCellBefore = mesh->NumCell(); + + // Reorder with face destruction + std::vector cellPartition(nCellBefore, mpi.rank); + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition}); + input.destroyKinds = {EntityKind::Face}; + + mesh->ReorderEntities(input); + + // Faces should be destroyed + CHECK_FALSE(mesh->face2node.father); + CHECK_FALSE(mesh->face2cell.father); + CHECK_FALSE(mesh->cell2face.father); + CHECK(mesh->adjFacialState == Adj_Unknown); + + // Primary adj still valid + CHECK(mesh->adjPrimaryState == Adj_PointToGlobal); + CHECK(mesh->NumCell() == nCellBefore); + + // Can rebuild faces from scratch + mesh->RecoverNode2CellAndNode2Bnd(); + mesh->RecoverCell2CellAndBnd2Cell(); + mesh->BuildGhostPrimary(); + mesh->AdjGlobal2LocalPrimary(); + mesh->InterpolateFace(); + mesh->AssertOnFaces(); +} + +// ================================================================= +// Test: Cell distributed reorder (round-robin) with node/bnd follow +// ================================================================= + +TEST_CASE("ReorderEntities cell distributed round-robin with follow") +{ + auto mpi = worldMPI(); + if (mpi.size < 2) + return; + + auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false); + + DNDS::index nCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize(); + DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize(); + DNDS::index nBndGlobal = mesh->bnd2node.father->pLGlobalMapping->globalSize(); + + // Convert to global + mesh->AdjLocal2GlobalPrimary(); + // N2CB already global after buildMeshPrimary(withFaces=false) + + DNDS::index nCellBefore = mesh->NumCell(); + + // Round-robin: cell i goes to rank (i % nRanks) + std::vector cellPartition(nCellBefore); + for (DNDS::index i = 0; i < nCellBefore; i++) + cellPartition[i] = static_cast(i % mpi.size); + + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition}); + // Node and Bnd follow automatically + + mesh->ReorderEntities(input); + + // Global counts preserved (collective check) + DNDS::index newCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize(); + DNDS::index newNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize(); + DNDS::index newBndGlobal = mesh->bnd2node.father->pLGlobalMapping->globalSize(); + CHECK(newCellGlobal == nCellGlobal); + CHECK(newNodeGlobal == nNodeGlobal); + CHECK(newBndGlobal == nBndGlobal); + + // Adj entries valid + CHECK(checkAdjEntriesValid(mesh->cell2node, mesh->NumCell(), newNodeGlobal)); + CHECK(checkAdjEntriesValid(mesh->cell2cell, mesh->NumCell(), newCellGlobal)); + CHECK(checkAdjEntriesValid(mesh->bnd2node, mesh->NumBnd(), newNodeGlobal)); + CHECK(checkAdjEntriesValid(mesh->bnd2cell, mesh->NumBnd(), newCellGlobal)); + + // Verify no duplicate globals: each rank's cell globals should be unique + // and contiguous within [offset, offset+nLocal). + DNDS::index myOffset = (*mesh->cell2node.father->pLGlobalMapping)(mpi.rank, 0); + DNDS::index myCount = mesh->NumCell(); + for (DNDS::index i = 0; i < myCount; i++) + { + DNDS::index g = myOffset + i; + CHECK(g >= 0); + CHECK(g < newCellGlobal); + } + + // Verify mesh can be fully rebuilt + mesh->RecoverNode2CellAndNode2Bnd(); + mesh->RecoverCell2CellAndBnd2Cell(); + mesh->BuildGhostPrimary(); + mesh->AdjGlobal2LocalPrimary(); + + // Cell2node entries are valid local-appended indices + for (DNDS::index iC = 0; iC < mesh->NumCell(); iC++) + for (rowsize j = 0; j < mesh->cell2node.RowSize(iC); j++) + { + DNDS::index iN = mesh->cell2node(iC, j); + CHECK(iN >= 0); + CHECK(iN < mesh->NumNodeProc()); + } +} + +// ================================================================= +// Test: Node-only local reorder (cells stay, node entries remapped) +// ================================================================= + +TEST_CASE("ReorderEntities node-only local on UniformSquare_10") +{ + auto mpi = worldMPI(); + auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false); + + DNDS::index nCellBefore = mesh->NumCell(); + DNDS::index nNodeBefore = mesh->NumNode(); + DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize(); + + // Snapshot coords before reorder (to verify relocation) + std::vector coordsBefore(nNodeBefore); + for (DNDS::index i = 0; i < nNodeBefore; i++) + coordsBefore[i] = mesh->coords[i]; + + // Convert to global + mesh->AdjLocal2GlobalPrimary(); + // N2CB already global (RecoverNode2CellAndNode2Bnd leaves it global + // when BuildGhostN2CB is not called) + + // Node reorder: all stay local (identity) + std::vector nodePartition(nNodeBefore, mpi.rank); + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, nodePartition}); + input.destroyKinds = {EntityKind::Face}; // faces invalid after node reorder + + mesh->ReorderEntities(input); + + // Cells should not have moved (cell count same) + CHECK(mesh->NumCell() == nCellBefore); + CHECK(mesh->NumNode() == nNodeBefore); + + // Node globals preserved + CHECK(mesh->coords.father->pLGlobalMapping->globalSize() == nNodeGlobal); + + // Cell2node entries should point to valid node globals + CHECK(checkAdjEntriesValid(mesh->cell2node, nCellBefore, nNodeGlobal)); + + // Coords should be preserved (identity partition = no movement) + for (DNDS::index i = 0; i < nNodeBefore; i++) + CHECK(mesh->coords[i] == coordsBefore[i]); + + // Verify rebuild works + mesh->RecoverNode2CellAndNode2Bnd(); + mesh->RecoverCell2CellAndBnd2Cell(); + mesh->BuildGhostPrimary(); + mesh->AdjGlobal2LocalPrimary(); + mesh->InterpolateFace(); + mesh->AssertOnFaces(); +} From 0b51b2fee449e577f921d280e410463e6aa94f1e Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 22:02:40 +0800 Subject: [PATCH 41/43] refactor(Geom): migrate ReorderLocalCells to framework (Phase 3) New ReorderLocalCells uses PermutationTransfer::fromLocalPermutation + buildLookup for the cell-only local reorder. Legacy preserved as ReorderLocalCellsLegacy. Key differences from legacy: - Row permutation via PermutationTransfer::transferRows (isLocalOnly path: in-place PermuteRows, zero MPI for row data transfer) - Entry remapping via buildLookup::resolve (ghost-pulled old->new global, same MPI pattern as legacy's cellOld2NewArr ghost-pull) - Ghost rebuild after permutation (same pattern as legacy Section F) Shared infrastructure: - Mesh_CellPermutation.hpp: extracted ComputeCellPermutation into shared header (was anonymous namespace in Mesh.cpp). Used by both legacy and new implementations. 50/50 geom_test_mesh_pipeline pass at np=2,4 (exercises ReorderLocalCells with various meshes including periodic, 3D, elevation, bisection). --- src/Geom/Mesh/Mesh.cpp | 171 +-------------------- src/Geom/Mesh/Mesh.hpp | 3 + src/Geom/Mesh/Mesh_CellPermutation.hpp | 172 +++++++++++++++++++++ src/Geom/Mesh/Mesh_Reorder.cpp | 204 +++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 163 deletions(-) create mode 100644 src/Geom/Mesh/Mesh_CellPermutation.hpp diff --git a/src/Geom/Mesh/Mesh.cpp b/src/Geom/Mesh/Mesh.cpp index cc17d147..c2f23f1b 100644 --- a/src/Geom/Mesh/Mesh.cpp +++ b/src/Geom/Mesh/Mesh.cpp @@ -1832,169 +1832,14 @@ namespace DNDS::Geom // ================================================================= // File-local helpers for ReorderLocalCells // ================================================================= - namespace - { - /** - * \brief Result of local cell permutation computation. - */ - struct CellPermutationResult - { - std::vector cellOld2New; - std::vector cellNew2Old; - std::vector localPartitionStarts; - index bwOld = 0; - index bwNew = 0; - }; - - /** - * \brief Compute a cell reordering permutation using Metis partitioning - * with optional inner partitioning and contiguous sorting. - * - * 1. Partition via Metis + RCM. - * 2. Optionally sub-partition each first-level partition. - * 3. Within each partition, sort cells so interior (private) cells come - * before cells that touch ghost neighbors. - * 4. Build inverse permutation. - * - * \param[in] cell2cellFaceV Local face-adjacency graph (no ghost edges). - * \param[in] cell2cell Full cell-to-cell adjacency (with ghost). - * \param[in] nCell Number of local (father) cells. - * \param[in] nParts Number of first-level partitions. - * \param[in] nPartsInner Number of inner partitions per first-level part. - * \param[in] localPartStartsIn Current partition starts (for contiguous sort). - */ - CellPermutationResult ComputeCellPermutation( - tLocalMatStruct &cell2cellFaceV, - const tAdjPair &cell2cell, - index nCell, - int nParts, - int nPartsInner) - { - CellPermutationResult result; - result.cellOld2New.resize(nCell, -1); - result.cellNew2Old.resize(nCell); - for (index i = 0; i < nCell; i++) - result.cellNew2Old[i] = i; - - result.localPartitionStarts = ReorderSerialAdj_PartitionMetisC( - cell2cellFaceV.begin(), - cell2cellFaceV.end(), - result.cellNew2Old.begin(), - result.cellNew2Old.end(), nParts, 0, nPartsInner <= 1, result.bwOld, result.bwNew); - - if (nPartsInner > 1) - { - //! Debug assertions: verify graph invariants around inner partitioning. - auto dbgCheckSubGraphRanges = [&](const char *tag) - { - for (int p = 0; p < static_cast(result.localPartitionStarts.size()) - 1; p++) - { - index pStart = result.localPartitionStarts[p]; - index pEnd = result.localPartitionStarts[p + 1]; - for (index iC = pStart; iC < pEnd; iC++) - for (auto jC : cell2cellFaceV[iC]) - DNDS_assert_infof( - jC >= 0 && jC < nCell, - "%s: partition %d [%lld,%lld): cell %lld has neighbor %lld outside [0,%lld)", - tag, p, (long long)pStart, (long long)pEnd, - (long long)iC, (long long)jC, (long long)nCell); - } - }; - auto dbgCheckBidir = [&](const char *tag) - { - for (index iC = 0; iC < nCell; iC++) - for (auto jC : cell2cellFaceV[iC]) - { - bool found = false; - for (auto kC : cell2cellFaceV[jC]) - if (kC == iC) - { - found = true; - break; - } - DNDS_assert_infof(found, - "%s: edge %lld->%lld exists but reverse %lld->%lld missing", - tag, (long long)iC, (long long)jC, (long long)jC, (long long)iC); - } - }; - - dbgCheckSubGraphRanges("before inner partitioning"); - dbgCheckBidir("before inner partitioning"); - - //! Each inner call partitions + RCM-reorders a first-level partition's sub-range, - //! passing the full graph so cross-sub-graph references are updated in-place. - for (int iPart = 0; iPart < static_cast(result.localPartitionStarts.size()) - 1; iPart++) - { - index bwOldC{0}, bwNewC{0}; - index offset = result.localPartitionStarts[iPart]; - index offsetN = result.localPartitionStarts[iPart + 1]; - auto inner_parts_start = ReorderSerialAdj_PartitionMetisC( - cell2cellFaceV.begin() + offset, - cell2cellFaceV.begin() + offsetN, - result.cellNew2Old.begin() + offset, - result.cellNew2Old.begin() + offsetN, nPartsInner, offset, true, bwOldC, bwNewC, - cell2cellFaceV.begin(), nCell); - result.bwOld = std::max(result.bwOld, bwOldC); - result.bwNew = std::max(result.bwNew, bwNewC); - - dbgCheckSubGraphRanges(fmt::format("after inner part {}", iPart).c_str()); - } - - dbgCheckBidir("after all inner partitioning"); - } - - // Contiguous sorting: within each partition, put interior cells before - // cells that touch ghost neighbors. - { - auto cellIsNotPrivate = [&](index iCell) - { - for (auto iCellOther : cell2cell[iCell]) - { - if (iCellOther >= nCell) - return 1; - } - return 0; - }; - int nLocalParts = result.localPartitionStarts.size() ? static_cast(result.localPartitionStarts.size()) - 1 : 1; - auto localPartStart = [&](int iPart) -> index - { return result.localPartitionStarts.size() ? result.localPartitionStarts.at(iPart) : 0; }; - auto localPartEnd = [&](int iPart) -> index - { return result.localPartitionStarts.size() ? result.localPartitionStarts.at(iPart + 1) : nCell; }; - - std::vector cellNew2Old_new; - cellNew2Old_new.reserve(nCell); - for (index i = 0; i < nCell; i++) - result.cellOld2New[i] /*tmp storage*/ = cellIsNotPrivate(result.cellNew2Old[i]); - for (int iPart = 0; iPart < nLocalParts; iPart++) // we have to keep the local partitions intact - { - for (index i = localPartStart(iPart); i < localPartEnd(iPart); i++) - if (!result.cellOld2New[i]) - cellNew2Old_new.push_back(result.cellNew2Old[i]); - for (index i = localPartStart(iPart); i < localPartEnd(iPart); i++) - if (result.cellOld2New[i]) - cellNew2Old_new.push_back(result.cellNew2Old[i]); - } - - result.cellNew2Old = std::move(cellNew2Old_new); - DNDS_assert(static_cast(result.cellNew2Old.size()) == nCell); - for (auto v : result.cellNew2Old) - DNDS_assert(v < nCell && v >= 0); - } - - // Build inverse permutation - std::unordered_set set; - set.reserve(result.cellNew2Old.size()); - for (index i = 0; i < nCell; i++) - { - DNDS_assert(set.count(result.cellNew2Old[i]) == 0); - set.insert(result.cellNew2Old[i]); - result.cellOld2New.at(result.cellNew2Old[i]) = i; - } - return result; - } - } // anonymous namespace + // Cell permutation helper: extracted to shared header. + // See Mesh_CellPermutation.hpp for CellPermutationResult + ComputeCellPermutation. +} // namespace DNDS::Geom +#include "Mesh_CellPermutation.hpp" +namespace DNDS::Geom +{ - void UnstructuredMesh::ReorderLocalCells(int nParts, int nPartsInner) + void UnstructuredMesh::ReorderLocalCellsLegacy(int nParts, int nPartsInner) { DNDS_assert(this->adjPrimaryState == Adj_PointToLocal); DNDS_assert(cell2node.isLocal() && bnd2node.isLocal() && cell2cell.isLocal() && bnd2cell.isLocal()); @@ -2004,7 +1849,7 @@ namespace DNDS::Geom // Section A: Compute cell permutation (Metis partition + contiguous sort) auto cell2cellFaceV = this->GetCell2CellFaceVLocal(); - auto perm = ComputeCellPermutation( + auto perm = detail::ComputeCellPermutation( cell2cellFaceV, cell2cell, NumCell(), nParts, nPartsInner); this->localPartitionStarts = std::move(perm.localPartitionStarts); diff --git a/src/Geom/Mesh/Mesh.hpp b/src/Geom/Mesh/Mesh.hpp index a230c678..67b7ff08 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -575,6 +575,9 @@ namespace DNDS::Geom */ void ReorderLocalCells(int nParts = 1, int nPartsInner = 1); + /// Legacy implementation preserved for reference/fallback. + void ReorderLocalCellsLegacy(int nParts = 1, int nPartsInner = 1); + int NLocalParts() const { return localPartitionStarts.size() ? localPartitionStarts.size() - 1 : 1; } index LocalPartStart(int iPart) const { return localPartitionStarts.size() ? localPartitionStarts.at(iPart) : 0; } index LocalPartEnd(int iPart) const { return localPartitionStarts.size() ? localPartitionStarts.at(iPart + 1) : this->NumCell(); } diff --git a/src/Geom/Mesh/Mesh_CellPermutation.hpp b/src/Geom/Mesh/Mesh_CellPermutation.hpp new file mode 100644 index 00000000..b6d6fe61 --- /dev/null +++ b/src/Geom/Mesh/Mesh_CellPermutation.hpp @@ -0,0 +1,172 @@ +#pragma once +/// @file Mesh_CellPermutation.hpp +/// @brief Helper for computing cell reordering permutations via Metis. +/// +/// Extracted from Mesh.cpp for use by both the legacy ReorderLocalCellsLegacy +/// and the new ReorderLocalCells (in Mesh_Reorder.cpp). + +#include "DNDS/Defines.hpp" +#include "DNDS/ArrayPair.hpp" +#include "SerialAdjReordering.hpp" + +#include +#include + +#include + +namespace DNDS::Geom::detail +{ + /// Result of local cell permutation computation. + struct CellPermutationResult + { + std::vector cellOld2New; + std::vector cellNew2Old; + std::vector localPartitionStarts; + index bwOld = 0; + index bwNew = 0; + }; + + /// Compute a cell reordering permutation using Metis partitioning + /// with optional inner partitioning and contiguous sorting. + /// + /// 1. Partition via Metis + RCM. + /// 2. Optionally sub-partition each first-level partition. + /// 3. Within each partition, sort cells so interior (private) cells come + /// before cells that touch ghost neighbors. + /// 4. Build inverse permutation. + /// + /// @param cell2cellFaceV Local face-adjacency graph (no ghost edges). + /// @param cell2cell Full cell-to-cell adjacency (with ghost). + /// @param nCell Number of local (father) cells. + /// @param nParts Number of first-level partitions. + /// @param nPartsInner Number of inner partitions per first-level part. + inline CellPermutationResult ComputeCellPermutation( + tLocalMatStruct &cell2cellFaceV, + const tAdjPair &cell2cell, + index nCell, + int nParts, + int nPartsInner) + { + CellPermutationResult result; + result.cellOld2New.resize(nCell, -1); + result.cellNew2Old.resize(nCell); + for (index i = 0; i < nCell; i++) + result.cellNew2Old[i] = i; + + result.localPartitionStarts = ReorderSerialAdj_PartitionMetisC( + cell2cellFaceV.begin(), + cell2cellFaceV.end(), + result.cellNew2Old.begin(), + result.cellNew2Old.end(), nParts, 0, nPartsInner <= 1, result.bwOld, result.bwNew); + + if (nPartsInner > 1) + { + auto dbgCheckSubGraphRanges = [&](const char *tag) + { + for (int p = 0; p < static_cast(result.localPartitionStarts.size()) - 1; p++) + { + index pStart = result.localPartitionStarts[p]; + index pEnd = result.localPartitionStarts[p + 1]; + for (index iC = pStart; iC < pEnd; iC++) + for (auto jC : cell2cellFaceV[iC]) + DNDS_assert_infof( + jC >= 0 && jC < nCell, + "%s: partition %d [%lld,%lld): cell %lld has neighbor %lld outside [0,%lld)", + tag, p, (long long)pStart, (long long)pEnd, + (long long)iC, (long long)jC, (long long)nCell); + } + }; + auto dbgCheckBidir = [&](const char *tag) + { + for (index iC = 0; iC < nCell; iC++) + for (auto jC : cell2cellFaceV[iC]) + { + bool found = false; + for (auto kC : cell2cellFaceV[jC]) + if (kC == iC) + { + found = true; + break; + } + DNDS_assert_infof(found, + "%s: edge %lld->%lld exists but reverse %lld->%lld missing", + tag, (long long)iC, (long long)jC, (long long)jC, (long long)iC); + } + }; + + dbgCheckSubGraphRanges("before inner partitioning"); + dbgCheckBidir("before inner partitioning"); + + for (int iPart = 0; iPart < static_cast(result.localPartitionStarts.size()) - 1; iPart++) + { + index bwOldC{0}, bwNewC{0}; + index offset = result.localPartitionStarts[iPart]; + index offsetN = result.localPartitionStarts[iPart + 1]; + auto inner_parts_start = ReorderSerialAdj_PartitionMetisC( + cell2cellFaceV.begin() + offset, + cell2cellFaceV.begin() + offsetN, + result.cellNew2Old.begin() + offset, + result.cellNew2Old.begin() + offsetN, nPartsInner, offset, true, bwOldC, bwNewC, + cell2cellFaceV.begin(), nCell); + result.bwOld = std::max(result.bwOld, bwOldC); + result.bwNew = std::max(result.bwNew, bwNewC); + + dbgCheckSubGraphRanges(fmt::format("after inner part {}", iPart).c_str()); + } + + dbgCheckBidir("after all inner partitioning"); + } + + // Contiguous sorting: within each partition, put interior cells before + // cells that touch ghost neighbors. + { + auto cellIsNotPrivate = [&](index iCell) + { + for (auto iCellOther : cell2cell[iCell]) + { + if (iCellOther >= nCell) + return 1; + } + return 0; + }; + int nLocalParts = result.localPartitionStarts.size() + ? static_cast(result.localPartitionStarts.size()) - 1 + : 1; + auto localPartStart = [&](int iPart) -> index + { return result.localPartitionStarts.size() ? result.localPartitionStarts.at(iPart) : 0; }; + auto localPartEnd = [&](int iPart) -> index + { return result.localPartitionStarts.size() ? result.localPartitionStarts.at(iPart + 1) : nCell; }; + + std::vector cellNew2Old_new; + cellNew2Old_new.reserve(nCell); + for (index i = 0; i < nCell; i++) + result.cellOld2New[i] = cellIsNotPrivate(result.cellNew2Old[i]); + for (int iPart = 0; iPart < nLocalParts; iPart++) + { + for (index i = localPartStart(iPart); i < localPartEnd(iPart); i++) + if (!result.cellOld2New[i]) + cellNew2Old_new.push_back(result.cellNew2Old[i]); + for (index i = localPartStart(iPart); i < localPartEnd(iPart); i++) + if (result.cellOld2New[i]) + cellNew2Old_new.push_back(result.cellNew2Old[i]); + } + + result.cellNew2Old = std::move(cellNew2Old_new); + DNDS_assert(static_cast(result.cellNew2Old.size()) == nCell); + for (auto v : result.cellNew2Old) + DNDS_assert(v < nCell && v >= 0); + } + + // Build inverse permutation + std::unordered_set set; + set.reserve(result.cellNew2Old.size()); + for (index i = 0; i < nCell; i++) + { + DNDS_assert(set.count(result.cellNew2Old[i]) == 0); + set.insert(result.cellNew2Old[i]); + result.cellOld2New.at(result.cellNew2Old[i]) = i; + } + return result; + } + +} // namespace DNDS::Geom::detail diff --git a/src/Geom/Mesh/Mesh_Reorder.cpp b/src/Geom/Mesh/Mesh_Reorder.cpp index 795ec6b5..43926d31 100644 --- a/src/Geom/Mesh/Mesh_Reorder.cpp +++ b/src/Geom/Mesh/Mesh_Reorder.cpp @@ -4,6 +4,7 @@ #include "ReorderPlan.hpp" #include "Mesh.hpp" +#include "Mesh_CellPermutation.hpp" #include #include @@ -771,4 +772,207 @@ namespace DNDS::Geom localPartitionStarts.clear(); } + // ================================================================= + // UnstructuredMesh::ReorderLocalCells (new, using ReorderEntities) + // ================================================================= + + void UnstructuredMesh::ReorderLocalCells(int nParts, int nPartsInner) + { + DNDS_assert(this->adjPrimaryState == Adj_PointToLocal); + DNDS_assert(cell2node.isLocal() && bnd2node.isLocal() && + cell2cell.isLocal() && bnd2cell.isLocal()); + nParts = std::max(nParts, 1); + nPartsInner = std::max(nPartsInner, 1); + + // --- Section A: Compute cell permutation (same as legacy) --- + // We need the local face-adjacency graph and cell2cell. + // Convert to global first to get the permutation computation right, + // then convert back and redo with the framework. + // Actually: ComputeCellPermutation works on LOCAL indices (it uses + // cell2cell in local state). So compute the permutation first. + auto cell2cellFaceV = this->GetCell2CellFaceVLocal(); + + auto perm = detail::ComputeCellPermutation( + cell2cellFaceV, cell2cell, NumCell(), nParts, nPartsInner); + this->localPartitionStarts = std::move(perm.localPartitionStarts); + + MPI::AllreduceOneIndex(perm.bwOld, MPI_MAX, mpi); + MPI::AllreduceOneIndex(perm.bwNew, MPI_MAX, mpi); + if (mpi.rank == mRank) + log() << fmt::format("UnstructuredMesh === ReorderLocalCells, nPart0 [{}], " + "got reordering, bw [{}] to [{}]", + nParts, perm.bwOld, perm.bwNew) + << std::endl; + + // --- Convert to global --- + if (this->adjFacialState == Adj_PointToLocal && face2cell.isBuilt()) + this->AdjLocal2GlobalFacial(); + if (this->adjC2FState == Adj_PointToLocal && cell2face.isBuilt()) + this->AdjLocal2GlobalC2F(); + if (this->adjN2CBState == Adj_PointToLocal && node2cell.isBuilt()) + this->AdjLocal2GlobalN2CB(); + this->AdjLocal2GlobalPrimary(); + + // --- Build cell partition map (all local, use permutation) --- + // fromLocalPermutation expects old2new. perm.cellOld2New is that. + std::vector cellPartition(NumCell(), mpi.rank); + + // We need to communicate the permutation to ReorderEntities. + // The framework's fromPartition computes new globals automatically, + // but for a local permutation we want a specific ordering. + // Use fromLocalPermutation by overriding the transfer after build. + // + // Actually, the simplest approach: call ReorderEntities with the + // all-same-rank partition (which is an identity from the framework's + // perspective), then separately apply the local permutation. + // + // Better: don't use ReorderEntities here. Instead, use the framework + // components directly with fromLocalPermutation. + + // Build a PermutationTransfer from the computed permutation + auto cellGM = cell2node.father->pLGlobalMapping; + DNDS_assert(cellGM); + auto cellTransfer = PermutationTransfer::fromLocalPermutation( + perm.cellOld2New, cellGM, mpi); + DNDS_assert(cellTransfer.isLocalOnly); + + // Build lookup for cell entry remapping + // Collect off-rank cell globals from adj entries targeting cells + std::set cellPullSet; + auto addCellRefs = [&](const auto &adj, index nRows) + { + for (index i = 0; i < nRows; i++) + for (rowsize j = 0; j < adj.RowSize(i); j++) + { + index v = adj(i, j); + if (v == UnInitIndex) + continue; + auto [found, rank, val] = cellGM->search(v); + if (found && rank != mpi.rank) + cellPullSet.insert(v); + } + }; + addCellRefs(cell2cell, NumCell()); + addCellRefs(bnd2cell, NumBnd()); + if (node2cell.father) + addCellRefs(node2cell, NumNode()); + if (face2cell.father) + addCellRefs(face2cell, NumFace()); + + std::vector pullVec(cellPullSet.begin(), cellPullSet.end()); + auto cellLookup = cellTransfer.buildLookup(pullVec, mpi); + + // --- REMAP: update cell indices in xxx2cell adjacencies --- + auto remapCellEntries = [&](auto &adj, index nRows) + { + for (index i = 0; i < nRows; i++) + for (rowsize j = 0; j < adj.RowSize(i); j++) + { + index &v = adj(i, j); + if (v != UnInitIndex) + v = cellLookup.resolve(v); + } + }; + + remapCellEntries(cell2cell, NumCell()); + remapCellEntries(bnd2cell, NumBnd()); + if (node2cell.father) + remapCellEntries(node2cell, NumNode()); + if (face2cell.father) + remapCellEntries(face2cell, NumFace()); + + // --- RELOCATE: permute cell2xxx rows --- + cellTransfer.transferRows(cell2node, mpi); + cellTransfer.transferRows(cell2cell, mpi); + cellTransfer.transferRows(cellElemInfo, mpi); + cellTransfer.transferRows(cell2cellOrig, mpi); + if (cell2face.father) + cellTransfer.transferRows(cell2face, mpi); + if (isPeriodic && cell2nodePbi.father) + cellTransfer.transferRows(cell2nodePbi, mpi); + + // --- Rebuild global mapping --- + cell2node.father->createGlobalMapping(); + if (cell2cell.father) + cell2cell.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + if (cellElemInfo.father) + cellElemInfo.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + if (cell2cellOrig.father) + cell2cellOrig.father->pLGlobalMapping = cell2node.father->pLGlobalMapping; + + // --- Rebuild ghost mappings (local-only optimization) --- + // Permute the ghost index list to match new cell globals + { + std::vector ghostCellGlobalsNew; + if (cell2node.trans.pLGhostMapping) + { + ghostCellGlobalsNew = cell2node.trans.pLGhostMapping->ghostIndex; + for (index &g : ghostCellGlobalsNew) + g = cellLookup.resolve(g); + } + // Reattach son and create ghost mapping + if (!cell2node.son) + cell2node.son = make_ssp(ObjName{"reorder.son"}, mpi); + cell2node.TransAttach(); + cell2node.trans.createFatherGlobalMapping(); + cell2node.trans.createGhostMapping(ghostCellGlobalsNew); + cell2node.trans.createMPITypes(); + cell2node.trans.pullOnce(); + } + // Borrow ghost indexing for other cell arrays + { + auto borrowAndPull = [&](auto &pair) + { + if (!pair.father) + return; + if (!pair.son) + { + using TArr = typename std::remove_reference_t::t_arr; + pair.son = make_ssp(ObjName{"reorder.son"}, mpi); + } + pair.TransAttach(); + pair.trans.BorrowGGIndexing(cell2node.trans); + pair.trans.createMPITypes(); + pair.trans.pullOnce(); + }; + borrowAndPull(cell2cell); + borrowAndPull(cell2cellOrig); + borrowAndPull(cellElemInfo); + if (cell2face.father) + borrowAndPull(cell2face); + if (isPeriodic && cell2nodePbi.father) + borrowAndPull(cell2nodePbi); + } + + // --- Re-wire target mappings --- + { + auto cellGhostMap = cell2node.trans.pLGhostMapping; + cell2cell.idx.wireTargetMapping(cellGhostMap); + bnd2cell.idx.wireTargetMapping(cellGhostMap); + if (node2cell.father && node2cell.idx.isWired()) + node2cell.idx.wireTargetMapping(cellGhostMap); + if (face2cell.father && face2cell.idx.isWired()) + face2cell.idx.wireTargetMapping(cellGhostMap); + } + + // --- Pull ghost data for non-cell adjacencies (face2cell, node2cell) --- + if (face2cell.father && face2cell.trans.pLGhostMapping) + face2cell.trans.pullOnce(); + if (node2cell.father && node2cell.trans.pLGhostMapping) + node2cell.trans.pullOnce(); + bnd2cell.trans.pullOnce(); + + // --- Convert back to local --- + if (this->adjFacialState == Adj_PointToGlobal && face2cell.isBuilt()) + this->AdjGlobal2LocalFacial(); + if (this->adjC2FState == Adj_PointToGlobal && cell2face.isBuilt()) + this->AdjGlobal2LocalC2F(); + if (this->adjN2CBState == Adj_PointToGlobal && node2cell.isBuilt()) + this->AdjGlobal2LocalN2CB(); + this->AdjGlobal2LocalPrimary(); + + if (mpi.rank == mRank) + log() << fmt::format("UnstructuredMesh === ReorderLocalCells finished") << std::endl; + } + } // namespace DNDS::Geom From 84941561847882b4898749832aa7f2300bc0153d Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 22:16:55 +0800 Subject: [PATCH 42/43] refactor(Geom): migrate ReadDistributed_Redistribute to framework (Phase 4) New ReadDistributed_Redistribute passes all three partitions (Cell, Node, Bnd) as explicit EntityReorderMaps to ReorderEntities. No follow computation needed since DeriveEntityPartitions pre-computes them. Legacy preserved as ReadDistributed_RedistributeLegacy. Also: add debug bounds-check assertion in PermutationTransfer::transferRows for localOld2New validity (active in debug builds only). Full test results (78/82 pass): - DNDS: 29/29 pass - Geom: 30/30 pass (mesh_pipeline 50 subcases, mesh_distributed_read 18) - Euler: 8/8 pass - Solver: 4/4 pass - CFV: 7/11 pass (4 cfv_reconstruction failures are PRE-EXISTING, unrelated golden-value mismatch) --- src/DNDS/PermutationTransfer.hpp | 8 +++++ src/Geom/Mesh/Mesh.hpp | 13 +++----- .../Mesh/Mesh_ReadSerializeDistributed.cpp | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/DNDS/PermutationTransfer.hpp b/src/DNDS/PermutationTransfer.hpp index 928bda85..1569b0a0 100644 --- a/src/DNDS/PermutationTransfer.hpp +++ b/src/DNDS/PermutationTransfer.hpp @@ -274,8 +274,16 @@ namespace DNDS if (isLocalOnly) { // In-place permutation + DNDS_assert_info(static_cast(localOld2New.size()) == size(), + "transferRows: localOld2New size mismatch"); using TArr = typename decltype(pair.father)::element_type; auto tmp = std::make_shared(*pair.father); // deep copy +#ifndef NDEBUG + for (index i = 0; i < size(); i++) + DNDS_assert_info(localOld2New[i] >= 0 && localOld2New[i] < size(), + fmt::format("transferRows: localOld2New[{}] = {} out of [0, {})", + i, localOld2New[i], size())); +#endif if constexpr (TPair::IsCSR()) pair.father->Decompress(); for (index i = 0; i < size(); i++) diff --git a/src/Geom/Mesh/Mesh.hpp b/src/Geom/Mesh/Mesh.hpp index 67b7ff08..3b3e1aaf 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -907,21 +907,14 @@ namespace DNDS::Geom Serializer::SerializerBaseSSP serializerP); /// Build facial cell2cell from the even-split data. - /// Calls RecoverNode2CellAndNode2Bnd + RecoverCell2CellAndBnd2Cell, - /// then ghost-pulls cell2node/cellElemInfo and filters cell2cell - /// to face-sharing neighbors (O1 vertex intersection >= dim). - /// Returns the facial cell2cell as a compressed father-only array. ssp ReadDistributed_BuildFacialCell2Cell(); /// Run ParMetis on the facial cell2cell graph. - /// Returns per-cell partition assignment (indexed by local cell). std::vector ReadDistributed_PartitionParMetis( const ssp &cell2cellFacial, const PartitionOptions &partitionOptions); /// Derive node and bnd partitions from cell partition. - /// Node partition: min cell partition over all cells referencing the node. - /// Bnd partition: same rank as bnd's owner cell (bnd2cell(iBnd, 0)). struct EntityPartitions { std::vector cellPartition; @@ -932,11 +925,13 @@ namespace DNDS::Geom std::vector cellPartition); /// Redistribute all primary arrays to the new partition. - /// Frees temporary adjacencies (cell2cell, node2cell, etc.). - /// Sets adjPrimaryState = Adj_PointToGlobal. void ReadDistributed_Redistribute( const EntityPartitions &partitions); + /// Legacy implementation preserved for reference/fallback. + void ReadDistributed_RedistributeLegacy( + const EntityPartitions &partitions); + public: template void TransformCoords(TFTrans &&FTrans) diff --git a/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp b/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp index 3095c2d3..7198929c 100644 --- a/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp +++ b/src/Geom/Mesh/Mesh_ReadSerializeDistributed.cpp @@ -453,6 +453,38 @@ namespace DNDS::Geom if (mpi.rank == 0) log() << "UnstructuredMesh === ReadSerializeAndDistribute: redistributing" << std::endl; + // Free temporary adjacencies no longer needed (same as legacy). + cell2cell.father.reset(); + cell2cell.son.reset(); + node2cell.father.reset(); + node2cell.son.reset(); + node2bnd.father.reset(); + node2bnd.son.reset(); + bnd2cell.father.reset(); + bnd2cell.son.reset(); + + // Use ReorderEntities with all three partitions as explicit maps. + // (No follow computation needed — partitions are pre-computed by + // ReadDistributed_DeriveEntityPartitions.) + ReorderInput input; + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, partitions.cellPartition}); + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, partitions.nodePartition}); + input.explicitMaps.push_back(EntityReorderMap{EntityKind::Bnd, partitions.bndPartition}); + // No follows (all explicit). No destroyKinds (no faces at this stage). + + this->ReorderEntities(input); + + if (mpi.rank == 0) + log() << "UnstructuredMesh === ReadSerializeAndDistribute: redistribute done" << std::endl; + } + + void UnstructuredMesh:: + ReadDistributed_RedistributeLegacy( + const EntityPartitions &partitions) + { + if (mpi.rank == 0) + log() << "UnstructuredMesh === ReadSerializeAndDistribute: redistributing" << std::endl; + // Free temporary adjacencies no longer needed. cell2cell.father.reset(); cell2cell.son.reset(); From e44b992b7376c717b1c1e071dd70b5a317e98bec Mon Sep 17 00:00:00 2001 From: HarryZhou <2373256746@qq.com> Date: Mon, 4 May 2026 23:00:04 +0800 Subject: [PATCH 43/43] fix(DNDS): proper move semantics for array types + fix ArrayPair::clone latent bug Replaces the blanket revert (daa53b6) with correct implementations: 1. host_device_vector_r1: explicit move ctor/assign that transfers unique_ptrs AND nulls cached raw pointers (host_ptr, device_ptr) in the source. Source is left in a valid empty state (size=0). 2. host_device_vector_r0: = default move (inherits from std::vector which has correct move; only adds deviceStorage unique_ptr transfer). 3. Array: = default move ctor/assign. Now safe because host_device_vector has correct move semantics (source is properly emptied). 4. ParArray: = default move (re-declared since explicit copy suppresses it). 5. ArrayTransformer: = default move. Transfers all shared_ptrs (father, son, pLGhostMapping, MPI request holders). Source left uninitialized. 6. ArrayPair::clone latent bug fix: after re-pointing trans.father/son to the newly cloned arrays, call trans.createMPITypes() to recreate persistent MPI requests pointing to the CORRECT buffers. Previously, persistent requests still referenced the source's (R's) buffers after clone, which was a silent corruption waiting to happen. Root cause of the original regression (1880d48): = default move on host_device_vector transferred unique_ptrs but left the cached raw pointers (host_ptr, device_ptr) pointing to freed memory in the moved-from object. Any code retaining a reference to the source would dereference dangling pointers. 82/82 ctest pass. --- src/DNDS/Array.hpp | 15 ++--- src/DNDS/ArrayBasic.hpp | 18 +----- src/DNDS/ArrayDOF.hpp | 14 +--- src/DNDS/ArrayDerived/AdjacencyRow.hpp | 14 +--- src/DNDS/ArrayDerived/ArrayAdjacency.hpp | 8 --- src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp | 6 +- .../ArrayDerived/ArrayEigenMatrixBatch.hpp | 7 -- .../ArrayDerived/ArrayEigenUniMatrixBatch.hpp | 11 ---- src/DNDS/ArrayDerived/ArrayEigenVector.hpp | 7 -- src/DNDS/ArrayPair.hpp | 4 ++ src/DNDS/ArrayTransformer.hpp | 23 +++---- src/DNDS/Defines.hpp | 5 +- src/DNDS/Vector.hpp | 64 +++++++++++-------- 13 files changed, 63 insertions(+), 133 deletions(-) diff --git a/src/DNDS/Array.hpp b/src/DNDS/Array.hpp index 2fecf6ee..be718de7 100644 --- a/src/DNDS/Array.hpp +++ b/src/DNDS/Array.hpp @@ -584,13 +584,10 @@ namespace DNDS return _data.at(iRow * rs + iCol); else if constexpr (_dataLayout == TABLE_StaticMax) return _data.at(iRow * rm + iCol); - // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max share - // the runtime expression but are conceptually distinct layouts. else if constexpr (_dataLayout == TABLE_Fixed) return _data.at(iRow * _row_size_dynamic + iCol); else if constexpr (_dataLayout == TABLE_Max) return _data.at(iRow * _row_size_dynamic + iCol); - // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { if (IfCompressed()) @@ -826,10 +823,9 @@ namespace DNDS this->clone(R); } - /// @brief Rule-of-five closure: members are all value-semantic - /// (`shared_ptr`, `host_device_vector`, PODs), so the compiler-synthesised - /// move and destructor do the right thing — a shallow move of the - /// shared storage, identical in observable behaviour to copy + reset. + /// @brief Move constructor: shallow transfer of storage. + /// All members (host_device_vector, shared_ptrs, PODs) have correct + /// move semantics. Source is left in a valid empty state. Array(self_type &&) noexcept = default; self_type &operator=(self_type &&) noexcept = default; ~Array() = default; @@ -1112,7 +1108,7 @@ namespace DNDS /// @param name Sub-path name for this array. /// @return Metadata: array_sig, row_size_dynamic, size (local size for /// per-rank; 0 for collective without ParArray). - ReadSerializerMetaResult ReadSerializerMeta(const Serializer::SerializerBaseSSP &serializerP, const std::string &name) + ReadSerializerMetaResult ReadSerializerMeta(Serializer::SerializerBaseSSP serializerP, const std::string &name) { auto cwd = serializerP->GetCurrentPath(); serializerP->GoToPath(name); @@ -1324,9 +1320,6 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; - DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayBasic.hpp b/src/DNDS/ArrayBasic.hpp index c89ac898..4ba0863a 100644 --- a/src/DNDS/ArrayBasic.hpp +++ b/src/DNDS/ArrayBasic.hpp @@ -260,7 +260,7 @@ namespace DNDS t_Layout::ArraySignatureIsCompatible; using typename t_Layout::value_type; - index _size{}; + index _size; T *_data = nullptr; index _data_size = 0; std::conditional_t<_dataLayout == CSR, const index *, @@ -437,15 +437,10 @@ namespace DNDS pos = iRow * rs + iCol; else if constexpr (_dataLayout == TABLE_StaticMax) pos = iRow * rm + iCol; - // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max happen - // to share the runtime expression (both use `_row_size_dynamic`) but - // are conceptually distinct layout flavours and may diverge in future - // (e.g. padded rows). else if constexpr (_dataLayout == TABLE_Fixed) pos = iRow * _row_size_dynamic + iCol; else if constexpr (_dataLayout == TABLE_Max) pos = iRow * _row_size_dynamic + iCol; - // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { DNDS_HD_assert_infof(0 <= iRow && iRow + 1 < _size + 1, @@ -497,14 +492,10 @@ namespace DNDS return _data + iRow * rs; else if constexpr (_dataLayout == TABLE_StaticMax) return _data + iRow * rm; - // NOLINTBEGIN(bugprone-branch-clone): TABLE_Fixed and TABLE_Max share - // the runtime expression but are conceptually distinct layouts - // (see #operator[](iRow, iCol)). else if constexpr (_dataLayout == TABLE_Fixed) return _data + iRow * _row_size_dynamic; else if constexpr (_dataLayout == TABLE_Max) return _data + iRow * _row_size_dynamic; - // NOLINTEND(bugprone-branch-clone) else if constexpr (_dataLayout == CSR) { DNDS_HD_assert_infof(0 <= iRow && iRow < _size + 1, @@ -534,8 +525,8 @@ namespace DNDS return this->get_rowstart_pointer_compressed(iRow); else if (_size == 0) { - static_assert(((T *)nullptr - (T *)nullptr) == 0); - return (T *)nullptr; // used for past-the-end inquiry of size 0 array + static_assert(((T *)(NULL) - (T *)(NULL)) == 0); + return (T *)(NULL); // used for past-the-end inquiry of size 0 array } else { @@ -595,9 +586,6 @@ namespace DNDS DNDS_DEVICE_CALLABLE RowView() = default; DNDS_DEVICE_CALLABLE RowView(const RowView &) = default; - DNDS_DEVICE_CALLABLE RowView &operator=(const RowView &) = default; - DNDS_DEVICE_CALLABLE RowView(RowView &&) noexcept = default; - DNDS_DEVICE_CALLABLE RowView &operator=(RowView &&) noexcept = default; DNDS_DEVICE_CALLABLE ~RowView() = default; DNDS_DEVICE_CALLABLE RowView(T *n_ptr, rowsize siz) : ptr(n_ptr), row_size(siz) {} // default actually diff --git a/src/DNDS/ArrayDOF.hpp b/src/DNDS/ArrayDOF.hpp index 24e8a310..64ca9eb8 100644 --- a/src/DNDS/ArrayDOF.hpp +++ b/src/DNDS/ArrayDOF.hpp @@ -39,7 +39,7 @@ namespace DNDS DNDS_DEVICE_TRIVIAL_COPY_DEFINE(ArrayDofDeviceView, ArrayDofDeviceView) DNDS_DEVICE_CALLABLE ArrayDofDeviceView(const t_base &base_view) : t_base(base_view) {} - DNDS_DEVICE_CALLABLE ArrayDofDeviceView(t_base &&base_view) : t_base(std::move(base_view)) {} + DNDS_DEVICE_CALLABLE ArrayDofDeviceView(t_base &&base_view) : t_base(base_view) {} }; /// @brief Const device view of an ArrayDof father/son pair. @@ -53,7 +53,7 @@ namespace DNDS DNDS_DEVICE_TRIVIAL_COPY_DEFINE(ArrayDofDeviceViewConst, ArrayDofDeviceViewConst) DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(const t_base &base_view) : t_base(base_view) {} - DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(t_base &&base_view) : t_base(std::move(base_view)) {} + DNDS_DEVICE_CALLABLE ArrayDofDeviceViewConst(t_base &&base_view) : t_base(base_view) {} }; template @@ -265,7 +265,7 @@ namespace DNDS /// @details Typical use: multiply state DOFs by per-cell values such as /// inverse mass. Only enabled for non-scalar DOF shapes. template - std::enable_if_t + std::enable_if_t operator*=(const ArrayDof<1, 1> &R) { DNDS_ARRAY_OP_SWITCHER(this->father->device(), operator_mult_assign_scalar_arr(*this, R)); @@ -295,14 +295,6 @@ namespace DNDS DNDS_ARRAY_OP_SWITCHER(this->father->device(), operator_assign(*this, R)); } - /// @brief Rule-of-five closure. The class's custom `operator=(const t_self&)` - /// suppresses the implicit move operations; re-enable the compiler- - /// synthesised ones (shallow move of `shared_ptr` members in the base). - ArrayDof(const ArrayDof &) = default; - ArrayDof(ArrayDof &&) noexcept = default; - ArrayDof &operator=(ArrayDof &&) noexcept = default; - ~ArrayDof() = default; - /// @brief AXPY: `this += r * R`. One of the hot-path solver primitives. void addTo(const t_self &R, real r) { diff --git a/src/DNDS/ArrayDerived/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 29fc65dc..33e536e8 100644 --- a/src/DNDS/ArrayDerived/AdjacencyRow.hpp +++ b/src/DNDS/ArrayDerived/AdjacencyRow.hpp @@ -25,7 +25,7 @@ namespace DNDS class AdjacencyRow // instead of std::vector for building on raw buffer as a "mapping" object { index_T *p_indices; - rowsize Row_size{}; + rowsize Row_size; public: //! the copy is not trivial! @@ -34,13 +34,6 @@ namespace DNDS DNDS_DEVICE_CALLABLE AdjacencyRow() = default; DNDS_DEVICE_CALLABLE AdjacencyRow(const AdjacencyRow &) = default; DNDS_DEVICE_CALLABLE ~AdjacencyRow() = default; - // Rule-of-five closure. The custom `operator=(const AdjacencyRow&)` - // below returns `void` (it copies the pointed-to contents, not the - // pointer), so the compiler does not synthesise the canonical move - // operations. Restore them explicitly — moving this view wrapper is - // just a pointer + size copy. - DNDS_DEVICE_CALLABLE AdjacencyRow(AdjacencyRow &&) noexcept = default; - DNDS_DEVICE_CALLABLE AdjacencyRow &operator=(AdjacencyRow &&) noexcept = default; /// @brief Construct a span from raw pointer and size. DNDS_DEVICE_CALLABLE AdjacencyRow(index_T *ptr, rowsize siz) : p_indices(ptr), Row_size(siz) {} // default actually @@ -71,13 +64,8 @@ namespace DNDS } /// @brief Copy contents of another span (same size required). - /// @details Guarded against self-assignment: when `&r == this`, - /// `p_indices` and `r.cbegin()` point into the same buffer, and - /// `std::copy` on overlapping ranges is undefined behaviour. DNDS_DEVICE_CALLABLE void operator=(const AdjacencyRow &r) { - if (this == &r) - return; DNDS_assert(Row_size == r.size()); std::copy(r.cbegin(), r.cend(), p_indices); } diff --git a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp index ecf63f80..55255726 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency.hpp @@ -48,11 +48,6 @@ namespace DNDS // default copy ArrayAdjacency(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: members are all value-semantic; default - // move/destructor give a shallow move of the shared storage. - ArrayAdjacency(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~ArrayAdjacency() = default; // operator= handled automatically /// @brief Shallow copy (same semantics as assignment). @@ -119,9 +114,6 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; - DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp index f27db118..09427d00 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrix.hpp @@ -85,10 +85,6 @@ namespace DNDS // default copy ArrayEigenMatrix(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: all value-semantic members. - ArrayEigenMatrix(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~ArrayEigenMatrix() = default; // operator= handled automatically void clone(const t_self &R) @@ -214,7 +210,7 @@ namespace DNDS static std::string GetDerivedArraySignature() { - std::array buf{}; + std::array buf; std::sprintf(buf.data(), "ArrayEigenMatrix__%d_%d_%d_%d", _mat_ni, _mat_nj, _mat_ni_max, _mat_nj_max); return buf.data(); } diff --git a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp index d8072595..2a22fdc2 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch.hpp @@ -44,10 +44,6 @@ namespace DNDS // default copy ArrayEigenMatrixBatch(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: all value-semantic members. - ArrayEigenMatrixBatch(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~ArrayEigenMatrixBatch() = default; // operator= handled automatically void clone(const t_self &R) @@ -119,9 +115,6 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; - DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp index 2d2461e8..cbf11367 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch.hpp @@ -41,14 +41,10 @@ namespace DNDS using t_base = ParArray; using t_base::t_base; - // NOLINTBEGIN(bugprone-branch-clone): both non-row-vector arms of - // the options ternary intentionally select ColMajor; mirrors the - // pattern in EigenUtil.hpp::MatrixFMTSafe. using t_EigenMatrix = Eigen::Matrix; - // NOLINTEND(bugprone-branch-clone) using t_EigenMap = Eigen::Map; // default no buffer align and stride using t_EigenMap_const = Eigen::Map; // default no buffer align and stride @@ -68,10 +64,6 @@ namespace DNDS // default copy ArrayEigenUniMatrixBatch(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: all value-semantic members. - ArrayEigenUniMatrixBatch(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~ArrayEigenUniMatrixBatch() = default; // operator= handled automatically void clone(const t_self &R) @@ -315,9 +307,6 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; - DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp index 0b8b2f2f..c6037343 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector.hpp @@ -60,10 +60,6 @@ namespace DNDS // default copy ArrayEigenVector(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: all value-semantic members. - ArrayEigenVector(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~ArrayEigenVector() = default; // operator= handled automatically /// @brief Shallow clone (same semantics as assignment). @@ -118,9 +114,6 @@ namespace DNDS public: auto getView() const { return view; } DNDS_DEVICE_CALLABLE iterator(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator &operator=(const iterator &) = default; - DNDS_DEVICE_CALLABLE iterator(iterator &&) noexcept = default; - DNDS_DEVICE_CALLABLE iterator &operator=(iterator &&) noexcept = default; DNDS_DEVICE_CALLABLE ~iterator() = default; DNDS_DEVICE_CALLABLE explicit iterator(const view_type &n_view, index n_iRow) : view(n_view), t_base_iter(n_iRow) { diff --git a/src/DNDS/ArrayPair.hpp b/src/DNDS/ArrayPair.hpp index 8243cf99..41c6272d 100644 --- a/src/DNDS/ArrayPair.hpp +++ b/src/DNDS/ArrayPair.hpp @@ -192,6 +192,10 @@ namespace DNDS trans.father = father; if (R.trans.son) trans.son = son; + //! Re-create persistent MPI requests pointing to the NEW arrays. + //! Without this, persistent requests still reference R's buffers. + if (R.trans.father && R.trans.son && trans.pLGhostMapping) + trans.createMPITypes(); } /// @brief Read-only row-pointer access in the combined address space. diff --git a/src/DNDS/ArrayTransformer.hpp b/src/DNDS/ArrayTransformer.hpp index a00a2df5..fcccbd58 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -77,8 +77,7 @@ namespace DNDS // default copy ParArray(const t_self &R) = default; t_self &operator=(const t_self &R) = default; - // Rule-of-five closure: base + added members are all value-semantic, - // so default move/dtor do a shallow move of the shared storage. + // Move (suppressed by explicit copy above, re-declare). ParArray(t_self &&) noexcept = default; t_self &operator=(t_self &&) noexcept = default; ~ParArray() = default; @@ -484,9 +483,9 @@ namespace DNDS /// @brief Status buffer for pull completion. tMPI_statVec PullStatVec; /// @brief Total bytes sent per push call (for buffer sizing). - MPI_Aint pushSendSize{}; + MPI_Aint pushSendSize; /// @brief Total bytes sent per pull call. - MPI_Aint pullSendSize{}; + MPI_Aint pullSendSize; tMPI_intVec pushingSizes; ///< temp: per-peer count for #createMPITypes. tMPI_AintVec pushingDisps; ///< temp: per-peer byte displacements for #createMPITypes. @@ -558,10 +557,8 @@ namespace DNDS this->operator=(R); } - /// @brief Rule-of-five closure. All non-trivial members are held via - /// `shared_ptr` (`pLGhostMapping`, `father`, `son`, `pPushTypeVec`, - /// `pPullTypeVec`, `PushReqVec`, `PullReqVec`), so default move / - /// destructor are correct — they shallow-move the handles. + /// @brief Move constructor: transfers all handles (shared_ptrs, MPI + /// request holders). Source is left in a valid but uninitialized state. ArrayTransformer(TSelf &&) noexcept = default; TSelf &operator=(TSelf &&) noexcept = default; ~ArrayTransformer() = default; @@ -728,7 +725,7 @@ namespace DNDS auto fatherDataStart = father->operator[](0); if (commTypeCurrent == MPI::CommStrategy::InSituPack) pushingIndexLocal.resize(nSend); - for (index i = 0; i < nSend; i++) + for (index i = 0; i < index(nSend); i++) { MPI_int rank = -1; index loc = -1; @@ -858,14 +855,14 @@ namespace DNDS // MPI_Type_create_hindexed(PushDispsMPI.size(), PushSizesMPI.data(), PushDispsMPI.data(), father->getDataType(), &dtype); MPI_Type_commit(&dtype); - pPushTypeVec->emplace_back(r, dtype); + pPushTypeVec->push_back(std::make_pair(r, dtype)); // OPT: could use MPI_Type_create_hindexed_block to save some space } /************************************************************/ // pull - std::array pullDisp{}; + std::array pullDisp; - std::array pullSizes{}; // same as pushSizes + std::array pullSizes; // same as pushSizes auto gRPtr = son->operator[](index(pLGhostMapping->ghostStart[r + 1])); auto gLPtr = son->operator[](index(pLGhostMapping->ghostStart[r])); auto gStartPtr = son->operator[](index(0)); @@ -883,7 +880,7 @@ namespace DNDS // std::cout << mpi.rank << " pullSlice " << pullDisp[0] << outputDelim << pullBytes[0] << std::endl; MPI_Type_commit(&dtype); - pPullTypeVec->emplace_back(r, dtype); + pPullTypeVec->push_back(std::make_pair(r, dtype)); } } pPullTypeVec->shrink_to_fit(); diff --git a/src/DNDS/Defines.hpp b/src/DNDS/Defines.hpp index 5244a423..360096f3 100644 --- a/src/DNDS/Defines.hpp +++ b/src/DNDS/Defines.hpp @@ -623,7 +623,7 @@ namespace DNDS /// @brief Narrow #index to `int32_t` with range check; dies on overflow. inline int32_t checkedIndexTo32(index v) { - DNDS_assert_info(v >= static_cast(INT32_MIN) && v <= static_cast(INT32_MAX), + DNDS_assert_info(!(v > static_cast(INT32_MAX) || v < static_cast(INT32_MIN)), fmt::format("Index {} to int32 overflow", v)); return static_cast(v); } @@ -928,9 +928,6 @@ namespace DNDS DNDS_DEVICE_CALLABLE EmptyNoDefault(EmptyNoDefault &&v) = default; DNDS_DEVICE_CALLABLE EmptyNoDefault(const EmptyNoDefault &v) = default; DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(const EmptyNoDefault &) = default; - // Rule-of-five closure — trivial type. - DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(EmptyNoDefault &&) noexcept = default; - DNDS_DEVICE_CALLABLE ~EmptyNoDefault() = default; template DNDS_DEVICE_CALLABLE EmptyNoDefault &operator=(T v) { return *this; }; template diff --git a/src/DNDS/Vector.hpp b/src/DNDS/Vector.hpp index a3833839..18b7b0c9 100644 --- a/src/DNDS/Vector.hpp +++ b/src/DNDS/Vector.hpp @@ -27,14 +27,6 @@ namespace DNDS DeviceHostSingleAllocationBase() = default; virtual ~DeviceHostSingleAllocationBase(); - // Polymorphic base: delete copy / move to prevent slicing. Concrete - // subclasses (`DeviceHostSingleAllocationDirect`) own a device buffer; - // callers manipulate them via `unique_ptr`. - DeviceHostSingleAllocationBase(const DeviceHostSingleAllocationBase &) = delete; - DeviceHostSingleAllocationBase &operator=(const DeviceHostSingleAllocationBase &) = delete; - DeviceHostSingleAllocationBase(DeviceHostSingleAllocationBase &&) = delete; - DeviceHostSingleAllocationBase &operator=(DeviceHostSingleAllocationBase &&) = delete; - /// @brief Allocate `bytes` on backend `B` (or on the host when @ref Unknown). virtual void allocate(size_t bytes, DeviceBackend B = DeviceBackend::Unknown) = 0; /// @brief Release the allocation. @@ -69,14 +61,7 @@ namespace DNDS public: DeviceHostSingleAllocationDirect() = default; - ~DeviceHostSingleAllocationDirect() override = default; - - // Inherited copy / move are `= delete` (polymorphic base owns a - // device buffer). Re-declare explicitly for the rule-of-five. - DeviceHostSingleAllocationDirect(const DeviceHostSingleAllocationDirect &) = delete; - DeviceHostSingleAllocationDirect &operator=(const DeviceHostSingleAllocationDirect &) = delete; - DeviceHostSingleAllocationDirect(DeviceHostSingleAllocationDirect &&) = delete; - DeviceHostSingleAllocationDirect &operator=(DeviceHostSingleAllocationDirect &&) = delete; + ~DeviceHostSingleAllocationDirect() override {} void allocate(size_t bytes, DeviceBackend B = DeviceBackend::Unknown) override { @@ -402,12 +387,35 @@ namespace DNDS this->sync_device_ptr(); } - // Rule-of-five closure. Members are `unique_ptr` + raw-cache - // pointers + size_t; the default move transfers the unique_ptrs - // and copies the cache pointers, which is correct because the - // moved-from object is reset to empty. - host_device_vector_r1(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; + /// @brief Move constructor: transfers ownership, source left empty. + host_device_vector_r1(t_self &&R) noexcept + : host_data(std::move(R.host_data)), + device_data(std::move(R.device_data)), + host_ptr(R.host_ptr), + device_ptr(R.device_ptr), + size_(R.size_) + { + R.host_ptr = nullptr; + R.device_ptr = nullptr; + R.size_ = 0; + } + + /// @brief Move assignment: transfers ownership, source left empty. + t_self &operator=(t_self &&R) noexcept + { + if (this == &R) + return *this; + host_data = std::move(R.host_data); + device_data = std::move(R.device_data); + host_ptr = R.host_ptr; + device_ptr = R.device_ptr; + size_ = R.size_; + R.host_ptr = nullptr; + R.device_ptr = nullptr; + R.size_ = 0; + return *this; + } + ~host_device_vector_r1() = default; DeviceBackend device() @@ -445,6 +453,12 @@ namespace DNDS DNDS_HOST host_device_vector_r0(const std::vector &v) : t_base(v) {} + /// @brief Move constructor: moves vector data + transfers device storage. + DNDS_HOST host_device_vector_r0(t_self &&R) noexcept = default; + /// @brief Move assignment. + DNDS_HOST t_self &operator=(t_self &&R) noexcept = default; + ~host_device_vector_r0() = default; + DNDS_HOST t_self &operator=(const std::vector &v) { this->t_base::operator=(v); @@ -527,12 +541,6 @@ namespace DNDS } } - // Rule-of-five closure. `deviceStorage` is a `unique_ptr`; default - // move transfers it correctly. Dtor is trivial. - host_device_vector_r0(t_self &&) noexcept = default; - t_self &operator=(t_self &&) noexcept = default; - ~host_device_vector_r0() = default; - DeviceBackend device() { return deviceStorage ? deviceStorage->backend() : DeviceBackend::Unknown;