Add receiver-local static cut-link mapping to BlockLayout2D
Each physical link belongs once to the block owning its fluid receiver core; the reflected source population is written into that block's own core/halo copy (no halo copy between BFL and pull). Rejects repeated links and repeated local writers; one row per link so force counts once. Documents stale remote solid-core slots vs unique core ownership, and that the 4-cell refinement halo is separate. Static geometry only. Tests: address equivalence incl. cross-block q<.5 donors, and static BFL+pull bit-equal to the dense operator on fluid cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a64af5bece
commit
2aadffe183
@@ -12,12 +12,25 @@ Periodicity is explicit per axis. Across a non-periodic exterior, halo cells
|
||||
have no pair and stay invalid (NaN after packing): no wrap, no implied BC.
|
||||
|
||||
Preparation only: no CUDA, no boundary conditions, no refinement levels.
|
||||
The one-cell same-level halo here is unrelated to the four-cell fine halo of
|
||||
the fixed refinement patch (refinement.py), whose semantics are unchanged.
|
||||
|
||||
Static cut links (map_cut_links) use RECEIVER-LOCAL staging: each physical
|
||||
link belongs once to the block whose core holds its fluid receiver. Order per
|
||||
step: gather halos -> BFL writes the reflected source population into that
|
||||
block's own core/halo copy of the solid cell -> pull/collide core cells, with
|
||||
NO halo copy between BFL and pull. Reflected populations are temporary,
|
||||
receiver-local boundary values: the solid's owner-core copy of that slot may
|
||||
be stale, which is distinct from the unique core ownership of stored state.
|
||||
No moving geometry is implied.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from numbers import Integral
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .descriptors import D2Q9_EX, D2Q9_EY
|
||||
|
||||
NQ = 9
|
||||
FACE, CORNER = 0, 1
|
||||
|
||||
@@ -34,6 +47,23 @@ def _positive_int(value, name):
|
||||
return int(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class BlockCutLinks:
|
||||
"""One row per input link, in input order (so force is counted once).
|
||||
|
||||
``block`` owns the receiver core cell; ``receiver`` and ``donor`` are block
|
||||
cell ids in that block (donor = NO_DONOR unless q<.5 with a legal donor);
|
||||
``write`` is the flat blocked slot of the reflected population.
|
||||
"""
|
||||
block: np.ndarray
|
||||
receiver: np.ndarray
|
||||
donor: np.ndarray
|
||||
write: np.ndarray
|
||||
|
||||
|
||||
NO_DONOR = np.iinfo(np.uint64).max
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class BlockLayout2D:
|
||||
"""Fixed-size block partition of an (ny, nx) grid; construct with build().
|
||||
@@ -138,6 +168,50 @@ class BlockLayout2D:
|
||||
raise ValueError("cell is not represented in this block's storage")
|
||||
return int(block) * ly * lx + ry * lx + rx
|
||||
|
||||
def _neighbor(self, y, x, dy, dx):
|
||||
ny, nx = self.shape
|
||||
y, x = y + dy, x + dx
|
||||
if self.periodic[1]:
|
||||
y %= ny
|
||||
if self.periodic[0]:
|
||||
x %= nx
|
||||
if not (0 <= y < ny and 0 <= x < nx):
|
||||
raise ValueError("cut-link stencil crosses a non-periodic exterior")
|
||||
return y, x
|
||||
|
||||
def map_cut_links(self, links) -> BlockCutLinks:
|
||||
"""Map dense static cut links (fluid_idx, dir, q, fallback) to blocks.
|
||||
|
||||
Rejects repeated physical links and repeated local writers. Addresses
|
||||
are geometry-free: q, fallback and BFL math stay as built on the dense
|
||||
grid. Halo must be at least one cell (always true here).
|
||||
"""
|
||||
ny, nx = self.shape
|
||||
cells = int(np.prod(self.local_shape))
|
||||
rows, seen, writers = [], set(), set()
|
||||
for link in links:
|
||||
r, q = int(link.fluid_idx), int(link.dir)
|
||||
if not (0 <= r < ny * nx and 1 <= q < NQ) or (r, q) in seen:
|
||||
raise ValueError("cut link is invalid or repeated")
|
||||
seen.add((r, q))
|
||||
y, x = divmod(r, nx)
|
||||
receiver = int(self.owner[y, x])
|
||||
block = receiver // cells
|
||||
sy, sx = self._neighbor(y, x, D2Q9_EY[q], D2Q9_EX[q])
|
||||
opp = q + 1 if q % 2 else q - 1
|
||||
write = int(self.slot(self.block_cell(block, sy, sx), opp))
|
||||
if write in writers:
|
||||
raise ValueError("duplicate local boundary writer")
|
||||
writers.add(write)
|
||||
donor = NO_DONOR
|
||||
if float(link.q) < .5 and int(link.fallback) == 0:
|
||||
dy, dx = self._neighbor(y, x, -D2Q9_EY[q], -D2Q9_EX[q])
|
||||
donor = self.block_cell(block, dy, dx)
|
||||
rows.append((block, receiver, donor, write))
|
||||
table = np.array(rows, dtype=np.uint64).reshape(-1, 4)
|
||||
return BlockCutLinks(_readonly(table[:, 0], np.uint32),
|
||||
*(_readonly(table[:, i], np.uint64) for i in (1, 2, 3)))
|
||||
|
||||
def exchange(self, blocked) -> None:
|
||||
"""Fill every valid halo cell in place from its owner's core."""
|
||||
blocked = self._check_blocked(blocked)
|
||||
|
||||
Reference in New Issue
Block a user