- Hierarchy documents level/pair/shape/frame conventions; edge_frame(l) replaces the stepper's inline origin/spacing loop. - Public per-level read-only state: layouts, boundaries, jets, leaf. - set_jet_amplitude/read_raw_exchange/read_exchange_window: level= required. - remap_child(child_objects=) -> remap_finest(objects=). - _kernel_configs, _upload_boundary, _observed_boundary/_summarize split construction and observation duplication; behavior unchanged. - fine_level_origin -> fine_level_offset (a (y, x) index, not an origin). - docs: migration map in docs/development/src_coherence.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Synchronized leaf G remap and manual continuation
This is a CPU state-transfer component, not a runtime adaptive solver. It changes only an L2 rectangle union in a three-level periodic hierarchy. The body-free case is shown first; stationary L2 bodies/Jets follow the bounded support contract below. L0/L1 topology, relaxation parameters, direction order, physical frame and completed time remain unchanged. Existing geometry checks validate child support/nesting and actual transfer dependencies, without a fixed rectangle-gap rule. The three-level remap limit is unchanged by the stepper's arbitrary-depth level-list support. The CPU helper itself adds no CUDA, scheduler, allocator, boundary handling, collision or ghost refill. A bounded runtime adapter is described below.
from CelerisLab.lbm.refinement import FixedPatchSet2D
from CelerisLab.lbm.synchronized_remap import RemapTopology, remap_synchronized
# cfg: D2Q9/SRT/FP32/unshifted/double_buffer, nz=1, no LES
parent = FixedPatchSet2D.from_config(cfg, [(4, 4, 20, 28), (24, 4, 44, 28)])
old = RemapTopology.from_config(cfg, parent, [(16, 16, 32, 40)])
new = RemapTopology.from_config(cfg, parent, [(20, 16, 36, 40)])
candidate = remap_synchronized(
old, new, current_g, base_count=7, level_steps=(7, 14, 28),
phase="postcollision_g", origin=(0., 0.), spacing=1.,
)
The example has a 48×32 root; both child supports allow 4×4 blocks.
Child bounds use the parent's allocated L1 coordinates, not rectangle-local
indices. A scalar FixedPatch2D parent is also supported: its shifted L1 window
origin is retained. Physical lower edges are origin + spacing*(x,y) on L0,
origin + spacing*(parent.fine_origin + (x,y)/2) on L1 and
origin + spacing*(parent.fine_origin + (x,y)/4) on L2.
Factory options coarse_boundary and objects must declare "periodic" and
an empty collection; L2 bodies use objects as described below.
LBMConfig's channel settings do not select this operator's boundary mode.
Geometry metadata is factory-created;
manual mutation/forging of metadata is unsupported. Caller certifies the
actual state matches the declared boundary and has no forcing, that all device
work and old readers are quiescent, and that inputs are the current owned-leaf
postcollision G.
Count/phase stamps check consistency, not freshness or active-buffer identity.
current_g is three NumPy Q-major arrays (9,ny,nx). For blocked inputs,
pass layouts=(base_layout, fine_layout, child_layout); each optional None
entry denotes dense input. Every leaf must have an owner. Only owner cores
are read, never redundant block halos. Mixed precision is rejected. FP64 is
accepted for CPU reference tests; this does not enable FP64 runtime storage.
Outputs candidate.g are separate dense arrays regardless of input layout.
Memory scales with public grid extents, not only sparse allocated blocks.
Same-level leaf overlap is copied bitwise in physical coordinates. Newly refined L2 copies all nine values from its valid old L1 parent. Newly exposed L1 receives the complete four-child mean, including q0. Covered parents, ghosts and holes never serve as donors; NaN/Inf outside leaves is ignored, while nonfinite required leaves or nonpositive density reject the operation. Restriction uses FP64 quarter-weight accumulation and rounds to the input dtype. New nonleaves are NaN. Source arrays/metadata are not changed.
candidate retains base_count, level_steps, phase, origin, spacing
and new topology.leaf_masks. Existing FixedPatchStepper.initialize resets
the counter: using it is not a continuation adapter. This component neither
publishes a candidate nor retires old state. Runtime table rebuild/halo refill
and correct first-step scheduling are handled only by the explicit adapter below.
Tests accumulate leaf-area integrals in FP64 for each population, mass and
momentum. The declared absolute tolerance is 2e-13 * sum(abs(old population integrals)) for FP64, 2e-7 * scale for FP32, accounting for reduction and
restriction rounding (not a solver accuracy bound). Coarse→fine→coarse copy
round-trips preserve state; coarsening arbitrary fine structure loses it and
is not invertible. No kinetic accuracy or stability claim follows.
PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_remap.py' -v
Explicit runtime continuation
replacement = current.remap_finest(rectangles) returns a new ready
FixedPatchStepper at the same absolute step_count. It supports only
periodic three-level runs with no L0/L1 bodies and an existing L2
FixedPatchSet2D; stationary L2 bodies/Jets follow the support contract below.
Scalar or union L1 parents are supported; scalar child= is deliberately
rejected. L0/L1, relaxation and block sizes stay fixed.
No tagger, scheduler, in-place replacement, or general rollback is provided.
The method holds the stepper lock, validates geometry, synchronizes its stream,
and downloads the actual current owned leaves. It certifies postcollision G
and completed 1:2:4 counts internally, applies the CPU helper, rebuilds all
geometry/transfer/kind/halo buffers, and uploads both current and spare buffers.
No old halo is reused. The regular step schedule fills transit ghosts/halos
before use. initialize itself still resets the count to zero.
The old stepper remains ready, unchanged and independently usable. The caller owns both objects and must close both before closing their shared CUDA context. Candidate construction or upload failure releases only candidate resources; an actual old-device synchronization/read failure marks the old stepper failed, consistent with its existing failure semantics.
replacement = current.remap_finest([(20, 16, 36, 40)])
# Explicitly choose the returned continuation only after success.
current.close()
current = replacement
current.step() # advances the preserved absolute count by one
CPU lifecycle mocks cover fresh current-buffer reads, exact transfer, separate
ownership, time preservation, ordinary initialization reset, unsupported states
and geometry, and candidate/source failure cleanup. GPU tests are predeclared
in tests/validation/multigrid/test_runtime_child_remap_gpu.py: identity next
step bitwise, shifted A→B→A with two intervals between changes, uniform moving
and nonequilibrium states, exact CPU remap and overlap, both buffers reset,
finite leaves, exact masks and absolute counts. They were not run on the CPU
host. The separate independent FP64 capture/replay gate is max population
error 3e-6, with exact masks; remap mass/momentum allow FP32 roundoff.
Per-population conservation is not an advancement target under collision.
Bounded pinned-body extension
replacement = current.remap_finest(rectangles, objects=original_objects)
also accepts stationary Circles inside the L2 union. Each body's physical
boundary, complete BFL support and current per-link velocity must remain
identical. Rectangles are not pinned: they may be split, merged or resized,
and other fluid coverage may change, including within the same connected union,
provided that physical support is unchanged. Existing nesting, union transfer
dependencies, full-circle containment, reverse cut-link
coverage and two-hop BFL transfer-footprint checks still apply. This is manual
remapping, not adaptive body tracking or runtime body editing.
Supply all original root edge-frame definitions in their construction order.
Do not pass current.child_boundary.objects: those are projected fine-local copies.
There is no retained mutable original-object registry. Before reading device
state, remapping reprojects the supplied definition onto the old topology and
compares exact ordered circle centers/radii, flags, physical cut links, body
IDs, q, lever arms, hits, BFL support,
legacy SurfaceActuation specification and current per-link velocity against
the existing boundary. Omission, changed geometry/actuation or changed physical
boundary/BFL support fail without device reads or allocations; reordered bodies fail
too. Root object IDs are still assigned by list position, as in the constructor.
Jets are taken from the live L2 boundary, including the most recent successful
set_jet_amplitude(..., level=2). There is no replacement-Jets argument or
constructor-time Jet snapshot. New blocked addresses may differ; the physical
boundary and velocities must match exactly.
RemapTopology.from_config(..., child_objects=..., child_jets=...) prepares a
typed FineBoundary2D; it derives L2 fluid leaves as ACTIVE minus SOLID. No
caller mask is accepted. Solids, ghosts and holes are never donors and remain
NaN, physical-fluid overlap is exact, and exposed L1 still restricts all four
old fluid children. The helper checks old/new physical boundary and velocity
equality too. Its immutable pinned_rectangles is legacy containment metadata,
not a remap eligibility constraint; it may be empty for a seam-spanning body.
Scalar-parent window offsets remain part of the coordinate conversion.
Focused CPU mocks: tests/unit/test_pinned_body_remap.py.
tests/unit/test_union_body_support.py also checks seam-spanning circles,
rectangle repartition/resize, unchanged physical support and live-Jet
preservation. These host checks do not establish GPU evolution accuracy.
GPU declarations:
tests/validation/multigrid/test_pinned_body_remap_gpu.py, covering no-op
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
physical boundary/velocity equality, source independence and absolute counts.
Multiple-body CPU coverage: tests/unit/test_multiple_pinned_remap.py.
tests/validation/multigrid/test_multiple_pinned_remap_gpu.py adds two pins,
two independently updated Jets, variable fluid-region counts and add/move/remove
immediate comparisons with CPU transfer. All nine solid-excluded area-weighted
population integrals, exact overlap and nonleaf NaNs are checked.
These new GPU cases have not been run here. They do not establish evolution
accuracy. A 3e-6 FP32-vs-FP64 evolution gate requires a separate independent
BFL reference; the body-free replay gate above does not establish that claim.