BlockLayout2D: explicit selected block origins with hole owners

build(..., origins=None|[(y0, x0), ...]) stores only grid-aligned selected
blocks (unique, strictly row-major, no silent reordering). Cells of absent
blocks own layout.hole (= nblocks*L, one past the last block cell) so an
unguarded index fails rather than wrapping like -1; layout.owned is the
dense mask. Halo pairs never read holes, to_blocked ignores hole values,
to_dense returns NaN there, map_cut_links rejects receivers, solid
neighbors or q<.5 donors in absent blocks, blocked_link_tables stores
owned flags only. origins=None or every origin reproduces the regular
layout exactly. Storage only: no CUDA or stepper changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-16 03:09:15 +00:00
co-authored by Claude Opus 5.5
parent 99ddb5fc27
commit c397e08565
2 changed files with 226 additions and 21 deletions
+136 -1
View File
@@ -1,12 +1,13 @@
"""CPU block layout contracts: ownership, halo pairs, packing and tagged pull."""
import sys
import unittest
from types import SimpleNamespace
import numpy as np
from CelerisLab.body.geometry.base import CutLink
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.lbm.blocks import CORNER, FACE, NO_DONOR, NQ, BlockLayout2D
from CelerisLab.lbm.blocks import CORNER, FACE, NO_DONOR, NQ, BlockLayout2D, blocked_link_tables
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
CASES = ( # (ny, nx, by, bx, halo)
@@ -285,5 +286,139 @@ class BlockLayoutTests(unittest.TestCase):
self.assertFalse(array.flags.writeable)
def all_origins(ny, nx, by, bx):
return [(y, x) for y in range(0, ny, by) for x in range(0, nx, bx)]
def link(y, x, d, q, nx=12, fallback=0):
return SimpleNamespace(fluid_idx=y * nx + x, dir=d, q=q, fallback=fallback)
# 6x12 grid of 3x4 blocks (2x3 block grid); stored L: (0,0), (0,4), (3,4).
L_ORIGINS = [(0, 0), (0, 4), (3, 4)]
SPARSE = ( # (ny, nx, by, bx, halo, origins)
(6, 12, 3, 4, 1, L_ORIGINS),
(6, 12, 3, 4, 1, [(3, 8)]), # N=1 corner block
(8, 8, 4, 4, 2, [(0, 4), (4, 0)]), # diagonal pair, wider halo
(6, 8, 3, 4, 1, [(0, 0), (0, 4), (3, 0)]), # missing one of 2x2
)
class SparseBlockLayoutTests(unittest.TestCase):
"""Explicit selected block origins: absent blocks are holes, not wrapped -1."""
def test_all_origins_reproduce_full_layout(self):
for ny, nx, by, bx, h in CASES + ((4, 8, 4, 8, 1),): # last: N=1 full grid
for periodic in PERIODIC:
with self.subTest(case=(ny, nx, by, bx, h), periodic=periodic):
full = BlockLayout2D.build(ny, nx, by, bx, halo=h, periodic=periodic)
explicit = BlockLayout2D.build(ny, nx, by, bx, halo=h, periodic=periodic,
origins=all_origins(ny, nx, by, bx))
for name in ("origins", "owner", "halo_dst", "halo_src", "halo_kind"):
np.testing.assert_array_equal(getattr(explicit, name), getattr(full, name))
self.assertEqual(getattr(explicit, name).dtype, getattr(full, name).dtype)
self.assertTrue(full.owned.all())
self.assertEqual(explicit.hole, full.hole)
def test_holes_and_ownership(self):
layout = BlockLayout2D.build(6, 12, 3, 4, periodic=(False, False), origins=L_ORIGINS)
np.testing.assert_array_equal(layout.origins, L_ORIGINS)
self.assertEqual(layout.nblocks, 3)
cells = int(np.prod(layout.local_shape))
self.assertEqual(layout.hole, 3 * cells)
expected = np.zeros((6, 12), dtype=bool)
expected[0:3, 0:8] = expected[3:6, 4:8] = True
np.testing.assert_array_equal(layout.owned, expected)
self.assertTrue((layout.owner[~expected] == layout.hole).all())
for b, (y0, x0) in enumerate(L_ORIGINS):
for y in range(y0, y0 + 3):
for x in range(x0, x0 + 4):
self.assertEqual(layout.block_cell(b, y, x), layout.owner[y, x])
owned = layout.owner[expected].tolist()
self.assertEqual(len(owned), len(set(owned)))
with self.assertRaises(ValueError): # the hole value is never a valid slot
layout.slot(layout.hole, 0)
def test_halo_pairs_skip_absent_blocks(self):
layout = BlockLayout2D.build(6, 12, 3, 4, periodic=(False, False), origins=L_ORIGINS)
# Block 0: 3 east faces + SE corner; block 1: 3 west + 4 south faces;
# block 2: 4 north faces + NW corner. Nothing reads (3..5, 0..3) or x>=8.
self.assertEqual(len(layout.halo_dst), 16)
np.testing.assert_array_equal(np.bincount(layout.halo_kind, minlength=2), [14, 2])
for ny, nx, by, bx, h, origins in SPARSE:
for periodic in PERIODIC:
with self.subTest(origins=origins, periodic=periodic):
layout = BlockLayout2D.build(ny, nx, by, bx, halo=h, periodic=periodic,
origins=origins)
core = set(layout.owner[layout.owned].tolist())
dst = layout.halo_dst.tolist()
self.assertEqual(dst, sorted(dst))
self.assertEqual(len(dst), len(set(dst)))
self.assertTrue(core.isdisjoint(dst))
self.assertTrue(set(layout.halo_src.tolist()) <= core)
self.assertLess(max(dst + [-1]), layout.hole)
def test_pack_unpack_and_pull_with_holes(self):
for ny, nx, by, bx, h, origins in SPARSE:
for periodic in PERIODIC:
with self.subTest(origins=origins, periodic=periodic):
layout = BlockLayout2D.build(ny, nx, by, bx, halo=h, periodic=periodic,
origins=origins)
owned = layout.owned
a = tags(ny, nx)
holed = a.copy()
holed[:, ~owned] = np.nan
blocked = layout.to_blocked(a)
np.testing.assert_array_equal(layout.to_dense(blocked), holed)
poisoned = a.copy()
poisoned[:, ~owned] = np.inf # hole values are ignored by packing
np.testing.assert_array_equal(layout.to_blocked(poisoned), blocked)
self.assertEqual(np.isfinite(blocked[:, 0]).sum(),
owned.sum() + len(layout.halo_dst))
expected = dense_pull(holed, periodic)
expected[:, ~owned] = np.nan
got = layout.to_dense(blocked_pull(layout, blocked))
np.testing.assert_array_equal(got, expected)
def test_cut_links_in_or_next_to_absent_blocks_are_rejected(self):
layout = BlockLayout2D.build(6, 12, 3, 4, periodic=(False, False), origins=L_ORIGINS)
ok = [link(1, 1, 3, .6), link(1, 1, 3, .3), link(1, 7, 2, .6), link(1, 4, 2, .3)]
mapped = layout.map_cut_links(ok) # fully owned stencils, incl. across a seam
cells = int(np.prod(layout.local_shape))
np.testing.assert_array_equal(mapped.block, [0, 0, 1, 1])
np.testing.assert_array_equal(mapped.receiver, [layout.owner[1, 1]] * 2
+ [layout.owner[1, 7], layout.owner[1, 4]])
self.assertEqual(int(mapped.donor[1]), int(layout.owner[0, 1]))
self.assertTrue((mapped.write < np.uint64(layout.nblocks * NQ * cells)).all())
for bad in (link(4, 1, 1, .6), # receiver in an absent block
link(2, 1, 3, .6), # solid neighbor (3,1) absent
link(1, 7, 2, .3), # q<.5 donor (1,8) absent
link(2, 7, 1, .6)): # solid neighbor (2,8) absent
with self.subTest(fluid=bad.fluid_idx, dir=bad.dir, q=bad.q):
with self.assertRaises(ValueError):
layout.map_cut_links([bad])
def test_link_tables_store_owned_flags_only(self):
layout = BlockLayout2D.build(6, 12, 3, 4, periodic=(False, False), origins=L_ORIGINS)
flags = np.arange(1, 6 * 12 + 1, dtype=np.uint16)
tables, _ = blocked_link_tables(layout, flags, [], [], [], [])
owned = layout.owned.ravel()
stored = tables["flags"]
self.assertEqual(stored.shape, (layout.hole,))
np.testing.assert_array_equal(stored[layout.owner.ravel()[owned].astype(np.intp)], flags[owned])
self.assertEqual(np.count_nonzero(stored), owned.sum())
def test_invalid_origins_rejected(self):
for origins in ([(0, 4), (0, 0)], # not row-major
[(3, 0), (0, 4)], # not row-major across rows
[(0, 0), (0, 0)], # repeated
[(1, 0)], [(0, 2)], # misaligned
[(6, 0)], [(0, 12)], [(-3, 0)], [(0, -4)], # outside
np.zeros((0, 2), dtype=np.int64), # empty
[(0.0, 0.0)], [(True, False)], [0, 0], [(0, 0, 0)]):
with self.subTest(origins=origins), self.assertRaises(ValueError):
BlockLayout2D.build(6, 12, 3, 4, origins=origins)
if __name__ == "__main__":
unittest.main()