diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..199a25df --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,178 @@ +# 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. +# - 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. +# -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. +# -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). +# -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 +# 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, + -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, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-avoid-non-const-global-variables, + +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/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/dev/clang_tidy_plan.md b/docs/dev/clang_tidy_plan.md new file mode 100644 index 00000000..e15ee48b --- /dev/null +++ b/docs/dev/clang_tidy_plan.md @@ -0,0 +1,831 @@ +# 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 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 + +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 — 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. [NOLINT markers in the tree](#nolint-markers-in-the-tree) +7. [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 + +**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 + +**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 + +**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 + +**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 + +**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 + +**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 + +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`. | +| `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 + +**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`. +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 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 +`.clang-tidy`, not inside the `Checks:` block. diff --git a/docs/dev/distributed_reorder_design.md b/docs/dev/distributed_reorder_design.md index 7b736c50..cfaf2864 100644 --- a/docs/dev/distributed_reorder_design.md +++ b/docs/dev/distributed_reorder_design.md @@ -1,41 +1,71 @@ -# Distributed Entity Reordering — Design Document - -> **Status:** Proposal (not yet implemented). The existing -> `ReorderLocalCells` and `ReadDistributed_Redistribute` remain the -> active code paths. +# Distributed Entity Reordering — Design Document (v2) + +> **Status:** Detailed design, not yet implemented. +> +> **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) @@ -44,412 +74,1603 @@ 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 +--- + +## Concepts and Terminology + +### Reorder map + +A per-entity specification that says where each owned entity goes: + +```cpp +struct EntityReorderMap +{ + EntityKind kind; + /// For each father slot i: target rank after reorder. + /// Size == father size of the canonical array for this kind. + std::vector targetRanks; +}; +``` + +For local-only reorder: `targetRanks[i] == mpi.rank` for all i. The new +local ordering is determined by a separate permutation vector (see below). + +For distributed reorder: `targetRanks[i]` may differ from `mpi.rank`. +New global indices are computed by `PermutationTransfer` (contiguous +prefix-sum across ranks). + +### Explicit vs follow reorder + +- **Explicit reorder**: the caller provides an `EntityReorderMap` directly. + The caller chose this entity's new placement. Example: Metis assigns + each cell to a rank. + +- **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. + +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. + +### Canonical array + +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) + +Current canonicals: +| Kind | Canonical pair | Fallback | +|------|---------------|----------| +| Cell | `cell2node` | `cell2cell`, `cell2face` | +| Node | `coords` | — | +| Bnd | `bnd2node` | `bnd2cell`, `bnd2face` | +| Face | `face2node` | `face2cell`, `face2bnd` | + +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. + +### Adjacency role classification + +For a specific reorder operation, each registered adjacency `A→B` falls +into exactly one category: + +| 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 | + +"Relocate" = move rows between ranks (or permute locally). +"Remap" = replace entry values with new global indices. + +### Companion array + +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 + +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.). + +--- + +## Entity Dependency Graph and Propagation -### Decision 1: Two-level permutation vs arbitrary entity reordering +### The entity reference graph -**Question:** Should we allow arbitrary combinations of entity reorderings, -or restrict to a "primary" two-level pattern (Cell + Node, with Face/Edge -fully reconstructed)? +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: -**Answer:** Support arbitrary combinations, but document the common patterns. +``` +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) +``` + +### 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): + +``` +for each local B entity b: + candidateRanks = { A.targetRank : A references b via A->B } + b.targetRank = min(candidateRanks) +``` + +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** -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 +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. -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 +**Rule 3: Derived entities are destroyed, not followed** -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. +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. + +### Follow computation + +``` +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} +``` -### Decision 2: Reorder map representation +### Which entities can follow which? -Each reordered entity needs a map from old slot to new identity. +| 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 -/// Per-entity reorder specification. -struct EntityReorderMap +struct FollowSpec { - 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; + 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. }; ``` -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. +### 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. -### Decision 3: MPI collectivity for local-only detection +### Classification algorithm ```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); +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; + + if (!fromReordered && !toReordered) return AdjAction::SKIP; + if (fromReordered && !toReordered) return AdjAction::RELOCATE; + if (!fromReordered && toReordered) return AdjAction::REMAP; + return AdjAction::RELOCATE_REMAP; // both reordered +} ``` -When all ranks agree it is local-only, skip all MPI communication in the -data transfer step. Use in-place `PermuteRows` + direct entry replacement. +### 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: + +``` +1. REMAP entries (replace old target globals with new target globals) +2. RELOCATE rows (move rows to new source layout) +``` -### Decision 4: Ghost rebuild is the caller's responsibility +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). -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 +### Ordering between adjacencies -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 +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). -## Algorithm +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. -### Input +Therefore the global ordering is simply: ``` -ReorderEntities( - mesh, - reorderMaps: vector, // one per entity to reorder - registry: MeshConnectivity // from fillRegistry() -) +Phase 1: REMAP all entries (all adjacencies, all entity kinds) +Phase 2: RELOCATE all rows (all adjacencies, all entity kinds) ``` -### Precondition +### Ghost data during reorder + +**Precondition**: All adjacencies must be in `Adj_PointToGlobal` state. +Son arrays (ghost data) exist but may be stale after reorder. + +**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. -All adjacencies must be in `Adj_PointToGlobal` state. (The caller -converts to global before calling.) +**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. -### Step 1: Classify adjacencies +**Post-condition**: Son arrays are stale. Ghost mappings (`pLGhostMapping`) +on reordered entities are stale. The caller must rebuild ghosts. + +--- + +## PermutationTransfer Utility + +### Motivation -For each registered adjacency A→B in the registry: +Both the serial-read distribution (`ReadDistributed_Redistribute`) and +the local cell reorder (`ReorderLocalCells`) need to: -| 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 | +1. Convert a partition assignment or forward map into push/permute indices +2. Compute new global numbering (prefix sums across ranks) +3. Transfer or permute array data -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. +Currently this is done by ad-hoc helpers in `Mesh_PartitionHelpers.hpp`: +`Partition2LocalIdx`, `Partition2Serial2Global`, `TransferDataSerial2Global`, +`ConvertAdjSerial2Global`. These should be unified into a reusable tool. -### Step 2: Build lookup arrays (old → new global) +### Data members -For each reordered entity kind E: +```cpp +struct PermutationTransfer +{ + /// Per father slot: target rank after reorder. + std::vector targetRanks; -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. + /// New global index for each father slot. + /// Computed by prefix-sum across ranks grouped by target rank. + std::vector newGlobalIndices; -This is the same pattern as `ReorderLocalCells` uses for `cellOld2NewArr`. + /// 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 -For local-only reorder, step 3-4 can be skipped — all lookups are local. + /// 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; -### Step 3: Update entries in TARGET_ONLY and BOTH adjacencies + /// Whether this is a pure rank-local permutation (no cross-rank). + bool isLocalOnly{false}; -For each adj X→E where E is reordered: + /// New global offsets: newGlobalOffsets[r] = first global index + /// owned by rank r after reorder. Size = nRanks + 1. + std::vector newGlobalOffsets; +}; ``` -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 + +### Factory methods + +**`fromPartition`**: Used by redistribution (ReadDistributed, ParMetis). +Caller provides only target ranks. New global indices are computed +automatically. + +```cpp +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; +} ``` -For SELF adjacencies (E→E), update entries first, then relocate rows. +**`fromLocalPermutation`**: Used by local cell reorder. Caller provides +a local old-to-new permutation. All entities stay on the same rank. -If the adj also has a son (ghost data), pull the updated entries after -all father entries are rewritten: +```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; +} ``` -adj.trans.pullOnce() // if ghost comm exists + +### 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 + } +} ``` -For local-only reorder, the lookup is a direct local array access. +**`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; +} +``` -### Step 4: Relocate rows in SOURCE_ONLY, BOTH, and SELF adjacencies +### Collecting the pull set -For each adj E→X where E is reordered: +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: -**Local-only path:** ``` -PermuteRows(adj, NumE(), old2newLocal) +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 (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). + 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; +}; + +// 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); +}; ``` -**Distributed path:** -Uses `PermutationTransfer` (see below): +### Precondition + +- 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.) + +### Full algorithm + ``` -auto transfer = PermutationTransfer::fromForwardMap(entityMap, globalMapping, mpi); -transfer.execute(adj.father, mpi); -// adj.father is now the redistributed array +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 ``` -Same for companion arrays. +### Post-condition + +- 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 + +--- + +## Companion Array Handling + +### Which arrays are companions? + +| Entity kind | Companion arrays | +|-------------|-----------------| +| Cell | `cellElemInfo`, `cell2cellOrig`, `cell2nodePbi` (if periodic) | +| Node | `coords`, `node2nodeOrig`, `coordsElevDisp` (if elevation), `nodeWallDist` | +| Bnd | `bndElemInfo`, `bnd2bndOrig`, `bnd2nodePbi` (if periodic) | +| Face | `faceElemInfo`, `face2nodePbi` (if periodic) | + +### Callback-based registration -### Step 5: Rebuild global mappings +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: -For each reordered entity E: +```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 ``` -canonicalPair.father->createGlobalMapping() // new contiguous global numbering + +### 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); ``` -Other pairs for the same entity borrow via `pLGlobalMapping = canonical->pLGlobalMapping`. +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 -### Step 6: Update idx states +During `ReorderPlan::apply`: -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): 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 ``` -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. +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. -### Post-condition +### Pbi arrays as companions -- 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 +`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. -## `PermutationTransfer` — Dedicated Utility +### The `cell2parentCell`, `node2parentNode`, `node2bndNode` arrays -### Motivation +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). -Both the serial-read distribution and the distributed reorder need to: -1. Convert a forward map (old_slot → new_rank) into push indices (CSR) -2. Compute new global numbering (prefix sums across ranks) -3. Transfer array data between ranks +--- + +## Dynamic Reorder Registry (Decoupled from UnstructuredMesh) + +### 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. + +### Solution: ReorderRegistry + callback-based participation -This is currently done by ad-hoc helpers in `Mesh_PartitionHelpers.hpp` -(`Partition2LocalIdx`, `Partition2Serial2Global`, `TransferDataSerial2Global`, -`ConvertAdjSerial2Global`). These should be unified into a reusable tool. +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. -### API +### How `buildReorderRegistry` works ```cpp -/// Encapsulates a distributed permutation: moving rows of arrays -/// between ranks according to a forward map. -struct PermutationTransfer +ReorderRegistry UnstructuredMesh::buildReorderRegistry( + const std::unordered_set &destroyKinds) const { - /// Per father slot: target rank. - std::vector targetRanks; + 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; +} +``` - /// New global index for each father slot. - std::vector newGlobalIndices; +### External code extends the registry - /// 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; -}; +```cpp +// 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); +``` + +### How ReorderPlan::apply works with the registry + +```cpp +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 == 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); + } +} +``` + +### 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 ``` -### Usage in existing code +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 + +Before reorder: +- All adj must be `Adj_PointToGlobal` (or `Adj_PointToLocal` for + local-only, converted to global first). + +During reorder: +- No idx transitions happen. Entries are remapped (still global, just + different globals). Rows are relocated (still global). + +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). +- `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. + +The mesh wrapper also updates the group state variables +(`adjPrimaryState`, `adjFacialState`, etc.) to `Adj_PointToGlobal`. + +### Post-reorder idx state update (mesh wrapper responsibility) -**Serial-read distribution** (`PartitionReorderToMeshCell2Cell`): ```cpp -auto cellTransfer = PermutationTransfer::fromPartition(cellPartition, cellGlobal, mpi); -auto nodeTransfer = PermutationTransfer::fromPartition(nodePartition, nodeGlobal, mpi); +// 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. +``` + +### buildReorderRegistry skip logic -// Update cell2node entries: old node globals → new node globals -auto nodeLookup = nodeTransfer.buildLookup(referencedNodeGlobals, nodeGlobal, mpi); -ConvertAdjEntries(cell2node, ..., [&](index g) { return nodeLookup.resolve(g); }); +When `destroyKinds` is specified (e.g., {Face}), `buildReorderRegistry` +skips adjacencies involving that kind: -// Transfer rows -cellTransfer.transferRows(cell2node, mpi); -cellTransfer.transferRows(cellElemInfo, mpi); -nodeTransfer.transferRows(coords, mpi); +```cpp +auto shouldSkip = [&](AdjKind kind) { + return destroyKinds.count(kind.from) || destroyKinds.count(kind.to); +}; ``` -**Local cell reorder** (`ReorderLocalCells`): +This prevents registering callbacks for adjacencies that will be +destroyed before the reorder runs. + +--- + +## Local-Only Fast Path + +### Detection + ```cpp -auto cellTransfer = PermutationTransfer::fromForwardMap(cellForwardMap, cellGlobal, mpi); -assert(cellTransfer.isLocalOnly); // all ranks local +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; +``` + +### Optimizations for local-only + +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. + +### 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: -auto cellLookup = cellTransfer.buildLookup(referencedCellGlobals, cellGlobal, mpi); -// Update face2cell, node2cell, bnd2cell entries -// Permute cell2node, cell2face, cellElemInfo rows +``` +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() ``` -## Multi-Entity Reordering +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. -### Problem +--- -When repartitioning, both cells and nodes move between ranks. The order -matters: +## Concrete Use Cases -1. Node indices in `cell2node` must be updated to new node globals - **before** `cell2node` rows are relocated (otherwise the entries - are stale after relocation). +### Use case 1: Local cell reorder (ReorderLocalCells replacement) -2. Cell indices in `bnd2cell` must be updated to new cell globals. +```cpp +// Caller computes cell permutation via Metis +auto perm = ComputeCellPermutation(...); + +// Build explicit map +EntityReorderMap cellMap{EntityKind::Cell, /*targetRanks all = mpi.rank*/}; +// (localOld2New is embedded in PermutationTransfer) -3. Both `cell2node` and `bnd2node` have entries pointing to nodes - (update needed) and rows belonging to cells/bnds (relocate 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) -### Ordering rule +mesh.ReorderEntities(input); +// All adj now in Adj_PointToGlobal, ghost mappings rewired (local-only opt) +``` -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) +### Use case 2: Distributed redistribution (ReadDistributed replacement) -When both A and B are reordered: -- Update entries (B's new globals) first -- Relocate rows (A's new layout) second +```cpp +// Caller computes cell partition via ParMetis +auto cellPartition = ReadDistributed_PartitionParMetis(...); -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. +EntityReorderMap cellMap{EntityKind::Cell, cellPartition}; -### Algorithm for multi-entity reorder +// 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 ``` -ReorderEntities(mesh, maps: {Cell→mapC, Node→mapN, Bnd→mapB}, registry): - 1. Build all lookup arrays: - cellLookup = mapC.buildLookup(...) - nodeLookup = mapN.buildLookup(...) - bndLookup = mapB.buildLookup(...) +### Use case 3: Node-only reorder (hypothetical RCM on node graph) - 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 +```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. +``` - 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) +### Use case 4: Solver-participates redistribution (external arrays) - 4. Rebuild global mappings for Cell, Node, Bnd +```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); - 5. Destroy derived adjacencies (face2cell, face2node, cell2face, etc.) - — caller will reconstruct from the redistributed primary data +// 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 ``` -### Face handling +### Use case 5: Node-only reorder with solver participation + +```cpp +// Hypothetical: RCM reorder on node graph for FEM bandwidth reduction -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: +EntityReorderMap nodeMap{EntityKind::Node, /*all local*/}; +ReorderInput input; +input.explicitMaps = {nodeMap}; +input.destroyKinds = {EntityKind::Face}; -1. Destroy facial adjacencies before reorder -2. Reorder primary entities (Cell, Node, Bnd) -3. Rebuild: `RecoverNode2CellAndNode2Bnd` → `RecoverCell2CellAndBnd2Cell` → - `BuildGhostPrimary` → `InterpolateFace` → etc. +auto plan = mesh.buildReorderPlan(input); +mesh.ReorderEntities(input); -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. +// 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 -### Phase 1: `PermutationTransfer` utility +### 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: ReorderPlan + ReorderEntities framework + +**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 + +Contents: +- `EntityReorderMap`, `FollowSpec`, `ReorderInput` structs +- `ReorderPlan` with `build()`, `remapEntries()`, `relocateRows()` +- `classifyAdj` free function +- `ComputeFollowMap` free function +- `collectPullSet` free function +- `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 + +- 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 -- 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/` +### Phase 5: Cleanup -### Phase 2: `fillRegistry` on UnstructuredMesh +- Deprecate `Partition2LocalIdx`, `Partition2Serial2Global`, + `TransferDataSerial2Global`, `ConvertAdjSerial2Global` +- Remove redundant manual code in old methods +- Update AGENTS.md with new API -- Implement `fillRegistry(MeshConnectivity&, set skip)` (Plan 2) -- Canonical global mapping sources documented +--- -### Phase 3: Single-entity `ReorderEntity` +## Appendix: Re-evaluation Notes (v1) -- 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 +> 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). -### Phase 4: Multi-entity `ReorderEntities` +### 2026-04-27, post PR #6 -- 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 +Reviewed against the merged PR #6 changes (InterpolateGlobal, face pipeline +split, templatization, pybind11 bindings, file reorganization into +`src/Geom/Mesh/`). -### Phase 5: Cleanup +**Key updates for v2:** + +- Face reconstruction after reorder uses the modular + `InterpolateFace -> BuildGhostFace -> MatchFaceBoundary` pipeline. + +- `parent2entityPbi` is computed locally -- 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`). -- Remove or deprecate the old ad-hoc helpers in `Mesh_PartitionHelpers.hpp` -- Remove redundant code in `ReorderLocalCells` and - `ReadDistributed_Redistribute` (they become thin wrappers) +- Arrays destroyed before reorder and reconstructed after: + `cell2face`, `face2cell`, `face2node`, `face2bnd`, `bnd2face`, + `faceElemInfo`, `face2nodePbi`, `cell2facePbi` (when periodic). diff --git a/docs/guides/style_guide.md b/docs/guides/style_guide.md index 473728d8..b1919965 100644 --- a/docs/guides/style_guide.md +++ b/docs/guides/style_guide.md @@ -70,12 +70,84 @@ 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. + +**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) 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..075c803d --- /dev/null +++ b/scripts/run_clang_tidy.py @@ -0,0 +1,559 @@ +#!/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( + # 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: + 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)." + ), + ) + 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 + + +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] + + # --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 : {effective_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=effective_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/Array.hpp b/src/DNDS/Array.hpp index 808283fb..be718de7 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, @@ -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) @@ -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()) @@ -823,6 +823,13 @@ namespace DNDS this->clone(R); } + /// @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; + /// @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 @@ -854,7 +861,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 +883,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 +989,7 @@ namespace DNDS { } // doing data - this->__WriteSerializerData(serializerP, offset); + this->WriteSerializerData(serializerP, offset); serializerP->GoToPath(cwd); } @@ -1067,10 +1074,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 +1134,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 +1179,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 +1188,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 0d9a1ffb..4ba0863a 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). @@ -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, @@ -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/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/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/ArrayDOF_bind.hpp b/src/DNDS/ArrayDOF_bind.hpp index 008a9858..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) @@ -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); @@ -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/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/AdjacencyRow.hpp b/src/DNDS/ArrayDerived/AdjacencyRow.hpp index 73025be6..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 index_T *cbegin() const { return __p_indices; } - DNDS_DEVICE_CALLABLE 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.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/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/ArrayAdjacency_bind.hpp b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp index e7b001da..95c26ebe 100644 --- a/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayAdjacency_bind.hpp @@ -36,13 +36,13 @@ 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()); 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_)); } @@ -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); }); @@ -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), ...); @@ -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 e2f4e8d6..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; @@ -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) @@ -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) @@ -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/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/ArrayEigenMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenMatrixBatch_bind.hpp index 954694da..c25412b0 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" @@ -23,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< @@ -35,11 +37,11 @@ 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"); - 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( @@ -49,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_) @@ -83,7 +85,7 @@ namespace DNDS false); } - inline auto pybind11_ArrayEigenMatrixBatch_setitem(ArrayEigenMatrixBatch &self, std::tuple index_, 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); @@ -93,7 +95,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, @@ -142,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_ @@ -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); + pybind11_ArrayEigenMatrixBatch_setitem(self, index_, row); }); ArrayEigenMatrixBatch_ @@ -215,10 +217,10 @@ 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); }); + self.runFunctionAppendedIndex(index_, [&](auto &ar, index iC) //*note the auto&& reference here!!! + { pybind11_ArrayEigenMatrixBatch_setitem_row(ar, iC, row); }); }); Pair_ @@ -232,10 +234,10 @@ 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); }); + 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/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 697ee416..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); @@ -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); @@ -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) @@ -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); }); @@ -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), ...); @@ -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.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/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/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenUniMatrixBatch_bind.hpp index 8e13a8f6..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); @@ -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, @@ -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); @@ -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++) { @@ -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); }); @@ -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), ...); @@ -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.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/ArrayDerived/ArrayEigenVector_bind.hpp b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp index 4175c7d0..23ee17a7 100644 --- a/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp +++ b/src/DNDS/ArrayDerived/ArrayEigenVector_bind.hpp @@ -48,13 +48,13 @@ 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()); 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()); } } @@ -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); }); @@ -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), ...); @@ -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/ArrayPair.hpp b/src/DNDS/ArrayPair.hpp index a5e501c5..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. @@ -255,13 +259,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 +674,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/ArrayRedistributor.hpp b/src/DNDS/ArrayRedistributor.hpp index 7c289344..d17d2bc9 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)); }; @@ -101,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]++; } @@ -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; @@ -157,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]++; } @@ -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..fcccbd58 100644 --- a/src/DNDS/ArrayTransformer.hpp +++ b/src/DNDS/ArrayTransformer.hpp @@ -77,6 +77,10 @@ namespace DNDS // default copy ParArray(const t_self &R) = default; t_self &operator=(const t_self &R) = default; + // Move (suppressed by explicit copy above, re-declare). + ParArray(t_self &&) noexcept = default; + t_self &operator=(t_self &&) noexcept = default; + ~ParArray() = default; // operator= handled automatically @@ -309,8 +313,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)); } @@ -381,9 +385,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(); } }; @@ -553,6 +557,12 @@ namespace DNDS this->operator=(R); } + /// @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; + /** * @brief Attach father and son arrays. First setup step. * @@ -835,7 +845,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 +874,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); @@ -1087,7 +1097,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"); @@ -1122,8 +1132,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; @@ -1157,7 +1167,7 @@ namespace DNDS } else if (commTypeCurrent == MPI::CommStrategy::InSituPack) { - __InSituPackStartPush(B); + InSituPackStartPush(B); } else { @@ -1171,7 +1181,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"); @@ -1179,8 +1189,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; @@ -1258,7 +1268,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 7b4242d2..93122f3d 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) @@ -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); @@ -490,12 +488,18 @@ 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()); + // 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")); @@ -504,7 +508,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(); @@ -561,7 +565,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), ...); } @@ -570,12 +574,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 /*unused*/) { (_pybind11_ParArray_define_dispatch(m), ...); } @@ -584,12 +588,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 /*unused*/) { (_pybind11_ArrayTransformer_define_dispatch(m), ...); } @@ -598,12 +602,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 /*unused*/) { (_pybind11_ParArrayPair_define_dispatch(m), ...); } @@ -612,7 +616,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{}); } } @@ -636,7 +640,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/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..ef3557ef 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,31 @@ 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"; + // 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: + return "string"; + case ConfigTypeTag::Array: + return "array"; + case ConfigTypeTag::Object: + return "object"; + case ConfigTypeTag::ArrayOfObjects: + return "array"; + case ConfigTypeTag::MapOfObjects: + return "object"; + // NOLINTEND(bugprone-branch-clone) + case ConfigTypeTag::Json: + return {}; } return {}; } @@ -659,45 +686,50 @@ 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); \ - } \ +// 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() \ + { \ + 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) +// NOLINTEND(bugprone-macro-parentheses) diff --git a/src/DNDS/Config/ConfigRegistry.hpp b/src/DNDS/Config/ConfigRegistry.hpp index 418dba56..88e9315b 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. @@ -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..7719ce2c 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(); } @@ -63,7 +64,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; @@ -172,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/Defines.hpp b/src/DNDS/Defines.hpp index 32055d88..360096f3 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) /***************/ @@ -613,7 +618,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) @@ -681,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 @@ -720,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/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.cpp b/src/DNDS/Device/DeviceStorage.cpp index 182e131f..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; } @@ -53,7 +57,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 +65,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 +73,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/Device/DeviceStorage.hpp b/src/DNDS/Device/DeviceStorage.hpp index bf379b89..b20f4e10 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. }; @@ -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/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/EigenUtil.hpp b/src/DNDS/EigenUtil.hpp index 32abb43c..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`. */ - template struct MatrixFMTSafe : public Matrix + // NOLINTEND(bugprone-branch-clone) { using Base = Matrix; using Base::Base; @@ -275,7 +279,7 @@ namespace DNDS h_data.resize(this->size()); } - rowsize rows() const + [[nodiscard]] rowsize rows() const { if constexpr (M >= 0) return M; @@ -283,7 +287,7 @@ namespace DNDS return M_dynamic; } - rowsize cols() const + [[nodiscard]] rowsize cols() const { if constexpr (N >= 0) return N; @@ -291,7 +295,7 @@ namespace DNDS return N_dynamic; } - rowsize size() const + [[nodiscard]] rowsize size() const { return rows() * cols(); } diff --git a/src/DNDS/Errors.hpp b/src/DNDS/Errors.hpp index 1ee081a1..ca8d7592 100644 --- a/src/DNDS/Errors.hpp +++ b/src/DNDS/Errors.hpp @@ -28,6 +28,7 @@ #include #include +#include #include namespace DNDS { @@ -61,10 +62,13 @@ 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); + // 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); - std::cerr << format_buf << std::endl; + std::cerr << format_buf.data() << std::endl; std::abort(); } 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/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/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/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/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/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.cpp b/src/DNDS/MPI.cpp index 76305233..67b85832 100644 --- a/src/DNDS/MPI.cpp +++ b/src/DNDS/MPI.cpp @@ -24,7 +24,6 @@ # include #endif - #ifdef NDEBUG # define NDEBUG_DISABLED # undef NDEBUG @@ -71,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()); @@ -116,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(); @@ -144,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 @@ -159,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 @@ -175,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; } @@ -184,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, @@ -196,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; } @@ -204,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 @@ -213,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; } @@ -221,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; } @@ -232,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 @@ -241,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; } @@ -271,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); @@ -288,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 } @@ -316,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) @@ -339,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(); @@ -350,13 +349,18 @@ 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"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _array_strategy = InSituPack; auto mpi = MPIInfo(); @@ -369,13 +373,18 @@ 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"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _use_strong_sync_wait = true; auto mpi = MPIInfo(); @@ -388,13 +397,18 @@ 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"); - if (ret != NULL && (std::stoi(ret) != 0)) + if (ret != nullptr && (std::stoi(ret) != 0)) { _use_async_one_by_one = true; auto mpi = MPIInfo(); @@ -407,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.hpp b/src/DNDS/MPI.hpp index 091fc263..b98e2e5b 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 @@ -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(); } @@ -234,10 +234,10 @@ 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; + + 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); } @@ -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() { @@ -484,8 +507,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; } @@ -512,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) @@ -582,17 +605,23 @@ 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); 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). @@ -608,8 +637,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 @@ -683,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); } @@ -756,10 +791,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/MPI_bind.cpp b/src/DNDS/MPI_bind.cpp index f3d3192b..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,8 +77,9 @@ namespace DNDS::MPI // Capture the modified arguments into output_args: std::vector pArgvOut; + pArgvOut.reserve(*pargc); 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 @@ -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); }); @@ -93,7 +105,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/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/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/PermutationTransfer.hpp b/src/DNDS/PermutationTransfer.hpp new file mode 100644 index 00000000..1569b0a0 --- /dev/null +++ b/src/DNDS/PermutationTransfer.hpp @@ -0,0 +1,345 @@ +#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 + 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++) + { + 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/src/DNDS/Profiling.hpp b/src/DNDS/Profiling.hpp index 98c3f566..45f0e6ff 100644 --- a/src/DNDS/Profiling.hpp +++ b/src/DNDS/Profiling.hpp @@ -22,43 +22,49 @@ 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, - 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; + static const int Ntype = EndTimerType; static const int Ntype_Past = 64; static const int Ntype_All = Ntype + Ntype_Past; private: std::array timer = {0}; - std::array tStart; + 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/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 b1c11202..83068e33 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; } @@ -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; @@ -211,7 +219,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/Serializer/SerializerFactory.hpp b/src/DNDS/Serializer/SerializerFactory.hpp index ec911f2f..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,10 +39,11 @@ 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) { + // 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,11 +55,12 @@ 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. /// @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") @@ -89,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, 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") { @@ -97,9 +101,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") diff --git a/src/DNDS/Serializer/SerializerH5.cpp b/src/DNDS/Serializer/SerializerH5.cpp index 56e530a1..a20b1f09 100644 --- a/src/DNDS/Serializer/SerializerH5.cpp +++ b/src/DNDS/Serializer/SerializerH5.cpp @@ -177,18 +177,26 @@ 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; - std::string get_indent() const + [[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, ' '); } }; 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; @@ -196,7 +204,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 +226,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; } @@ -251,8 +259,8 @@ 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); - data->contents.attributes.push_back(attr_name); + 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 return 0; // Continue iteration @@ -264,8 +272,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) @@ -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) @@ -363,7 +375,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 +390,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); @@ -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 @@ -636,7 +656,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]; @@ -649,9 +669,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; } @@ -859,7 +879,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()); @@ -875,14 +895,16 @@ 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 { 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 +915,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()); @@ -909,14 +931,16 @@ 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 { 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); diff --git a/src/DNDS/Serializer/SerializerH5.hpp b/src/DNDS/Serializer/SerializerH5.hpp index 8a3b2bcf..a4610b3d 100644 --- a/src/DNDS/Serializer/SerializerH5.hpp +++ b/src/DNDS/Serializer/SerializerH5.hpp @@ -67,11 +67,18 @@ 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); } + // 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.cpp b/src/DNDS/Serializer/SerializerJSON.cpp index 8bed27c4..da146a9f 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; } @@ -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 { diff --git a/src/DNDS/Serializer/SerializerJSON.hpp b/src/DNDS/Serializer/SerializerJSON.hpp index 9d3572d4..e3f923d3 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; } @@ -85,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 5d674652..8b5cd8c3 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,11 +44,11 @@ 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); using tSerializer = SerializerJSON; @@ -57,11 +59,11 @@ 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); using tSerializer = SerializerH5; @@ -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); 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 fab4dc04..18b7b0c9 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 { @@ -387,6 +387,37 @@ namespace DNDS this->sync_device_ptr(); } + /// @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() { return device_data ? device_data->device() : DeviceBackend::Unknown; @@ -422,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); 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/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/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..c2f23f1b 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,172 +1736,110 @@ namespace DNDS::Geom control.getILUCode()); } - // ================================================================= - // File-local helpers for ReorderLocalCells - // ================================================================= - namespace + void UnstructuredMesh::fillRegistry( + MeshConnectivity &dag) const { - /** - * \brief Result of local cell permutation computation. - */ - struct CellPermutationResult + 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) { - std::vector cellOld2New; - std::vector cellNew2Old; - std::vector localPartitionStarts; - index bwOld = 0; - index bwNew = 0; + if (pair.father && skip.find(kind) == skip.end()) + dag.registerAdj(kind, pair); }; - /** - * \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) + // 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 { - 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"); + for (auto &gm : candidates) + if (gm) + return gm; + return nullptr; + }; - //! 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()); - } + auto getGM = [](const auto &pair) -> ssp + { + if (pair.father && pair.father->pLGlobalMapping) + return pair.father->pLGlobalMapping; + return nullptr; + }; - dbgCheckBidir("after all inner partitioning"); - } + 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)}); - // Contiguous sorting: within each partition, put interior cells before - // cells that touch ghost neighbors. + // Register if found; check_throw if any registered adj needs it + auto regMap = [&](EntityKind kind, const ssp &gm) + { + if (gm) { - 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); + dag.registerGlobalMapping(kind, gm); } - - // Build inverse permutation - std::unordered_set set; - set.reserve(result.cellNew2Old.size()); - for (index i = 0; i < nCell; i++) + else { - DNDS_assert(set.count(result.cellNew2Old[i]) == 0); - set.insert(result.cellNew2Old[i]); - result.cellOld2New.at(result.cellNew2Old[i]) = i; + // 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))); } - return result; - } - } // anonymous namespace + }; + + regMap(EntityKind::Cell, cellMap); + regMap(EntityKind::Node, nodeMap); + regMap(EntityKind::Bnd, bndMap); + regMap(EntityKind::Face, faceMap); + } + + // ================================================================= + // File-local helpers for ReorderLocalCells + // ================================================================= + // 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()); @@ -1965,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 14f24239..3b3e1aaf 100644 --- a/src/Geom/Mesh/Mesh.hpp +++ b/src/Geom/Mesh/Mesh.hpp @@ -12,6 +12,8 @@ #include "DNDS/ObjectUtils.hpp" #include "DNDS/Config/ConfigParam.hpp" #include "AdjIndexInfo.hpp" +#include "MeshConnectivity.hpp" +#include "ReorderPlan.hpp" namespace DNDS::Direct { @@ -211,6 +213,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 +325,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 +490,74 @@ 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; + + // ================================================================= + // 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(); /** @@ -480,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(); } @@ -809,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; @@ -834,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) @@ -885,6 +978,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)", @@ -895,6 +989,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{}); @@ -979,6 +1074,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)", @@ -988,6 +1084,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/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_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_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..7198929c 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( @@ -455,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(); diff --git a/src/Geom/Mesh/Mesh_Reorder.cpp b/src/Geom/Mesh/Mesh_Reorder.cpp new file mode 100644 index 00000000..43926d31 --- /dev/null +++ b/src/Geom/Mesh/Mesh_Reorder.cpp @@ -0,0 +1,978 @@ +/// @file Mesh_Reorder.cpp +/// @brief Implementation of ReorderPlan::build, ReorderPlan::apply, and +/// UnstructuredMesh::buildReorderRegistry / ReorderEntities. + +#include "ReorderPlan.hpp" +#include "Mesh.hpp" +#include "Mesh_CellPermutation.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); + + // 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; + + 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); + } + } + } + + // ================================================================= + // 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); + + // --- 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; + } + + // ================================================================= + // 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.father->createGlobalMapping(); + // 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.father->createGlobalMapping(); + if (node2nodeOrig.father) + node2nodeOrig.father->pLGlobalMapping = coords.father->pLGlobalMapping; + } + else if (kind == EntityKind::Bnd && bnd2node.father) + { + bnd2node.father->createGlobalMapping(); + 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.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) + { + 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(); + } + + // ================================================================= + // 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 diff --git a/src/Geom/Mesh/ReorderPlan.hpp b/src/Geom/Mesh/ReorderPlan.hpp new file mode 100644 index 00000000..2f0072db --- /dev/null +++ b/src/Geom/Mesh/ReorderPlan.hpp @@ -0,0 +1,262 @@ +#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; + + /// 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 = {}) + { + 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/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--) 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 - diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt index 265f5341..f307547e 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}) @@ -215,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 @@ -225,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/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); + } +} diff --git a/test/cpp/Geom/test_MeshReorder.cpp b/test/cpp/Geom/test_MeshReorder.cpp new file mode 100644 index 00000000..58318ebd --- /dev/null +++ b/test/cpp/Geom/test_MeshReorder.cpp @@ -0,0 +1,645 @@ +/** + * @file test_MeshReorder.cpp + * @brief Unit tests for ReorderPlan, ReorderRegistry, and classification. + * + * Tests: + * - AdjAction classification logic + * - ReorderRegistry registration and lookup + * - 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); + 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); +} + +// ================================================================= +// 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(); +}