Files
CelerisLab/docs/synchronized_leaf_remap.md

10 KiB
Raw Permalink Blame History

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.boundaries[2].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.