P1 (12/12): one FixedPatchStepper construction and initialize API

Removes the legacy entry points now that every caller is migrated:
FixedPatchStepper(cfg, *, context, refinements, coarse_boundary,
level_blocks, level_objects, level_jets, observe_*, explosion) and
initialize(*level_g). Deleted: positional bounds, fine_block, base_block,
fine_objects, jets, child, child_block, child_objects, child_jets,
child_rectangles_in_parent, initialize(coarse_g/fine_g/child_g),
_legacy_levels and _initial_levels. Docstring, README and docs describe
the level lists. Compatibility views (patch, fine_boundary, ...) and the
PeriodicPatchStepper alias are unchanged (separate follow-up).

Error -> rule table (legacy keyword check -> level-list rule now firing):
- several patches without fine_block/base_block -> _check_levels
  'several patches require fine_block and base_block' (unchanged rule)
- base_block without fine_block -> 'a blocked level requires blocked
  refined levels below it'
- nested child without all blocks -> 'three or more levels must all be
  blocked' / 'a blocked level requires ...'
- fine_objects/jets with a nested child ('only on L2') -> 'bodies/Jets are
  supported only on the finest level of a nested hierarchy'
- child_objects/child_jets without a nested child -> 'level_objects/
  level_jets needs one entry per level'
- scalar child with several L1 patches ('scalar child requires one parent
  patch') -> hierarchy 'a scalar child requires a scalar parent'
- child vs child_rectangles_in_parent mutually exclusive -> no level-list
  equivalent (cannot be expressed); covered by the new explicit
  RemovedLegacyAPI test: removed keywords and positional bounds raise
  TypeError before any context use, initialize has only *level_g.
The InitialLevels test of the removed adapter is replaced by that test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-21 02:02:29 +00:00
1 parent 84b76f49cc
commit ec9b3d721c
5 files changed
+93 -164

No files matched your search

+13 -12
View File
@@ -112,8 +112,9 @@ from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
cfg = LBMConfig(nx=32, ny=24, viscosity=.1) cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0), radius=2.15, body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0), radius=2.15,
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx, with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8, 6, 24, 18)],
fine_objects=[body], fine_block=(16, 20)) as run: level_blocks=[None, (16, 20)],
level_objects=[(), (body,)]) as run:
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None] rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
# (9, ny, nx) postcollision populations per level; non-leaf cells are discarded. # (9, ny, nx) postcollision populations per level; non-leaf cells are discarded.
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)), run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
@@ -133,19 +134,19 @@ Scope and limits:
- Bodies must be stationary circles whose whole boundary stencil lies in the fine - Bodies must be stationary circles whose whole boundary stencil lies in the fine
active region, clear of the coarse/fine transfer cells; violations are rejected active region, clear of the coarse/fine transfer cells; violations are rejected
before any GPU allocation. before any GPU allocation.
- `fine_block=(by, bx)` stores the fine level in fixed-size blocks; `by`/`bx` must - `level_blocks=[None, (by, bx)]` stores the fine level in fixed-size blocks; `by`/`bx` must
divide the WHOLE fine allocation, i.e. `2*(y1-y0)+8` by `2*(x1-x0)+8` including the divide the WHOLE fine allocation, i.e. `2*(y1-y0)+8` by `2*(x1-x0)+8` including the
four-cell ghost band. It gives results identical to dense storage and is four-cell ghost band. It gives results identical to dense storage and is
infrastructure for future block AMR, not a speedup. infrastructure for future block AMR, not a speedup.
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as - Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
`bounds`, with both `fine_block` and `base_block`. Transfers are built once `bounds`, with both levels blocked (`level_blocks`). Transfers are built once
from their union; adjacent, overlapping and concave unions are accepted when from their union; adjacent, overlapping and concave unions are accepted when
their actual dependencies close. There is no fixed rectangle-gap rule. their actual dependencies close. There is no fixed rectangle-gap rule.
The fine level is then one level-wide grid `(2*ny, 2*nx)`: The fine level is then one level-wide grid `(2*ny, 2*nx)`:
`run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the `run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the
grid-aligned tiles intersecting the union plus its four-cell ghost band are stored. grid-aligned tiles intersecting the union plus its four-cell ghost band are stored.
Block sizes must divide the level-wide grid; rectangle edges need not align Block sizes must divide the level-wide grid; rectangle edges need not align
with tiles. Order of rectangles does not matter. `fine_objects`/`jets` work with tiles. Order of rectangles does not matter. L1 bodies/Jets (`level_objects[1]`/`level_jets[1]`) work
as for one patch: each circle (same coarse cell-edge frame) must lie wholly as for one patch: each circle (same coarse cell-edge frame) must lie wholly
inside the union's active region with inside the union's active region with
its boundary stencil clear of transfer cells; a jet may not select links of two its boundary stencil clear of transfer cells; a jet may not select links of two
@@ -156,8 +157,8 @@ Scope and limits:
cfg = LBMConfig(nx=32, ny=16, viscosity=.1) cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None] rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
with CudaContext(0) as ctx, FixedPatchStepper( with CudaContext(0) as ctx, FixedPatchStepper(
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx, cfg, context=ctx, refinements=[((4, 4, 12, 12), (16, 4, 24, 12))],
fine_block=(4, 4), base_block=(4, 4)) as run: level_blocks=[(4, 4), (4, 4)]) as run:
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)), run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64) np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
run.step(32) run.step(32)
@@ -166,12 +167,12 @@ Scope and limits:
``` ```
- Legacy three-level scalar chain: add `child=(x0, y0, x1, y1)` in - Legacy three-level scalar chain: add `child=(x0, y0, x1, y1)` in
**L1 window cells** (the patch's fine grid including its 4-cell ghost band) and **L1 window cells** (the patch's fine grid including its 4-cell ghost band) and
`child_block`, with `fine_block` and `base_block`. L2 must sit inside L1's all three `level_blocks` entries. L2 must sit inside L1's
ACTIVE cells, away from L1's ghost band and transfer footprint (checked before ACTIVE cells, away from L1's ghost band and transfer footprint (checked before
any GPU allocation). `initialize` and `download` take/return three arrays; each any GPU allocation). `initialize` and `download` take/return three arrays; each
level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1 level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1
steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets
live on L2 via `child_objects` and `child_jets`; `fine_objects`/`jets` retain live on L2 via `level_objects[2]` and `level_jets[2]`; L1 bodies/Jets retain
their L1 meaning and must be empty in this mode. All object and Jet geometry their L1 meaning and must be empty in this mode. All object and Jet geometry
uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has
dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and
@@ -188,9 +189,9 @@ Scope and limits:
jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002) jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002)
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None] rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
with CudaContext(0) as ctx, FixedPatchStepper( with CudaContext(0) as ctx, FixedPatchStepper(
cfg, (4, 4, 12, 12), context=ctx, fine_block=(4, 4), base_block=(4, 4), cfg, context=ctx, refinements=[(4, 4, 12, 12), (8, 8, 16, 16)],
child=(8, 8, 16, 16), child_block=(4, 4), level_blocks=[(4, 4)] * 3, level_objects=[(), (), (body,)],
child_objects=[body], child_jets=[jet]) as run: level_jets=[(), (), (jet,)]) as run:
shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape) shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape)
run.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24² run.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24²
run.step(4) run.step(4)
+1 -1
View File
@@ -1,6 +1,6 @@
# Blocked AMR root channel increment # Blocked AMR root channel increment
Opt in with `FixedPatchStepper(..., coarse_boundary="channel", base_block=(by,bx), fine_block=(fy,fx))`, or the existing level-list form. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference. Opt in with `FixedPatchStepper(..., coarse_boundary="channel", refinements=[bounds], level_blocks=[(by,bx), (fy,fx)])`. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference.
`step/block_channel.cu` is a small root-only kernel. Fine kernels, transfer kernels and recursive hop scheduling are unchanged. Global coordinates and the global profile height/velocity are separate from block-local storage strides. Corner ownership and collision phases match `OneStep`: `step/block_channel.cu` is a small root-only kernel. Fine kernels, transfer kernels and recursive hop scheduling are unchanged. Global coordinates and the global profile height/velocity are separate from block-local storage strides. Corner ownership and collision phases match `OneStep`:
+6 -6
View File
@@ -16,8 +16,8 @@ Keep the existing scalar `child=(x0,y0,x1,y1)` for a single scalar parent patch.
Its coordinates retain their original meaning: half-open integer indices in the Its coordinates retain their original meaning: half-open integer indices in the
allocated L1 window, including its ghost band. allocated L1 window, including its ghost band.
The new keyword `child_rectangles_in_parent=[...]` is mutually exclusive with A rectangle list as the second `refinements` entry (formerly the keyword
`child`. It accepts a scalar or a set parent and interprets every rectangle in `child_rectangles_in_parent=[...]`) accepts a scalar or a set parent and interprets every rectangle in
the actual allocated L1 array, including ghost bands and holes. It never infers the actual allocated L1 array, including ghost bands and holes. It never infers
region-local coordinates. The physical root cell edge corresponding to an L1 region-local coordinates. The physical root cell edge corresponding to an L1
index `(x,y)` is `parent.fine_origin + (x,y)/2`: index `(x,y)` is `parent.fine_origin + (x,y)/2`:
@@ -47,9 +47,9 @@ tile edges.
```python ```python
run = FixedPatchStepper( run = FixedPatchStepper(
cfg, [(4, 4, 20, 28), (24, 4, 44, 28)], context=ctx, cfg, context=ctx,
base_block=(4, 4), fine_block=(4, 4), child_block=(4, 4), refinements=[[(4, 4, 20, 28), (24, 4, 44, 28)], [(16, 16, 32, 40), (56, 16, 80, 40)]],
child_rectangles_in_parent=[(16, 16, 32, 40), (56, 16, 80, 40)], level_blocks=[(4, 4)] * 3,
) )
``` ```
@@ -57,7 +57,7 @@ This example uses a 48x32 root. `initialize` and `download` use three Q-major
dense arrays. Public arrays and masks still scale with the full allocated dense arrays. Public arrays and masks still scale with the full allocated
extent; sparse device buffers do not remove that host-memory limit. extent; sparse device buffers do not remove that host-memory limit.
Stationary whole-circle `child_objects` and `child_jets` use root edge Stationary whole-circle L2 bodies and Jets (`level_objects[2]`, `level_jets[2]`) use root edge
coordinates and a single L2 boundary object. Body IDs are physical object-list coordinates and a single L2 boundary object. Body IDs are physical object-list
positions, not region IDs or caller `obj_id`. Complete boundary stencils must positions, not region IDs or caller `obj_id`. Complete boundary stencils must
avoid the transfer footprint. Change a child Jet with avoid the transfer footprint. Change a child Jet with
+57 -131
View File
@@ -65,52 +65,8 @@ def _level_name(level):
return ("base", "fine", "child")[level] if level < 3 else f"level {level}" return ("base", "fine", "child")[level] if level < 3 else f"level {level}"
def _legacy_levels(bounds, *, fine_block, base_block, child, child_block, child_rectangles_in_parent,
fine_objects, jets, child_objects, child_jets):
"""Normalise the original two/three-level keywords to per-level lists (original errors)."""
if child is not None and child_rectangles_in_parent is not None:
raise ValueError("child and child_rectangles_in_parent are mutually exclusive")
nested = child is not None or child_rectangles_in_parent is not None
bounds = tuple(bounds)
if not is_rectangle(bounds) and (fine_block is None or base_block is None):
raise ValueError("several patches require fine_block and base_block")
fine_objects, jets = tuple(fine_objects), tuple(jets)
child_objects, child_jets = tuple(child_objects), tuple(child_jets)
if not nested and (child_objects or child_jets):
raise ValueError("child_objects/child_jets require a nested child")
if base_block is not None and fine_block is None:
raise ValueError("base_block requires fine_block")
refinements, blocks = [bounds], [base_block, fine_block]
objects, level_jets = [(), fine_objects], [(), jets]
if nested:
if child is not None and not is_rectangle(bounds):
raise ValueError("scalar child requires one parent patch; use child_rectangles_in_parent")
if fine_block is None or base_block is None or child_block is None:
raise ValueError("a nested child needs fine_block, base_block and child_block")
if fine_objects or jets:
raise ValueError("a nested child supports bodies/Jets only on L2; use child_objects/child_jets")
refinements.append(tuple(child) if child is not None else tuple(child_rectangles_in_parent))
blocks.append(child_block)
objects.append(child_objects)
level_jets.append(child_jets)
return refinements, blocks, objects, level_jets
def _initial_levels(level_g, coarse_g, fine_g, child_g):
"""One tuple of per-level arrays from initialize()'s positional or legacy named form."""
if coarse_g is None and fine_g is None:
if child_g is not None and len(level_g) != 2:
raise TypeError("child_g needs exactly the coarse and fine arrays before it")
return tuple(level_g) + (() if child_g is None else (child_g,))
if level_g:
raise TypeError("pass level arrays positionally or as coarse_g/fine_g/child_g, not both")
if coarse_g is None or fine_g is None:
raise TypeError("coarse_g and fine_g are both required in the named form")
return (coarse_g, fine_g) + (() if child_g is None else (child_g,))
def _check_levels(refinements, blocks, objects, jets): def _check_levels(refinements, blocks, objects, jets):
"""Candidate limits shared by the list and the legacy entry points.""" """Candidate limits of the level lists, checked before any device work."""
depth = len(refinements) + 1 depth = len(refinements) + 1
if depth < 2: if depth < 2:
raise ValueError("at least one refinement level is required") raise ValueError("at least one refinement level is required")
@@ -134,7 +90,14 @@ def _check_levels(refinements, blocks, objects, jets):
class FixedPatchStepper: class FixedPatchStepper:
"""D2Q9/SRT/FP32, one unforced internal 2:1 patch in an x-periodic box. """D2Q9 SRT/MRT FP32 fixed 2:1 refinement hierarchy, built from level lists.
``refinements[l]`` gives level l+1 in level-l integer cells: one rectangle
(x0, y0, x1, y1) (FixedPatch2D, a fine window with its own origin) or a
sequence of rectangles (FixedPatchSet2D, a dependency-validated union on one
level-wide grid, public fine arrays (2*ny, 2*nx), NaN outside ACTIVE cells);
see lbm.hierarchy. ``level_blocks``, ``level_objects`` and ``level_jets``
hold one entry per level, L0 included (L0 bodies/Jets are not supported).
``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``: ``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``:
a bottom on-site slot and top fixed-density reservoir from ``cfg``. a bottom on-site slot and top fixed-density reservoir from ``cfg``.
@@ -145,68 +108,53 @@ class FixedPatchStepper:
actual channel closure footprint. Channel remapping is not supported. actual channel closure footprint. Channel remapping is not supported.
Periodic mode does not apply the wall/slot boundary fields in ``cfg``. Periodic mode does not apply the wall/slot boundary fields in ``cfg``.
Outer walls remain coarse; fine halo donors must be ordinary fluid. Outer walls remain coarse; fine halo donors must be ordinary fluid.
``fine_objects`` accepts fixed circles in coarse physical coordinates;
velocities are in coarse lattice units (dx=dt=1). Independent local copies
are projected at fine cell centers. Their complete stencil must be active
and disjoint from the actual two-hop transfer footprint.
These use the edge/cell-center frame (coarse centers I+.5), whereas ordinary Simulation objects use the integer-node frame.
``initialize(coarse_g, fine_g)`` accepts physical Q-major (9,ny,nx) arrays Bodies are fixed circles in root (L0) physical coordinates; velocities are
already in postcollision phase. It does not collide or build channel flags. in root lattice units (dx=dt=1). Independent local copies are projected at
Every ``step()`` publishes only after both levels complete on one stream. fine cell centers; their complete stencil must be active and disjoint from
On a device/launch failure the instance becomes unusable: close and recreate the actual two-hop transfer footprint. These use the edge/cell-center frame
it, rather than pretending the partially advanced fine buffer rolled back. (coarse centers I+.5), whereas ordinary Simulation objects use the
integer-node frame. Jets (body.jets.Jet) use the same frame and drive cut
links selected at their true hits; ``set_jet_amplitude(index, amplitude,
level=l)`` re-uploads the per-link velocity column only.
``initialize(*level_g)`` accepts one physical Q-major (9, ny, nx) array per
level, already in postcollision phase. It does not collide or build channel
flags. Every ``step()`` publishes only after all levels complete on one
stream. On a device/launch failure the instance becomes unusable: close and
recreate it, rather than pretending a partially advanced buffer rolled back.
A ``level_blocks`` entry (by, bx) stores that level in blocks.cu storage:
the ENTIRE level shape, including the four-cell patch ghost band, is
partitioned by a non-periodic BlockLayout2D with one-cell same-level halos.
Patch kinds stay per cell (OUTER invalid, INNER stream only, ACTIVE
collide); halos are gathered before each hop. Public arrays keep the dense
shapes; only the allocation differs. A blocked level requires blocked
refined levels below it; several L1 rectangles and depth >= 3 require all
levels blocked. Blocked bodies are stationary circles, optionally
surface-actuated: prepare_fine_boundary decides legality on the dense grid,
then cut links are staged receiver-local per hop: halo -> BFL -> step, same
buffer. A blocked parent gathers halos before receiver-local coalescence
staging and pull/collision; covered cells are poisoned on every step.
``fine_block=(by, bx)`` (opt-in) stores the fine level in
blocks.cu storage: the ENTIRE fine_shape, including the four-cell patch
ghost band, is partitioned by a non-periodic BlockLayout2D with one-cell
same-level halos. Patch kinds stay per cell (OUTER invalid, INNER stream
only, ACTIVE collide); halos are gathered before each fine hop. Public
arrays keep the dense (9, ny, nx) fine shape; only the allocation differs.
Blocked fine_objects are stationary circles, optionally surface-actuated:
prepare_fine_boundary decides legality on the dense fine grid, then cut
links are staged receiver-local per hop: halo -> BFL -> step, same buffer.
``base_block=(by, bx)`` also blocks the parent; it requires ``fine_block``.
Parent halos precede receiver-local coalescence staging and pull/collision;
covered cells are explicitly poisoned on every step. Public shapes stay dense.
``observe_raw_exchange=True`` records blocked physical cut-link exchange ``observe_raw_exchange=True`` records blocked physical cut-link exchange
without changing populations. read_raw_exchange reports the last complete without changing populations. read_raw_exchange reports the last complete
base interval or last substep in base-lattice units per unit depth. This base interval or last substep in base-lattice units per unit depth. This
is boundary momentum reaction, not a validated nozzle/stress load. Dense is boundary momentum reaction, not a validated nozzle/stress load. Dense
body layouts are not supported by this new diagnostic in this candidate. body levels are not supported by this diagnostic.
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second ``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
per-link FP64 ledger accumulated by the same kernel between per-link FP64 ledger accumulated by the same kernel between
start_exchange_window() and read_exchange_window(); disabled by default. start_exchange_window() and read_exchange_window(); disabled by default.
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
drive fine cut links selected at their true hits; ``set_jet_amplitude``
re-uploads the per-link velocity column only.
Several patches: pass ``bounds`` as a sequence of (x0, y0, x1, y1) Nested levels: a child set's coordinates index the allocated parent array,
rectangles (FixedPatchSet2D, dependency-validated union). This requires including ghost bands and holes: root edge = parent fine_origin + index / 2
fine_block and base_block; fine_objects/jets are projected once onto the (zero origin for a parent set; a scalar parent keeps its shifted window
level-wide fine grid (each circle inside the union, body ids = object origin). A scalar child requires a scalar parent. Each pair requires gap >= 4
positions, jet and writer checks level-wide), and in its own parent-cell units. All regions share one buffer pair per level
uses one level-wide fine grid (2*ny, 2*nx): public fine arrays have that and the fixed schedule of 2**l hops per base step at level l. Bodies/Jets are supported only on the
shape, NaN outside the patches' ACTIVE cells. The step sequence is the finest level of a nested hierarchy (L2 dx=dt=.25 in root units, unchanged
blocked one above, with unique union transfers in one launch each. lattice velocity); every whole circle and its BFL stencil must fit strictly
away from that level's transfer footprint.
Three-level separated unions: ``child_rectangles_in_parent=[...]`` is
mutually exclusive with scalar ``child`` and accepts either parent form.
Coordinates index the allocated L1 array, including ghost bands and holes:
root edge = patch.fine_origin + L1 index / 2. A parent set has origin zero;
a scalar parent retains its shifted window origin. The child set's public
extent is twice the allocated L1 shape, even though storage is sparse.
Each pair requires gap >= 4 in its own parent-cell units. All regions share
one buffer pair per level and the fixed 1:2:4 schedule; L2-only bodies/Jets
below apply unchanged, with object IDs assigned over the whole level.
Three-level single chain: ``child`` bounds use L1 window cells; all three
levels must be blocked. ``child_objects`` and ``child_jets`` use the L0
edge frame, with L2 dx=dt=.25 and unchanged lattice velocity. L1 bodies
and Jets remain unsupported in a chain. Every whole circle and its BFL
stencil must fit strictly away from the L2 transfer footprint. L2 runs
halo -> BFL -> step four times per base step. Runtime child Jet changes
require ``set_jet_amplitude(index, amplitude, level=2)``.
``explosion="projected_linear"`` selects experimental transverse linear ``explosion="projected_linear"`` selects experimental transverse linear
reconstruction on each adjacent pair. Default "constant" is unchanged. reconstruction on each adjacent pair. Default "constant" is unchanged.
@@ -214,43 +162,21 @@ class FixedPatchStepper:
bodies/Jets or LES; this backend supplies no forcing. Geometry is generic, bodies/Jets or LES; this backend supplies no forcing. Geometry is generic,
but union/multilevel GPU and matched-physics qualification remain pending. but union/multilevel GPU and matched-physics qualification remain pending.
Level lists (any depth): ``refinements[l]`` gives level l+1 in level-l One recursive hop advances every level: a child explodes from its parent's
integer cells (scalar window or rectangle list, see lbm.hierarchy); current hop source, takes two hops, then the parent gathers halos, stages
``level_blocks``/``level_objects``/``level_jets`` hold one entry per level, restriction, applies BFL and steps.
L0 included. They are mutually exclusive with the legacy keywords above,
which are normalised to the same lists. One recursive hop advances every
level: a child explodes from its parent's current hop source, takes two
hops, then the parent gathers halos, stages restriction, applies BFL and
steps. Depth >= 3 must be fully blocked with bodies/Jets on the finest level.
""" """
def __init__(self, cfg, bounds=None, *, context, coarse_boundary="periodic", fine_objects=(), def __init__(self, cfg, *, context, refinements, coarse_boundary="periodic",
fine_block=None, base_block=None, jets=(), child=None, child_block=None, level_blocks=None, level_objects=None, level_jets=None,
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"): observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"):
if HALO != 4: if HALO != 4:
raise RuntimeError("refinement.cu requires a four-cell fine halo") raise RuntimeError("refinement.cu requires a four-cell fine halo")
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"): if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel") raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel")
if refinements is None: depth = len(refinements) + 1
if bounds is None: level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
raise ValueError("bounds or refinements is required") level_objects = [()] * depth if level_objects is None else list(level_objects)
if level_blocks is not None or level_objects is not None or level_jets is not None: level_jets = [()] * depth if level_jets is None else list(level_jets)
raise ValueError("level_blocks/level_objects/level_jets require refinements")
refinements, level_blocks, level_objects, level_jets = _legacy_levels(
bounds, fine_block=fine_block, base_block=base_block, child=child,
child_block=child_block, child_rectangles_in_parent=child_rectangles_in_parent,
fine_objects=fine_objects, jets=jets, child_objects=child_objects,
child_jets=child_jets)
else:
if (bounds is not None or child is not None or child_rectangles_in_parent is not None
or fine_block is not None or base_block is not None or child_block is not None
or tuple(fine_objects) or tuple(jets) or tuple(child_objects) or tuple(child_jets)):
raise ValueError("refinements and the legacy patch keywords are mutually exclusive")
depth = len(refinements) + 1
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
level_objects = [()] * depth if level_objects is None else list(level_objects)
level_jets = [()] * depth if level_jets is None else list(level_jets)
level_objects = [tuple(o) for o in level_objects] level_objects = [tuple(o) for o in level_objects]
level_jets = [tuple(j) for j in level_jets] level_jets = [tuple(j) for j in level_jets]
level_blocks = [None if b is None else tuple(b) for b in level_blocks] level_blocks = [None if b is None else tuple(b) for b in level_blocks]
@@ -540,13 +466,13 @@ class FixedPatchStepper:
if self._ddf_shifting and any(layout is None for layout in self._layouts): if self._ddf_shifting and any(layout is None for layout in self._layouts):
raise ValueError("shifted populations require all-blocked levels") raise ValueError("shifted populations require all-blocked levels")
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None): def initialize(self, *level_g):
"""Upload physical postcollision G, encoding before any FP32 rounding. """Upload physical postcollision G, encoding before any FP32 rounding.
Shifted input should be physical FP64 to retain small perturbations. Shifted input should be physical FP64 to retain small perturbations.
Public initialization resets the step count; non-leaves are discarded. Public initialization resets the step count; non-leaves are discarded.
""" """
given = _initial_levels(level_g, coarse_g, fine_g, child_g) given = tuple(level_g)
with self._lock, self.ctx.activate(): with self._lock, self.ctx.activate():
self._require("new", "ready") self._require("new", "ready")
self._check_storage() self._check_storage()
@@ -315,21 +315,23 @@ class ConstructorAndSchedule(unittest.TestCase):
class InitialLevels(unittest.TestCase): class RemovedLegacyAPI(unittest.TestCase):
"""initialize() entry adapter: positional per-level arrays or the legacy names, never both.""" """The pre-level-list constructor/initialize forms are gone and fail loudly."""
def test_forms(self): def test_removed_constructor_forms_raise_type_error(self):
a, b, c = object(), object(), object() import inspect
f = host._initial_levels context = Mock()
self.assertEqual(f((a, b), None, None, None), (a, b)) with self.assertRaises(TypeError): # positional bounds
self.assertEqual(f((a, b, c), None, None, None), (a, b, c)) host.FixedPatchStepper(CFG, PARENTS, context=context)
self.assertEqual(f((a, b), None, None, c), (a, b, c)) # old initialize(c, f, child_g=g) removed = dict(bounds=PARENTS, fine_block=(4, 4), base_block=(4, 4), fine_objects=(),
self.assertEqual(f((), a, b, None), (a, b)) jets=(), child=(8, 8, 16, 16), child_block=(4, 4), child_objects=(),
self.assertEqual(f((), a, b, c), (a, b, c)) child_jets=(), child_rectangles_in_parent=CHILDREN)
for args in (((a,), None, None, c), ((a, b, c), None, None, c), ((a,), a, b, None), for name, value in removed.items():
((), a, None, None), ((), None, b, c)): with self.subTest(keyword=name), self.assertRaises(TypeError):
with self.subTest(args=args), self.assertRaises(TypeError): host.FixedPatchStepper(CFG, context=context, **levels(), **{name: value})
f(*args) context.activate.assert_not_called()
self.assertEqual(list(inspect.signature(host.FixedPatchStepper.initialize).parameters),
["self", "level_g"]) # no coarse_g/fine_g/child_g
if __name__=='__main__': if __name__=='__main__':