The core boundary — what is shared infrastructure, what is MPI, and what each is called¶
Design note, 2026-09-21. Answers the brief at /tmp/claude-1003/brief/core_boundary.md (the
question: how should core be decomposed and named, C++ and Python separately, so its MPI-free
functionality is installable without an MPI toolchain, and how is that reached from the shipped 1.0
API under NAMING.md §0). Status: proposal — nothing here is implemented. Sections 6 and 7 are
written so that DECISIONS.md and NAMING.md entries can be lifted verbatim once accepted.
0. Verdict in five lines¶
- The C++ identity does not move.
peclet::core,peclet/core/..., repocore, tagsv*, Zenodo lineage, thepeclet::core/peclet::haloCMake targets — all unchanged. The MPI boundary already exists there (thecommon/mpi.hppshim, thehalo/directory, the two targets); what it lacks is a gate that stops it drifting. Add the gate; rename nothing. - The Python identity splits into two distributions cut at the install-time requirement and
named for what a user installs them to get:
peclet-geom→peclet.geom(wheels, numpy only, in thepecletmetapackage) andpeclet-halo→peclet.halo(sdist, MPI + mpi4py, behind[mpi]).peclet-coreon PyPI becomes a pure-Python compatibility shell for the ladder's duration and is frozen at its last 1.x at 2.0.0. - The brief's crux is right and I agree with it explicitly (§2.3): the C++ header library and the Python package are different products for different audiences with different change costs, and conflating them is what made the name deceptive. Splitting them is what makes the fix cheap.
- The
find_package(MPI)guard is the right shape of stopgap only in its loud, opt-in form, and it entrenches nothing — but it is not the right first move (§4). Step 1 of the ladder costs about a day, is permanent, and is what the gallery actually needs (a wheel, not a friendlier source build). Ship the guard as core 1.0.3 only if the gallery cannot wait for the next family release. - Everything worth doing is doable now without spending a major. The only thing the major buys is deleting the shell.
The one reserved decision is settled: the halo package is peclet.halo. Confirmed by the
maintainer 2026-09-21, taking the recommendation of §1.2 over the initially suggested peclet.mpi.
Every occurrence below is final, not provisional.
1. Target structure¶
1.1 C++ — unchanged identity, an explicit and gated boundary¶
| element | today | target | rationale |
|---|---|---|---|
| repo, tags, DOI | core, v1.0.2, 10.5281/zenodo.21132435 |
unchanged | six repos pin PECLET_CORE_TAG; check_release_state.sh checks every consumer include against that tag; a rename buys a user nothing |
| namespace, header path | peclet::core, peclet/core/... |
unchanged | build-time only; the layering is already expressed by directory (halo/) and target (peclet::halo) |
| CMake targets | peclet::core (header-only, MPI-free), peclet::halo (peclet::core + MPI::MPI_CXX, or the stub) |
unchanged | this is the C++ boundary and it is already right |
| MPI manifest | implicit — whichever headers happen to include common/mpi.hpp |
explicit and gated: the set is halo/*.hpp (8) + decomp/grid_redistribute.hpp (as of 2026-09-21; 13 headers since 2026-10-03, see §2.1); core's tools/check_mpi_manifest.sh, the mpi-manifest job in quality.yml (core 2e44f6b — the gate was documented here but did not exist until then), fails if a header outside the manifest includes common/mpi.hpp or <mpi.h>, or if a manifest header stops including it |
the boundary drifted once already (grid_redistribute.hpp lives under decomp/, and the brief counted it MPI-free); a gate is cheaper than the next audit |
| geom closure | implicit | gated: geom/{primitives,scene,scene_builder,scene_query,quadrature,body_properties}.hpp + common/{types,portable}.hpp compile with no Kokkos and no MPI on the include path — peclet-geom's CI proves this by construction (§1.2) |
this closure is what makes the wheel trivial; a #include <Kokkos_Core.hpp> creeping into scene_builder.hpp would silently make it a Kokkos wheel |
grid_redistribute.hpp location |
decomp/ |
leave in place, listed in the manifest | flow includes it at the current path (flow/src/mac_cutcell_mg.hpp); a forwarding header across a tag boundary is more machinery than the tidy is worth |
| Kokkos + no-MPI | amr/CLAUDE.md:19-20 says it was never a valid core configuration; nothing records it |
recorded as unsupported (DECISIONS entry, §6) — mpi_stub.hpp covers the host halo, the Kokkos GridHalo is untested against it and CI's no-mpi job has no Kokkos |
out of scope to fix; in scope to stop a future session "fixing" the stub to cover it without deciding to |
Nothing in this column changes a byte of any consumer's build. The one new artefact is a CI step.
1.2 Python — two distributions, cut at the install-time requirement¶
peclet-geom |
peclet-halo |
peclet-core (during the ladder only) |
|
|---|---|---|---|
| import | peclet.geom |
peclet.halo |
peclet.core.geom, peclet.core.mpi — re-exports |
| contents | SceneBuilder (analytic-SDF authoring, CSG, instancing, encode, eval*, bake, body_properties, principal_frame) — today's peclet.core.geom, unchanged |
ParticleMigrator, ParticleHalo, build_toolchain — today's peclet.core.mpi, unchanged |
three pure-Python files |
| build needs | C++20 compiler + nanobind; no Kokkos, no MPI, no OpenMP | MPI toolchain; mpi4py at runtime | nothing |
| ships as | wheels — the §11.2 matrix (manylinux x86-64 + aarch64, win_amd64, macosx arm64, cp310–cp314) + sdist | sdist only (MPI ABI, as today) | pure-Python wheel |
| runtime deps | numpy |
numpy (+ mpi4py, unpinned, as today) |
peclet-geom==X, peclet-halo==Y |
| pulled by | peclet and peclet-cu13 metapackages (one wheel serves both — it is host-only, there is no -cu13 variant) |
[mpi] extra |
[mpi] extra during the ladder (so peclet[mpi] keeps providing peclet.core.mpi) |
| repo | new sibling repo peclet-geom, a consumer of core's headers at PECLET_CORE_TAG through the same peclet_sibling_include + PECLET_VENDOR_DEPS machinery every method code uses (the peclet-amr precedent, D6/G.2) |
the core repo's pyproject.toml, renamed |
core/packaging/pyproject-core.toml, built by core's release.yml the way the umbrella builds peclet-cu13 from packaging/pyproject-cu13.toml |
| module layout | peclet/geom/__init__.py + _geom.*.so + _geom.pyi (the peclet/flow/_flow.*.so convention) |
peclet/halo/__init__.py + _halo.*.so + _halo.pyi |
peclet/core/{__init__,geom,mpi}.py |
| version at step 1 | 1.0.0 — the shipped, unchanged 1.0 API, first standalone release | core's version (1.1.0) — the tag consumers pin is the tag peclet-halo is cut from, exactly as peclet-core is today |
core's version |
Why halo, not mpi, for the second package. The suite's naming rule is that an identifier
names what a thing is, never where it runs (the Device/*Kokkos retirement, umbrella
CLAUDE.md). mpi names a dependency; halo names the thing — and it is already the thing's name
on the C++ side (peclet::halo, halo/, GridHalo, ParticleHalo). init_mpi / step_mpi /
mpi_block in the method codes are variant suffixes on a verb, the kept prose-like exception; a
top-level package name is the identity, not a variant. peclet.mpi would be the only package in
the family named for what it links against. This was the one class of decision the standing
policy reserves for the maintainer, and it was put to them and settled on halo (2026-09-21)*;
peclet.mpi was technically equivalent (same ladder, same shell) and is recorded as the rejected
alternative in §6.
Why geom gets a repo and halo does not. Geom needs the four-platform cibuildwheel release
matrix and a CI that proves "no Kokkos, no MPI" by never installing them — a different release
machine from core's (MPI, Kokkos, np=1..8), and it will grow user-facing Python conveniences the way
voro.scenes did. Halo is a thin binding over core's own headers whose tests (test_mpi.py
np=1,2,4,8; state_hash.py) are part of core's counted battery (54 / 69 / 6); moving them out
to a third repo would split the MPI battery for symmetry alone. The asymmetry is deliberate and
recorded (§5, R9).
1.3 Where the C++ and Python identities meet¶
peclet-geom and (after the ladder) peclet-halo are both consumers of the C++ core at a
pinned tag, like flow and amr. The release order gains one rung and loses nothing:
core (tag) → geom, morton → flow, pnm, dem, voro → coupling → peclet / peclet-cu13 → containers, sites, gallery
peclet-halo is cut from the core tag itself (it is core's pyproject.toml), so it sits on the
first rung. No method wheel depends on peclet-geom at runtime: dem's scene_particle.build
takes the builder duck-typed, flow's set_scene takes the encoded arrays. That stays so — the
physical-units plan's D2 rejected a wheel-to-wheel runtime dependency on core, and the reasoning
(a wheel must not pull a source build) is unchanged even though geom is now a wheel: the coupling
is through arrays, and it should remain so.
2. Why this boundary¶
2.1 The C++ principle: a header is on the MPI side iff it includes common/mpi.hpp¶
That is the shim's whole purpose (mpi.hpp:1-4: "Include this instead of <mpi.h>"). It gives a
mechanical, greppable definition of the boundary, which is why the manifest gate in §1.1 is a
CI step and not a judgement. On 2026-09-21 it yielded halo/ (8 headers) plus
grid_redistribute.hpp — nine headers of forty-seven. On 2026-10-03 (core v1.4.0) it is thirteen of
fifty-three: the same nine, the coarse-level stage headers decomp/{gather_by_global_id,
redistribute_topology,stage_comm}.hpp (host-staged, MPI only) and solver/anderson_mpi.hpp (the MPI
side of the Anderson accelerator). The list lives in core's tools/check_mpi_manifest.sh. The layering the manifest expresses:
common/{types,portable} ← C++17-clean, Kokkos-optional, MPI-free
├─ geom/*, decomp/{block_decomposer,block_indexer,morton_indexer}, solver/*, vof/*, scheme/*, field/*, interp/*
│ ← MPI-free (some Kokkos-dependent: solver/graph_amg_device, common/view, python/*)
└─ halo/*, decomp/grid_redistribute ← MPI (real, or the single-rank stub under PECLET_CORE_NO_MPI)
"MPI-free" is not "dependency-free": python/ndarray_interop.hpp and kokkos_teardown.hpp need
Kokkos and nanobind, solver/graph_amg_device.hpp needs Kokkos. The Python geom module's closure
is the strict subset that needs neither — six geom/ headers and two common/ ones — and that
subset, not "the MPI-free part of core", is what peclet-geom builds. geom/device_scene.hpp and
geom/grid_sdf.hpp (Kokkos-side, View-based) stay where they are; they are the bridge the method
codes compile, not something a Python user imports.
2.2 The Python principle: a distribution is cut at its install-time requirement and named for the one thing a user installs it to get¶
Two consequences, and they are the whole design:
- Nothing that needs no MPI may live behind an MPI toolchain.
peclet.core.geomviolates this today — a pure-geometry authoring API that eleven gallery pages import cannot be installed on Colab withoutapt-get install libopenmpi-devand a source build. The fix is not a friendlier source build; it is a wheel, and a wheel needs a distribution whose build closure is wheel-able. That distribution ispeclet-geom. peclet-coreis a distribution named for a C++ layer, and nobody pip-installs "the core". They install the particle halo or the scene builder. Once geom leaves, what remains inpeclet-coreis the halo, and a package containing exactly the halo should say so. That is why the second rename rides the same ladder rather than waiting: the ladder's cost is per release, not per name, and "getting it wrong twice costs two majors" applies equally to getting it half-right.
2.3 The crux: the C++ identity and the Python identity are separate decisions¶
I agree with the brief, and want to state why in a form that survives this note:
| C++ header library | Python package | |
|---|---|---|
| audience | developers of the eight repos | users of PyPI |
| binding | #include "peclet/core/..." at a pinned git tag |
import peclet.core.… at a pinned PyPI version |
| what a rename costs | a mechanical edit across ~330 peclet/core/ include lines in six repos (flow 114, amr 125, voro 55, dem 24, pnm 9, coupling 4), six repins, forwarding headers across a tag boundary, and a check_release_state.sh rewrite |
a three-release ladder (bind beside, warn, remove at the major) plus a metapackage bump |
| what a rename buys | nothing a user sees | the name a user reads, and the install path a user takes |
| the honest name | "core" — it is the shared foundation (common, decomp, geom, solver, vof, halo) |
"core" — deceptive: the package is two unrelated tools, one of which needs MPI and one of which does not |
| naming principle | what it is to the suite | what it is to the user |
The name is the same string, but it is two names. The C++ one is right and expensive to change; the Python one is wrong and cheap to change. Treating them as one name is what produced the brief's dilemma ("rename without re-cutting the boundary would relabel the problem"); treating them as two dissolves it — re-cut the Python boundary, relabel the Python name, leave the C++ alone.
3. The migration ladder¶
Family versions as they stand: metapackage peclet 1.1.1; peclet-core 1.0.2; peclet-flow
1.1.0; the rest 1.0.2. The ladder below is NAMING.md §0 applied literally: N binds the new
name beside the old, N+1 warns on the old, the next major removes it. D9 makes the removal a
major whatever N+2 is numbered.
Step 0 (optional, now) — core 1.0.3: the loud opt-in guard¶
Only if the gallery cannot wait for step 1 (§4 for the verdict). python/CMakeLists.txt:
option(PECLET_CORE_PYTHON_MPI "Build peclet.core.mpi (needs an MPI toolchain); OFF builds geom only" ON)
if(PECLET_CORE_PYTHON_MPI)
find_package(MPI COMPONENTS CXX)
if(NOT MPI_FOUND)
message(FATAL_ERROR "peclet-core: peclet.core.mpi needs MPI and none was found. Install an MPI "
"toolchain (Ubuntu/Colab: apt-get install libopenmpi-dev), or build geom only: "
"pip install --config-settings=cmake.define.PECLET_CORE_PYTHON_MPI=OFF peclet-core")
endif()
# … mpi_bindings target, tests, exactly as today …
endif()
# geom_bindings unconditional, as today
Costs an hour. Breaks nothing. Never falls back silently (§4 for why that matters). Dies with the file at step 1.
Step 1 (family 1.2.0) — bind the new names beside the old¶
| repo | change | breaks | cost |
|---|---|---|---|
new peclet-geom |
repo created; python/geom_bindings.cpp, packaging/core_geom.pyi, the geom half of state_hash.py + its reference entries moved with history (git filter-repo, as amr was); cmake/PecletDeps.cmake with PECLET_CORE_TAG; NB_MODULE(_geom) installed as peclet/geom/_geom.*.so + __init__.py (from ._geom import *) + _geom.pyi + py.typed; CI = build on ubuntu with no MPI and no Kokkos installed + state-hash + a SceneBuilder pytest; release.yml = cibuildwheel on the §11.2 matrix + sdist; PyPI Trusted Publisher; Zenodo; README/CITATION/LICENSE; quality.yml (clang-format) |
nothing | ~1 day (the G.2 precedent) |
| core 1.1.0 | pyproject.toml name → peclet-halo; NB_MODULE(_halo) installed as peclet/halo/; mpi_bindings.cpp → halo_bindings.cpp; stubs renamed; state_hash.py keeps its mpi half, imports peclet.halo; test_mpi.py imports peclet.halo; python/CMakeLists.txt loses geom_bindings; packaging/pyproject-core.toml = the shell (hatchling, peclet/core/{__init__,geom,mpi}.py, dependencies = ["peclet-geom==1.0.0", "peclet-halo==1.1.0"]); release.yml builds both (the umbrella's cp packaging/… pyproject.toml pattern); the C++ manifest gate (§1.1) added to quality.yml; CHANGELOG |
nothing: peclet.core.geom and peclet.core.mpi keep working, same objects, same docstrings, no warning |
~½ day |
| shell modules | peclet/core/geom.py: """peclet.core.geom — the pre-1.2 spelling of peclet.geom;from peclet import geomis canonical.""" then from peclet.geom import * and __all__; same for mpi.py over peclet.halo; __init__.py keeps __version__ from the peclet-core dist |
peclet.core.geom.SceneBuilder is peclet.geom.SceneBuilder → True; pickles, isinstance, docs all unchanged |
trivial |
| umbrella | pyproject.toml + packaging/pyproject-cu13.toml: dependencies += ["peclet-geom==1.0.0"]; mpi = ["peclet-core==1.1.0"] (the shell, so peclet[mpi] still provides peclet.core.mpi); check_release_state.sh SUBS += geom, DIST += [geom]=peclet-geom, and its consumer-includes loop covers geom/python (today it greps $s/include $s/src); check_docs_snippets.py: INSTALLED_MODULES += ["peclet.geom", "peclet.halo"], add_leaf leaves BUILD_GATED (there is a wheel now); gen_python_api.py → docs/python/{geom,halo}.md, core.md reduced to a pointer; DEPLOYMENT.md, index.md, README.md, NAMING.md §2 + §4, DECISIONS.md (§6); tools/hpc/install_{snellius,lumi}.sh install peclet-geom peclet-halo and smoke peclet.halo |
nothing | ~½ day, mostly Sonnet-grade edits |
| in-suite callers → canonical names | dem: examples/driver_distributed.py, tests/python/mpi/{conftest,_mpi_common,test_*}.py, tests/regression/state_hash.py, packaging/scene_particle.py docstring; voro: mpi/validate_voronoi*.py (all but validate_voronoi_halo.py since removed, voro d7371c9), python/test_voro.py, packaging/voro_scenes.py docstring; amr: python/state_hash.py; flow: scripts/compare_amr_sdflow_field.py (since removed, flow cd7211d); docs/notebooks/quickstart_sphere.ipynb prose |
nothing (the old names still work; this is hygiene so the suite's own CI never sees the step-2 warning) | ~2 h, mechanical |
| peclet-examples | the eleven pages: from peclet import geom; the bootstrap loses the peclet-core install, the SystemExit and the apt advice (pip install peclet now provides geom); api-check.yml drops libopenmpi-dev and peclet-core; re-freeze |
nothing | ~1 h, a net deletion |
Proof the move is faithful. The state-hash byte gate is the instrument: the geom entries in
state_hash_reference.json produced by core 1.0.2's peclet.core.geom must be byte-identical to
those produced by peclet-geom 1.0.0's peclet.geom on the same toolchain, and the mpi entries
likewise across the peclet.halo rename. A changed digit is a bug in the move.
User-visible text at step 1: none, by rule. The old modules' docstrings name the canonical spelling; the CHANGELOG entry reads:
peclet.core.geomis nowpeclet.geom(packagepeclet-geom, wheels for Linux/Windows/macOS, included inpip install peclet), andpeclet.core.mpiis nowpeclet.halo(packagepeclet-halo, sdist,pip install peclet[mpi]). The old spellings keep working unchanged in this release and gain aDeprecationWarningin the next;peclet.coreis removed in 2.0.0.
Step 2 (family 1.3.0) — warn on the old spelling¶
Status: landed in core 1.3.1 (2026-09-27), released with family 1.3.0 — core 1.3.0 had shipped without it. The in-suite callers were moved to the canonical names in the same pass.
Core 1.2.0: the two shell modules gain, at import,
warnings.warn(
"peclet.core.geom is peclet.geom since peclet 1.2.0 — use `from peclet import geom`. "
"peclet.core is removed in peclet 2.0.0.", DeprecationWarning, stacklevel=2)
(and the mpi → halo twin). The shell's pins move to peclet-geom>=1.0,<2,
peclet-halo>=1.2,<2 — a range with a <2 ceiling so that the frozen shell can never silently
downgrade or straddle a 2.x family (step 3). A ctest in core asserts the warning fires
(python -W error::DeprecationWarning -c "import peclet.core.geom" must exit non-zero), so the
ladder step is itself gated rather than trusted. Breaks nothing. Cost: an hour.
Step 3 (2.0.0) — remove¶
core/packaging/pyproject-core.toml deleted; core's release.yml publishes peclet-halo only;
[mpi] = ["peclet-halo==2.0.0"]; peclet.core gone from the docs and from INSTALLED_MODULES.
peclet-core on PyPI is frozen at its last 1.x, not tombstoned: a 2.x environment that runs
pip install peclet-core fails loudly at dependency resolution on the <2 ceiling with the
constraint named in the message, and the PyPI description (editable without an upload) says
"renamed: peclet-geom + peclet-halo". A pinned 1.x family keeps working forever, which is what
"someone's script imports it" requires. Breaks: import peclet.core on a 2.x family, after one
full release of warnings — the removal the ladder promised. Cost: an hour.
What is worth doing now versus at the major¶
| now (1.2.0) | next minor (1.3.0) | next major (2.0.0) | never |
|---|---|---|---|
peclet-geom wheels in the metapackage; peclet.halo; the shell; the C++ manifest gate; callers and gallery moved |
the warnings | delete the shell; [mpi] → peclet-halo |
rename peclet::core / peclet/core/...; split the C++ repo |
The defect is fixed at step 1. Steps 2 and 3 are bookkeeping the rule requires.
4. The interim fix — verdict¶
Is the find_package(MPI) guard right? In one form yes, in the other no, and the difference
is the whole answer:
- Silent form (make line 19 non-REQUIRED, wrap
mpi_bindingsinif(MPI_FOUND)): no. It converts a loud build failure into a silent partial install.pip install peclet[mpi]on a box without MPI headers would succeed and yield a package with nompiin it;check_docs_snippets --installedskips absent modules by design ("a module that is simply absent is skipped, so CI must install what it wants checked or the pass silently narrows"), so the sweep would not notice either. That is the packaging twin of the test failure mode QUALITY_PLAN §3.D forbids ("never pass silently"). It would also be the first place in the suite where the contents of an install depend on what the build happened to find — precisely the class of behaviour the CPU-budget decision (D-register, core: "not by the visible CPU count") was recorded to prevent. - Loud opt-in form (§3, step 0): acceptable as a stopgap. An explicit option, default ON, fatal with a message that says both remedies when MPI is missing. It changes no name, adds no API, and is deleted with the file at step 1 — it entrenches nothing, because it does not touch the boundary at all; it only makes today's boundary navigable.
Is it the right first move? Lean no. The gallery has a legible failure in place already
(SystemExit naming the cause, since peclet-examples 4c48344); the loud guard improves that to
"works after a one-minute source build on Colab, via a package and an extra both named mpi". Step
1 improves it to "works with pip install peclet", which is the state the eleven pages were written
for, and costs about one working day. If a family release is more than a couple of weeks out, ship
step 0 as core 1.0.3 in the meantime; if not, skip it. Either way step 0 is not a fork in the road —
it is on the road.
Does the interim measure already shipped entrench anything? No. Installing libopenmpi-dev in
api-check.yml and the pages' SystemExit are deletions at step 1.
5. Rejected alternatives¶
| # | alternative | why rejected |
|---|---|---|
| R1 | Do nothing but fix the build (loud guard, keep peclet.core.{mpi,geom} forever) |
pip install peclet still does not provide geom unless the metapackage pulls peclet-core, which drags an MPI sdist into every install (R6); every Colab visitor compiles; dem's shipped scene_particle.build stays unusable from the wheels; the distribution name stays a C++ layer's name. It fixes the symptom the CI saw and none of the ones users see. |
| R2 | Silent guard (auto-disable mpi when MPI is absent) |
§4: silent narrowing of an install; the [mpi] extra would "succeed" without MPI; the doc sweep would not notice. |
| R3 | Rename the C++ namespace / header path (peclet::geom, peclet::halo top-level; peclet/geom/...) |
~330 include lines across six repos, six repins, forwarding headers that must exist at every pinned tag, check_release_state.sh's include-vs-tag check rewritten — for a change no user sees. The C++ layering is already expressed by halo/ and by the two targets. |
| R4 | Split the C++ repo (peclet-geom carrying the geom/ headers) |
geom/ is included by flow (4), amr (8), dem (5), voro (4) at build time through the single core pin; device_scene.hpp and grid_sdf.hpp bridge geom into Kokkos consumers, so the C++ geom is not the pure-host subset the Python module is. Two tags to pin instead of one, and the manifest gate gives the same guarantee for free. |
| R5 | Wheels for peclet-core as it is |
the halo links MPI; there is no portable MPI ABI. This is why it has been sdist-only since 0.1.0 and why the halo stays an sdist here. |
| R6 | Add peclet-core (guarded) to the metapackage |
pulls a source build (compiler, ~1 min, MPI-or-opt-out) into pip install peclet for every user of every wheel. Wheels are the rule for anything that can be a wheel. |
| R7 | peclet.mpi as the halo's name |
names where it runs, not what it is; the only package in the family named for a dependency; halo already names it on the C++ side. Put to the maintainer and rejected in favour of halo, 2026-09-21. |
| R8 | Put geom's Python into peclet-dem or peclet-flow |
geom is shared by flow, dem, voro, amr and coupling; it would create the wheel-to-wheel runtime dependency the physical-units plan's D2 rejected, in the other direction. |
| R9 | A third repo for halo, for symmetry with geom | splits core's counted MPI battery (test_mpi.py np=1..8, state_hash) out of the repo whose headers it tests; halo has no wheel matrix to justify separate release machinery. |
| R10 | Tombstone peclet-core 2.0.0 (metadata-only release depending on the new packages) |
pip install peclet-core would then install something that does not provide peclet.core — a silent removal. Freezing at 1.x with a <2 ceiling makes the same situation a loud resolver error. |
| R11 | Keep one distribution, rename it peclet-geom, ship the halo inside as optional |
one distribution cannot be both a wheel and an MPI sdist; the halo would again be unbuildable from a wheel and again silently absent or loudly required. This is R1/R2 with a different label. |
| R12 | Fix only the gallery (apt + SystemExit) |
shipped already as the interim measure; it makes the failure legible and fixes nothing. |
6. What to record in docs/DECISIONS.md (and docs/decisions/core.md / suite-wide.md)¶
Each entry in the register's form: decision, rejected alternative, reason, provenance. Provenance
for all of them: this note, docs/CORE_BOUNDARY.md, 2026-09-21.
core
- The C++ identity of core does not follow the Python split:
peclet::core,peclet/core/..., repocoreand its tag lineage stay. Rejected: a top-levelpeclet::geom/peclet::halonamespace and header path; splitting the header repo. Why: build-time-only identity, six consumers pin it by tag, no user-visible gain; the layering is already carried byhalo/and thepeclet::core/peclet::halotargets. - The MPI boundary in core is "includes
common/mpi.hpp", and the manifest ishalo/*.hpp+decomp/grid_redistribute.hpp, gated in CI. Rejected: an implicit boundary (the audit of 2026-09-21 miscountedgrid_redistribute.hpp); relocatinggrid_redistribute.hppwith a forwarding header. Why: a grep gate is cheaper than the next audit; flow includes the header at its current path across a tag pin. - **The
peclet-geomclosure isgeom/{primitives,scene,scene_builder,scene_query,quadrature,body_properties}.hpp common/{types,portable}.hpp, and it compiles with neither Kokkos nor MPI on the include path.* Rejected: "the MPI-free part of core" as the wheel's definition. Why:* MPI-free is not dependency-free (python/*,solver/graph_amg_device,common/viewneed Kokkos); the closure is what makes the wheel a one-minute host build on four platforms.- Kokkos + no-MPI is not a supported core configuration. Rejected: extending
mpi_stub.hppto cover the KokkosGridHalo. Why: never valid, never tested (CI'sno-mpijob has no Kokkos;amr/CLAUDE.md:19-20); recorded so it is not "fixed" without a decision. - An MPI-requiring Python module is built, or the build fails loudly, or it is disabled by an
explicit option — never skipped because MPI was not found. Rejected:
if(MPI_FOUND)aroundmpi_bindings. Why: a silent partial install;pip install peclet[mpi]must not succeed without MPI; the doc sweep skips absent modules by design.
suite-wide
- A Python distribution is cut at its install-time requirement and named for the one thing a user
installs it to get; a distribution is never named for a C++ layer. Rejected: one PyPI
distribution per C++ repo as a rule. Why:
peclet-corebundled a wheel-able geometry API with an MPI sdist under an MPI-named extra, and the geometry became uninstallable without an MPI toolchain for that reason alone. peclet-geomships wheels and is a dependency of both metapackages; there is no-cu13variant. Rejected: addingpeclet-coreto the metapackage; a CUDA twin. Why: host-only, no Kokkos; one wheel serves both families.- No method wheel depends on
peclet-geomorpeclet-haloat runtime; coupling is through the encoded arrays (SceneBuilder.encode()→flow.set_scene/dem.add_scene_shape). Rejected:peclet-demdepending onpeclet-geomforscene_particle. Why: the physical-units plan's D2 reasoning — a wheel must not acquire a cross-wheel dependency — holds even now that geom is a wheel; the array contract is the interface. peclet-coreon PyPI is frozen at its last 1.x with a<2ceiling at 2.0.0, not tombstoned. Rejected: a code-free 2.0.0 depending on the new packages. Why: a loud resolver error beats a silent import failure.- The halo package is
peclet.halo, notpeclet.mpi— maintainer's decision, 2026-09-21. Rejected:peclet.mpi. Why: names what it is, not where it runs;mpinames a dependency, and it would be the only package in the family named for what it links against;halois already the thing's name on the C++ side (peclet::halo,halo/,GridHalo,ParticleHalo).
7. What to record in docs/NAMING.md¶
§2, the peclet.core table gains (and the heading becomes "peclet.geom, peclet.halo, and
peclet.core (peclet.core.geom / peclet.core.mpi until 2.0.0)"):
| former | canonical | status |
|---|---|---|
peclet.core.geom (dist peclet-core, sdist behind [mpi]) |
peclet.geom (dist peclet-geom, wheels, in pip install peclet) |
aliased 1.2.0 → warns 1.3.0 → removed 2.0.0 |
peclet.core.mpi (dist peclet-core) |
peclet.halo (dist peclet-halo, sdist, pip install peclet[mpi]) |
aliased 1.2.0 → warns 1.3.0 → removed 2.0.0 |
from peclet.core import geom, mpi |
from peclet import geom, halo |
same ladder |
extra [mpi] |
— | canon — it names the requirement of what it pulls, which is what an extra is for; it pins the shell through the ladder and peclet-halo from 2.0.0 |
§0 needs no change — this is the ladder it prescribes. §4 (history) gains one line at step 1:
2026-MM-DD —
peclet.coresplit by install-time requirement (CORE_BOUNDARY.md):peclet.geom(peclet-geom, the family's first host-only wheel) andpeclet.halo(peclet-halo, sdist);peclet.core.{geom,mpi}bound beside them unchanged, warning from 1.3.0, gone at 2.0.0. The C++peclet::coreis untouched.
8. How the answer is verified (extends the brief's §7)¶
- In a venv on a host with no MPI dev headers:
pip install --only-binary=:all: peclet-geomsucceeds (the--only-binaryflag turns "installs" into "installs the wheel"), andpython -c "from peclet import geom; geom.SceneBuilder()"runs. Today this fails atfind_package(MPI REQUIRED). - The eleven gallery pages run from PyPI on a bare Colab-like environment with the simplified
bootstrap (no apt, no
peclet-core). - Byte gate across the move: the geom entries of
state_hash_reference.jsonare identical between core 1.0.2 (peclet.core.geom) andpeclet-geom1.0.0 (peclet.geom); the mpi entries identical acrosspeclet.core.mpi→peclet.halo. Same toolchain key, same bytes. peclet.core.geom.SceneBuilder is peclet.geom.SceneBuilderandpeclet.core.mpi.ParticleMigrator is peclet.halo.ParticleMigratorareTruewith the shell installed; both old imports emit no warning at 1.2.0 and aDeprecationWarningat 1.3.0 (the latter asserted by a ctest, not by inspection).- All eight repos configure and build against core 1.1.0;
tools/release/check_release_state.shpasses withgeominSUBS, including the consumer-includes-vs-pinned-tag check over the geom repo's binding source. - Core's battery unchanged: 54 plain / 69 Kokkos (host and CUDA) / 6 Python, np 1–8 — the six
Python ctests now importing
peclet.halo. dem'stests/python/mpiand voro'smpi/validate_*pass onpeclet.halo. - The manifest gate: a scratch commit adding
#include "peclet/core/common/mpi.hpp"togeom/scene.hppfails core'squality.yml; reverting it passes. The geom repo's CI has no MPI and no Kokkos installed, so the closure is proved by construction on every push. pip install peclet[mpi]in an environment without MPI still fails, loudly, at every step of the ladder (the invariant R2 protects).
9. Corrections to the brief, for the record¶
- §3.4: the MPI surface is
halo/plusdecomp/grid_redistribute.hpp(includes the shim at line 23; used byflow/src/mac_cutcell_mg.hpp).solver/csr_bicgstab.hppmentions MPI only in a comment and takes the reduction as a callable — MPI-free by design. Counts: 36 MPI-free, 9 MPI-bound, 2 shim. - §3.5:
dem/packaging/scene_particle.pyimports nothing frompeclet.core— the builder is duck-typed and thefrom peclet.core import geomis in its docstring example. That makes the case stronger: a feature shipped in the dem wheel is unusable today without an MPI toolchain, because its only input comes frompeclet.core.geom. - §3.5:
voro/packaging/voro_scenes.pywas written specifically to avoid importingpeclet.core(its docstring says so) — evidence the boundary was already costing the suite workarounds. - §4: the
peclet-cu13metapackage carries the same[mpi]extra and needs the same edits. - The top-level
core/CMakeLists.txt:4comment ("Wheels exist for Linux, Windows x64 and macOS arm64") describes the family after the §11.2 probe, notpeclet-core, whoserelease.ymlis sdist-only. The brief was right; the comment is misleading and should be reworded at step 1.