Suite quality plan — making 1.0.0 the foundation¶
Status: active work list, opened 2026-09-08 after a read-only audit of all seven repos (six audit passes: flow, dem, voro, core, pnm+coupling+morton, cross-cutting). Every finding below cites a path. Items marked DONE were executed the same day; the rest are ordered work packages with an effort estimate. This file supersedes the "additive, never breaking" rule of NAMING.md §0 for the one release that becomes 1.0.0 (decisions D1, D9).
0. Is the next release (planned as 0.8.0) ready?¶
Mechanically, yes. As a foundation, no. The release workflow is proven (0.7.0–0.7.2 shipped
2026-09-05/06), every test matrix is green on host and CUDA, and tools/release/check_release_state.sh
flags only the expected pre-tag version bumps. What the audit found is not broken code but
accumulated shape: the same concept spelled two ways in every API, instruments shipped beside
physics, campaign notes standing in for documentation, CI that is green because tests skip
themselves, and a "shared core" that is 56 % a second flow solver. Releasing from this
state would bake all of that into the first version with a physical-units API — the version
every later script will be written against.
The recommendation is therefore: the next release is the clean-break release, and because it breaks every API it is 1.0.0 — family and every package (decision D9). Do work packages A–D (one spelling per concept, dead code and artefacts out, versions single-sourced, CI honest) before tagging; ship E–H (the structural refactors) across 1.x under the alias ladder.
1. Decisions¶
- D1 — Clean break at 1.0.0. There are no external users. Every non-canonical spelling is removed, not aliased; keyword arguments are renamed outright. The CHANGELOG lists every removal with its replacement. NAMING.md §0's alias ladder is suspended for this one release and becomes binding from 1.0.0 on, and NAMING.md says so.
- D2 — Two tiers of Python API. Public: what a user of the method needs (flow single-phase
≈ 35 methods, VoF ≈ 65 of today's 266). Diagnostics: instruments, ablations, timers, census
calls, test hooks — bound under a
diagnosticssub-object (solver.diagnostics.wall_area_probe()) or an underscore prefix, excluded from the API pages. Retired modes are deleted, not kept "unreachable". - D3 — No environment variable changes numerics. An env var may switch logging or
profiling. Anything that changes a result (flow: 8 of 30
getenvsites; dem: 16 constructor and step knobs; core:PECLET_CORE_GPS_*) becomes an explicit setter or is deleted. Function-static first-call caches go with them. - D4 — One version source per repo.
pyproject.tomlis the source; CMakeproject(VERSION),packaging/pyproject-cuda.toml,CITATION.cff, Doxyfile and the consumers'PECLET_*_TAGderive from or are checked against it by the pre-flight script, which becomes step 1 of everyrelease.yml. - D5 — CI tests what it claims. A test that cannot run in a configuration is skipped by
ctest (
SKIP_RETURN_CODE), never silently green. The MPI suites (flow 103, dem 24, voro 18, pnm 6 ctests) run at np=1,2,4 somewhere in CI. coupling gets a CI. - D6 — core is infrastructure; AMR is preserved, as its own package. AMR is under active
development and nothing of it is deleted. The whole
core/include/peclet/core/amr/tree — the Navier–Stokes solver AND the octree infrastructure under it — is relocated with its history into a new eighth package,peclet-amr(peclet.amr), which depends onpeclet-coreand on nothing else in the suite. The released core is then decomposition, halo, geometry and load balancing: the layer the method codes actually use. (Maintainer, 2026-09-08: "AMR is being developed. So, do not throw it away. You might move things to a development branch."; decision refined 2026-09-10 after the dependency audit in G.2.) - D7 — Docs describe the code that exists. Campaign notes move to
archive/; INTERFACES.md's six unrealised concepts are either implemented asconcepts.hppwithstatic_asserts or the file is rewritten as the duck-typed contract; STYLE.md's namespace and CI sections are corrected. - D9 — 1.0.0, then semver. An API-breaking release is a major release: the clean break
ships as peclet 1.0.0 with every package at 1.0.0 (RELEASE.md's bump table is rewritten
accordingly at tag time). After 1.0.0 a breaking change needs a major bump, which is what makes
the alias ladder worth its cost. One exception, decided 2026-09-11 (maintainer: "I agree"):
peclet-amrstays at 0.x — it is functional and tested (92 C++ + 3 Python ctests, np 1–8) but a research code under active development with no external validation page, unfinished rungs (sub-face closures, pocket exclusion, M2b, D3b) and an API that will churn; pre-1.0 semver lets it break in a minor. It is published with the family (sdist,peclet[amr]) and labelled "under development"; it graduates to 1.0.0 on its own merits. - D8 — Old identifiers go.
transport_core/tpx_*/PECLET_TPX_TAG,sdflow,vorflow,mortonarith,DEM_MPI: renamed in CMake,.gitmodules, docs and CI in one pass.
2. The rename table (D1, applied at 1.0.0)¶
| module | removed | use instead |
|---|---|---|
| flow | get_spacing(), get_resolution(), global_resolution() |
spacing, cells, global_cells (get_ox/oy/oz are the openness fields, not origin accessors — they stay, with real docstrings) |
| flow | cell_centres(), scene_instance_count(), vof_block_colour() |
cell_centers(), num_scene_instances(), vof_block_color() |
| flow | set_velocity_streams() (a no-op) |
— |
| flow | set_solid(…, pressure_coarse=) (commented-out parameter shown in docs) |
drop from docs |
| dem | initialize() (different defaults from initialize_shape) |
initialize_shape(...) |
| dem | enable_periodicity(x,y,z), pbc_enabled |
set_periodic(x,y,z), periodic |
| dem | get_domain_min()/get_domain_max(), positional set_domain(lx,ly,lz,px,py,pz) |
origin, origin + extent, set_domain(extent=, origin=, periodic=) |
| dem | get_num_contacts()/get_num_manifolds()/get_max_overlap() |
num_contacts, num_manifolds, max_overlap |
| voro | set_box(L) on Tessellation/Simulation; step(n, dt)'s dt |
set_domain(extent=); set_dt(dt) + step(n) |
| voro | sphere_centres= kwarg |
sphere_centers= |
| pnm | extract_topology_gpu(shape=) |
extract_topology(shape_zyx=) |
| coupling | CfdDem(smooth_width=, h=) |
smooth_length=; the spacing comes from the flow solver |
| coupling | ResolvedCfdDem(rho_f=, periodic=bool, move=) |
rho=, periodic=(bx,by,bz), move_particles= (same words as CfdDem) |
| core | Octree(brick=), h0 |
Octree(cells=), spacing |
| core | Migrator(origin, size, gsize), class names Migrator/Halo |
ParticleMigrator(origin, extent, cells), ParticleHalo |
Everything in NAMING.md §2 marked aliased is in this table; the table in NAMING.md is updated to removed 1.0.0 as each lands.
3. Work packages¶
Effort: S < 1 day · M 1–3 days · L > 3 days. "Breaking" = Python API changes.
A. One spelling per concept (S–M per repo, breaking) — the 1.0.0 gate¶
DONE 2026-09-08 in all seven repos (A.1–A.4; NAMING.md §2 = the record); the gallery sources are rewritten (peclet-examples 948173d, 2f75f39, 282534b — not pushed, not re-rendered: that needs the 1.0.0 wheels).
- Apply §2 in
flow/src/flow_bindings.cpp,dem/src/dem_bindings.cpp,voro/src/voro_bindings.cpp,pnm/src/pnm_bindings.cpp,coupling/python/peclet_coupling/{driver,resolved}.py,core/python/{amr,mpi}_bindings.cpp; update every in-suite caller (scripts, tests, README, CLAUDE.md) and thepeclet-examplesgallery pages; rebuild host modules and run each repo's test battery. - coupling: one
eps_mindefault (driver 0.25 / binding 0.2 / README 0.4 today), drop_couplingfrom__all__, drop duplicate drag-name aliases (bvk/beetstra,bvk2/tang). - voro: unify getters (
Tess.volumes()vsSim.get_volumes()vsFlow.get_cell_volume(),kinetic_energy()vsget_kinetic_energy(),neighbor_counts()vsget_num_neighbors()) per NAMING §1.2; docstrings on the 11 undocumentedFlowSolvermethods. - core:
geomadded to__all__; regenerate.pyistubs with nanobind's stubgen for all three modules (the trackedcore_amr.pyimisses ≥ 9 members;mpi/geomhave none); flow's trackedsdflow.pyi(pybind11-era, 37 of 266 members) is deleted and replaced the same way.
B. Dead code and artefacts out (S per repo, not breaking)¶
DONE 2026-09-08 for every row of the table below except the umbrella "move out" items (the
maintainer's own files, listed not moved) and the worktree prune; the dem script sort and flow's
tests/study move are still open (the dem agent's move-where list is in commit b9a9b17's report).
| repo | delete / untrack | why |
|---|---|---|
| flow | doc/1-s2.0-S0010465523004113-main.pdf (5.5 MB) |
third-party Elsevier paper in a public repo |
| flow | output/*.png, notebooks/output/*.png (14 files), build_project.sh (bare cmake .., cannot work), codebase_to_text.py, sdflow.pyi, cfd_utils/ (not installed, not in the wheel) |
artefacts and scratch, some already in .gitignore |
| flow | tests/cuda_bench/bench_rbgs.cu + its LANGUAGES CXX CUDA CMake |
raw CUDA after "CUDA retired"; CUDA_ARCHITECTURES 120 hard-coded |
| dem | verify_output.txt (2.5 MB, 44 % of tracked bytes), stacking_test.vtp, test.vtp, .vscode/, activate_env.sh, requirements.txt |
artefacts; the venv is the suite's |
| dem | mpi/ C++ harness (includes core/include/tpx/..., gone), mpi/test_device_halo.cu, tests/test_periodicity_corner.cpp (includes a non-existent simulation.h), 0-byte tests/inspect_vti.py, tests/generate_dense_packing.py, byte-identical tests/test_hollow_cylinder_overlap{,_2}.py, python/{simulation_script,visualizer}.py ("placeholder") |
provably dead |
| dem | root debug_*.py, diag_*.py, phase{0,1,2}_*.py, verify_packing_hollow_cylinders_test.py, tests/debug_*.py, tests/convergence_test.py, tests/study_sphere_packing.py, tests/verify_sdf_{sphere,cylinder,cylinder_highres}.py, tests/verify_vti_periodic.py, tests/verify_cylinders.py, tests/verify_flexible.py |
one-off investigations (Dec-2025 … Jun-2026) |
| voro | include/peclet/voro/voronoi.zip (the retired half-edge engine, installed by install(DIRECTORY include/)), docs/__pycache__/*.pyc, docs/create_document.py, docs/Doxyfile (never read), physics/interface.hpp (zero callers), extern_bench/ |
retired / orphaned |
| morton | legacy/ (no target, not included; needs C++20 in a C++17 library), third_party/libmorton_morton.h (byte-identical duplicate) |
history lives in git |
| core | python/build_td_*.log; decide docs/data/*.log (31 campaign logs, cited by the AMR notes → move with them to docs/archive/data/) |
|
| coupling | build_td_*.log ×4; add *.log to .gitignore |
|
| umbrella | full.log; .gitignore the flow-*/tel/ worktrees and *.log; prune the 13 merged flow worktrees (~12 GB) once their sessions close. DONE 2026-09-22: 69503dc29254db50bfdf4f41/ (an Overleaf clone of the Robust Scaled IBM Solver paper, the hash being its project id) → ~/Codes/paper-robust-scaled-ibm-solver, clean and in sync with its remote, which still carries the id; and .sdf-campaign-probes/ → flow/tests/study/sdf_campaign/ and now TRACKED (flow 8789e2b) — nine documents cited those gates and their measured numbers while a fresh clone had none of them; the six captured .bin/.npy/.npz artifacts stay gitignored. DONE 2026-09-08: sphere-cfd-validation/ (its own git repo) and proposal/ moved to ~/Codes/; voro's PR-required branch protection removed so all eight repos have the same (none); morton/octree/ removed, its 151-line std::map octree kept as core's test-local oracle (core/tests/oracle/) |
Also in B: dem's remaining root scripts are sorted, not deleted — assert-bearing ones
(test_hertz.py, test_hertz_shapes.py, test_pair_materials.py, test_cone_friction.py,
test_colored_gs.py, tests/verify_{sdf_particle,rotating_drum,restitution}.py,
mpi/validate_*.py, mpi/verify_*.py) become tests/python/test_*.py under pytest; demos
(verify_packing_*.py, verify_collision_*.py, verify_stacking*.py, pack*.py,
generate_particles.py) go to examples/. flow's tests/study/ (76 files, 1.4 MB JSON, two
absolute /home/frankp/... paths) moves to studies/ or peclet-examples.
C. One version source, old identifiers gone (S, CMake-breaking only)¶
DONE 2026-09-08: C.1 (CMake reads pyproject in all seven; the pre-flight flags a drifting literal and
morton's vcpkg manifest), C.2 (peclet_core/peclet::core, PECLET_CORE_TAG/PECLET_CORE_DIR/
PECLET_CORE_INCLUDE, peclet_flow, vorflow/mortonarith gone; .gitmodules names still transport-core/
sdflow/vorflow — cosmetic, open), C.4, C.5. Open: C.3 (one shared cmake module; today the
pnm/dem/voro/coupling PecletDeps.cmake are byte-identical and flow's differs only by its sibling-override block).
- CMake reads the version from
pyproject.toml(file(STRINGS … REGEX "^version")) in every repo; fixes morton (VERSION 0.1.0vs pyproject 0.2.1; conan/vcpkg 0.1.0 too) and voro (VERSION 1.0.0).check_release_state.shgains--ciand runs first in everyrelease.yml. project(transport_core)→peclet_core; targetstpx_core/tpx::core/tpx_halo→peclet_core/peclet::core/peclet::halo;PECLET_TPX_TAG→PECLET_CORE_TAG; flow targetsdflow→peclet_flow;.gitmodulesnamestransport-core/sdflow/vorflow→core/flow/voro; voro'sadd_vorflow_kokkos_test,VORFLOW_*,project(vorflow_mpi_tests); morton'smorton_arithmetic/libmortonarith_c/mortonarith; dem's-DDEM_MPIin README and CI.cmake/PecletDeps.cmake(five hand-synced copies, already diverged: flow's has thePECLET_SIBLING_*override) andSuiteNanobind.cmake(two copies) come from one place — apeclet-cmakeFetchContent or the core sdist — with a byte-identity check in the pre-flight.- voro
CMAKE_CXX_STANDARD 17→ 20 (every target already forces 20); corepython/CMakeLists.txtmpi bindings 17 → 20;add_compile_options(-Wno-…)blanket in voro removed. - coupling
pyprojectsibling floorspeclet-flow>=0.3.0,peclet-dem>=0.3.2→ the 1.0.0 numbers.
D. CI that is honest (M, not breaking)¶
pnm DONE 2026-09-08 (ec646c8…72cf6f4): 0 → 9 registered ctests (single-rank contract with
hand-derived counts, determinism, Python smoke, the 7199-pore gate that SKIPS without the data file,
MPI np=1,2,4), CI runs them all (39 s + 74 s with the Kokkos cache), blocking clang-format after one
reformat commit.
dem DONE 2026-09-08 (5bb9bfd, edf07dc, 8124d57): three standalone test trees (8 + 2 + 24)
and unregistered Python scripts → one tree: -DPECLET_DEM_BUILD_TESTS=ON registers 11 (kokkos +
arborx + pytest), + -DPECLET_DEM_MPI=ON 47 (+24 kokkos_mpi, +12 Python MPI at np=1,2,4); every test
SKIP_RETURN_CODE 77, MPI ones labelled mpi with PROCESSORS. The 34 root-level scripts sorted:
13 → tests/python/test_*.py, 4 → tests/python/mpi/, 17 → examples/ (all smoke-run); the statics
battery cut 96k→1.7k grains, the drum ω 2→0.5 (ω=2 was centrifuging). CI: single-rank 2 m 47 s
(also configures against the default PECLET_CORE_TAG as the stale-pin check) + mpi 3 m 17 s
(np=1,2,4 oversubscribed) + Quality 9 s; clang-format blocking after one reformat (375 violations,
20 files); .clang-tidy = voro's; artifact actions v7/v8. Findings: tests/arborx had bit-rotted
(fixed); the MPI scripts were dead against the current API (ported); three legacy scripts "passed"
while printing FAILURE (velocity solve is off by default, no default gravity, the (N,4) w column is
the inverse mass — recorded in dem's CLAUDE.md); docs/mpi.md's "np=2/4 differ by float noise" is
false on the stiff random IC (per-particle max 0.11, even np=1 at 2 threads gives 0.08 — asserted as
measured). Open numerics finding (not D): single-rank periodic wrap contacts whose far partner
sits more than one radius beyond the face are resolved one-sidedly (sim.hpp ghostBand = maxRad);
the MPI step is symmetric. Fix under G.4.
core DONE 2026-09-08 (69d6b0f, 3ccf838, f3a2a34, 6bd091d): every morton-guarded binary (49 +
4 studies + bench_amr_flow) exits 77 through one registration helper (cmake/PecletCoreTest.cmake,
labels mpi/np8/bench/python); tests/python registered (7 ctests: test_mpi.py np=1,2,4,8,
test_amr.py serial + np=2, ndarray interop); 104 → 109 plain, 158 → 164 Kokkos, 0 → 7 Python. CI:
gcc/clang × Debug/Release with morton v0.2.1 checked out beside it (nothing skipped), Kokkos-OpenMP
+ MPI np=1,2,4 + the Python modules (15 min), a no-MPI stub job, Quality with blocking clang-format
(one 60-file reformat, token-stream verified identical; amr/ excluded from the check until the AMR
branch lands — 19 of 35 AMR headers still differ). Findings: the Python MPI ctests had been running N
singletons (ParaView's mpiexec cached — the CLAUDE.md trap; now pinned by
cmake/PecletCorePinMpiexec.cmake and the scripts fail when comm.size != PECLET_CORE_TEST_NP); a
rank exiting 77 before MPI_Init aborts prterun (Open MPI 5) → tests/test_skip_mpi.hpp inits
first; the old CI matrix had no gcc job (include: overwrote the axis). PECLET_CORE_BUILD_TESTS
stays default ON (the wheel builds from python/, not the root).
voro DONE 2026-09-08 (1cbf005, a487777): one tree (-DPECLET_VORO_BUILD_TESTS=ON, default OFF)
registers 24 + 18 MPI = 42 (tests/kokkos_mpi folded in, standalone form kept); the 10 bench_*
binaries + the Voro++ fetch opt-in under PECLET_VORO_BUILD_BENCHMARKS (Voro++ pinned to commit
b0dac575 — its only tag, v0.4.6 of 2013, predates its CMake); warnings 622 → 145 (all 51 in
include/ fixed; the 120 left are morton's __int128 -Wpedantic, 25 in tests/); CI runs all
single-rank tests (-LE bench, 2 threads) + a new mpi job np=1,2,4 (3 m 55 s) + a configure
against the pinned sibling tags (PECLET_VENDOR_SIBLINGS=ON) + quality.yml with blocking
clang-format after one 20-file reformat. Silent-green trap closed: find_package(MPI) took
ParaView's MPICH mpiexec while linking OpenMPI, so every np=N test ran N singletons and "agreed"
with single-rank trivially (same trap as core's Python tree); the launcher now comes from beside
MPI_CXX_COMPILER and the binaries fail when the communicator size ≠ PECLET_VORO_EXPECT_NP.
Honestly red, then fixed the same day (9ce81c8, ce698c1): test_sdf_curved (never in the old
7-test regex) failed in CI with cavity sagitta 2.311e-4 vs the 2e-4 gate, and the wall-facet count
varied run to run at fixed threads. Root cause = two silent caps in the cold build whose victims are
chosen by the OpenMP order of the atomic scatter slot: ConvexCell::clip counted every committed
plane (including ones later made redundant) against MAXP and published a zero-volume cell at the
cap (one 29-face cavity cell = the whole missing volume), and the facet over-buffer sized at the
Poisson–Voronoi mean silently published facetCount = 0 for the 400–600 cells that finished last.
Fix: compact the plane set at the cap, overflow only on live planes; exact-demand reserve with one
re-run at the measured size. Sagitta 4.3e-7 with identical facet counts at 1/2/4/8 threads (3 runs
each), 42/42, gate unchanged. test_sdf_dynamic's tol1e-4 flake is probably the same class.
flow DONE 2026-09-08 (5b2b73b, 92c99d5, ee7ac7f): option(PECLET_FLOW_BUILD_TESTS OFF) folds
tests/kokkos (44) and, with PECLET_FLOW_MPI, tests/kokkos_mpi (106 at np=1,2,4[,8]) into the
module's own tree — ~110 build_* directories collapse to one per backend, both test directories
still configure standalone; the regression suite and three verify scripts are ctests with
SKIP_RETURN_CODE 77; vof_timing + bench_rbgs labelled bench. ctest -N = 155. CI: build-test
at OMP_NUM_THREADS=2 with -j1 --timeout 1200 -LE bench (the unbounded pool is what starved the
4-core runner in run 34173816982), then the Python gates, then a configure-only pass at the
default PECLET_CORE_TAG; a new mpi job runs np=1,2 always and np=4/8 oversubscribed when np=2
stayed under 20 min; nanobind pinned; clang-format blocking after one reformat (92c99d5), the six
files the vof-w4 worktree edits excluded with the reason in the workflow. Release pre-flight
item (D4): the tag pass is configure-only because PECLET_CORE_TAG v0.6.1 predates VofMetric/
vofPhysNormal — re-pin the tag at release and make it a compile.
flow in progress.
- core: 50 of 80 test binaries
return 0with "skipping" when morton is absent, and CI never provides morton → every AMR/octree test is green-by-no-op. Add morton (tag) + a Kokkos-OpenMP job; convert the skips toSKIP_RETURN_CODE; add anENABLE_MPI=OFFjob; wirepython/test_amr.py,test_mpi.py,tests/python/test_ndarray_interop.pyinto ctest. - voro: run all 23 tests, not the 7 named in
ci.yml:74; pin the Voro++ tag (mastertoday);tools/check_include_graph.shchecks files that no longer exist andcontinues — rewrite for the real header set sotest_include_graphcan fail. - flow/dem/voro/pnm: fold
tests/kokkos_mpiinto the main CMake under<PKG>_BUILD_TESTS(today: separate projects withTPX_DIRhard-wired to../../../core, three trees per backend, hence flow's ~110build_*directories); run np=1,2,4 on the host runner. flow's regression suite + three small verify scripts in CI;test_vof_timinglabelledbench. - coupling:
ci.ymlbuilding flow + dem + coupling on the OpenMP prefix, running the four single-rank tests as pytest functions (they are__main__scripts today, not collectable). - pnm: a single-rank test target (synthetic SDF from
test_pnm_mpi.cpp:28) registered by the root CMake; MPI np=1,2 in CI;quality.yml+.clang-format. - Suite-level: one composite action for the Kokkos bootstrap (six inline copies today) that
calls
tools/bootstrap_deps.sh; pinnanobind==in CI; onecibuildwheeland one artifact-action major per repo (dem mixes v7/v4); build against the core tag as well asmain. - Style: the 62-line
.clang-formatinto core (different file), pnm, coupling; voro's.clang-tidyeverywhere; a repo-wide reformat commit per repo (flow 826 violations in three files, voro 761) and then--Werrorblocking. Aruff.tomlbeyond the four critical codes.
E. Environment variables → API (S–M per repo, breaks campaign scripts only)¶
flow: PECLET_FLOW_{APERTURE_ORDER,APERTURE_FLOOR,OUTFLOW_RHO,OUTFLOW_COEFF,VRES,ADV_WALLVEL,
ADV_FILL_MODE,UBC_EXCHANGE,CA,DECOMP_LEVELS,TELESCOPE,MG_BCGHOST} and the process-global
CutcellMG::setDecompositionLevels static. dem: the 16 PECLET_DEM_* reads in the Simulation
constructor and solve_driver*.hpp (_REST_MODEL, _SLEEP*, _VERLET_SKIN, _NO_GRAPH,
_NO_FUSED, _NO_INCR_COLOR, _REST_NEWTON_OFF, _REST_ONESIDED, _ML_GATES, …). core:
PECLET_CORE_GPS_RHO/MAXN (the only numerics-changing reads in core; both in amr/, so core's E is
folded into G.2 — the AMR relocation — rather than touching amr/ twice). Each becomes a setter with a documented default, or is deleted with
the ablation it served. Keep *_DEBUG, *_VERBOSE, *_PROFILE*, *_TIMEOUT, GPU_AWARE_MPI.
flow DONE 2026-09-08 (ad917b1): 31 reads / 25 distinct variables, not the 12 the plan named
(it missed MG_ASPECT, MG_RESFILL, MG_DIAGRESUM, AGGLOM_EXTENT, DECOMP_MAX_IMBALANCE,
TELESCOPE_MIN_EXTENT, EXACT_RESIDUAL, VOF_WISP_EPS, VMG_AUTO_CELLS/_MIN_GLOBAL,
HOST_SERIAL_CELLS, PRESSURE_STRICT and the unprefixed PECLET_PC_DEPOSIT_FALLBACK); src/ is
down to five reads, all *_DEBUG, and a no_env_knobs ctest fails if another appears.
CutcellMG::setDecompositionLevels and every other numerics-affecting function static are gone —
decomposition() takes levels/maxImbalance, IbmSolver holds them per solver, flow.mpi_block()
takes them as keywords; only a host launch-size cutoff stays process-wide, as a constexpr.
PECLET_FLOW_CA's four states became set_comm_avoiding('both'|'off'|'momentum'|'pressure').
The finding, and the reason D3 exists: the exact-residual flag was process-wide, so an earlier
gate's enableVof() silently changed a LATER single-phase solver's pressure solve
(test_vof_bc.cpp composedGate, max|div| 2e-15 → 9e-11 once the leak was closed; the gate now
asks for it explicitly and its printed output is byte-identical to the old binary). Gate: SHA-256 of
u,v,w,p[,C] identical before/after on lid cavity, cut-cell sphere, VoF bubble and outflow duct at 1
and 4 threads; four setter-vs-env variants bit-identical to the old env runs; 154/154; CI green.
Also fixed: CLAUDE.md documented PECLET_FLOW_BFS_RE800, which never existed.
dem DONE 2026-09-08 (c7a89ec, 54ca44c): the reads were in four files, not two (sim.hpp,
solve_driver.hpp, solver_fused.hpp, solve_driver_force.hpp); 11 vars → setters (six of them into
one set_sleeping(...)), four deleted with their code paths (_FUSED_GRID, _ML_GATES,
_REST_NEWTON_OFF, _REST_ONESIDED — the two Poisson ablations took five dead PGSManifoldSweep
members with them), _HERTZ_PROFILE + the compile-time macros kept; getenv in src/ is one call
and tests/python/test_no_env_knobs.py greps for a return. Gate: SHA-256 of final positions identical
before/after on XPBD (sleeping on/off), Hertz, step_mpi np=1,2 at one thread; eight setter-vs-env
variants each distinct and setter == env; 47/47; CUDA build smoke-run (graph/fused paths are
CUDA-only). Findings: PECLET_DEM_SYMMETRIC_PGS and _STAB_MODE never existed in code — the gallery's
Dosta production scripts set the former (no-op; set_stabilization_mode('off') was meant);
benchmarks/porous-scaling/snellius/probe_dem6.sh sets the four GPU-submission vars → setters;
the XPBD path is not run-to-run deterministic at 4 threads (record for G.4). F should tier
set_cuda_graphs/set_fused_sweeps (pure performance) into diagnostics.
F. API tiering (M per repo, breaking) — D2¶
flow: ~180 of 266 members move to diagnostics (the *_diagnostics/_stats/_census/_budget/
_ledger/_probe/_timing families, set_vof_timing, vof_block_census, wall_area_probe, …);
retired set_face_interp modes (1/2/10 throw; 3–7, 11–13 are ablations) and gauge-2a deleted
with their kernels; set_face_interp/set_collocated_scheme/set_ghost_projection collapse to
one; on/off pairs (set_contact_angle_dynamic/_off, set_phase_change_thermal/_off,
_energy/_off) become one setter; integer codes (set_domain_bc(type=), set_advection_scheme,
set_csf_mode, add_scalar(scheme=)) become enums/strings like set_pressure_bottom already is;
call-order requirements ("call BEFORE set_solid", 11 docstrings) enforced by state checks.
dem: debug_coloring_conflicts, get_rest_*_stats, set_velocity_use_gs, stabilization modes
'escalate'/'ordered', wall_sdf_at, get_profiling_info → diagnostics; set_stabilization(bool)
+ set_stabilization_mode(str) → one; initialize_shape(shape_type:int) → enum; triples as
tuples everywhere (today std::tuple, std::array, three scalars and (N,3) arrays coexist);
grids passed as a 3-D array, not (grid, nx, ny, nz, …); set_positions bounds-checked against
capacity; step() requires dt. voro: string modes → enums; redistribute_pore_mesh and
sphere_union_scene (245 lines of algorithm) out of __init__.py; bind
DistributedMovingTessellation or document VoronoiHalo as the whole MPI story; minimize_interface
stops returning energy through maxVolErr.
Review 2026-09-10 (before executing F in the remaining repos) — decisions taken against the code:
Enum convention: "integer/bool modes → enums" means VALIDATED STRINGS with the accepted set in the error
message (set_pressure_bottom, dem's shapes and modes); no C++ enums cross the Python boundary.
voro: the parked wip/package-f-tiering (e3e5ebb) fast-forwards onto main and its design stands
(four diagnostics views over an owner pointer, InterfaceResult{iters, energy, energyRatio,
converged} — the maxVolErr item is done there, OptimizeResult objects, parseWallMode/parseMethod,
and a DistributedTessellation binding over DistributedMovingTessellation — DECIDED: bind it, keep
VoronoiHalo as the primitive; the halo-only story hands the user the global skin-trip reduction that
already deadlocked at np≥4). But it is import-broken: the lazily imported peclet.voro.pore_mesh and
.scenes were never written/committed and CMake stages only voro_init.py; it also deleted
FlowSolver(amg=) silently and dropped set_tolerance's default — both reversed (evidence or a
diagnostics setter; a numeric default is not a mode). _union_sdf (used by the gallery) becomes
scenes.sphere_union_sdf.
flow: 278 unique members, not 266. The plan's "3–7 are ablations" is contradicted by
flow_ibm.hpp:855-860: 5/6/7 are the Basilisk embed port and must stay reachable. DECIDED: one public
set_collocated_scheme('ghost'|'gauge-exact'|'plain'|'embed') (embed = mode 7); the intermediate rungs
only as diagnostics.set_face_interp(5|6); modes 1, 2, 3, 4, 10, 11, 12, 13, gauge-2a and
set_fv_relax deleted with the kernels only they reach (most of mac_approx_projection.hpp; no
registered ctest calls set_face_interp). Faces become strings too ('-x'…'+z'), types
'periodic'|'wall'|'inflow'|'outflow'|'slip'. Eleven "call BEFORE" docstrings have ZERO state checks;
each becomes a check only where the late call is silently wrong (verified in the C++), else the
docstring is corrected. Pressure-driver selection, set_decomposition, set_backflow_stabilization,
the VoF block container, hydro_force_torque(_reaction), the copying field registry and
unit_scales stay public (coupling and the documented usage need them); field_view,
exchange_field*, rebalance_by_weights, bcast_from_root are diagnostics — coupling follows.
pnm: no diagnostics object (nothing to put in it); mpi_rank/mpi_size deleted (zero callers);
mpi_block's integer voxel offset renamed origin_zyx → offset_zyx (it collided with the physical
origin_zyx every other call takes); and the defect the plan missed — segment_volume /
extract_pore_network returned the segmentation as a Python list[int] (millions of boxed ints on
packing_ring) and extract_topology converted it back element-wise → ndarrays in and out
(int32 (Nz,Ny,Nx), connections (M,2)), byte-identical values. Pore.x/y/z stay (self-named
scalars carry no axis-order ambiguity; NAMING §1.7 records the exception).
core: mpi/geom have nothing diagnostic; peclet.amr.Flow gets a diagnostics for its iteration
counters. Every hash-gate script is COMMITTED under tests/ this time (dem's SHA gate and pnm's
54-file comparison were ad hoc and are lost).
core DONE 2026-09-10 — as peclet.amr F (amr eeeac1e): Flow.diagnostics (ten instruments, listed in
NAMING §2); mpi/geom had nothing to tier. Executed inside G.2 (§3.G.2).
flow DONE 2026-09-10 (853816f tier, cf7ebf6 scheme collapse + deletions, 85147a6 strings/on-off
+ tests/regression/state_hash.py, 66deaa1 state checks, b927a07 docs; CI green): 278 members →
142 public + 125 diagnostics (the plan's "~180 of 266" was ~125 of 278); one set_collocated_scheme
with 'embed' = mode 7; modes 1–4/10–13, gauge-2a, set_fv_relax, set_aperture_floor and the
kernels only they reached deleted (fvViscousApply, centerGradAperture(Scaled), centerGradOpenCapped,
wdef_); faces and types are strings everywhere; seven setters raise after geometry / init_mpi, a
late set_rho/set_mu was SILENTLY WRONG (stale stencil) and now rebuilds. Gate: 154/154 before and
after (48 non-MPI + 106 MPI), regression suite PASS without --update, 11 fixed-seed paths bit-identical
at every milestone. Finding: the porous path's default MG-PCG reports a non-finite preconditioner on 2
of 5 steps (deterministic; the same signal §5 recorded for coupling's Ergun test — G.6/SCALING #1 territory).
Callers: coupling (driver.py field_view/exchange_field*/rebalance_by_weights → diagnostics,
examples/fluidized_bed.py int BCs) and 76 gallery files / 273 hits — both handled in this session.
voro DONE 2026-09-10 (16363b5, 9e87eee on top of the parked e3e5ebb, fast-forwarded; the wip
branch deleted): four diagnostics views; pore_mesh and scenes written as lazily imported
submodules and staged/installed by CMake (the parked branch had neither file); OptimizeResult /
InterfaceResult / RedistributeResult; FlowSolver(amg=) deleted with evidence (zero callers);
set_tolerance default restored; DistributedTessellation bound (np=2 vs a single-rank cold rebuild:
max |dV|/V̄ = 8.8e-14). Gate: 24/24 + 18/18 mpi before and after; python/state_hash.py COMMITTED
(17 paths identical + the new distributed path); Quality/Doxygen/MPI green. Gallery hits (five pages +
src/peclet_examples/pore_mesh.py) and voro's CLAUDE.md (the brief wrongly said none exists) in the
agent's report; coupling: none.
pnm DONE 2026-09-10 (6ee6399, b07ad75): public surface 12 names, no diagnostics object; ndarray
returns (segment_volume int32 (Nz,Ny,Nx), connections/throats (M,2) int32, network-flow scalars
float64 — the whole dict, not only connections), extract_topology reads the 3-D array in place,
mpi_block → (offset_zyx, shape_zyx), mpi_rank/mpi_size deleted (zero callers). Gate: 9/9 before
and after; tests/regression/state_hash.py COMMITTED (61 hashes: lattice + packing_ring, staged +
fused, network flow ± openness, np=2) — all identical to the pristine build; CI green. Gallery:
pore-network-extraction/index.qmd:94 keeps a now-redundant reshape (valid); coupling: no callers.
dem DONE 2026-09-08 (2213849): public surface ≈ 90 members in eight groups (shapes, domain,
physics, walls, state, stepping, read-out, policy, MPI); sim.diagnostics holds 13 developer members;
deletions: set_sphere_shape (alias), set_stabilization(bool), the flat [3N] set_positions
fallback — no kernel was an unreached ablation. Gate: 47/47 before and after, SHA-256 of final
positions identical on XPBD / Hertz / step_mpi np=1,2. Findings: get_sdf_grid returned x-fastest
data with C strides (transposed on non-cubic grids — fixed, Fortran order); set_velocities read
(N,4) input as (N,3) and mis-indexed (three dem tests did exactly that); step() had a silent
dt = 1e-3 default and step() with no argument was a dt=0 relaxation (now relax(n)); NAMING §2's
dem row said counts stay methods — §1.2 wins, row fixed. Callers to update: coupling (resolved.py,
driver.py, six tests, examples/fluidized_bed.py) and 52 gallery files (both in flight).
G. Structure (L, mostly not breaking)¶
- DONE 2026-09-11 (flow
834ae22,e0525f6,fd9e407,470b6b4,73cc63c; CI green). DECIDED mechanism (survey 2026-09-10): NOT CRTP/mixins — every domain touches the shared state (e_/u_/C[]/sdf_/cField_/vofAdv_/mg_/distributed_) and nvcc's extended-lambda rule keeps the kernels in onepublic:region, so a dependent base would forcethis->on thousands of lines and nothing stays byte-identical. Instead: declarations + state stay inflow_ibm.hpp, definitions move verbatim as out-of-classSolver<Grid>::members into domain headers included at its bottom. Landed (floweac5b07,a0a8c27,9f27439, bit-exact on the committed hash gate + regression + 155/155):fillVelGhostsKeepOutflowmerged intofillVelGhostsTo(…, doOutflow)— the ONE sibling merge that is bit-exact;project()(523 lines) → 5 stage members andsetSolidDevice(540) → 9; thebuildRhs×5 /addCsfRhs×3 families are different arithmetic (different terms and operand order) and are NOT merged;hydroForceTorque/Reactioncompute different quantities and stay. The header split:flow_ibm.hpp11 886 → 4 688 lines (declarations, nested structs, state) and twelve domain headers included at its bottom —flow_ibm_vof.hpp2203,_phase_change1576,_core1743,_project1194,_scene715,_geometry691,_hydro544,_closures389,_diagnostics340,_mpi317,_scalars307,_bc292 — every definition verbatim as an out-of-classSolver<Grid>::member (517 functions classified by the state they read, e.g.setSolidFromScene→ scene,vofInterfaceArea→ phase change because it readspcAreaMode_); each commit bit-exact on the hash gate + regression + 155/155. The split tooling had three generation bugs (double-nested namespace, a comment leaking into a definition, a tokenizer eating the blank line before declarations — the last invisible under clang-format and caught only becauseflow_ibm.hppis exempt); all fixed and the headers re-verified byte-identical.73cc63calso templatesGpOverlayonMReal(G.6's last flow-side gap; the core-sidescheme/ghost_closure.hppclosure math stays float — two narrowing casts remain, exempt with that reason). Original item:flow/src/flow_ibm.hpp(11 802 lines, one class, 483 member functions, 221 data members) split by physics domain: single-phase core / scene + moving geometry / VoF / phase change / porous + closures / MPI state, as CRTP or mixin headers;project()(522 lines) andsetSolidDevice(540) cut below ~150; the nine self-labelled "sibling" functions (buildRhs×5,hydroForceTorque×2,addCsfRhs×2,fillVelGhostsTo×2) merged behind parameters. core/amr/→ a newpeclet-amrpackage (D6). Decided 2026-09-10; supersedes the earlier "move the solver into flow". The audit behind it is below — re-read it before deviating.
The move. The whole amr/ tree goes, solver and octree together, with its git history, into
a new repo peclet-amr → Python peclet.amr, a submodule like every other package (own CMake,
CI, CLAUDE.md, wheel, Doxygen, Zenodo DOI). It depends on peclet-core (find_package(peclet-core
CONFIG)) and on nothing else in the suite. core/python/amr_bindings.cpp goes with it, so
peclet.core.amr.{Octree,DistributedOctree,Poisson,Flow} becomes peclet.amr.* — breaking,
hence before the 1.0.0 tag. barnes_hut.hpp is not AMR: it moves elsewhere in core or goes.
Why the whole tree, not just the solver. Nothing outside amr/ consumes it. flow includes
no AMR header at all; dem, pnm and coupling none; the only includer of block_octree.hpp /
distributed_octree.hpp outside amr/ is core's own amr_bindings.cpp; and the Lagrangian
rebalance path lives in halo/particle_rebalance.hpp, not in the octree. voro reaches into
amr/momentum.hpp for exactly one function, greedyColoring → move it to core/solver/coloring.hpp
(otherwise voro would acquire a dependency on the AMR package). Folding 10.6 k lines of a second,
unrelated solver stack into flow — whose own flow_ibm.hpp is being split under G.1 because
it is too big — trades one oversized package for another.
part of amr/ |
lines |
|---|---|
| solver (flow, oracle, Poisson, momentum, MG, PCG, cut cell, ghost projection, coarse-fine, scalar transport, distributed variants) | 10595 |
| infrastructure (block/distributed octree, adapt, refine, indicators, leaf field + halo, distributed view/fv, VTU I/O, CSR) | 4167 |
Why peclet-amr does NOT depend on peclet-flow. The two solvers have different data models,
so the discretization does not transfer as code: flow computes with fixed-offset stencils on
structured (x,y,z) Kokkos views, while the AMR path assembles a per-cell diagonal plus a general
face CSR and applies it with weighted Jacobi + BiCGStab because the operator is non-symmetric.
Hanging nodes are why cf_scheme.hpp exists and has no flow counterpart. Packaging says the same:
core installs an exported CMake package, flow installs only its Python extension, so a C++
dependency on flow would first require making flow a consumable header package, then pinning three
repos together — a heavy price for a few hundred lines of scalar mathematics.
What IS shared, and where it goes. The cut-cell closure polynomials are duplicated today,
written independently in flow/src/cut_cell_ibm.hpp (float, KOKKOS_INLINE_FUNCTION) and
amr/cut_cell.hpp (double, MORTON_HD), and the AMR ghost projection cites flow's campaign
notes as its authority for the method it implements. That layer is pure functions of local
geometry with no data layout in them: lift poly_D / poly_Nc / poly_N_nb and their siblings
into core, templated on the scalar type (which also serves G.6), and have both packages call
the one copy. Python-level composition stays available and costs nothing: peclet.amr may import
peclet.flow for a uniform-grid reference solution or shared I/O without any C++ coupling.
Prerequisites and traps. Three infrastructure headers reach back into solver headers and must
be cut first: distributed_view.hpp → multigrid.hpp, distributed_poisson.hpp, and
distributed_fv.hpp → distributed_poisson.hpp. core/python/build*/ hold stale artefacts naming
the retired tpx_amr target — do not carry them across. Move the ~30 AMR ctests and the AMR docs
with the code; core's tests/oracle/morton_octree.hpp stays in core (it is BlockOctree's test
oracle). Update the umbrella CLAUDE.md table, ARCHITECTURE.md, mkdocs.yml, docs/python/,
tools/release/, RELEASE.md's package list and PecletDeps.cmake for an eighth package.
Corrections from the 2026-09-10 re-audit (verified against the tree; they supersede the text
above where they differ). (1) voro uses MORE than greedyColoring: ot_optimizer.hpp:95,228 uses
MomentumSolver<21> / MomentumOp (the Jacobi-preconditioned BiCGStab over a face CSR), so the
lift into core/solver/ is the colouring AND the CSR operator + BiCGStab solver (verbatim; the AMR
package keeps using aliases; MomentumMG stays in amr). (2) barnes_hut.hpp includes
block_octree.hpp + leaf_field.hpp and is already inside the 4167-line infrastructure count: it
is an octree consumer and moves with the tree. (3) Four AMR headers include morton/morton.hpp:
peclet-amr depends on core AND morton — nothing else in the suite. (4) tests/oracle/
morton_octree.hpp has one includer, test_block_octree.cpp (AMR): it moves. (5) The movable
ctest set is 47 (plain) / 52 (host+MPI) / 92 (Kokkos) per core tree plus python_amr{,_np2}, not
~30. (6) No consumer uses find_package(peclet-core CONFIG); every package vendors core through
its own cmake/PecletDeps.cmake sibling include with a PECLET_CORE_TAG pin, and there is no
umbrella PecletDeps.cmake — peclet-amr uses the same mechanism. (7) The back-edges
(distributed_view.hpp/distributed_fv.hpp → solver headers) are genuine symbol uses but do not
block the split, since nothing that stays in core depends on amr/; they are internal hygiene for
the new package. (8) E for amr: PECLET_CORE_GPS_RHO/MAXN change numerics → Flow.set_ghost_sampled(
rho=, max_samples=) setters, defaults inert; PECLET_CORE_PROFILE_* are prints (renamed
PECLET_AMR_*); core's grid_halo.hpp variables are transport/logging and stay. (9) core's
CLAUDE.md advertises 104/158 ctests; no current tree reproduces those counts — fixed with the strip.
DONE 2026-09-10. core d93c323 (B: the face-CSR operator layer lifted to core/solver/ —
face_csr.hpp, csr_operator.hpp (MomentumOp), csr_bicgstab.hpp (MomentumSolver, vestigial
Bits dropped), coloring.hpp, vector_ops.hpp; new ctest csr_solver), ae1affe (D: the strip),
5810ab5 (toolchain-aware byte gate). New repo peclet-amr = 174 filtered commits (include/peclet/
core/amr/ and its tpx/amr/ ancestry → include/peclet/amr/, the 51 AMR test sources, the study
drivers, bench_amr_flow, the AMR docs and archive notes, tests/oracle/morton_octree.hpp) + scaffold
2533f16, E 0c1de95, F eeeac1e, G.5 8e74940, a format-only 48f1801, CI cfe5a50; submodule
amr in the umbrella. Counts: core 109/164/7 → 53/68/6 (+csr_solver); amr 100 ctests (79 + 16 np8 +
5 bench) + 3 Python. Hashes: core mpi/geom 15 keys identical pristine → B → D; amr 13 keys identical
across the relocation (same toolchain; the gate records per toolchain and SKIPs elsewhere — -O0
and -O3 differ). Findings: core's single-rank halo stub is not Kokkos-clean, so amr REQUIRES MPI;
-mfma must stay module-only (host-vs-device bit-exact tests drift with it); filter-repo carried
core's release tags into the new history — deleted locally, never pushed; PECLET_CORE_TAG pins in
the consumers lag core d93c323 (release session).
Not in scope, and not implied. If the long-term goal is one solver serving both uniform and
adaptive meshes, the mechanism is templating the discretization on a mesh policy — the pattern
G.3 proved in pnm (GridGeo / BlockGeo) — not a package dependency. That is a deliberate
project to decide on its own merits, never a side effect of this split.
3. pnm — DONE 2026-09-08 (pnm 5ad3898, 0e0c2cd): src/pore_kernels.hpp holds every stage kernel
once, templated on a geometry policy (GridGeo single-rank, BlockGeo MPI); the two pipeline
files keep orchestration only (2873 → 2458 lines, parallel_for 66 → 37, no stage body twice).
Gates: 9/9 ctests host and CUDA, packing_ring 7199/53020 unchanged, and a 54-file byte comparison
of pores/segmentation/connections/network-flow (single-rank staged+fused, np=2 per rank, open and
cut-cell) against the pre-refactor build: 0 differ.
4. dem — DONE 2026-09-10 (ee34cfe gate, 4839ccf split, 9dad55d, 9fa890c dedups, 93eec07
fix; CI green). sim.hpp 1917 → 1047: free drivers → src/step_solve.hpp + step_solve_mpi.hpp,
the shape registry → src/shape_registry.hpp as a protected base ShapeRegistry (bodies verbatim;
addAnalyticWall stayed with the walls); the world-radius fill was written FOUR times (not three)
→ one fillWorldRadiiKokkos; the gid-keyed search THREE times (not two) → pairKeyFromGids +
lowerBoundKey. Gate: tests/regression/state_hash.py (COMMITTED; nine fixed-seed cases at one
thread incl. step_mpi/step_hertz_mpi np=1,2) identical after every structural commit; 47/47.
The numerics fix is its own commit: single-rank periodic wrap contacts were one-sided because the
partner beyond one maxRad of the face had no image (the survey had the sides swapped: the far,
un-imaged partner moved, by half the de-penetration); band = 2·maxRad + margin, ghost capacity
follows; tests/python/test_periodic_wrap_symmetry.py RED before / GREEN after, and
test_validate_periodic.py's straddlers are now asymmetric (np=2 error 0). Hashes changed only
for the four single-rank periodic cases; Hertz and every MPI case identical.
5. core Python — DONE 2026-09-10 in peclet-amr (8e74940): the shared members (16, not 14) are bound
once through bindOctreeCommon<T>; attribute sets identical before/after.
6. DONE 2026-09-11 (one gap deferred) — flow a0a8c27: option(PECLET_FLOW_OPERATOR_DOUBLE) (the macro
renamed everywhere, pass-through for pip install . -C cmake.define…); the "three hard casts" of
SCALING_ISSUES #1 were ALREADY MReal — the live exposure was the cut-cell IBM overlay (mac_ibm.hpp
mreal = float, FV32), now templated on MReal (default float build byte-identical); ctest
no_float_operator_casts greps src/ with a // PRECISION-EXEMPT: allowlist; the double build
compiles and runs 153/155 (two float-tuned gates: lid-cavity rms 0.0247 vs the 0.02 Ghia threshold at
400 steps, vardensity_mpi_np4 Chebyshev count off by one) — no CI job until those get their own
tolerances. ghost_projection.hpp's GpOverlay (the AUTO-default 'ghost' scheme) is still float
(marked exempt) — templated in 73cc63c (G.1's last commit). The shared closure polynomials: core
b4596aa/5c79205 scheme/cut_cell_closure.hpp (templated, tested at float and double against the
literal formulas), amr 902ee6c and flow a0a8c27 call it — the two copies were byte-identical
formulas. CONVENTIONS §3 corrected (core's ghost_closure.hpp and amr's GhostOverlay matrix weights
are float by design; "core never hard-codes float" was false).
Original item: Precision as a typed policy: flow's MReal = float unless a raw -DPECLET_FLOW_MREAL_DOUBLE
(no CMake option; three "hard (float) casts that survived the templating") is
SCALING_ISSUES #1; make it option(PECLET_FLOW_OPERATOR_DOUBLE), grep-test that no (float)
touches an operator view, and document core's float closure storage
(ghost_projection.hpp:71-76, scheme/ghost_closure.hpp) in CONVENTIONS §3 as the same exposure.
7. voro — DONE 2026-09-10 (5ffb0ea, 76eed19, 2407c58, d58373b, aee039b, c3d3416,
4653c0b, 4ab848e; CI green). include/peclet/voro/params.hpp is the one home of the engine
defaults (every template default, default argument and bench fixture names them; the bindings'
defaults re-exports them) — the only capacity that changed is ConvexCell's template default
64/96 → 64/112, used by one unit test. PECLET_VORO_PROFILE (the library's last getenv) is gone:
diagnostics.set_profile(on) on Tessellation/Simulation/DistributedTessellation, and the
silent over-buffer rebuild is counted (build_report()['over_buffer_rebuilds']). The pore-space
export runs on the device (pore_cells.hpp: the tessellator's own gather, count → scan → fill in
seed order, no atomics), gated by test_pore_cells (invariants + device vs a certified host
oracle: 805/805 cells, volumes 1e-12, vertex sets 1e-9 L; CUDA RTX 5080 6e-11, run-to-run
identical). THE GATE FOUND: the old host reconstruction's fixed 80-nearest gather missed planes on
8/805 cells (volumes off by up to 2.5e-3) — so the two pore hashes changed because the VALUES
were wrong before (edbc5056… → ca5485fc…, f498cf1b… → aac38ed6…; the other 16 identical);
buildTessellation judged kIncomplete against a window the grid clamps (fixed c3d3416). OPEN
CUDA DEFECT: at 128/256 the device SDF clip is wrong on CUDA (every wall cell off up to 22 %,
deterministic; 64/112 correct; 128/256 exact on OpenMP) — the device pore path runs at 64/112 with
overflow counted; investigate clipCellAgainstSdf at 128/256 under nvcc. Warnings 25 → 0 (all
in tests/). aee039b switched mesh_optimizer.hpp/ot_optimizer.hpp/bench_mesh_optimizer.cpp
to peclet::core::solver (G.2's B4).
- flow build time — explicit instantiation of the solver (S–M, not breaking). DONE 2026-09-12,
flow
61e9f58+8f79c3b. The problem:Solver<Grid>is a ~12 k-line class template (517 out-of-line members across the twelve domain headers G.1 split it into, 98 Kokkos device lambdas) and every one of the 45 test executables naming it re-instantiated all of it at-O3, as did the bindings for both grids — measured: a kernel unit test 4.5 s (0.3 MB object), one MPI solver test 56 s (5.6 MB),flow_bindings.cpp1 m 41 s (12.3 MB). Flags are-O3 -DNDEBUG -std=gnu++20 -fopenmp, no LTO and no-march, so there was NO cross-TU optimization to lose. What shipped:src/flow_solver_staggered.cppandsrc/flow_solver_colocated.cpphold the two explicit instantiations;cmake/PecletFlowSolver.cmakebuilds them into a STATIC library the module and every test link;flow_ibm.hppends with the matchingextern templatedeclarations, skipped in those TUs byPECLET_FLOW_INSTANTIATING. The second commit guards the twelve domain#includes with the same macro, so a consumer sees declarations only. No function body was touched; member templates and themac_*.hppkernel headers are untouched. Measured (host-openmp,-j8, 48-core host; CPU time is the load-independent number):
| before | after | |
|---|---|---|
| full clean rebuild | 6 m 46 s wall / 44 m 29 s CPU | 2 m 17 s / 12 m 07 s |
| one domain header touched | 6 m 19 s / 41 m 10 s, 45 TUs | 1 m 12 s / 4 m 35 s, 4 TUs |
one consumer TU (test_sdflow_mpi.cpp) |
59.6 s, 5.5 MB object | 5.3 s, 59 KB |
full clean rebuild, nvidia-cuda |
13 m 51 s / 90 m 08 s | 8 m 11 s / 49 m 36 s |
_flow.so grows 6.49 → 6.97 MB: an explicit instantiation emits all 517 members where implicit
instantiation emitted only what each TU used. The CUDA gain is smaller because nvcc's fixed
per-TU front-end cost dominates a thin consumer.
Gates, all met at the float-operator default in force when G.8 was written: 155/155 on
host-openmp before and after (49 single-rank + 106 MPI); CUDA single-rank 49/49 before and after;
sdflow_regression.py PASS both sides with every metric at +0.00 % and every iteration count
identical; state_hash.py identical on all ten single-rank entry paths and at np = 2, checked
separately after each of the two commits; step time within run-to-run noise on a back-to-back A/B
of the two modules (64³ and 48³ staggered beds and the 48³ collocated ghost bed, min of 3 × 20
steps, ≤ 1.3 % either way); wheel path pip install . into a scratch venv imports and runs, and
_flow.so carries no undefined Solver<...> symbol, so the instantiation objects ship inside it.
Decisions taken in the pass: STATIC not OBJECT (the link is inert for the 27 targets that never
name Solver, so tests/kokkos links it from every target in the directory and a new test needs
no edit); PECLET_FLOW_MPI became a PUBLIC property of the MPI library variant and the 31
per-test target_compile_definitions were deleted, which makes an ODR mismatch between
instantiation and consumer inexpressible rather than merely unlikely; PIC + hidden visibility on
the library mirror what nanobind_add_module sets on peclet_flow, so the module's code
generation is unchanged, which is what the no-performance-loss condition is judged on; no PCH
(the residual per-consumer time is 5.3 s, so target_precompile_headers is not warranted) and
no single test runner (the cost now lives in four TUs, not forty-five).
The one rule the second commit imposes, stated at the guard in flow_ibm.hpp: a member
TEMPLATE of Solver that a test or the bindings calls must be defined in flow_ibm.hpp itself —
an explicit instantiation of the class does not cover member templates, so a definition left in a
domain header would not link. The class has exactly one member template today and it is used only
from inside the class.
CI (ubuntu-latest, 4 cores, -j4), G.6 73cc63c -> G.8 8f79c3b: the build step of the
mpi job 24 m 17 s -> 5 m 03 s and of the single-rank job 9 m 51 s -> 3 m 11 s; whole
jobs 53 m 54 s -> 28 m 36 s and 41 m 36 s -> 38 m 57 s. The single-rank job barely moves
because it is test-bound, not build-bound — the regression suite and the verify scripts run at two
threads on a four-core runner and now dominate it completely.
ccache, the second half of G.8: wired as an opt-in launcher
(-DCMAKE_CXX_COMPILER_LAUNCHER=ccache, never forced) and verified on the host prefix — the
solver library cold 69 s, warm 0.3 s, 100 % direct hits. It cannot work on a CUDA/HIP prefix,
and the reason is structural, not a flag: Kokkos routes device compilation by putting
kokkos_launch_compiler <nvcc_wrapper> <c++> in the global RULE_LAUNCH_COMPILE, and that script
redirects only when the executable immediately following it is the compiler it was handed. A
compiler launcher expands exactly there, so the script sees ccache, declines to redirect, and
plain c++ is given nvcc's flags (unrecognized command-line option '-arch=sm_120') at the end
of a long build. flow's CMake now rejects that combination at configure time with the reason,
and CMakePresets.json carries host-openmp-ccache and deliberately no CUDA counterpart.
H. Docs describe the code (S–M, not breaking) — D7¶
- Umbrella — DONE 2026-09-08 except the two items at the end: CONVENTIONS.md §4 states the
measured precision policy (double state, float operator storage), :92's array-shape sentence and
:79's
tpx/path fixed; STYLE.md §Namespaces and §CI say what ships; INTERFACES.md carries its "design sketch, not realised" status banner; ROADMAP :77-84's.cuhfile names marked historical and the header points at this file as the active work list; ARCHITECTURE :63block_decomposerphrased as history; the phantomDOC_CI_WORK_NOTES.mdnot_in_naventry removed; the 13 plan notes are indocs/archive/(AMR, AMR_GEOMETRY_SETUP_REQUIREMENTS, ANALYTIC_SDF_GEOMETRY, COMMUNICATION_SCALING, DEFECT_CORRECTION_PLAN+PROMPT, DEVICE_RESIDENCY_PLAN, MG_TELESCOPING_PLAN, MULTIPHYSICS_PLAN, VOF_PLAN, VOF_NEXT_SESSION, VORONOI_METHODS_PLAN, peclet_cuda_wheel_prototype) behind anarchive/README.mdindex and a collapsed "Archive" nav section, 24 links re-based,mkdocs build --strictclean (umbrellaafda9d4); PHYSICAL_UNITS_PLAN stays until Phases 3–4 land. Open: the D7 decision on INTERFACES.md (implementconcepts.hpp+static_asserts, or rewrite as the written contract), and the RELEASE_PREP re-cut for 1.0.0 on release day. The submodule follow-up is done except flow: coupling (2672318) and core (b39ffe4, which also citedAMR_GEOMETRY_SETUP_REQUIREMENTS.md) now name thearchive/paths; the two inflow/src/flow_bindings.cpp(DEFECT_CORRECTION_PLAN.md,MG_TELESCOPING_PLAN.md) wait for the package-E agent to finish in that file. - flow — DONE 2026-09-08 (
628d3dadoc content,ef25feb,013d3b3): CLAUDE.md 178 KB / 2110 lines → 20 KB / 292 lines, the old file archived verbatim asdoc/history/claude_md_2026-09-08.md;doc/split into reference (17 live design docs) anddoc/history/(the fivevof_workorders*, eight collocated campaign notes,colocated_study/, ghost-hardening, advective cut-wall, the two packing reports) behind two dated indexes; 79 relative links checked, 0 broken; ctest counts corrected to 156 registered / 154 with-LE bench;AGENTS.mdreduced to a true pointer andGEMINI.mddeleted (it held another tool's generic workflow memory, nothing about this repo). The plan's item 3 was wrong on all four counts —CFD_BUILD_MPI, theSolverColocated"ghost" default,pyproject's "single-rank" and the pybind11 mention were already fixed by A/B/C and D. Left for later, as core did: ~10doc/…citations insidesrc/andtests/*.cppgain thehistory/prefix when those files are next edited. - dem — DONE 2026-09-08 (
f680c3d,acb0fd5): README quick-start written and RUN against the 1.0.0 API, folder listing and venv paragraph corrected,docs/solver_details.mdrewritten for the Kokkos stack (it narrated the retiredsrc/cuda/*.cu), CLAUDE.md carries the E/F traps and the right ctest counts, two campaign notes →docs/archive/with an index, zero broken links; themax_overlapdocstring andexamples/pack.pyfollow the moved path. - voro — DONE 2026-09-08 (
23c088f): CLAUDE.md written by D + H (5 KB, one-tree recipe,docs/vsdocs/archive/rule); Lees–Edwards and the half-edge oracle dropped from README/CITATION.cff (zero occurrences ininclude/); 23 campaign notes (not 17) →docs/archive/with an index;docs/keepsarchitecture.dox,distributed_voronoi.md,performance_report.md. - core — DONE 2026-09-08 (
c52989f): lasttpx/transport_core/block_decomposerprose gone;-LE benchcounts corrected to 104/158 (totals 109/164/7 include thebenchlabel); six campaign notes →docs/archive/with an index, four AMR reference docs stay (nothing deleted);amr_advection_session_prompt.mdwas already gone. CLAUDE.md 18.7 KB. Left: ~25 code comments cite the olddocs/<name>.mdpaths (prefixarchive/when those files are next edited). - pnm — DONE 2026-09-08 (already in
978f340; re-swept in329eae2): guard +@briefsay pnm, nocout/cerr/fprintfleft insrc/, every failure path throws (the MPI ones after a collectiveMPI_Allreduceof the condition so no rank hangs);docs/Doxyfilewas the last self-as-flow line. Left for G: global-namespaceSDFDatainsdf_reader.h. - morton — DONE 2026-09-08 (verified against the tree, not just the plan): the README H1 is
peclet-morton, nomortonarithor "python 3.8+" claim survives (requires-python = ">=3.9"),docs/ROADMAP.mdcarries no "v0.3",bindings/morton_c.his installed beside the headers so the C ABI is documented, andoctree/is gone (item 5; the std::map octree lives on as core's test oracle). Package C and item 5 had already done the work the plan still listed as open.
Package H is complete in all eight repositories.
4. Order and gates¶
| step | packages | gate |
|---|---|---|
| 1 | A + B + C + H1 (umbrella docs) | every repo's existing battery green on host-openmp; check_release_state.sh clean; gallery pages grep clean of removed names |
| 2 | D | a red test in CI is a real failure; MPI suites run in CI |
| 3 | E + F + G.2 (the breaking parts) | env vars retired; diagnostics tier; core/amr out of peclet.core into peclet-amr (D6) — every removal in the CHANGELOG |
| 4 | tag 1.0.0 ("physical domains", the clean-break release) | RELEASE.md phases A–I; CUDA + MPI matrices |
| 5 | G (rest) + H2–7 | flow_ibm.hpp split, dem sim.hpp split, precision policy, doc diets — 1.x, non-breaking |
Order corrected 2026-09-08 (was: tag after D, E + F in 1.x "breaking → 2.0"): F is breaking by
definition (D2) and G.2 moves public Python names out of peclet.core; under D9 (strict semver from
1.0.0) they belong BEFORE the tag, not in a 2.0 weeks later. E is launched per repo as its D pass
lands; F and G.2 follow in the same repo.
5. Log¶
- 2026-09-08 — audit; this file; decisions D1–D9 taken (maintainer: "quality is the prime
objective … be API breaking if needed"; "if the version should be major, do so"; "AMR is being
developed, do not throw it away"). Executed the same day, all pushed: packages A, B, C and the
per-repo stale-line fixes of H — flow
14f3388→64adc17, demae88d3e→885f85f, voro9e7b1b0→ef99da9, pnm978f340→6167f23, core8d9f3c2→6604ed0, morton8614353/b502598, coupling8603faa→34ad637; umbrellae263459…; CHANGELOG[Unreleased] — 1.0.0. Batteries on host-openmp after the changes: flowtests/kokkos44/44 + regression PASS + verify Poiseuille (vof subset re-run 16/16 after the second flow pass), demtests/kokkos8/8 + verify_packing + three test scripts, voro 24/24, pnm 6/6 MPI + 7199 pores, core 104/104 + 158/158 + Python np=1,2,4, morton all four configurations + pytest 9 + wheel, coupling pytest 4 passed / 1 skipped. Not re-run: the CUDA and MPI matrices of flow/dem/voro (RELEASE.md §3 on the day). Corrections the execution surfaced: flowget_ox/oy/ozare openness fields (table fixed); flow has 44 kernel tests and core 158 Kokkos ctests (docs fixed); dem'sset_positionsmust precedeset_shape_ids(docstring fixed); voro'svolumes()was not the getter model (§1.2 restated); morton's PDEP/PEXT grep test did not exist (now does); pnm'srelease.ymlwas already consistent. New observations for the open packages: voro's blanket-Wno-*removal exposes 622 warnings (fix under G.7); coupling'stest_fixed_bed_ergun_porouspasses but flow'sCutcellMG::solvePCGprints "preconditioner produced non-finite z" on it (flow-side, check under D/G.6); voro's GitHub repo has a PR-required branch rule that direct pushes bypass; thepre-legacy-removaltag keeps morton's history reachable. - 2026-09-08, package D — pnm
ec646c8…72cf6f4(0→9 ctests, CI 39 s + 74 s) and G.35ad3898/0e0c2cd(one kernel set, 2873→2458 lines, 54-file output byte-identical, 9/9 host + CUDA); dem5bb9bfd…8124d57(47 ctests in one tree, scripts sorted, CI 2 m 47 s + 3 m 17 s). Details under §3.D / §3.G.3. core69d6b0f…6bd091d(109/164/7 ctests, six CI jobs, longest 17 min); voro1cbf005/a487777(42 ctests one tree, MPI launcher trap closed, 622 → 145 warnings;test_sdf_curvedhonestly RED, then root-caused the same day: order-dependent silent overflow caps,9ce81c8/ce698c1); morton5f35316(headers-Wpedantic-clean, voro's 120 remaining warnings gone). - 2026-09-08, packages E/F/H and the voro engine fix — E: dem
c7a89ec/54ca44c(bit-exact), core's folded into G.2. F: dem2213849(public +sim.diagnostics), callers followed in couplingf98e6d6and the gallery (local6a749f8, not pushed — the site is re-rendered only on 1.0.0 wheels). H: pnm329eae2, corec52989f, voro23c088f. voro9ce81c8/ce698c1closed thetest_sdf_curvedblocker; morton5f35316made its headers-Wpedantic-clean for consumers. Package D is now done in all six repos with tests. Interrupted, resume here: voro's package F was cut off mid-flight and left UNCOMMITTED edits in the voro working tree (src/voro_bindings.cpp,include/peclet/voro/mesh_optimizer.hpp,packaging/voro_init.py,tests/kokkos/test_mesh_optimizer.cpp) — inspect withgit -C voro diffbefore starting anything there; dem's G.4 (sim.hpp split + the periodic-wrap one-sidedness) never started. - 2026-09-08, after the model switch — dem H.3 landed (
f680c3d,acb0fd5, CI green) anddocs/python/dem.mdwas regenerated from the tiered module;tools/gen_python_api.pynow emitsDiagnosticsbesideSimulation, so the developer tier is documented rather than hidden behind a property. The other generated pages are untouched (their modules did not change). - 2026-09-08, package E complete — flow
ad917b1(25 variables, the process-global statics, and a real cross-solver leak closed), dem54ca44c, core's folded into G.2. Nine gallery files and one Snellius batch script still name retiredPECLET_FLOW_*variables (list in §3.E);couplinghas none. flow486cdb3finished H.1's citation follow-up, hand-wrapped becauseflow_bindings.cppsits on quality.yml's temporary vof-w4 exclude list and must not meet clang-format until W4 merges. - 2026-09-08, gallery callers —
peclet-examples6a749f8(dem's 1.0.0 API across 50 files; int shape codes were 1/2/3, not 0-based, and three latent bugs surfaced: an(N,4)set_velocitiescall that dem used to mis-index, a six-scalaradd_plane, and a C-orderget_sdf_gridread) andff21290(flow's retiredPECLET_FLOW_*). Local commits, NOT pushed, NOT re-rendered — that waits on the 1.0.0 wheels. Dated records inISSUES.md/PROGRESS.mdkeep their historical commands with a bracketed note naming the replacement setter. - 2026-09-08, a coordination lesson worth keeping — flow
628d3dacarries BOTH apredict_hierarchyfix and the H.2 doc-diet agent's in-progress files: two agents were committing in the same checkout and a baregit committakes the whole shared index, not just the paths you staged. With concurrent agents in one repo, always commit with a pathspec (git commit <paths> -m ...). The agent preambles now say so. - 2026-09-08, a package-E regression caught by the gallery pass — removing the process-global
decomposition state left
CutcellMG::predictcallingdecomposition()with the defaults, soflow.predict_hierarchysilently predicted the ALIGNED hierarchy for a coarse-first job while its docstring claimed otherwise. Fixed in628d3da: the depth and imbalance tolerance are parameters (decomposition_levels=,max_imbalance=). On 160^3/np=6 and 144^3/np=12 the prediction now differs from the default, as it must; 48/48 non-MPI green. Lesson for F and G: when a process-global goes away, every pure/preflight function that used to read it needs the value passed in. -
2026-09-08, H.2 and its fallout — flow's doc diet also flagged that
scripts/check_decomposition.py --predictprinted IDENTICAL hierarchies for--mode 0,4under headings naming different depths, and silently overwrote--decomp-levels; fixed in013d3b3(aligned 80x48x160 vs coarse-first(4) 80x52x160 on 160^3/np=6). Umbrella links re-pointed atflow/doc/history/…in eight files,docs/archive/AMR.mdcitedflow/doc/sdflow_colocated_plan.mdwhich never existed (it isflow_colocated_plan.md), anddocs/python/flow.mdwas regenerated from the MPI build so it no longer documents the retiredPECLET_FLOW_MG_ASPECT. -
2026-09-10, F/G review and launch — the remaining packages were reviewed against the code before cutting (five read-only surveys: voro's parked branch, flow's 278 members, core's AMR dependency graph, pnm's surface, dem's
sim.hpp). Findings and decisions are recorded in place: §3.F ("Review 2026-09-10") and §3.G.2 ("Corrections from the 2026-09-10 re-audit"). Plan errors found: voro's AMR use is the CSR BiCGStab solver, not one function;barnes_hut.hppis an octree consumer; peclet-amr needs morton; flow's face-interp 5/6/7 are the embed port, not ablations; pnm returned the segmentation as a list of boxed ints; dem's ledger carry is written three times, not two; the G.3 and dem SHA gates were never committed..gitmodulesstill namestransport-core/sdflow/vorflow(a D8 leftover, renamed when the eighth submodule is added).preamble_G.mdwritten (byte-comparison gate mandatory, hash scripts committed). The empty repopeclet-amrwas created. Execution order: wave 1 in parallel — voro F, flow F, pnm F, dem G.4 — plus core F + G.2 + G.5 as one pass; then the voro include switch, the sharedpoly_*lift, G.1, G.6, G.7; then callers, CHANGELOG/NAMING, this file, pointers. No version bump, no tag. -
2026-09-10/11, packages F and G executed — all eight repos, every battery green before and after, every structural commit byte-exact on a COMMITTED hash script (flow
tests/regression/state_hash.py, demtests/regression/state_hash.py, pnmtests/regression/state_hash.py, voropython/state_hash.py, core + amrpython/state_hash.py), CI green everywhere. F: pnm6ee6399/b07ad75, voro16363b5/9e87eee, flow853816f…b927a07(+a0a8c27tuples and the BC value-vs-type rule), core-as-amreeeac1e. G.2+G.5: cored93c323/ae1affe/5810ab5, the newpeclet-amr(submoduleamr, 174 filtered commits +2533f16…cfe5a50,902ee6c). G.4: demee34cfe…93eec07(the wrap fix isolated, RED→GREEN test). G.6: flowa0a8c27/73cc63c, coreb4596aa/5c79205(scheme/cut_cell_closure.hpp). G.7: voro5ffb0ea…4ab848e. G.1: floweac5b07/834ae22…73cc63c. Callers: coupling5b2eb4b,16e8b8a; gallery local4d6e0cf, 22 commits to7be21f7,3c9d5e2,1b370a0(NOT pushed, NOT re-rendered). Umbrella0e8382d…296aebe;docs/python/*regenerated from the 1.0.0 build trees. Defects the gates found (none would have shown in a green battery): dem's periodic wrap contacts were one-sided on a single rank (the un-imaged far partner moved, by half); voro's host pore-space reconstruction missed planes on 8/805 cells; flow'sset_rho/set_muafter geometry left a stale stencil; flow's cut-cell IBM overlay was hard float in the "double" build; pnm boxed millions of ints; a per-stepset_domain_bc_profile(a ramped inflow) must stay legal after geometry — the first cut of the state checks forbade it. Open, recorded in place: the CUDAclipCellAgainstSdfdefect at 128/256 (§3.G.7); flow's double build has two float-tuned gates (§3.G.6); core'sghost_closure.hppclosure math is float;PECLET_CORE_TAGpins lag core (release session). Lesson three for the handoff: an agent that ends its turn to "wait for a monitor" never wakes — every executor stalled at least once until told to block in the foreground (timeout … tail --pid=<pid> -f /dev/null). -
2026-09-12 — G.8 executed and pushed (flow
61e9f58,8f79c3b; umbrella record this commit).Solver<Grid>is now compiled once per grid instead of once per consumer, and consumers see declarations only. Full clean rebuild 44 m 29 s -> 12 m 07 s of CPU on host-openmp and 90 m 08 s -> 49 m 36 s on nvidia-cuda; a one-domain-header edit went from 45 objects and 41 m 10 s to 4 objects and 4 m 35 s; CI's build step 24 m 17 s -> 5 m 03 s (mpi job) and 9 m 51 s -> 3 m 11 s (single-rank). Gated as §3.G.8 records: 155/155 host and 49/49 CUDA single-rank before and after, regression PASS with every metric at +0.00 %,state_hash.pyidentical after each of the two commits, step time within noise on a back-to-back module A/B, and the wheel path verified into a scratch venv.ccacheis opt-in and host-only, and the CMake now refuses it on a device prefix with the reason (Kokkos owns the compile rule's launcher). Two things this pass found that the plan had wrong: G.8 predicted a PCH might still be needed and offered one test runner instead of 45 — both are now pointless, because the residual per-consumer compile is 5.3 s and the cost lives in four TUs; and G.8 assumedccache"works through the Kokkos launch compiler", which is false by construction. Also corrected: the spelling isflow_solver_colocated.cpp, matching theColocatedpolicy tag.
6. Handoff — starting packages F and G¶
Written 2026-09-08 at the end of the day A–E+H were executed, for whoever picks up F and G. Read
§1 (decisions), §3.F/§3.G, docs/NAMING.md §1, and the repo's own CLAUDE.md before touching code.
Where the work stands (rewritten 2026-09-11)¶
Every package is done in every repo: A–H, including F in voro/flow/pnm/core(→amr), G.1 (flow_ibm split),
G.2+G.5 (peclet-amr, the eighth package), G.3 (pnm), G.4 (dem split + the wrap fix), G.6 (precision
option + grep test + the shared closure polynomials in core), G.7 (voro). Every submodule pointer is bumped
and pushed, CI is green in all nine repos, and tools/release/check_release_state.sh flags only the
deliberate "bump at tag time" versions (plus peclet-amr 0.1.0, a new package that gets the family version).
What the next session does: tag 1.0.0. RELEASE.md phases A–I with EVERY package at 1.0.0 (D9) — EXCEPT
peclet-amr, which ships its first release as 0.1.0 and stays 0.x (D9's recorded exception; sdist only; peclet[amr] extra; PyPI project + Trusted Publishing + Zenodo DOI
still to be created — RELEASE.md's new-package steps), then the CUDA + MPI matrices, then push and re-render
the gallery (~/Codes/peclet-examples holds ~30 local commits against the 1.0.0 API; nothing is pushed until
the wheels exist). Before tagging: bump the PECLET_CORE_TAG/PECLET_MORTON_TAG pins in every consumer's
cmake/PecletDeps.cmake (they lag core d93c323; amr's CI uses core main), and regenerate docs/python/*
from MPI builds one last time (a regeneration from the venv's OLD wheels silently regresses the pages —
always set PYTHONPATH to the build trees).
G.8 is DONE (2026-09-12, flow 61e9f58 + 8f79c3b) — flow's explicit solver instantiation and
ccache. Numbers, decisions and the one new rule it imposes are in §3.G.8; the short version is that a
full flow rebuild costs a quarter of the CPU it did and a one-header edit a tenth, bit-exact, with no
step-time change. Nothing in the release depends on it.
Open items recorded in place, none blocking the tag: §3.G.7's CUDA clipCellAgainstSdf defect at
128/256; §3.G.6's two float-tuned gates in flow's double build (no CI job until they get tolerances) and
core's float scheme/ghost_closure.hpp; the porous path's "non-finite preconditioner" report (§5,
2026-09-10 flow F); C.3 (one shared cmake module), D.6 (one composite Kokkos action), the D7 decision on
INTERFACES.md, the flow tests/study move, the RELEASE_PREP re-cut.
The three lessons — they will bite the release too¶
- When a process-global disappears, every pure or pre-flight function that read it needs the value
passed in (
628d3da,013d3b3). F and G moved members and files; nothing regressed because every move was gated by a committed hash script, not by a green battery. - With concurrent agents in one checkout, commit with a pathspec —
git commit <paths> -m …. - A green battery is not a gate; a byte comparison is. Every defect this pass found (dem wrap asymmetry, voro's inexact host reconstruction, flow's stale stencil and hard-float overlay, pnm's boxed ints, the over-strict BC check) was invisible to a green battery. The hash scripts are now IN the repos; run them before and after anything that claims to be bit-exact.
How the packages were run¶
Agent briefs live in .claude/preambles/ (gitignored): preamble.md (git/build/report contract),
preamble_F.md (tiering rules), preamble_G.md (structure: byte-comparison mandatory, hash scripts
committed). Executors ran one per repo in parallel; the coordinator kept the design decisions, wrote the
umbrella records after each report, and bumped pointers last. Two practical traps: the scratchpad is shared
between concurrent agents (use a subdirectory), and an agent that ends its turn "waiting for a monitor" never
wakes — tell it to block in the foreground.
Callers¶
suite/coupling follows every breaking pass in its own commits (5b2eb4b, 16e8b8a). The gallery at
~/Codes/peclet-examples has local, unpushed commits against the whole 1.0.0 API (dem, flow ×2, voro, pnm,
amr); dated records (ISSUES.md, PROGRESS.md) keep historical commands with bracketed notes naming the
replacement; live pages were executed on small grids against the build trees. It is re-rendered and pushed
only on the 1.0.0 wheels.