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
@@ -299,6 +299,51 @@ def _transfer_footprint(patch):
|
||||
return fine, coarse, _readonly(sorted(reads), np.int64)
|
||||
|
||||
|
||||
def nest_patch(cfg, parent, child_bounds):
|
||||
"""Child FixedPatch2D inside a fine patch window, plus the composed middle kind.
|
||||
|
||||
``cfg`` is the base (L0) configuration and ``parent`` the L0->L1
|
||||
FixedPatch2D; ``child_bounds`` (x0, y0, x1, y1) are in L1 window cells
|
||||
(its dense fine grid, ghost band included). Returns (child, kind): the
|
||||
L1->L2 FixedPatch2D (viscosity 2*nu on L1) and the L1 kind array with
|
||||
inherited INNER/OUTER kept and only the child-covered ACTIVE cells made
|
||||
OUTER (invalid). Rejects, from the actual tables before any allocation:
|
||||
coverage outside inherited ACTIVE; explosion donors or coalescence
|
||||
receivers that are not uncovered ACTIVE; any surviving ACTIVE pull that
|
||||
would read a covered or OUTER cell without a child-supplied arrival; and
|
||||
child coverage of L1's own two-hop restriction footprint toward L0.
|
||||
"""
|
||||
fy, fx = parent.fine_shape
|
||||
middle = replace(cfg, nx=fx, ny=fy, viscosity=2 * cfg.viscosity, y_wall_bc="bounce_back",
|
||||
top_wall_bc="velocity", wall_slot_width=0, wall_normal_velocity=0.0,
|
||||
velocity=0.0)
|
||||
child = FixedPatch2D.from_config(middle, child_bounds)
|
||||
kind = parent.fine_kind
|
||||
covered = child.covered
|
||||
active = (kind == ACTIVE) & ~covered
|
||||
if not (kind[covered] == ACTIVE).all():
|
||||
raise ValueError("child must cover only inherited ACTIVE cells")
|
||||
cells = fx * fy
|
||||
receivers = child.coalesce_coarse % np.uint64(cells)
|
||||
if (not active.ravel()[child.ghost_coarse].all()
|
||||
or not active.ravel()[receivers.astype(np.intp)].all()):
|
||||
raise ValueError("child donors/receivers must be uncovered parent ACTIVE cells")
|
||||
supplied = set(map(int, child.coalesce_coarse))
|
||||
invalid = covered | (kind == OUTER_GHOST)
|
||||
for y, x in zip(*np.nonzero(active)):
|
||||
for q in range(1, 9):
|
||||
sy, sx = y - D2Q9_EY[q], x - D2Q9_EX[q]
|
||||
inside = 0 <= sy < fy and 0 <= sx < fx
|
||||
if (not inside or invalid[sy, sx]) and q * cells + y * fx + x not in supplied:
|
||||
raise ValueError("a surviving parent ACTIVE pull would read an invalid cell")
|
||||
footprint, _, _ = _transfer_footprint(parent)
|
||||
if covered.ravel()[sorted(footprint)].any():
|
||||
raise ValueError("child covers the parent's restriction footprint")
|
||||
composed = kind.copy()
|
||||
composed[covered] = OUTER_GHOST
|
||||
return child, _readonly(composed, np.uint8)
|
||||
|
||||
|
||||
def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=()):
|
||||
"""Project fixed circles and reject boundary/transfer overlap before CUDA.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from .curved_links import surface_link_velocity
|
||||
from ..body.jets import jet_link_shapes, jet_link_velocity
|
||||
from .block_stepper import MAX_GRID_DIM
|
||||
from .refinement import (ACTIVE, HALO, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D,
|
||||
prepare_fine_boundary)
|
||||
nest_patch, prepare_fine_boundary)
|
||||
from .descriptors import SOLID, D2Q9_EX, D2Q9_EY
|
||||
|
||||
|
||||
@@ -31,15 +31,36 @@ def blocked_fine_tables(patch, layout, order):
|
||||
unused halo entries OUTER). Coarse donors/targets are unchanged.
|
||||
"""
|
||||
owner = layout.owner.ravel().astype(np.intp)
|
||||
owned = layout.owned.ravel() # sparse multipatch levels leave holes unstored
|
||||
cells = patch.fine_shape[0] * patch.fine_shape[1]
|
||||
q, cell = np.divmod(patch.coalesce_fine[order], np.uint64(cells))
|
||||
kind = np.full(layout.nblocks * int(np.prod(layout.local_shape)), OUTER_GHOST, np.uint8)
|
||||
kind[owner[owned]] = patch.fine_kind.ravel()[owned]
|
||||
kind = blocked_kind(layout, patch.fine_kind)
|
||||
return (owner[patch.ghost_fine.astype(np.intp)].astype(np.uint64),
|
||||
layout.slot(owner[cell.astype(np.intp)], q), kind)
|
||||
|
||||
|
||||
def blocked_kind(layout, dense_kind):
|
||||
"""Per-block-cell kind; unused halo entries and unstored holes stay OUTER."""
|
||||
owner = layout.owner.ravel().astype(np.intp)
|
||||
owned = layout.owned.ravel() # sparse multipatch levels leave holes unstored
|
||||
kind = np.full(layout.nblocks * int(np.prod(layout.local_shape)), OUTER_GHOST, np.uint8)
|
||||
kind[owner[owned]] = np.asarray(dense_kind).ravel()[owned]
|
||||
return kind
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Pair:
|
||||
"""Device transfer tables of one nested parent->child relationship."""
|
||||
explode: object # BlockExplode from the CHILD module (child TOTAL_CELLS)
|
||||
coalesce: object
|
||||
ghosts: object
|
||||
donors: object
|
||||
targets: object # receiver-local staged parent slots
|
||||
children: object
|
||||
n_ghosts: int
|
||||
n_rows: int
|
||||
parent_cells: int # parent block window cells (explode donor stride)
|
||||
|
||||
|
||||
def blocked_base_tables(patch, layout, order, wall_aware=False):
|
||||
"""Parent donor cells and receiver-local arrival slots, in transfer row order.
|
||||
|
||||
@@ -119,7 +140,7 @@ class FixedPatchStepper:
|
||||
blocked one above, with every patch's transfers in one launch each.
|
||||
"""
|
||||
def __init__(self, cfg, bounds, *, context, coarse_boundary="periodic", fine_objects=(),
|
||||
fine_block=None, base_block=None, jets=()):
|
||||
fine_block=None, base_block=None, jets=(), child=None, child_block=None):
|
||||
if HALO != 4:
|
||||
raise RuntimeError("refinement.cu requires a four-cell fine halo")
|
||||
if coarse_boundary not in ("periodic", "slot_reservoir"):
|
||||
@@ -165,6 +186,22 @@ class FixedPatchStepper:
|
||||
if (self.fine_layout.nblocks > MAX_GRID_DIM
|
||||
or self.fine_layout.local_shape[0] > MAX_GRID_DIM):
|
||||
raise ValueError("fine blocks exceed launch limits")
|
||||
self.child_patch = self.child_layout = None
|
||||
self._middle_kind = None
|
||||
if child is not None:
|
||||
if multipatch or fine_block is None or base_block is None or child_block is None:
|
||||
raise ValueError("a nested child needs one patch plus fine_block, base_block "
|
||||
"and child_block")
|
||||
if tuple(fine_objects) or tuple(jets):
|
||||
raise ValueError("a nested child does not support fine_objects or jets yet")
|
||||
if len(child_block) != 2:
|
||||
raise ValueError("child_block must be (by, bx)")
|
||||
self.child_patch, self._middle_kind = nest_patch(cfg, self.patch, child)
|
||||
self.child_layout = BlockLayout2D.build(*self.child_patch.fine_shape, *child_block,
|
||||
periodic=(False, False))
|
||||
if (self.child_layout.nblocks > MAX_GRID_DIM
|
||||
or self.child_layout.local_shape[0] > MAX_GRID_DIM):
|
||||
raise ValueError("child blocks exceed launch limits")
|
||||
self._jets = tuple(jets)
|
||||
self.fine_boundary = prepare_fine_boundary(
|
||||
cfg, self.patch, fine_objects, wall_aware=self._wall_aware, jets=self._jets)
|
||||
@@ -173,7 +210,11 @@ class FixedPatchStepper:
|
||||
fluid_idx, dirs, q, _, _, _, _, fallback = self.fine_boundary.curved_host
|
||||
self._fine_tables, _ = blocked_link_tables(
|
||||
self.fine_layout, self.fine_boundary.flags, fluid_idx, dirs, q, fallback)
|
||||
self._fine_leaf = self.patch.fine_kind == ACTIVE
|
||||
self._fine_leaf = (self.patch.fine_kind if self._middle_kind is None
|
||||
else self._middle_kind) == ACTIVE
|
||||
self._child_leaf = (None if self.child_patch is None
|
||||
else self.child_patch.fine_kind == ACTIVE)
|
||||
self._child = None
|
||||
if self.fine_boundary is not None:
|
||||
self._fine_leaf &= (self.fine_boundary.flags.reshape(self.patch.fine_shape) & SOLID) == 0
|
||||
self.ctx = context
|
||||
@@ -208,9 +249,12 @@ class FixedPatchStepper:
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self._stream = cuda.Stream()
|
||||
for index, (config, layout, tau, name) in enumerate((
|
||||
(replace(cfg), self.base_layout, self.patch.tau_coarse, "PatchCoarseStep"),
|
||||
(fine_cfg, self.fine_layout, self.patch.tau_fine, "PatchFineStep"))):
|
||||
specs = [(replace(cfg), self.base_layout, self.patch.tau_coarse, "PatchCoarseStep"),
|
||||
(fine_cfg, self.fine_layout, self.patch.tau_fine, "PatchFineStep")]
|
||||
if self.child_patch is not None:
|
||||
specs.append((replace(fine_cfg, viscosity=4 * cfg.viscosity), self.child_layout,
|
||||
self.child_patch.tau_fine, "PatchFineStep"))
|
||||
for index, (config, layout, tau, name) in enumerate(specs):
|
||||
source = "refinement.cu"
|
||||
if layout is not None:
|
||||
config = replace(config, nx=layout.local_shape[1], ny=layout.local_shape[0],
|
||||
@@ -233,7 +277,8 @@ class FixedPatchStepper:
|
||||
if size != struct.calcsize("=ffffI"):
|
||||
raise RuntimeError("LBMParams host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=ffffI", config.omega, 0, 0, 0, n_objects))
|
||||
shape = (9, *(self.patch.coarse_shape if index == 0 else self.patch.fine_shape))
|
||||
shape = (9, *(self.patch.coarse_shape, self.patch.fine_shape,
|
||||
self.child_patch and self.child_patch.fine_shape)[index])
|
||||
size = int(np.prod(shape if layout is None else layout.blocked_shape)) * 4
|
||||
grid = ((config.nx + self._block[0] - 1) // self._block[0], config.ny,
|
||||
1 if layout is None else layout.nblocks)
|
||||
@@ -252,7 +297,7 @@ class FixedPatchStepper:
|
||||
cuda.memcpy_htod(ptr, struct.pack("=IIIf", cfg.nx, cfg.ny,
|
||||
cfg.wall_slot_width, cfg.wall_normal_velocity))
|
||||
p = self.patch
|
||||
c, f = self._levels
|
||||
c, f = self._levels[:2]
|
||||
order = np.argsort(p.coalesce_coarse)
|
||||
fine_tables = (None if f.layout is None else
|
||||
blocked_fine_tables(p, f.layout, order))
|
||||
@@ -268,10 +313,23 @@ class FixedPatchStepper:
|
||||
self._explode = f.module.get_function("PatchExplode" if fine_tables is None
|
||||
else "BlockExplode")
|
||||
if fine_tables is not None:
|
||||
f.kind = self._upload(fine_tables[2])
|
||||
f.kind = self._upload(fine_tables[2] if self._middle_kind is None
|
||||
else blocked_kind(f.layout, self._middle_kind))
|
||||
if base_tables is not None:
|
||||
c.kind = self._upload(base_tables[2])
|
||||
self._coalesce = c.module.get_function("BlockCoalesce")
|
||||
if self.child_patch is not None:
|
||||
g, cp = self._levels[2], self.child_patch
|
||||
order = np.argsort(cp.coalesce_coarse)
|
||||
ghosts, children, g_kind = blocked_fine_tables(cp, g.layout, order)
|
||||
donors, targets, _ = blocked_base_tables(cp, f.layout, order)
|
||||
g.kind = self._upload(g_kind)
|
||||
g.halo_copy = g.module.get_function("BlockHaloCopy")
|
||||
self._child = _Pair(g.module.get_function("BlockExplode"),
|
||||
f.module.get_function("BlockCoalesce"),
|
||||
self._upload(ghosts), self._upload(donors.astype(np.uint64)),
|
||||
self._upload(targets), self._upload(children),
|
||||
len(ghosts), len(targets), int(np.prod(f.layout.local_shape)))
|
||||
if self._fine_tables is not None:
|
||||
self._fine_flags = self._upload(self._fine_tables["flags"])
|
||||
self._link_tables = {k: self._upload(v) for k, v in self._fine_tables.items()
|
||||
@@ -314,24 +372,28 @@ class FixedPatchStepper:
|
||||
def step_count(self):
|
||||
return self._step_count
|
||||
|
||||
def _mask_nonleaves(self, coarse, fine):
|
||||
def _mask_nonleaves(self, coarse, fine, child=None):
|
||||
coarse[:, self.patch.covered] = np.nan
|
||||
if self._wall_aware:
|
||||
coarse[:, (0, -1), :] = np.nan
|
||||
fine[:, ~self._fine_leaf] = np.nan
|
||||
if child is not None:
|
||||
child[:, ~self._child_leaf] = np.nan
|
||||
|
||||
def initialize(self, coarse_g, fine_g):
|
||||
"""Upload an explicit G_0; invalid non-leaves are discarded."""
|
||||
def initialize(self, coarse_g, fine_g, child_g=None):
|
||||
"""Upload an explicit G_0 (child_g only with a nested child); non-leaves are discarded."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("new", "ready")
|
||||
arrays = [np.array(a, dtype=np.float32, order="C", copy=True)
|
||||
for a in (coarse_g, fine_g)]
|
||||
given = (coarse_g, fine_g) + (() if child_g is None else (child_g,))
|
||||
if len(given) != len(self._levels):
|
||||
raise ValueError("Expected one Q-major array per level")
|
||||
arrays = [np.array(a, dtype=np.float32, order="C", copy=True) for a in given]
|
||||
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
||||
raise ValueError("Expected Q-major (9,ny,nx) arrays for both levels")
|
||||
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
||||
leaf = ~self.patch.covered
|
||||
if self._wall_aware:
|
||||
leaf[[0, -1]] = False
|
||||
for array, mask in zip(arrays, (leaf, self._fine_leaf)):
|
||||
for array, mask in zip(arrays, (leaf, self._fine_leaf, self._child_leaf)):
|
||||
if not np.isfinite(array[:, mask]).all() or (array[:, mask].sum(axis=0) <= 0).any():
|
||||
raise ValueError("Leaf populations must be finite with positive density")
|
||||
self._mask_nonleaves(*arrays)
|
||||
@@ -412,6 +474,16 @@ class FixedPatchStepper:
|
||||
np.uint32(count), velocity if velocity is not None else np.uintp(0),
|
||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
|
||||
def _advance_child(self, parent_buffer, **kw):
|
||||
"""Explode from the given L1 buffer, then two L2 hops (halo -> step)."""
|
||||
pair, g = self._child, self._levels[2]
|
||||
pair.explode(parent_buffer, g.current, pair.ghosts, pair.donors, np.uint32(pair.n_ghosts),
|
||||
np.uint64(pair.parent_cells),
|
||||
grid=((pair.n_ghosts + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
for src, dst in ((g.current, g.spare), (g.spare, g.current)):
|
||||
self._gather_halo(g, src, **kw)
|
||||
g.kernel(src, dst, np.uint32(g.layout.halo), np.uintp(0), g.kind, grid=g.grid, **kw)
|
||||
|
||||
def _gather_halo(self, level, buffer, **kw):
|
||||
pairs = len(level.layout.halo_dst)
|
||||
if pairs:
|
||||
@@ -441,7 +513,17 @@ class FixedPatchStepper:
|
||||
# Same buffer per hop: halo -> receiver-local BFL -> step.
|
||||
flags = self._fine_flags if self._fine_flags is not None else np.uintp(0)
|
||||
for src, dst in ((f.current, f.spare), (f.spare, f.current)):
|
||||
if self._child is not None:
|
||||
# L1 feeds L2 from its CURRENT G (fresh at the midpoint),
|
||||
# L2 takes two hops, then is restricted into src.
|
||||
self._advance_child(src, **kw)
|
||||
self._gather_halo(f, src, **kw)
|
||||
if self._child is not None:
|
||||
pair = self._child
|
||||
pair.coalesce(self._levels[2].current, src, pair.targets,
|
||||
pair.children, np.uint32(pair.n_rows),
|
||||
grid=((pair.n_rows + self._block[0] - 1)
|
||||
// self._block[0], 1), **kw)
|
||||
self._blocked_fine_boundary(src, **kw)
|
||||
f.kernel(src, dst, np.uint32(self.fine_layout.halo), flags,
|
||||
f.kind, grid=f.grid, **kw)
|
||||
@@ -477,7 +559,7 @@ class FixedPatchStepper:
|
||||
raise
|
||||
|
||||
def download(self):
|
||||
"""Return two synchronized G arrays, NaN on all non-leaves; no host cache."""
|
||||
"""Return synchronized G arrays (one per level), NaN on all non-leaves; no host cache."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
arrays = [np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
||||
@@ -508,7 +590,7 @@ class FixedPatchStepper:
|
||||
except Exception as exc:
|
||||
error = exc
|
||||
self._levels.clear()
|
||||
self._explode = self._coalesce = None
|
||||
self._explode = self._coalesce = self._child = None
|
||||
if getattr(self, "_fine_curved", None) is not None:
|
||||
try:
|
||||
self._fine_curved.free()
|
||||
|
||||
@@ -233,5 +233,32 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
self.build(self.FACE).fine_layout(block)
|
||||
|
||||
|
||||
class NestPatchTests(unittest.TestCase):
|
||||
"""Three-level single chain: L2 bounds in L1 window cells (probe fixture)."""
|
||||
def setUp(self):
|
||||
self.cfg = config()
|
||||
self.l1 = FixedPatch2D.from_config(self.cfg, (4, 4, 12, 12)) # 24x24 window
|
||||
|
||||
def test_probe_fixture_composes_kinds(self):
|
||||
child, kind = refinement.nest_patch(self.cfg, self.l1, (8, 8, 16, 16))
|
||||
self.assertEqual(child.fine_shape, (24, 24))
|
||||
self.assertAlmostEqual(child.tau_coarse, self.l1.tau_fine)
|
||||
base = self.l1.fine_kind
|
||||
for role in (INNER_GHOST, OUTER_GHOST):
|
||||
np.testing.assert_array_equal(kind[base == role], role)
|
||||
self.assertTrue((kind[child.covered] == OUTER_GHOST).all())
|
||||
self.assertEqual(int((kind == ACTIVE).sum()), 192)
|
||||
self.assertEqual(int((kind == INNER_GHOST).sum()), 144)
|
||||
self.assertEqual(int(child.covered.sum()), 64)
|
||||
|
||||
def test_invalid_nesting_rejected(self):
|
||||
for bounds in ((2, 8, 16, 16), # covers inherited INNER
|
||||
(4, 8, 16, 16), # explosion donors in INNER
|
||||
(8, 8, 20, 16), # donors reach OUTER/INNER at the far side
|
||||
(5, 8, 16, 16)): # one-cell outward: restriction footprint/donors
|
||||
with self.subTest(bounds=bounds), self.assertRaises(ValueError):
|
||||
refinement.nest_patch(self.cfg, self.l1, bounds)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
@@ -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