Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850b66862f | ||
|
|
fd8f1da9f5 | ||
|
|
1573942673 | ||
|
|
8db7cee9d5 |
@@ -1,9 +1,9 @@
|
||||
"""Public API demo: fixed 2:1 patch with a stationary, optionally actuated circle.
|
||||
|
||||
From this checkout, after checking the selected physical GPU is idle:
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py --fine-block 16 20
|
||||
... --speed -0.002 --fine-block 16 20 --mode slot_reservoir
|
||||
Omit --fine-block for the (equivalent) dense fine storage.
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py
|
||||
... --speed -0.002 --mode slot_reservoir
|
||||
Both levels use blocked storage (--base-block, --fine-block tiles).
|
||||
|
||||
A short smoke run (default 128 coarse steps): it shows setup, stepping and
|
||||
fluid-only diagnostics, not a developed flow, accuracy or long-time stability.
|
||||
@@ -71,8 +71,10 @@ def main():
|
||||
parser.add_argument("--mode", choices=("periodic", "slot_reservoir"), default="periodic")
|
||||
parser.add_argument("--speed", type=float, default=.002,
|
||||
help="normal surface velocity, coarse lattice units (+ blows, - sucks, 0 passive)")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"),
|
||||
help="blocked fine storage; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--base-block", type=int, nargs=2, metavar=("BY", "BX"), default=(12, 8),
|
||||
help="root tile; must divide the 24 x 32 root")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"), default=(16, 20),
|
||||
help="fine tile; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--steps", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
if args.steps < 0:
|
||||
@@ -86,20 +88,20 @@ def main():
|
||||
body = SimObject(0, geometry=CircleGeometry(*CENTER, RADIUS), center=CENTER, radius=RADIUS,
|
||||
surface_actuation=spec)
|
||||
cfg = config(args.mode)
|
||||
fine_block = tuple(args.fine_block) if args.fine_block else None
|
||||
# The caller owns the CUDA context; the stepper must be closed first (the
|
||||
# nested with-statement does that).
|
||||
with CudaContext(0) as context, FixedPatchStepper(
|
||||
cfg, context=context, coarse_boundary=args.mode, refinements=[BOUNDS],
|
||||
level_blocks=[None, fine_block], level_objects=[(), (body,)]) as run:
|
||||
level_blocks=[tuple(args.base_block), tuple(args.fine_block)],
|
||||
level_objects=[(), (body,)]) as run:
|
||||
p = run.patch
|
||||
# Rest equilibrium is already postcollision G, so no initial collision is
|
||||
# needed. Non-leaf entries (covered, ghost, solid) are discarded by initialize.
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
run.initialize(np.broadcast_to(rest, (9, *p.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *p.fine_shape)))
|
||||
storage = f"blocked {fine_block}" if fine_block else "dense"
|
||||
print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} "
|
||||
storage = f"blocked root {tuple(args.base_block)} fine {tuple(args.fine_block)}"
|
||||
print(f"mode={args.mode} storage={storage} speed={args.speed:+g} "
|
||||
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
|
||||
masks = leaf_masks(run, args.mode)
|
||||
start = leaf_diagnostics(*run.download(), masks)
|
||||
|
||||
@@ -31,6 +31,7 @@ from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
BOUNDS, AMPLITUDE, WIDTH = (16, 4, 32, 28), .02, 12
|
||||
LEVEL_BLOCKS = [(12, 8), (8, 8)] # blocked storage: 48x48 root, 56x40 fine window
|
||||
TIMES, MAX_MACH, MAX_SECONDS = (0, 300, 1200), .1, 120
|
||||
|
||||
|
||||
@@ -132,7 +133,8 @@ def main():
|
||||
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
||||
store_precision='FP32', ddf_shifting=False, les_enabled=False)
|
||||
report = dict(config=dict(nx=48, ny=48, bounds=BOUNDS, W=WIDTH, A=AMPLITUDE, nu=.1,
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir'),
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir',
|
||||
level_blocks=LEVEL_BLOCKS),
|
||||
guards=dict(check_every=50, positive_density=True, finite=True,
|
||||
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
||||
timing='cooperative checks, not an exact interruption deadline'),
|
||||
@@ -149,7 +151,8 @@ def main():
|
||||
start, run = time.monotonic(), None
|
||||
try:
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context,
|
||||
coarse_boundary='slot_reservoir', refinements=[BOUNDS]) as run:
|
||||
coarse_boundary='slot_reservoir', refinements=[BOUNDS],
|
||||
level_blocks=LEVEL_BLOCKS) as run:
|
||||
p = run.patch
|
||||
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
||||
masks[0][[0, -1]] = False
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""The public examples run all-blocked: their level tiles must reach the device unchanged.
|
||||
|
||||
Host only: a context whose activation raises proves construction (layouts, tables,
|
||||
boundary checks) succeeded up to the first CUDA call.
|
||||
"""
|
||||
import ast
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
EXAMPLES = Path(__file__).resolve().parents[2] / "examples"
|
||||
|
||||
|
||||
def literal(name, constant):
|
||||
"""Module-level literal from an example, read without importing it (no pycuda)."""
|
||||
tree = ast.parse((EXAMPLES / name).read_text())
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
names = target.elts if isinstance(target, ast.Tuple) else [target]
|
||||
for i, t in enumerate(names):
|
||||
if isinstance(t, ast.Name) and t.id == constant:
|
||||
value = node.value.elts[i] if isinstance(target, ast.Tuple) else node.value
|
||||
return ast.literal_eval(value)
|
||||
raise KeyError(constant)
|
||||
|
||||
|
||||
def forbidden():
|
||||
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
||||
|
||||
|
||||
class ExampleTiles(unittest.TestCase):
|
||||
def test_fixed_patch_jet_tiles(self):
|
||||
bounds, blocks = literal("fixed_patch_jet.py", "BOUNDS"), literal("fixed_patch_jet.py", "LEVEL_BLOCKS")
|
||||
width, amplitude = literal("fixed_patch_jet.py", "WIDTH"), literal("fixed_patch_jet.py", "AMPLITUDE")
|
||||
cfg = LBMConfig(nx=48, ny=48, viscosity=.1, velocity=0., wall_slot_width=width,
|
||||
wall_normal_velocity=amplitude, y_wall_bc="on_site_velocity",
|
||||
top_wall_bc="reservoir")
|
||||
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||
FixedPatchStepper(cfg, context=forbidden(), coarse_boundary="slot_reservoir",
|
||||
refinements=[bounds], level_blocks=[tuple(b) for b in blocks])
|
||||
|
||||
def test_fixed_patch_circle_default_tiles(self):
|
||||
bounds = literal("fixed_patch_circle.py", "BOUNDS")
|
||||
nx, ny = literal("fixed_patch_circle.py", "NX"), literal("fixed_patch_circle.py", "NY")
|
||||
center, radius = literal("fixed_patch_circle.py", "CENTER"), literal("fixed_patch_circle.py", "RADIUS")
|
||||
body = SimObject(0, geometry=CircleGeometry(*center, radius), center=center, radius=radius)
|
||||
periodic = LBMConfig(nx=nx, ny=ny, viscosity=.1)
|
||||
slot = replace(periodic, velocity=0, wall_normal_velocity=.001, wall_slot_width=8,
|
||||
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
|
||||
for mode, cfg in (("periodic", periodic), ("slot_reservoir", slot)):
|
||||
with self.subTest(mode=mode), self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||
FixedPatchStepper(cfg, context=forbidden(), coarse_boundary=mode,
|
||||
refinements=[bounds], level_blocks=[(12, 8), (16, 20)],
|
||||
level_objects=[(), (body,)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,6 +2,34 @@
|
||||
|
||||
Run CPU and GPU classes in separate processes. GPU callers must first check
|
||||
resources/device availability; this file never selects or reserves a GPU.
|
||||
|
||||
Dense/blocked route parity (DenseBlockedRouteParity): the same configuration,
|
||||
initial state, body/Jet schedule and tilings on three storage routes, only
|
||||
level_blocks varying: D/D [None, None], D/B [None, FINE_BLOCK] (dense root,
|
||||
the mixed pattern of current examples) and B/B [(12, 8), FINE_BLOCK]. Gate:
|
||||
every public and owner array identical in FP32 bits and NaN masks at steps
|
||||
0, 1, 2, 8 and after re-initialize; D/B and B/B raw-exchange ledgers equal.
|
||||
No tolerance. A mismatch is reported with route pair/level/step and diagnosed
|
||||
(boundary semantics vs arithmetic ordering), never widened.
|
||||
|
||||
Route capabilities (rejections checked before CUDA by test_route_capability_matrix):
|
||||
|
||||
combination D/D D/B B/B
|
||||
periodic / slot_reservoir root, fine circle yes yes yes (parity gate)
|
||||
actuated circle, velocity Jet + amplitude yes yes yes (parity gate)
|
||||
raw/window exchange on a body level rejected yes yes
|
||||
shifted storage rejected rejected yes
|
||||
projected explosion rejected rejected yes
|
||||
channel root rejected rejected yes
|
||||
several L1 rectangles rejected rejected yes
|
||||
depth >= 3 with a dense level rejected rejected yes
|
||||
Slot/reservoir walls are compile-time WALL_* macros on a dense root and the
|
||||
d_block_wall constant on a blocked root: BC-encoding parity, bit-identical by
|
||||
this gate. Dense fine bodies use CurvedLinkSoA + CurvedBoundaryKernel
|
||||
(n_objects > 0), blocked ones BlockCurvedBoundary; same links and physics,
|
||||
compared through the population rows. Parked here: MRT (covered by
|
||||
test_refinement_mrt_gpu::test_dense_versus_blocked_mrt_two_level), shifted and
|
||||
depth >= 3 (no dense route to compare), several bodies (later slice).
|
||||
"""
|
||||
from contextlib import ExitStack, contextmanager, nullcontext
|
||||
from dataclasses import replace
|
||||
@@ -20,6 +48,7 @@ from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST, FixedPatch2D
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper, blocked_base_tables
|
||||
from test_refinement_blocked_fine import NX, NY, BOUNDS, config, state, circle, actuation
|
||||
from test_refinement_jets import SPEED, jet
|
||||
|
||||
BASE_BLOCKS = ((24, 32), (12, 8), (3, 4), (1, 1))
|
||||
FINE_BLOCK = (16, 20)
|
||||
@@ -324,5 +353,128 @@ class BlockedBaseGPU(unittest.TestCase):
|
||||
print(f"PASS {label}: full finite bits/NaN masks at 0,1,2,8 + reset", flush=True)
|
||||
|
||||
|
||||
ROUTES = (("D/D", [None, None]), ("D/B", [None, FINE_BLOCK]), ("B/B", [(12, 8), FINE_BLOCK]))
|
||||
|
||||
|
||||
def same_summary(a, b):
|
||||
"""Exact equality of nested exchange summaries (dicts, sequences, arrays, numbers)."""
|
||||
if isinstance(a, dict):
|
||||
return a.keys() == b.keys() and all(same_summary(a[k], b[k]) for k in a)
|
||||
if isinstance(a, (list, tuple)):
|
||||
return len(a) == len(b) and all(same_summary(x, y) for x, y in zip(a, b))
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
if a.dtype.kind in "biufc" and b.dtype.kind in "biufc":
|
||||
return a.shape == b.shape and np.array_equal(a, b, equal_nan=True)
|
||||
return a.dtype == b.dtype and np.array_equal(a, b) # labels such as the window name
|
||||
|
||||
|
||||
class DenseBlockedRouteCPU(unittest.TestCase):
|
||||
def test_same_summary_compares_labels_and_every_number(self):
|
||||
ledger = {"window": "last_base_step", "time_start": 3, "total": {"force": [1.0, -2.0],
|
||||
"torque": 0.5}, "aperture": {"force": np.array([np.nan, 1.0])}}
|
||||
self.assertTrue(same_summary(ledger, replace_value(ledger, ())))
|
||||
for path, value in ((("window",), "last_substep"), (("time_start",), 4),
|
||||
(("total", "force"), [1.0, -2.000001]), (("total", "torque"), 0.25),
|
||||
(("aperture", "force"), np.array([0.0, 1.0]))):
|
||||
with self.subTest(path=path):
|
||||
self.assertFalse(same_summary(ledger, replace_value(ledger, path, value)))
|
||||
self.assertFalse(same_summary(replace_value(ledger, path, value), ledger))
|
||||
|
||||
def test_route_capability_matrix(self):
|
||||
context = Mock(activate=Mock(side_effect=AssertionError("CUDA reached")))
|
||||
periodic = config("periodic")
|
||||
inner = (BOUNDS[0] + 2, BOUNDS[1] + 2, BOUNDS[2] - 2, BOUNDS[3] - 2)
|
||||
second = (BOUNDS[0], BOUNDS[1], BOUNDS[0] + 2, BOUNDS[1] + 2)
|
||||
cases = (
|
||||
("raw exchange on dense body level", periodic, dict(
|
||||
level_blocks=[None, None], level_objects=[(), (circle(),)], observe_raw_exchange=True)),
|
||||
("shifted D/D", replace(periodic, ddf_shifting=True), dict(level_blocks=[None, None])),
|
||||
("shifted D/B", replace(periodic, ddf_shifting=True), dict(level_blocks=[None, FINE_BLOCK])),
|
||||
("projected D/D", periodic, dict(level_blocks=[None, None], explosion="projected_linear")),
|
||||
("projected D/B", periodic, dict(level_blocks=[None, FINE_BLOCK], explosion="projected_linear")),
|
||||
("channel D/B", periodic, dict(level_blocks=[None, FINE_BLOCK], coarse_boundary="channel")),
|
||||
("several L1 rectangles", periodic, dict(refinements=[[BOUNDS, second]], level_blocks=[None, None])),
|
||||
("depth 3 dense", periodic, dict(refinements=[BOUNDS, inner], level_blocks=[None, None, None])),
|
||||
)
|
||||
for label, cfg, kw in cases:
|
||||
kw.setdefault("refinements", [BOUNDS])
|
||||
with self.subTest(label), self.assertRaises(ValueError):
|
||||
FixedPatchStepper(cfg, context=context, **kw)
|
||||
context.activate.assert_not_called()
|
||||
|
||||
|
||||
def replace_value(tree, path, value=None):
|
||||
"""Deep copy of a nested dict with the item at ``path`` replaced (no change for ())."""
|
||||
if isinstance(tree, dict):
|
||||
return {k: (value if path == (k,) else replace_value(v, path[1:], value) if path[:1] == (k,)
|
||||
else replace_value(v, (), None)) for k, v in tree.items()}
|
||||
return np.array(tree, copy=True) if isinstance(tree, np.ndarray) else tree
|
||||
|
||||
|
||||
class DenseBlockedRouteParity(unittest.TestCase):
|
||||
"""D/D vs D/B vs B/B, unshifted SRT, one circle or one velocity Jet; see module docstring."""
|
||||
|
||||
def run_routes(self, mode, objects=(), jets=(), switch=None):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cfg = config(mode)
|
||||
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||
arrays = state(NY, NX, 1), state(*p.fine_shape, 2)
|
||||
results = {}
|
||||
with CudaContext(0) as context:
|
||||
for name, blocks in ROUTES:
|
||||
observe = bool(objects) and blocks[1] is not None
|
||||
snapshots, ledgers = [], []
|
||||
with FixedPatchStepper(cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS],
|
||||
level_blocks=blocks, level_objects=[(), tuple(objects)],
|
||||
level_jets=[(), tuple(jets)],
|
||||
observe_raw_exchange=observe) as backend:
|
||||
backend.initialize(*arrays)
|
||||
for step in (0, 1, 2, 8):
|
||||
if step == 8 and switch is not None:
|
||||
backend.set_jet_amplitude(0, switch)
|
||||
backend.step(step - backend.step_count)
|
||||
snapshots.append(backend.download() + raw(backend))
|
||||
if observe and step:
|
||||
ledgers.append(tuple(backend.read_raw_exchange(0, window=window)
|
||||
for window in ("last_base_step", "last_substep")))
|
||||
backend.initialize(*arrays)
|
||||
snapshots.append(backend.download() + raw(backend))
|
||||
results[name] = snapshots, ledgers
|
||||
return results
|
||||
|
||||
def check(self, label, results):
|
||||
reference, _ = results["D/D"]
|
||||
for name in ("D/B", "B/B"):
|
||||
for step, actual, expected in zip((0, 1, 2, 8, "reset"), results[name][0], reference):
|
||||
for index, (a, b) in enumerate(zip(actual, expected)):
|
||||
exact(self, a, b, f"{label} {name} vs D/D step={step} array={index}")
|
||||
if results["D/B"][1]:
|
||||
for step, a, b in zip((1, 2, 8), results["D/B"][1], results["B/B"][1]):
|
||||
self.assertTrue(same_summary(a, b), f"{label} raw exchange D/B vs B/B step={step}")
|
||||
for name, (snapshots, _) in results.items():
|
||||
masses = [float(np.nansum(level.astype(np.float64))) for level in snapshots[3][:2]]
|
||||
print(f"{label} {name}: step 8 mass by level {masses}", flush=True)
|
||||
print(f"PASS {label}: D/D = D/B = B/B bits/NaN masks at 0,1,2,8 + reset", flush=True)
|
||||
|
||||
def test_body_free(self):
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
self.check(f"{mode} body-free", self.run_routes(mode))
|
||||
|
||||
def test_passive_circle(self):
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
self.check(f"{mode} circle", self.run_routes(mode, objects=[circle()]))
|
||||
|
||||
def test_actuated_circle(self):
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
for speed in (SPEED, -SPEED):
|
||||
self.check(f"{mode} actuated {speed}",
|
||||
self.run_routes(mode, objects=[circle(actuation(speed))]))
|
||||
|
||||
def test_velocity_jet_with_amplitude_switch(self):
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
self.check(f"{mode} jet switch",
|
||||
self.run_routes(mode, objects=[circle()], jets=[jet()], switch=-SPEED))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user