Multipatch: FixedPatchSet2D host metadata and opt-in FixedPatchStepper path

Several separated (>= 4 coarse cells) body-free 2:1 patches share one
level-wide fine grid stored by a sparse BlockLayout2D over the patch
supports. FixedPatchSet2D re-addresses each FixedPatch2D's tables to level
coordinates (same rows/child order, canonical patch order). The stepper
accepts a sequence of rectangles as bounds (requires fine_block and
base_block; rejects fine_objects/jets), reusing the existing blocked step
sequence and kernels. Helpers use a covered mask instead of one rectangle
and skip unstored fine holes. No kernel changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-16 14:00:50 +00:00
1 parent 93d1cddd27
commit 4c8240ea13
3 files changed
+206 -13

No files matched your search

+67
View File
@@ -163,5 +163,72 @@ class FixedPatchTests(unittest.TestCase):
p.check_synchronization(c, f, phase=phase)
class FixedPatchSetTests(unittest.TestCase):
"""Several separated patches: level-wide re-addressing of FixedPatch2D tables."""
FACE = ((4, 4, 8, 12), (12, 4, 16, 12)) # 16x24 coarse, face gap 4
S1 = ((4, 4, 12, 12), (20, 4, 28, 12)) # 16x32 coarse, translation copy
def build(self, rects, ny=16, nx=24):
return refinement.FixedPatchSet2D.from_config(LBMConfig(nx=nx, ny=ny, viscosity=.1), rects)
def test_tables_are_shifted_single_patch_tables(self):
s = self.build(self.FACE)
fy, fx = s.fine_shape
self.assertEqual((fy, fx), (32, 48))
rows = cells = 0
for p in s.patches:
y0, x0 = p.fine_level_origin
ly, lx = p.fine_shape
np.testing.assert_array_equal(s.fine_kind[y0:y0 + ly, x0:x0 + lx], p.fine_kind)
gy, gx = np.divmod(p.ghost_fine.astype(np.int64), lx)
np.testing.assert_array_equal(s.ghost_fine[cells:cells + len(gy)],
(gy + y0) * fx + gx + x0)
np.testing.assert_array_equal(s.ghost_coarse[cells:cells + len(gy)], p.ghost_coarse)
q, c = np.divmod(p.coalesce_fine.astype(np.int64), ly * lx)
cy, cx = np.divmod(c, lx)
np.testing.assert_array_equal(s.coalesce_fine[rows:rows + len(q)],
q * fy * fx + (cy + y0) * fx + cx + x0)
np.testing.assert_array_equal(s.coalesce_coarse[rows:rows + len(q)], p.coalesce_coarse)
rows, cells = rows + len(q), cells + len(gy)
self.assertEqual((rows, cells), (len(s.coalesce_coarse), len(s.ghost_fine)))
self.assertEqual(int((s.fine_kind == ACTIVE).sum()), 4 * int(s.covered.sum()))
def test_input_order_is_irrelevant(self):
a, b = self.build(self.FACE), self.build(self.FACE[::-1])
for name in ("fine_kind", "ghost_fine", "ghost_coarse", "coalesce_coarse", "coalesce_fine"):
np.testing.assert_array_equal(getattr(a, name), getattr(b, name))
self.assertEqual(a.bounds, b.bounds)
def test_one_patch_set_matches_fixed_patch(self):
p = FixedPatch2D.from_config(config(), (4, 4, 12, 12))
s = self.build(((4, 4, 12, 12),), nx=16)
np.testing.assert_array_equal(s.covered, p.covered)
self.assertEqual((s.tau_coarse, s.tau_fine), (p.tau_coarse, p.tau_fine))
np.testing.assert_array_equal(s.coalesce_coarse, p.coalesce_coarse)
np.testing.assert_array_equal(s.ghost_coarse, p.ghost_coarse)
def test_gap_and_overlap_rejected(self):
for gap in (0, 2, 3):
with self.subTest(gap=gap), self.assertRaisesRegex(ValueError, "closer"):
self.build(((4, 4, 8, 12), (8 + gap, 4, 12 + gap, 12)))
with self.assertRaises(ValueError):
self.build(())
with self.assertRaises(ValueError): # FixedPatch2D margin still applies
self.build(((1, 4, 8, 12), (12, 4, 16, 12)))
def test_sparse_fine_layout_stores_exactly_the_supports(self):
for rects, nx in ((self.FACE, 24), (self.S1, 32)):
s = self.build(rects, nx=nx)
layout = s.fine_layout((4, 4))
support = np.zeros(s.fine_shape, bool)
for p in s.patches:
y0, x0 = p.fine_level_origin
support[y0:y0 + p.fine_shape[0], x0:x0 + p.fine_shape[1]] = True
np.testing.assert_array_equal(layout.owned, support)
self.assertTrue(layout.owned.ravel()[s.ghost_fine.astype(np.intp)].all())
with self.assertRaisesRegex(ValueError, "aligned"):
self.build(self.FACE).fine_layout((5, 4))
if __name__ == "__main__":
unittest.main(verbosity=2)