diff --git a/examples/fixed_patch_circle.py b/examples/fixed_patch_circle.py index 6a5e3fb..44923e5 100644 --- a/examples/fixed_patch_circle.py +++ b/examples/fixed_patch_circle.py @@ -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 --fine-block 8 8 +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,12 +88,12 @@ 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. diff --git a/examples/fixed_patch_jet.py b/examples/fixed_patch_jet.py index 7c767b0..6c4ae54 100644 --- a/examples/fixed_patch_jet.py +++ b/examples/fixed_patch_jet.py @@ -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 diff --git a/tests/unit/test_example_tiles.py b/tests/unit/test_example_tiles.py new file mode 100644 index 0000000..ca8b6fe --- /dev/null +++ b/tests/unit/test_example_tiles.py @@ -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()