"""CPU geometry/contracts only: no time stepping, CUDA or reference edits.""" import hashlib import importlib.util from pathlib import Path import sys import unittest import numpy as np from CelerisLab.config import LBMConfig from CelerisLab.lbm import refinement from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY from CelerisLab.lbm.refinement import ACTIVE, INNER_GHOST, OUTER_GHOST, FixedPatch2D ROOT = Path(__file__).resolve().parents[2] def config(**kwargs): return LBMConfig(**dict(nx=16, ny=16, viscosity=.1, **kwargs)) class FixedPatchTests(unittest.TestCase): def test_source_and_cpu_import_boundary(self): self.assertEqual(Path(refinement.__file__).resolve(), ROOT / "src/CelerisLab/lbm/refinement.py") self.assertFalse(any(name == "pycuda" or name.startswith("pycuda.") for name in sys.modules)) self.assertNotIn("CelerisLab.simulation", sys.modules) self.assertNotIn("CelerisLab.lbm.field", sys.modules) def test_all_tables_match_pinned_reference_without_advancing(self): path = ROOT / "tests/validation/multigrid/cpu_reference.py" self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), "bba2a06a9f6517b32079ffff15e4060d8c3b7ed35f16ca134e1877226c9e4e1e") spec = importlib.util.spec_from_file_location("refinement_geometry_reference", path) ref = importlib.util.module_from_spec(spec) sys.modules[spec.name] = ref spec.loader.exec_module(ref) maps = ref.build_maps(ref.make_spec(collision="srt")) p = FixedPatch2D.from_config(config(), (4, 4, 12, 12)) np.testing.assert_array_equal(p.fine_kind, maps.fine_class) gy, gx = np.nonzero(~maps.fine_active) np.testing.assert_array_equal(p.ghost_fine, gy * 24 + gx) np.testing.assert_array_equal(p.ghost_coarse, maps.fine_parent[gy] * 16 + maps.fine_parent[gx]) targets = [q * 256 + y * 16 + x for q, y, x in maps.coalesce] sources = [[q * 576 + b * 24 + a for b in (4 + 2 * (y - 4), 5 + 2 * (y - 4)) for a in (4 + 2 * (x - 4), 5 + 2 * (x - 4))] for q, y, x in maps.coalesce] np.testing.assert_array_equal(p.coalesce_coarse, targets) np.testing.assert_array_equal(p.coalesce_fine, sources) self.assertEqual(len(targets), 100) self.assertEqual(len(set(p.coalesce_fine.ravel().tolist())), 400) self.assertEqual(np.count_nonzero(p.fine_kind == ACTIVE), 256) self.assertEqual(len(p.ghost_fine), 320) self.assertEqual(self.corner_pass_count(p), 8) @staticmethod def corner_pass_count(p): ny, nx = p.coarse_shape x0, y0, x1, y1 = p.bounds count = 0 for slot in p.coalesce_coarse.tolist(): q, cell = divmod(slot, nx * ny) y, x = divmod(cell, nx) sx, sy = x - D2Q9_EX[q], y - D2Q9_EY[q] count += not (x0 <= sx < x1 and y0 <= sy < y1) return count def test_rectangular_shifted_patch_against_geometric_definition(self): p = FixedPatch2D.from_config(LBMConfig(nx=22, ny=18, viscosity=.1), (7, 6, 13, 10)) expected = set() for y in range(18): for x in range(22): if 7 <= x < 13 and 6 <= y < 10: continue for q, (cx, cy) in enumerate(zip(D2Q9_EX, D2Q9_EY)): if q and 7 <= x + .5 - cx / 2 <= 13 and 6 <= y + .5 - cy / 2 <= 10: expected.add(q * 22 * 18 + y * 22 + x) self.assertEqual(set(p.coalesce_coarse.tolist()), expected) self.assertEqual(p.fine_shape, (16, 20)) self.assertEqual(p.fine_origin, (5., 4.)) fy, fx = p.fine_shape for coarse_slot, children in zip(p.coalesce_coarse.tolist(), p.coalesce_fine.tolist()): q, cell = divmod(coarse_slot, 22 * 18) y, x = divmod(cell, 22) centers = [] for slot in children: fq, fine_cell = divmod(slot, fy * fx) b, a = divmod(fine_cell, fx) self.assertEqual(fq, q) self.assertEqual(p.fine_kind[b, a], INNER_GHOST) centers.append((p.fine_origin[0] + (a + .5) / 2, p.fine_origin[1] + (b + .5) / 2)) np.testing.assert_array_equal(np.mean(centers, axis=0), (x + .5, y + .5)) self.assertEqual(self.corner_pass_count(p), 8) def test_smallest_patch_and_extreme_halo_margin(self): p = FixedPatch2D.from_config(LBMConfig(nx=5, ny=5, viscosity=.1), (2, 2, 3, 3)) self.assertEqual(p.fine_shape, (10, 10)) self.assertEqual(p.fine_origin, (0., 0.)) self.assertEqual((int(p.ghost_coarse.min()), int(p.ghost_coarse.max())), (0, 24)) self.assertEqual(np.count_nonzero(p.fine_kind == ACTIVE), 4) self.assertEqual(self.corner_pass_count(p), 8) self.assertTrue(np.all(p.fine_kind.ravel()[p.coalesce_fine % 100] == INNER_GHOST)) def test_config_and_level_scaling_are_isolated(self): cfg = config() before = vars(cfg).copy() p = FixedPatch2D.from_config(cfg, (4, 4, 12, 12)) self.assertEqual(vars(cfg), before) self.assertAlmostEqual(p.tau_coarse, .8) self.assertAlmostEqual(p.tau_fine, 1.1) self.assertAlmostEqual((p.tau_coarse - .5) / 3, (p.tau_fine - .5) / 3 * .5) cfg.nx = 999 self.assertEqual(p.coarse_shape, (16, 16)) def test_tables_are_compact_contiguous_and_readonly(self): small = FixedPatch2D.from_config(config(), (4, 4, 12, 12)) large = FixedPatch2D.from_config(LBMConfig(nx=512, ny=256, viscosity=.1), (4, 4, 12, 12)) self.assertEqual(small.table_bytes, large.table_bytes) self.assertEqual(small.table_bytes, 7136) for name, dtype in (("fine_kind", np.uint8), ("ghost_fine", np.uint32), ("ghost_coarse", np.uint32), ("coalesce_coarse", np.uint64), ("coalesce_fine", np.uint64)): array = getattr(small, name) self.assertEqual(array.dtype, dtype) self.assertTrue(array.flags.c_contiguous) with self.assertRaises(ValueError): array.flat[0] = 0 def test_invalid_geometry_and_index_overflow_rejected_before_allocation(self): for bounds in ((4, 4, 4, 12), (12, 4, 4, 12), (1, 4, 12, 12), (4, 4, 15, 12), (4., 4, 12, 12), (True, 4, 12, 12), (np.bool_(True), 4, 12, 12), (4, 4, 12)): with self.subTest(bounds=bounds), self.assertRaises(ValueError): FixedPatch2D.from_config(config(), bounds) for nx, ny, bounds in ((2**32, 10, (2, 2, 3, 3)), (50000, 50000, (2, 2, 49998, 49998))): with self.assertRaisesRegex(ValueError, "uint32"): FixedPatch2D.from_config(LBMConfig(nx=nx, ny=ny, viscosity=.1), bounds) def test_unsupported_backend_and_fine_relaxation_rejected(self): for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"}, {"ddf_shifting": True}, {"les_enabled": True}, {"store_precision": "FP16S"}, {"lattice_model": "D3Q19"}, {"data_type": "FP64"}, {"omega_min": 1.0}): cfg = config(**kwargs) before = vars(cfg).copy() with self.subTest(kwargs=kwargs), self.assertRaises(ValueError): FixedPatch2D.from_config(cfg, (4, 4, 12, 12)) self.assertEqual(vars(cfg), before) def test_handoff_rejects_wrong_phase_and_unsynchronized_counts(self): p = FixedPatch2D.from_config(config(), (4, 4, 12, 12)) p.check_synchronization(0, 0, phase="postcollision_g") p.check_synchronization(8, 16, phase="postcollision_g") for c, f, phase in ((8, 16, "arrival_f"), (8, 15, "postcollision_g"), (-1, -2, "postcollision_g"), (1., 2, "postcollision_g"), (True, 2, "postcollision_g"), (1, np.bool_(True), "postcollision_g")): with self.subTest(c=c, f=f, phase=phase), self.assertRaises(ValueError): p.check_synchronization(c, f, phase=phase) if __name__ == "__main__": unittest.main(verbosity=2)