Support bounded three-level separated patch unions

This commit is contained in:
TRON committed 2026-10-16 09:18:07 -07:00
1 parent 20fb16ded5
commit a24e6d2742
5 files changed
+647 -24

No files matched your search

@@ -0,0 +1,178 @@
"""Frozen GPU gates for the bounded three-level separated-union backend.
Run explicitly on an available GPU; CPU geometry/mocks live in tests/unit.
Snapshots 0/1/2/8: finite public leaves, exact NaN masks, raw invalid cores;
reversed order and aligned layout changes must be bit-identical. The scalar
window comparison uses root physical coordinates, never equal array indices.
These equivalence checks supplement, not replace, independent dense CPU replay.
"""
from dataclasses import replace
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.descriptors import SOLID
from CelerisLab.lbm.equilibrium import compute_feq_d2q9
from CelerisLab.lbm.refinement import OUTER_GHOST
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from test_refinement_blocked_base import raw, exact
CFG = LBMConfig(nx=48, ny=32, viscosity=.1)
PARENTS = ((4, 4, 20, 28), (24, 4, 44, 28))
CHILDREN = ((16, 16, 32, 40), (56, 16, 80, 40))
TOPOLOGIES = {
'twins': ((16, 16, 32, 40), (56, 16, 72, 40)),
'asymmetric': (CHILDREN[0],),
'multiple_children': ((16, 16, 32, 24), (16, 28, 32, 40)),
}
# 8-wide blocks require support edges divisible by 8, i.e. rectangle edges
# equal 2 modulo 4 in each pair's parent frame. The default fixture is 4-only.
ALIGNED_CFG = LBMConfig(nx=56, ny=40, viscosity=.1)
ALIGNED_PARENTS = ((6, 6, 22, 34), (26, 6, 50, 34))
ALIGNED_CHILDREN = ((18, 18, 34, 50), (58, 18, 90, 50))
STOPS = (0, 1, 2, 8)
def config(cfg, mode):
return cfg if mode == 'periodic' else replace(
cfg, velocity=0., wall_normal_velocity=.001, wall_slot_width=4,
y_wall_bc='on_site_velocity', top_wall_bc='reservoir')
def bodies():
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject
return [SimObject(99, geometry=CircleGeometry(x, 14., .8), center=(x, 14.), radius=.8)
for x in (12., 34.)]
def jets(speed):
from CelerisLab.body.jets import Jet, Tube, Parallel
return [Jet(Tube((x-1.5, 14.), 0., 3., .3), Parallel(0.), amplitude=speed)
for x in (12., 34.)]
def frames(run):
middle_origin = np.asarray(run.patch.fine_origin)
return ((np.zeros(2), 1.), (middle_origin, .5),
(middle_origin + .5*np.asarray(run.child_patch.fine_origin), .25))
def physical_state(shape, origin, spacing):
"""Same smooth non-equilibrium G at the same physical cell centers."""
y, x = np.indices(shape, dtype=np.float64)
x, y = origin[0] + spacing*(x+.5), origin[1] + spacing*(y+.5)
equilibrium = np.asarray(compute_feq_d2q9(1., .013, -.007), np.float64)[:, None, None]
q = np.arange(9)[:, None, None]
return (equilibrium + 1e-4*np.sin(.17*x + .23*y + q)).astype(np.float32)
def physical_leaves(array, origin, spacing):
y, x = np.nonzero(np.isfinite(array[0]))
coordinates = np.stack((origin[1]+spacing*(y+.5), origin[0]+spacing*(x+.5)), axis=-1)
return coordinates, array[:, y, x]
class ThreeLevelMultipatchGPU(unittest.TestCase):
def run_case(self, ctx, cfg=CFG, parents=PARENTS, children=CHILDREN,
mode='periodic', blocks=((4, 4), (4, 4)), objects=(), drive=(), switch=False,
scalar_child=None):
kwargs = dict(child_rectangles_in_parent=children) if scalar_child is None else dict(child=scalar_child)
with FixedPatchStepper(config(cfg, mode), parents, context=ctx, coarse_boundary=mode,
base_block=(4, 4), fine_block=blocks[0], child_block=blocks[1],
child_objects=objects, child_jets=drive, **kwargs) as run:
frame = frames(run)
run.initialize(*(physical_state(level.shape[1:], *f)
for level, f in zip(run._levels, frame)))
snapshots = []
for stop in STOPS:
run.step(stop-run.step_count)
if switch and stop == 2:
for index in range(len(drive)):
run.set_jet_amplitude(index, -.002, level=2)
public, stored = run.download(), raw(run)
masks = [~run.patch.covered, run._fine_leaf, run._child_leaf]
if mode != 'periodic':
masks[0][[0, -1]] = False
invalid = [run.patch.covered.copy(), run._middle_kind == OUTER_GHOST,
run.child_patch.fine_kind == OUTER_GHOST]
if mode != 'periodic':
invalid[0][[0, -1]] = True
if run.child_boundary is not None:
invalid[2] |= (run.child_boundary.flags.reshape(run.child_patch.fine_shape) & SOLID) != 0
for level, (g, mask, dense, bad) in enumerate(zip(public, masks, stored, invalid)):
label = f'{mode} stop={stop} level={level}'
self.assertTrue(np.isfinite(g[:, mask]).all(), label)
self.assertTrue(np.isnan(g[:, ~mask]).all(), label)
self.assertTrue(np.isnan(dense[:, bad]).all(), label)
snapshots.append((public, stored, frame))
return snapshots
def compare(self, actual, expected):
for stop, a, b in zip(STOPS, actual, expected):
for kind in (0, 1):
for level, (x, y) in enumerate(zip(a[kind], b[kind])):
exact(self, x, y, f'stop={stop} kind={kind} level={level}')
def test_topologies_reversed_order_and_masks(self):
from CelerisLab.cuda.context import CudaContext
with CudaContext(0) as ctx:
for mode in ('periodic', 'slot_reservoir'):
for name, children in TOPOLOGIES.items():
with self.subTest(mode=mode, topology=name):
expected = self.run_case(ctx, children=children, mode=mode)
actual = self.run_case(ctx, parents=PARENTS[::-1], children=children[::-1], mode=mode)
self.compare(actual, expected)
def test_independently_varied_aligned_layouts(self):
from CelerisLab.cuda.context import CudaContext
with CudaContext(0) as ctx:
for mode in ('periodic', 'slot_reservoir'):
kwargs = dict(cfg=ALIGNED_CFG, parents=ALIGNED_PARENTS,
children=ALIGNED_CHILDREN, mode=mode)
expected = self.run_case(ctx, **kwargs)
for middle in ((4, 4), (8, 4), (4, 8)):
for leaf in ((4, 4), (8, 4), (4, 8)):
self.compare(self.run_case(ctx, blocks=(middle, leaf), **kwargs), expected)
def test_one_element_equivalent_physical_frames(self):
from CelerisLab.cuda.context import CudaContext
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
parent = (4, 4, 12, 12)
with CudaContext(0) as ctx:
for mode in ('periodic', 'slot_reservoir'):
old = self.run_case(ctx, cfg=cfg, parents=parent, mode=mode, scalar_child=(8, 8, 16, 16))
# Test both set-on-scalar and set-on-set allocated frames.
for parents, child in ((parent, (8, 8, 16, 16)), ((parent,), (12, 12, 20, 20))):
new = self.run_case(ctx, cfg=cfg, parents=parents, children=[child], mode=mode)
for a, b in zip(new, old):
for level in range(3):
ax, av = physical_leaves(a[0][level], *a[2][level])
bx, bv = physical_leaves(b[0][level], *b[2][level])
np.testing.assert_array_equal(ax, bx)
exact(self, av, bv, f'physical level={level}')
def test_two_l2_bodies_zero_driven_switched_order_and_layout(self):
from CelerisLab.cuda.context import CudaContext
with CudaContext(0) as ctx:
for mode in ('periodic', 'slot_reservoir'):
kwargs = dict(cfg=ALIGNED_CFG, parents=ALIGNED_PARENTS,
children=ALIGNED_CHILDREN, mode=mode, objects=bodies())
static = self.run_case(ctx, **kwargs)
zero = self.run_case(ctx, drive=jets(0.), **kwargs)
self.compare(zero, static)
driven = self.run_case(ctx, drive=jets(.002), **kwargs)
switched = self.run_case(ctx, drive=jets(.002), switch=True, **kwargs)
self.assertFalse(np.array_equal(driven[-1][0][2], static[-1][0][2], equal_nan=True))
self.assertFalse(np.array_equal(driven[-1][0][2], switched[-1][0][2], equal_nan=True))
for drive, switch, expected in (((), False, static), (jets(0.), False, zero),
(jets(.002), False, driven), (jets(.002), True, switched)):
for middle, leaf in (((8, 4), (4, 8)), ((4, 8), (8, 4))):
actual = self.run_case(ctx, blocks=(middle, leaf), drive=drive, switch=switch,
**dict(kwargs, parents=ALIGNED_PARENTS[::-1], children=ALIGNED_CHILDREN[::-1]))
self.compare(actual, expected)
if __name__ == '__main__':
unittest.main()