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:
co-authored by
Claude Opus 5.5
parent
99ddb5fc27
commit
c397e08565
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user