173 lines
10 KiB
Markdown
173 lines
10 KiB
Markdown
# 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.
|
||
|
||
```python
|
||
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 `child_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.
|
||
|
||
```sh
|
||
PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_remap.py' -v
|
||
```
|
||
|
||
## Explicit runtime continuation
|
||
|
||
`replacement = current.remap_child(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.
|
||
|
||
```python
|
||
replacement = current.remap_child([(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_child(rectangles, child_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.
|