Files
CelerisLab/tests/unit/test_hierarchy.py
T
ClaudeandClaude Opus 5.5 eeb90ace9a Add host-only arbitrary-depth refinement hierarchy builder
lbm/hierarchy.py loops the existing pair logic (FixedPatch2D/FixedPatchSet2D
for L0->L1, nest_patch/nest_patch_set for deeper pairs, _middle_config for
the per-level config chain) and returns validated per-level configs, pair
tables and composed kinds for any depth. Legacy scalar window forms are
accepted so existing public shapes are unchanged. Validates each config copy
because LBMConfig.omega is only computed by validate(). Not yet used by the
stepper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:19:29 +00:00

95 lines
4.6 KiB
Python

"""CPU geometry tests for lbm.hierarchy (no CUDA). Geometry construction only, not LBM numerics."""
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.lbm import refinement as r
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
CFG = LBMConfig(nx=48, ny=48, viscosity=.02)
L1 = [(6, 6, 26, 26), (32, 30, 42, 42)] # L0 cells; L1 ACTIVE x/y 12..52 and x 64..84, y 60..84
L2 = [(20, 20, 44, 44), (72, 68, 76, 76)] # L1 cells; L2 ACTIVE 40..88 and x 144..152, y 136..152
L3 = [(56, 56, 72, 72)] # L2 cells
FOUR = [L1, L2, L3]
SMALL = LBMConfig(nx=16, ny=16, viscosity=.1)
class FourLevels(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.h = build_hierarchy(CFG, FOUR)
def test_config_chain(self):
self.assertEqual(self.h.depth, 4)
for level, c in enumerate(self.h.configs):
self.assertEqual((c.ny, c.nx), (48 << level, 48 << level))
self.assertAlmostEqual(c.viscosity, .02 * 2 ** level, places=12)
self.assertEqual(self.h.kinds[level].shape, (c.ny, c.nx))
for level, p in enumerate(self.h.pairs):
self.assertAlmostEqual(p.tau_coarse, 1 / self.h.configs[level].omega, places=12)
self.assertAlmostEqual(p.tau_fine, 1 / self.h.configs[level + 1].omega, places=12)
def test_partition_and_composition(self):
h = self.h
np.testing.assert_array_equal(leaf_partition(h), np.ones((48, 48)))
for level in (1, 2):
covered, raw = h.pairs[level].covered, h.pairs[level - 1].fine_kind
self.assertTrue((h.kinds[level][covered] == r.OUTER_GHOST).all())
np.testing.assert_array_equal(h.kinds[level][~covered], raw[~covered])
np.testing.assert_array_equal(h.kinds[3], h.pairs[2].fine_kind)
def test_depth_is_list_length(self):
for depth in range(1, 5):
h = build_hierarchy(CFG, FOUR[:depth - 1])
self.assertEqual(h.depth, depth)
np.testing.assert_array_equal(leaf_partition(h), np.ones((48, 48)))
for level in range(depth - 1):
np.testing.assert_array_equal(h.kinds[level], self.h.kinds[level])
def test_illegal_coverage_reports_first_guard(self):
cases = [([L1, [(10, 20, 30, 44)]], "inherited ACTIVE"),
([L1, L2[:1] + [(54, 54, 58, 58)]], "inherited ACTIVE"),
([L1, L2, [(40, 40, 60, 60)]], "donors/receivers"),
([L1, L2, [(41, 41, 60, 60)]], "donors/receivers"),
([L1, L2, [(56, 56, 64, 64), (66, 56, 72, 64)]], "closer than")]
for levels, reason in cases:
with self.subTest(levels=levels), self.assertRaisesRegex(ValueError, reason):
build_hierarchy(CFG, levels)
build_hierarchy(CFG, [L1, L2, [(42, 42, 60, 60)]]) # minimal legal margin
class LegacyForms(unittest.TestCase):
def test_scalar_parent_and_child_match_existing_calls(self):
local = r.FixedPatch2D.from_config(SMALL, (4, 4, 12, 12))
child, kind = r.nest_patch(SMALL, local, (8, 8, 16, 16))
h = build_hierarchy(SMALL, [(4, 4, 12, 12), (8, 8, 16, 16)])
np.testing.assert_array_equal(h.kinds[0], np.where(local.covered, r.OUTER_GHOST, r.ACTIVE))
np.testing.assert_array_equal(h.kinds[1], kind)
np.testing.assert_array_equal(h.kinds[2], child.fine_kind)
for name in ("ghost_fine", "ghost_coarse", "coalesce_coarse", "coalesce_fine"):
np.testing.assert_array_equal(getattr(h.pairs[1], name), getattr(child, name))
np.testing.assert_array_equal(leaf_partition(h), np.ones((16, 16)))
def test_set_child_under_scalar_parent(self):
local = r.FixedPatch2D.from_config(SMALL, (4, 4, 12, 12))
child, kind = r.nest_patch_set(SMALL, local, [(8, 8, 16, 16)])
h = build_hierarchy(SMALL, [(4, 4, 12, 12), [(8, 8, 16, 16)]])
np.testing.assert_array_equal(h.kinds[1], kind)
np.testing.assert_array_equal(h.pairs[1].coalesce_fine, child.coalesce_fine)
np.testing.assert_array_equal(leaf_partition(h), np.ones((16, 16)))
def test_scalar_child_under_set_parent_is_rejected(self):
with self.assertRaisesRegex(ValueError, "scalar parent"):
build_hierarchy(SMALL, [[(4, 4, 12, 12)], (12, 12, 20, 20)])
def test_wall_rows_and_single_level(self):
h = build_hierarchy(SMALL, [(4, 4, 12, 12)], wall_aware=True)
self.assertTrue((h.kinds[0][[0, -1]] == r.OUTER_GHOST).all())
one = build_hierarchy(SMALL, [])
self.assertEqual(one.depth, 1)
self.assertTrue((one.kinds[0] == r.ACTIVE).all())
if __name__ == "__main__":
unittest.main()