FixedPatchStepper(..., child=(x0,y0,x1,y1), child_block=(by,bx)) adds an L2 patch in L1 window cells (requires fine_block/base_block, one L1 patch, no bodies/jets). nest_patch() builds the L1->L2 FixedPatch2D (2*nu on L1) and the composed L1 kind (inherited INNER/OUTER kept, child-covered ACTIVE made OUTER), rejecting from the actual tables: coverage outside ACTIVE, donors or receivers not uncovered ACTIVE, surviving ACTIVE pulls of covered/OUTER cells without a child-supplied arrival, and overlap with L1's restriction footprint. Step order per L1 hop: explode L1(src)->L2, two L2 hops (halo, step), L1 halo, receiver-local L2->L1 staging into src, L1 step. The second hop thus feeds L2 from the new L1 G; L2 is never refilled from L0; L0 stays once per step (1:2:4). Existing kernels only; a small _Pair holds the child transfer bindings (explode from the child module, parent window stride). Two-level API unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
4.6 KiB
Python
90 lines
4.6 KiB
Python
"""Nested three-level FixedPatchStepper (child=..., single chain): CPU guards and GPU gates.
|
|
|
|
Fixture = reviewed CPU probe: L0 16x16 periodic, L1 (4,4,12,12) -> 24x24 window,
|
|
L2 (8,8,16,16) in L1 window cells -> 24x24 window; 4x4 blocks on every level.
|
|
Run CPU and GPU classes in separate processes. GPU gates (frozen before any run):
|
|
- uniform moving equilibrium stays on all three levels' ACTIVE leaves, |dG| <= 1e-6;
|
|
- seeded non-equilibrium: every level's ACTIVE leaves finite, raw owner cores NaN on
|
|
OUTER/covered (INNER partly invalid by contract, not asserted);
|
|
- each L1 hop feeds L2 from the L1 buffer it is about to step (current, then the new
|
|
G written by the first hop), never from L0: 2 child advances per coarse step.
|
|
Numerical comparison against the CPU probe (G vs C(f)) is done by the acceptance run.
|
|
"""
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import numpy as np
|
|
import pycuda.driver as cuda
|
|
|
|
from CelerisLab.config import LBMConfig
|
|
from CelerisLab.lbm.equilibrium import compute_feq_d2q9
|
|
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST
|
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
|
from test_refinement_blocked_fine import state, circle
|
|
from test_refinement_blocked_base import raw
|
|
|
|
CFG = LBMConfig(nx=16, ny=16, viscosity=.1)
|
|
L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
|
|
KW = dict(fine_block=(4, 4), base_block=(4, 4), child=L2, child_block=(4, 4))
|
|
|
|
|
|
class ThreeLevelCPU(unittest.TestCase):
|
|
def test_rejections_before_cuda(self):
|
|
context = object() # never activated: every case must fail first
|
|
bad = (dict(KW, child_block=None), dict(KW, fine_block=None),
|
|
dict(KW, fine_objects=[circle()]), dict(KW, child=(4, 8, 16, 16)),
|
|
dict(KW, child_block=(5, 4)))
|
|
for kw in bad:
|
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
|
FixedPatchStepper(CFG, L1, context=context, **kw)
|
|
with self.assertRaises(ValueError): # several L1 patches + child: not yet
|
|
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), ((4, 4, 12, 12), (20, 4, 28, 12)),
|
|
context=context, **KW)
|
|
|
|
|
|
class ThreeLevelGPU(unittest.TestCase):
|
|
def test_equilibrium_is_kept_on_every_level(self):
|
|
from CelerisLab.cuda.context import CudaContext
|
|
feq = np.asarray(compute_feq_d2q9(1.0, .03, -.02), np.float32)[:, None, None]
|
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
|
|
shapes = ((16, 16), run.patch.fine_shape, run.child_patch.fine_shape)
|
|
run.initialize(*(np.broadcast_to(feq, (9, *s)).copy() for s in shapes))
|
|
run.step(8)
|
|
self.assertEqual(run.step_count, 8)
|
|
for level, g in enumerate(run.download()):
|
|
leaf = np.isfinite(g[0])
|
|
self.assertTrue(leaf.any(), f"level {level}")
|
|
np.testing.assert_allclose(g[:, leaf], np.broadcast_to(feq, g.shape)[:, leaf],
|
|
atol=1e-6, rtol=0, err_msg=f"level {level}")
|
|
|
|
def test_nonequilibrium_masks_and_fresh_middle_feeds(self):
|
|
from CelerisLab.cuda.context import CudaContext
|
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
|
|
p1, p2 = run.patch, run.child_patch
|
|
run.initialize(state(16, 16, 1), state(*p1.fine_shape, 2), state(*p2.fine_shape, 3))
|
|
f = run._levels[1]
|
|
feeds, original = [], run._advance_child
|
|
def record(buffer, **kw):
|
|
feeds.append(int(buffer))
|
|
original(buffer, **kw)
|
|
with patch.object(run, "_advance_child", side_effect=record):
|
|
cur, spare = int(f.current), int(f.spare)
|
|
run.step(1)
|
|
self.assertEqual(feeds, [cur, spare]) # current G, then the new G of hop 1
|
|
run.step(7)
|
|
middle = run._middle_kind
|
|
for g, leaf in zip(run.download(), (~p1.covered, middle == ACTIVE, p2.fine_kind == ACTIVE)):
|
|
self.assertTrue(np.isfinite(g[:, leaf]).all())
|
|
self.assertTrue(np.isnan(g[:, ~leaf]).all())
|
|
rc, rf, rg = raw(run)
|
|
self.assertTrue(np.isnan(rc[:, p1.covered]).all())
|
|
self.assertTrue(np.isfinite(rf[:, middle == ACTIVE]).all())
|
|
self.assertTrue(np.isnan(rf[:, middle == OUTER_GHOST]).all()) # incl. child-covered
|
|
self.assertTrue(np.isnan(rf[:, p2.covered]).all())
|
|
self.assertTrue(np.isfinite(rg[:, p2.fine_kind == ACTIVE]).all())
|
|
self.assertTrue(np.isnan(rg[:, p2.fine_kind == OUTER_GHOST]).all())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|