Design decisions — core¶
Harvested verbatim from the project's working notes. Each entry records a decision, the
alternative it rejected, and the stated reason. This file is evidence, not a summary —
quotes are unedited. Read docs/DECISIONS.md first; come here for depth.
Bootstrapped by a one-time harvest of the working notes (2026-09-10); maintained by hand
from here on — add an entry when you make a decision a future session could undo. Regenerate
the index with docs/decisions/build_index.py after editing.
Do not reverse an entry here without recording a new decision that supersedes it.
Coarse-level redistribution lives in core; the hierarchies stay in the methods¶
- area: core
- source: amr/docs/amr_mg_core_boundary.md §1, §3, §4, §10; amr/docs/amr_mg_depth.md §5.6; docs/archive/MG_TELESCOPING_PLAN.md §4
- decided: 2026-09-24
- status: settled
- quote: |
The target decomposition of a multigrid level (sibling merge on the ORB tree, a fresh
proportional ORB on fewer ranks, or replication), the stage communicators and membership, and
the planned up/down movement of level fields are
peclet::core::decompinfrastructure, with flow and amr as consumers and voro taking the communicator bookkeeping and the id-keyed gather. What a level IS, its operator and openness on the target, the transfers, the smoother, the bottom and the V-cycle stay method-specific. The test is ARCHITECTURE.md's own: core provides where data lives and how it moves, not how the physics is integrated — everything on the core side is nameable withBlockDecomposer,Block,MPI_Comm,T*and an index functor, and nothing on the method side is. The planned movement isRedistributeTopology, following theGridHaloTopology/ParticleHaloTopologyprecedent of describe-once, move-many; the existing one-shotredistributeGridFieldskeeps its role for rebalancing. Trigger: flow's sibling-merge telescoping cannot handle the WEIGHTED decomposition that load-balanced CFD-DEM runs on (mac_cutcell_mg.hpp:513–517documents the limit and nothing enforces it; its depth search falls through to one block, the whole level on one rank per V-cycle), and amr's C1 needed the same machinery — three private copies were forming (flow'sTelescope, amr'sReplicatedTailStage, voro's coming GraphAMG bottom). flow's existing telescoping migrates onto it behind a byte-identity gate with no change of policy or default. - rejected: per-method private implementations (three were forming); a core "multigrid stage" that owns the continued hierarchy below the stage point (core cannot name a level, and V-cycle orchestration is deliberately not consolidated); leaving flow's telescoping in place and serving only new consumers (flow is the first production consumer of the missing Repartition kind)
- why: "the seam is below the hierarchy, not through it"
AMR PCG must mask solid AND project onto the fluid range (mask + fluid-only mean), not just deflate the constant mode¶
- area: core
- source: device-naming-retirement.md:89-95
- decided: undated
- status: settled
- quote: |
maskSolid FIX (transport-core 3aff1ab, pushed): the AMR PCG (
pcg.hpp) deflated only the constant (removeMeanVol), NO maskSolid ⇒ CG amplified the solid/near-disconnected null modes ⇒ the Stokes ~1e24 blow-up. AddedbuildFluidMask(op diagonal Σ_f w_f + bcDiag > tiny) + project every Krylov vector onto the fluid range (mask solid + fluid-only mean — the old mean wrongly averaged over ALL cells incl. pinned solid). - rejected: deflating only the constant/all-cell mean without masking solid cells
- why: without masking, CG amplified the solid/near-disconnected null modes, causing the Stokes blow-up (~1e24)
Anisotropic coarse-grid partitioning requires cellExtent, not raw cell-count kLargest¶
- area: core
- source: mg-decomposition-alignment.md:43
- decided: undated
- status: settled
- quote: |
- The anisotropy trap. First coarse-first attempt made imbalance WORSE (up to 4.0) and split
the channel's wall-normal y on 24/24 ranks: a coarse grid whose axes were coarsened by DIFFERENT
factors has anisotropic cells, so the ORB's raw-cell-count
kLargestpicks the wrong axis. HencecellExtent. Any new coarse-grid partitioning must carry it.
- The anisotropy trap. First coarse-first attempt made imbalance WORSE (up to 4.0) and split
the channel's wall-normal y on 24/24 ranks: a coarse grid whose axes were coarsened by DIFFERENT
factors has anisotropic cells, so the ORB's raw-cell-count
- rejected: partitioning by raw cell-count kLargest on an anisotropic coarse grid
- why: "a coarse grid whose axes were coarsened by DIFFERENT factors has anisotropic cells, so the ORB's raw-cell-count kLargest picks the wrong axis"
Convention: keep NBX tag families >= 64 apart¶
- area: core
- source: nbx-round-tag-race.md:28
- decided: 2026-09-02
- status: settled
- quote: |
How to apply: for any MPI hang, first
snellius/stack_census.sh(parallel gdb over a node), thenPECLET_CORE_HALO_TIMEOUT=<s>on a-gbuild (coredaf6881) [...] Keep NBX tag families >= 64 apart. - rejected: none stated
- why: none stated (follows from the round%64 tag scheme)
Device-vs-host numerical comparison policy: bit-exact assembly, tolerance-based apply on CUDA/HIP¶
- area: core
- source: kokkos-cuda-constexpr-required.md:27-30
- decided: 2026-07-23
- status: settled
- quote: |
- Device-vs-host comparison policy (locked into the core tests 2026-07-23): assembled CSRs/coefficients bit-exact on ALL backends; apply/V-cycle results bit-exact on OpenMP/Serial, round-off relative tolerance (1e-12 apply / 1e-9 V-cycle) on CUDA/HIP — FMA contraction reorders per-face sums.
- rejected: requiring bit-exact apply/V-cycle results on CUDA/HIP (impossible given FMA contraction)
- why: "FMA contraction reorders per-face sums" on CUDA/HIP, so only OpenMP/Serial can be bit-exact for apply/V-cycle; assembly itself must still be bit-exact everywhere
GPU-aware MPI auto-detection uses query + checksum loopback probe, never blind probing¶
- area: core
- source: suite-mpi-gpu-campaign.md:16
- decided: undated
- status: settled
- quote: | core 589969a: ... GPU-aware MPI auto-default = MPIX_Query_cuda_support AND checksum loopback probe (blind probing SEGFAULTS on non-cuda UCX — probe is validation only, not discovery); env PECLET_CORE_GPU_AWARE_MPI still forces;
- rejected: blind device-pointer probing to auto-detect CUDA-aware MPI
- why: blind probing segfaults on non-CUDA UCX builds
Gate the CUDA-aware MPI device path on an explicit env var, not the MPI query API¶
- area: core
- source: suite-distributed-status.md:353
- decided: undated
- status: settled
- quote: |
Device-pointer MPI works; gate the device path on env
TPX_CUDA_AWARE_MPI=1(NOTMPIX_Query_cuda_support(), which mis-reports 0). - rejected: relying on MPIX_Query_cuda_support() to detect CUDA-aware MPI
- why: MPIX_Query_cuda_support() mis-reports 0 in this environment
Halo topology vs exchange split: topology stays host MPI plumbing, exchange is the device kernel¶
- area: core
- source: device-naming-retirement.md:32-40
- decided: 2026-06-27
- status: settled
- quote: |
## Halos (Phase 2-3, transport-core + sdflow + dem)
The exchange and the topology are split: topology = host MPI plumbing (neighbour discovery, not a
kernel) — keep; exchange = the device kernel. So:
DeviceGridExchangeKokkos→GridHalo(canonical device exchange); hostGridHalo(topology + oracle host-exchange) →GridHaloTopology. Device built fromGridHaloTopology::flatten().DeviceParticleHaloKokkos→ParticleHalo; hostParticleHalo→ParticleHaloTopology.ParticleMigratorUNCHANGED (migration ≠ ghost halo; distinct op).
- rejected: none stated
- why: topology (neighbour discovery) is not itself a kernel and stays host-side plumbing; only the exchange is device work
Kokkos CUDA installs MUST set Kokkos_ENABLE_CUDA_CONSTEXPR=ON¶
- area: core
- source: kokkos-cuda-constexpr-required.md:10-22
- decided: 2026-07-23
- status: settled
-
quote: | Failure mode (hit 2026-07-23, cost hours):
extern/install/nvidia-cudawas built withoutKokkos_ENABLE_CUDA_CONSTEXPR⇒ no--expt-relaxed-constexpron the Kokkos interface ⇒ nvcc compiles a constexpr host function called from__host__ __device__code (morton'sstd::array::operator[]insideMorton::encode/decode) with only warning #20013/#20015 — and the device call silently returns 0. Symptom in core: every device-assembled AMR operator came out empty (nFaces dev=0), the AMR flow projection was a no-op (div ≡ 0, pres iters 0), Z&H flows free-accelerated. The device-assembly phases (D1–D6) had only ever been validated on OpenMP, where device==host hides it.Why: fixed in umbrella
tools/bootstrap_deps.sh(nvidia-cuda branch now passes-DKokkos_ENABLE_CUDA_CONSTEXPR=ON, commit 0375abc). Rebuild the install (tools/bootstrap_deps.sh nvidia-cuda) after pulling; with it core's device AMR suite passes 113/113 on CUDA (assembled CSRs bit-exact host==device; only the apply differs by FMA ~1e-16). - rejected: building the nvidia-cuda Kokkos install without Kokkos_ENABLE_CUDA_CONSTEXPR - why: without it, nvcc silently returns 0 from constexpr-host-called-from-device code (morton's std::array indexing), silently emptying device-assembled AMR operators
NBX consecutive rounds must use distinct message tags¶
- area: core
- source: amr-octree-status.md:786-791
- decided: undated
- status: settled
- quote: | NBX BUG FIXED: faceNeighborGather's 2 rounds shared tag 0 → a rank draining the request round Iprobe-received another rank's reply (same tag), 8-byte-misaligned parse → garbage reqId → segfault at np=4 PERIODIC (periodic=false didn't trigger it, so latent in the Phase-4 gather). Fix: distinct tags (req=11, reply=12). balance() OK (Allreduce separates its rounds). NBX cross-round aliasing lesson: Ibarrier guarantees all ENTER, NOT that a rank finished draining before others send the next round — consecutive unsynced NBX rounds MUST use distinct tags.
- rejected: sharing tag 0 across NBX request/reply rounds
- why: "Ibarrier guarantees all ENTER, NOT that a rank finished draining before others send the next round"
NBX inter-round tag race fixed by rotating the tag per round; buildTopology now verifies promised==requested cells¶
- area: core
- source: nbx-round-tag-race.md:17-19
- decided: 2026-09-02
- status: settled
- quote: |
Fixed in core
10294e6: tag = baseTag + round % 64, counter as an MPI communicator attribute; buildTopology allreduce-checks promised == requested cells and throws; gatecore/tests/test_nbx_rounds.cpp(ablated fix fails on a laptop, np=8: 62 wrong-round messages). - rejected: running consecutive NBX consensus rounds on one communicator with a single fixed tag
- why: "a rank that observed round k's Ibarrier complete posts round k+1's Issends while a neighbour still probing round k's tag swallows them"
NBX tag interaction: two standing rules for wire tags¶
- area: core
- source: amr-march-distributed-campaign.md:66-77
- decided: 2026-09-05
- status: settled
- quote: | Two standing rules came out of it, now in core/CLAUDE.md: direct point-to-point tags stay below 24576, and distinct NBX call sites need baseTags distinct modulo 128.
- rejected: none stated
- why: the per-round tag rotation (baseTag + round) walked over the AMR gather tags, breaking np>=4 AMR; fixed with a reserved wire-tag range [24576, 32768) and modulo-128 baseTag separation
No Rhie-Chow update at cell centres — RC is purely the face term, automatic once uf is separate from interp(u)¶
- area: core
- source: device-naming-retirement.md:106-108
- decided: 2026-06-28
- status: superseded
- quote: | Rhie–Chow note (Frank corrected me): there is NO Rhie–Chow update of the CELL centres — cells get the plain cell pressure gradient (= gradOf = ½(g⁻+g⁺), already there); RC is purely the FACE term (compact face grad vs averaged cell grad) and is automatic once uf is kept separate from interp(u).
- rejected: the earlier framing that treated Rhie-Chow as something applied at cell centres
- why: correction from Frank; RC is entirely a face-term effect
ParticleHalo gather() throws on capacity overflow instead of silently truncating¶
- area: core
- source: cuda-kokkos-migration.md:627-629
- decided: undated (packing MPI session)
- status: settled
- quote: | FIX: gather() now THROWS on no+ng>capacity (no silent truncate); size Simulation capacity for the worst-case band. LESSON: a "numerical instability" that only appears past a size threshold and recovers/varies -> suspect buffer overflow, compare against the single-GPU ghost count + positions with ample capacity BEFORE theorizing about the solver.
- rejected: silent truncation of ghost count on overflow
- why: silent truncation caused out-of-bounds SoA writes and corruption that looked like a numerical instability
ParticleHalo periodic self-ghosts default OFF for byte-identical compatibility¶
- area: core
- source: cuda-kokkos-migration.md:618-621
- decided: undated (packing MPI session)
- status: settled
- quote: | Fixed: ParticleHalo::build(pos,rcut,includePeriodicSelf=false) + ParticleMigrator::imagesWithinRcutOfBlock (enumerate up-to-3^Dim periodic images, keep those within rcut of a block; allowIdentity=false for self). Self-ghosts are LOCAL (no MPI self-messages), appended after received ghosts ([numReceived,numGhost)); forward/forwardPositions/ reverse + DeviceParticleHaloKokkos handle the tail (flatten() exposes selfIdx+numReceived). Default OFF => byte-identical (legacy CUDA device path / python / existing tests untouched, transport-core 12/12); packing KokkosParticleHalo opts IN.
- rejected: making periodic self-ghosts on by default
- why: "byte-identical (legacy CUDA device path / python / existing tests untouched...)"
Periodic axis needs >=2 ranks¶
- area: core
- source: suite-distributed-status.md:27-28
- decided: undated
- status: settled
- quote: |
Periodic-wrap validated (
mpi/validate_periodic.py): a periodic axis needs ≥2 ranks (no self-ghosts); deterministic 2-body + corner wrap exact; serial reference must use TRUE [0,L] domain. - rejected: none stated
- why: 1 rank on a periodic axis creates self-ghosts
Port transport-core's device layer first (foundation-first sequencing)¶
- area: core
- source: cuda-kokkos-migration.md:79-82
- decided: 2026-06-18
- status: settled
- quote: |
Confirmed decisions (2026-06-18):
- Foundation first: port
transport-core's device layer (DeviceGridExchangeingrid_halo_cuda.cuh) to a sharedtpx::View<T>/Kokkos::Viewfield + Kokkos halo exchange (keep host-staged fallback, add GPU-aware MPI). Then consumers. No double-porting.
- Foundation first: port
- rejected: porting each consumer's halo independently (double-porting)
- why: "No double-porting."
Rebalance is pure migration (same global mesh, new owners) and must never use transferField¶
- area: core
- source: dynamic-load-balancing.md:62
- decided: undated
- status: settled
- quote: |
- Migration ≠ remap: rebalance MOVES cells to new owners (same global mesh), it does NOT change refinement — so it's exactly conservative and the global field is bit-identical (no interpolation). Don't use transferField for it.
- rejected: using transferField (same-domain old→new octree conservative remap) to implement rebalance migration
- why: rebalance is exact/conservative (no interpolation), whereas transferField is a different operation (remap across changed refinement)
Treat nvcc warnings #20013/#20015 as errors, not style noise¶
- area: core
- source: kokkos-cuda-constexpr-required.md:26-27
- decided: 2026-07-23
- status: settled
- quote: |
- Treat nvcc warnings #20013/#20015 as ERRORS — they mean silent garbage on device, not style.
- rejected: treating #20013/#20015 as benign style warnings
- why: "they mean silent garbage on device"
Weighted ORB dynamic load balancing is one shared primitive that lives in the core layer¶
- area: core
- source: dynamic-load-balancing.md:18
- decided: undated
- status: settled
- quote: | The fix is ONE shared primitive ⇒ it lives in transport-core (the shared layer).
- rejected: implementing separate load-balancing logic per consumer (AMR, dem)
- why: both AMR and dem need the same weighted-ORB rebalance mechanism
Weighted ORB split boundary must remain on integer cell boundaries¶
- area: core
- source: dynamic-load-balancing.md:65
- decided: undated
- status: settled
- quote: |
- Weighted ORB split must stay on integer cell boundaries (blocks are cell-aligned boxes); pick the boundary whose cumulative weight is closest to the target fraction.
- rejected: none stated
- why: blocks are cell-aligned boxes
block_decomposer retired/archived, replaced by transport-core (core)¶
- area: core
- source: cuda-kokkos-migration.md:57-58
- decided: 2026-06-20
- status: settled
- quote: |
block_decomposerARCHIVED on GitHub + removed as a suite submodule. - rejected: none stated (superseded, not a chosen alternative)
- why: none stated in this note (see suite CLAUDE.md: reusable parts extracted into core/)
coarsenAlignment bug: natural-max alignment over-constrains ORB; cap alignment at 2^4¶
- area: core
- source: parallel-scaling-study.md:60-64
- decided: undated
- status: settled
- quote: | coarsenAlignment BUG FOUND+FIXED (flow 10872a7): natural-max alignment (64 for 192^3, 1024 for 3072-axis) over-constrains ORB → snap turns 96|96 into 128|64 (2:1 cascade, 12:1 worst) AND sub-boxes <2*align skip alignment → MG depth collapse. Measured: np24 192^3 27.2→8.0 iters, 10273→2091 ms (4.9x); weak np24 114→7.0 iters (6.4x). Fix = cap align at 2^4 (all 5 levels need). 33/33 ctests; np=2^k paths land on same splits (why channel 1-8 GPU never saw it).
- rejected: natural-max alignment for ORB decomposition snapping
- why: it over-constrains the decomposition and collapses multigrid depth on non-power-of-two rank counts
peclet-core sdist must vendor its own SuiteNanobind copy, not depend on the umbrella cmake/¶
- area: core
- source: release-workflow-prep.md:93
- decided: 2026-09-05
- status: settled
- quote: |
(3) the
peclet-core sdist was never self-contained (SuiteNanobind from the
umbrella cmake/) →
peclet[mpi]broken 0.1.0–0.6.0; vendored copy in core/cmake/, core 0.6.1 + peclet 0.7.1; - rejected: the core sdist referencing the umbrella's cmake/ directory for SuiteNanobind
- why: "peclet[mpi] broken 0.1.0–0.6.0"
toVector must repack a strided device subview to a contiguous buffer before cross-space deep_copy¶
- area: core
- source: dem-cubes-gpu-pyvista.md:30-34
- decided: undated
- status: settled
- quote: |
CUDA getter fix (core commit 1044a95): [[dem-global-scale-sphere-limitation]] sibling —
toVector(core/common/view.hpp) aborted for a STRIDED device subview (pos[0:numReal] of a grown LayoutLeft SoA): cross-space deep_copy of a strided layout has no copy mechanism. Fix: repack to a contiguous same-space buffer first. This is why GPU get_positions/get_quaternions failed mid-sim before. CPU (LayoutRight) never hit it. - rejected: cross-space deep_copy directly on a strided device subview
- why: "cross-space deep_copy of a strided layout has no copy mechanism"
The host backend is sized by the CPU BUDGET (cgroup quota ∧ affinity), not by the visible CPU count¶
- area: core
- source: user decision in session 2026-09-13; verified end-to-end 2026-09-16 (docs/SCALING_ISSUES.md issue 7)
- decided: 2026-09-13
- status: settled
- quote: |
Kokkos sizes its host backend from the CPUs it can SEE. A container — Colab, Binder, Docker, a
Slurm cgroup — normally shows the whole host while granting a fraction of it through a cgroup
CPU quota, and a quota is invisible to OpenMP:
omp_get_max_threads()returns the host count, the pool spin-waits at every barrier, and the run collapses. Measured on the 1.0.0 CPU wheel, a quick start that takes 25.7 s on two CPUs did NOT finish in fifteen minutes with 48 CPUs visible and two CPUs of quota — and took 26.0 s with the pool bounded. The trap is silent and it is the first thing a Colab user hits. - rejected: (a) documentation alone — the README/Colab bootstrap warning shipped 2026-09-12 and
was the state of the art for a day; rejected because the failure is silent and ≥35x, so a warning
protects only the user who already knows. (b) reading
/sys/fs/cgroup/cpu.maxdirectly, the obvious one-liner and what the notebook first shipped: that path is the cgroup ROOT and on a systemd host readsmaxwhile the real limit sits on the process's own scope below — it appears to work in a container solely because a container has its own cgroup namespace. (c) taking the affinity mask as the budget: an affinity mask is not the trap, because OpenMP honours a mask and sizes itself correctly; the mask is only the ceiling. (d) always callingset_num_threads: that would override the user's ownOMP_NUM_THREADSand would change the thread count, schedule and reduction order on an ordinary workstation. -
why: |
core/include/peclet/core/common/cpu_budget.hppresolves the process's own cgroup from/proc/self/cgroupand walks UP, taking the TIGHTEST quota on the chain (v2cpu.max, v1's cfs pair), capped by the affinity mask.defaultHostThreads()returns 0 — "say nothing" — wheneverKOKKOS_NUM_THREADS/OMP_NUM_THREADSis set on a backend that reads it, or the budget is the whole machine, which makes the fix INERT on a workstation: same thread count, same schedule, same numbers. It speaks up in exactly two cases: a quota narrower than the visible machine, and anOMP_NUM_THREADSthat nothing else is going to read (Kokkos::Threads— the Windows/macOS wheel backend — reads onlyKOKKOS_NUM_THREADSand defaults to ONE thread without hwloc, a 7x cut measured 2026-09-13).Applied at the ONE shared Kokkos init,
peclet::core::python::install()(core/include/peclet/core/python/kokkos_teardown.hpp), so flow, dem, voro, pnm, coupling and amr all inherit it from a single place rather than each repeating the policy.Consequence for releases: core is header-only, so the fix reaches a user only when the consumer is rebuilt against a core that carries it. It landed in core
640c511…967b221, taggedv1.0.1; every consumer'scmake/PecletDeps.cmakewas repinnedPECLET_CORE_TAG v1.0.0 → v1.0.1in its own 1.0.1 release commit, and the 1.0.1 wheels on PyPI were verified to carry it (2026-09-16:peclet-flow1.0.1 downloaded from PyPI builds a 2-thread pool under a 2-CPU cgroup quota where the pre-fix wheel builds 48). A future core-side fix of this kind needs the same repin, or it ships and does nothing — that is the trap this paragraph exists to prevent, and it is not visible from inside core.
The peclet-core DISTRIBUTION splits into peclet-geom and peclet-halo; the C++ identity does not move¶
- area: core
- source: docs/CORE_BOUNDARY.md §0-1
- decided: 2026-09-21
- status: settled
- quote: |
The distribution splits by INSTALL-TIME REQUIREMENT:
peclet-geom(peclet.geom, wheels) andpeclet-halo(peclet.halo, sdist). The C++ identitypeclet::core/peclet/core/...does NOT move. - rejected: a top-level
peclet::geom/peclet::haloC++ namespace and header path, and splitting the header repo - why: build-time-only identity, six consumers pin it by tag, no user-visible gain
The halo package is named peclet.halo, not peclet.mpi¶
- area: core
- source: docs/CORE_BOUNDARY.md §6
- decided: 2026-09-21
- status: settled
- quote: |
mpinames a dependency rather than the thing, it would be the only package in the family named for what it links against, andhalois already its C++ name (peclet::halo,GridHalo,ParticleHalo). Maintainer's decision. - rejected:
peclet.mpi - why: "mpi names a dependency rather than the thing"
A core header is on the MPI side iff it includes common/mpi.hpp, held by a CI manifest gate¶
- area: core
- source: docs/CORE_BOUNDARY.md §2.1
- decided: 2026-09-21
- status: settled (the gate was claimed but absent until core 2e44f6b, 2026-10-03:
tools/check_mpi_manifest.sh,quality.ymljobmpi-manifest; the manifest had drifted from 9 to 13 headers, all legitimately MPI-side) - quote: |
The MPI boundary is explicit and gated, because it had already drifted:
decomp/grid_redistribute.hpp:23pulls the shim andhalo/nbx.hppwhile sitting in an otherwise MPI-free directory. - rejected: leaving the MPI boundary implicit
- why: "it had already drifted"
No silent if(MPI_FOUND) guard on the core Python bindings; the stopgap is a loud opt-in¶
- area: core
- source: docs/CORE_BOUNDARY.md §4
- decided: 2026-09-21
- status: settled
- quote: |
A silent
if(MPI_FOUND)guard is rejected; the stopgap, if used, is a loud opt-inPECLET_CORE_PYTHON_MPIthat fails with both remedies named. - rejected: the silent guard
- why: "it turns a loud build failure into a silent partial install, where
pip install peclet[mpi]succeeds with nompimodule in it"
peclet-core becomes a pure-Python compatibility shell, frozen at its last 1.x with a <2 ceiling¶
- area: core
- source: docs/CORE_BOUNDARY.md §6
- decided: 2026-09-21
- status: settled
- quote: |
peclet-corebecomes a pure-Python compatibility shell for the ladder and is frozen at its last 1.x with a<2ceiling at 2.0.0. - rejected: a code-free 2.0.0 depending on the new packages
- why: "a loud resolver error beats a silent import failure"
Anderson acceleration (AndersonCore) lives in core peclet::core::solver from the start¶
- area: core
- source: flow
f6b89fedoc/steady_acceleration.md §1 D13 - decided: 2026-10-02
- status: settled (USER approved 2026-10-02; design — implementation pending)
- quote: | The grid-agnostic AndersonCore (state views + roles + mask + metric weight + gauge flag + comm) is core/include/peclet/core/solver/anderson.hpp; flow keeps only the adapter, marchState() and the Python driver. core is tagged and published before flow ships it.
- rejected: flow-local first, promoted when amr adopts it
- why: a later move is a second port with its own bit-identity proof; the core is grid-agnostic and amr's lmax = 0 march has the same (u, P) structure
AndersonState is views + roles (Velocity / Carried) + extent + ghost + comm; no pressure metric, no innerTolerance, no Gram blocks GG/GR (rev 1 WO-3b, rev 2 WO-3c)¶
- area: core
- source: flow doc/steady_acceleration.md "Revision 1" §9 WO-3b, Q14; "Revision 2" §6.1, §9 WO-3c; core 9ff3bd2, f9956ed
- decided: 2026-10-02
- status: settled (released core v1.4.0)
- supersedes: the descriptor list "(state views + roles + mask + metric weight + gauge flag + comm)" in "Anderson acceleration (
AndersonCore) lives in core…" — that entry's placement decision stands - quote: | AndersonState = padded state views + roles (Velocity = 0, Carried = 2) + extent + ghost + AndersonComm. The Pressure role, sdf, cP, gauged, pass 1 and innerTolerance are deleted; pass 2 reduces RR(s,j) and b_j only (2 mk + 2 doubles); no reduction reads dG.
- rejected: keeping the unused pressure metric or innerTolerance / GG / GR until after the tag (a breaking change then)
- why: no consumer uses them after rev 1 / rev 2; removes one collective per step and the sdf dependency; the branch was untagged, so this was the cheap moment
Cut-cell geometry and probe kernels are container-free core headers¶
- area: core
- source: flow doc/scalar_ibm_design.md D14, §7.1, §12 entry 12; core commits a031c6f, b1fcb6a (branch scalar-ibm)
- decided: 2026-10-02
- status: settled (design, core branch scalar-ibm; in force when it lands on main)
- quote: |
Container-free kernels in core (
scheme/cut_cell_geometry.hpp,scheme/probe_flux.hpp); driver, storage, operators, ScalarMG and Krylov in flow Lattice offsets are returned, not DOF indices. flow converts them to linear block offsets; amr maps them to leaf DOFs. - rejected: flow-private copies (amr and VoF would fork them); a core Krylov (core's MomentumSolver binds the operator to a CSR type and the vectors to a [0, n)+ghost-tail layout, and flow's blocks interleave ghosts)
- why: amr (per-leaf SDF samples) and the VoF species hook (plane source) reuse the same κ / aperture / facet / probe kernels, so the geometry stays consistent by construction across codes