Multipatch: validate fine block before modulo, raw-core GPU checks, README usage

FixedPatchSet2D.fine_layout rejects non-positive, float, bool or wrong-length
block sizes with ValueError before any modulo (was ZeroDivisionError/TypeError).
The face gap-4 GPU test now checks raw owner cores directly by kind/covered
for every patch (ACTIVE finite, OUTER/holes/covered NaN; INNER not asserted),
not only the masked download and reversed-order parity. README documents the
body-free multipatch call, scope and level-wide fine shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-16 14:22:00 +00:00
1 parent 0397b6ebb7
commit 6301887093
4 files changed
+47 -5

No files matched your search

+20 -2
View File
@@ -123,7 +123,7 @@ with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
``` ```
Scope and limits: Scope and limits:
- D2Q9, SRT, FP32, double buffer, unshifted, no LES or forcing; one fixed 2:1 patch. - D2Q9, SRT, FP32, double buffer, unshifted, no LES or forcing; fixed 2:1 patch(es), two levels.
- `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top - `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top
reservoir; patch needs `y0 >= 4`, `y1 <= ny-4`). Periodic mode ignores the wall/slot config fields. reservoir; patch needs `y0 >= 4`, `y1 <= ny-4`). Periodic mode ignores the wall/slot config fields.
- Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`), - Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`),
@@ -135,8 +135,26 @@ Scope and limits:
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 separated patches (body-free): pass a sequence of `(x0, y0, x1, y1)`
rectangles as `bounds`, at least 4 coarse cells apart, with both `fine_block`
and `base_block`. 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
patch windows (active region plus 4-cell ghost band, origin `(2*y0-4, 2*x0-4)`)
are stored; each window must align with `fine_block`. Order of rectangles does
not matter. No bodies/Jets, wall-touching patches or nesting yet.
```python
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
with CudaContext(0) as ctx, FixedPatchStepper(
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx,
fine_block=(4, 4), base_block=(4, 4)) as run:
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
run.step(32)
coarse_g, fine_g = run.download() # NaN outside every patch's active cells
```
- Not available: per-link surface exchange/force readout in blocked mode, - Not available: per-link surface exchange/force readout in blocked mode,
checkpoints, moving bodies, multiple patches or dynamic refinement, `Simulation` checkpoints, moving bodies, bodies/Jets with several patches, dynamic refinement, `Simulation`
integration. Validation so far is storage/operator equivalence and short runs, integration. Validation so far is storage/operator equivalence and short runs,
not accuracy, convergence or long-time stability. not accuracy, convergence or long-time stability.
+5 -1
View File
@@ -237,7 +237,11 @@ class FixedPatchSet2D:
def fine_layout(self, block): def fine_layout(self, block):
"""Sparse non-periodic layout storing exactly the patch supports.""" """Sparse non-periodic layout storing exactly the patch supports."""
from .blocks import BlockLayout2D from .blocks import BlockLayout2D
by, bx = block block = tuple(block)
if len(block) != 2 or any(isinstance(v, (bool, np.bool_)) or not isinstance(v, Integral)
or v <= 0 for v in block):
raise ValueError("fine block must be two positive integers (by, bx)")
by, bx = map(int, block)
origins = set() origins = set()
for p in self.patches: for p in self.patches:
y0, x0 = p.fine_level_origin y0, x0 = p.fine_level_origin
+3
View File
@@ -228,6 +228,9 @@ class FixedPatchSetTests(unittest.TestCase):
self.assertTrue(layout.owned.ravel()[s.ghost_fine.astype(np.intp)].all()) self.assertTrue(layout.owned.ravel()[s.ghost_fine.astype(np.intp)].all())
with self.assertRaisesRegex(ValueError, "aligned"): with self.assertRaisesRegex(ValueError, "aligned"):
self.build(self.FACE).fine_layout((5, 4)) self.build(self.FACE).fine_layout((5, 4))
for block in ((0, 4), (4, 0), (-4, 4), (4.0, 4), (True, 4), (4,), (4, 4, 4), (np.bool_(1), 4)):
with self.subTest(block=block), self.assertRaisesRegex(ValueError, "positive integers"):
self.build(self.FACE).fine_layout(block)
if __name__ == "__main__": if __name__ == "__main__":
@@ -6,7 +6,8 @@ Run CPU and GPU classes in separate processes. GPU gates (frozen before any run)
- S1 translation copy: two patches on 16x32 equal the 16x16 one-patch run tiled - S1 translation copy: two patches on 16x32 equal the 16x16 one-patch run tiled
twice, bit for bit, every step; twice, bit for bit, every step;
- reversed rectangle order gives bit-identical results; - reversed rectangle order gives bit-identical results;
- face gap-4 pair (16x24): ACTIVE leaves finite, covered/hole masks exact. - face gap-4 pair (16x24): ACTIVE leaves finite, covered/hole masks exact, also on
raw owner cores per patch (INNER partly invalid by contract, not asserted).
Weighted leaf mass is printed as a diagnostic only. Weighted leaf mass is printed as a diagnostic only.
""" """
from dataclasses import replace from dataclasses import replace
@@ -16,7 +17,7 @@ import numpy as np
import pycuda.driver as cuda import pycuda.driver as cuda
from CelerisLab.config import LBMConfig from CelerisLab.config import LBMConfig
from CelerisLab.lbm.refinement import ACTIVE, FixedPatch2D, FixedPatchSet2D from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from test_refinement_blocked_fine import BOUNDS, NX, NY, config, state, circle from test_refinement_blocked_fine import BOUNDS, NX, NY, config, state, circle
from test_refinement_blocked_base import exact, raw from test_refinement_blocked_base import exact, raw
@@ -112,6 +113,9 @@ class MultipatchGPU(unittest.TestCase):
fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)]) fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)])
kw = dict(fine_block=(4, 4), base_block=(4, 4)) kw = dict(fine_block=(4, 4), base_block=(4, 4))
leaf = s.fine_kind == ACTIVE leaf = s.fine_kind == ACTIVE
support = s.fine_layout((4, 4)).owned
outer = support & (s.fine_kind == OUTER_GHOST)
holes = ~support
with CudaContext(0) as context: with CudaContext(0) as context:
runs = [self.run_steps(context, cfg, bounds, coarse, fine, **kw) runs = [self.run_steps(context, cfg, bounds, coarse, fine, **kw)
for bounds in (FACE, FACE[::-1])] for bounds in (FACE, FACE[::-1])]
@@ -123,6 +127,19 @@ class MultipatchGPU(unittest.TestCase):
self.assertTrue(np.isnan(c[:, s.covered]).all()) self.assertTrue(np.isnan(c[:, s.covered]).all())
self.assertTrue(np.isfinite(f[:, leaf]).all()) self.assertTrue(np.isfinite(f[:, leaf]).all())
self.assertTrue(np.isnan(f[:, ~leaf]).all()) self.assertTrue(np.isnan(f[:, ~leaf]).all())
# Raw owner cores, independent of the download mask (every patch).
# INNER may be partly NaN after hop 2 by contract: not asserted.
rc, rf = a[2], a[3]
label = f"raw step {step}"
self.assertTrue(np.isfinite(rc[:, ~s.covered]).all(), label + " coarse leaves")
if step: # covered parent cores are poisoned by the parent step
self.assertTrue(np.isnan(rc[:, s.covered]).all(), label + " covered")
self.assertTrue(np.isfinite(rf[:, leaf]).all(), label + " fine ACTIVE")
self.assertTrue(np.isnan(rf[:, outer]).all(), label + " fine OUTER")
self.assertTrue(np.isnan(rf[:, holes]).all(), label + " fine holes")
for i, p in enumerate(s.patches):
active = window(s, i, rf)[:, p.fine_kind == ACTIVE]
self.assertTrue(np.isfinite(active).all(), f"{label} patch {i} ACTIVE")
mass = float(np.nansum(c, dtype=np.float64) + .25 * np.nansum(f, dtype=np.float64)) mass = float(np.nansum(c, dtype=np.float64) + .25 * np.nansum(f, dtype=np.float64))
print(f"face gap4 step {step}: weighted leaf mass {mass:.9g} (diagnostic)", flush=True) print(f"face gap4 step {step}: weighted leaf mass {mass:.9g} (diagnostic)", flush=True)