Three levels: nested body-free child patch on a single chain
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6301887093
commit
76555de4b8
@@ -0,0 +1,89 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user