diff --git a/README.md b/README.md index 5070a1a..5445ae5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/CelerisLab/lbm/refinement.py b/src/CelerisLab/lbm/refinement.py index ab91291..713960d 100644 --- a/src/CelerisLab/lbm/refinement.py +++ b/src/CelerisLab/lbm/refinement.py @@ -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 diff --git a/tests/unit/test_refinement.py b/tests/unit/test_refinement.py index a932042..4fcfe17 100644 --- a/tests/unit/test_refinement.py +++ b/tests/unit/test_refinement.py @@ -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__": diff --git a/tests/validation/multigrid/test_refinement_multipatch.py b/tests/validation/multigrid/test_refinement_multipatch.py index 8468aa9..3891ad3 100644 --- a/tests/validation/multigrid/test_refinement_multipatch.py +++ b/tests/validation/multigrid/test_refinement_multipatch.py @@ -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)