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:
Claude
2026-10-16 14:22:00 +00:00
co-authored by Claude Opus 5.5
parent 0397b6ebb7
commit 6301887093
4 changed files with 47 additions and 5 deletions
+20 -2
View File
@@ -123,7 +123,7 @@ with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
```
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
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)`),
@@ -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
four-cell ghost band. It gives results identical to dense storage and is
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,
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,
not accuracy, convergence or long-time stability.
+5 -1
View File
@@ -237,7 +237,11 @@ class FixedPatchSet2D:
def fine_layout(self, block):
"""Sparse non-periodic layout storing exactly the patch supports."""
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()
for p in self.patches:
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())
with self.assertRaisesRegex(ValueError, "aligned"):
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__":
@@ -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
twice, bit for bit, every step;
- 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.
"""
from dataclasses import replace
@@ -16,7 +17,7 @@ import numpy as np
import pycuda.driver as cuda
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 test_refinement_blocked_fine import BOUNDS, NX, NY, config, state, circle
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)])
kw = dict(fine_block=(4, 4), base_block=(4, 4))
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:
runs = [self.run_steps(context, cfg, bounds, coarse, fine, **kw)
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.isfinite(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))
print(f"face gap4 step {step}: weighted leaf mass {mass:.9g} (diagnostic)", flush=True)