442 lines
20 KiB
Python
442 lines
20 KiB
Python
"""Isolated NumPy CPU reference: one static 2:1 rectangular patch, volumetric transfer.
|
|
|
|
API version CPUREF-0.2 (ClaudeWorker, 2026-10-13). NOT EXECUTED BY THE AUTHOR:
|
|
written without a shell; Grok/Horsea runs it. An unvalidated reference to be
|
|
TESTED, not a certified truth. v0.1 (Drive 11eiOGWOwevZHpfopCvnEX1NZS9wIUwOn) is
|
|
kept unchanged for Grok's current tests.
|
|
|
|
Changes from v0.1 (no change to the numerical algorithm or public API):
|
|
* Collision and explosion counts are incremented inside the operations
|
|
(_collide, _explode), not filled from expected masks.
|
|
* Per-direction validity is propagated: fine validity (9, ny, nx) through
|
|
explosion, both hops, outer-ghost poisoning; coarse validity through stream
|
|
and coalescence. A collision requires all 9 directions valid at each cell it
|
|
touches; every consumed coalescence packet must be valid; otherwise raise.
|
|
* New diagnostics: "coalesce_paths" certifies each consumed packet's two-hop
|
|
path (intermediate and origin fine cell, their classes, validity), and
|
|
"fine_valid_after" / "coarse_valid_after".
|
|
* Budget note: closure_error is bookkeeping self-consistency only. It is
|
|
defined by subtracting interface_residual, so it is NOT conservation evidence;
|
|
test leaf_before/leaf_after and interface_residual independently.
|
|
|
|
Contract: AMR_Shared_CPU_API_2026-10-13.md
|
|
SHA256 ee8010e763d38937b95c34dadaf97415cdb8b6c23e3c79a9f1f32bd5d4e112f6
|
|
plus root amendment: make_spec(*, collision='identity'|'srt'), stored in Spec.
|
|
Snapshot: 20-file snapshot
|
|
SHA256 e45df47d0fabde7199d345ada0729bca9ee64a833f433cdd776a4774b6af0673
|
|
Dependencies: NumPy and the standard library only. No CelerisLab/CUDA imports.
|
|
|
|
Conventions
|
|
D2Q9 paired ordering cx=(0,1,-1,0,0,1,-1,1,-1), cy=(0,0,0,1,-1,1,-1,-1,1).
|
|
Arrays are Q-major [q, y, x], float64, unshifted physical populations.
|
|
Coarse cell (x, y) has centre (x+.5, y+.5) on the doubly periodic [0,16)^2.
|
|
The patch covers coarse cells [4,12)^2 (covered). Fine index a = 0..23 (halo 4)
|
|
has centre 4 + (a-4+.5)/2 and coarse parent 4 + (a-4)//2; active fine is
|
|
[:, 4:20, 4:20]. Fine cell volume is 1/4 of a coarse cell.
|
|
Invalid entries (covered coarse, fine ghosts at synchronisation) hold NaN.
|
|
|
|
Synchronised phase "arrival_f". One advance_coarse_once (Schornbaum & Ruede
|
|
2016, Sec. 4 order):
|
|
1 collide coarse active cells g_c = C_c(f_c(t)) tau_c
|
|
2 collide fine active cells g_f = C_f(f_f(t)) tau_f
|
|
3 explode: every fine ghost cell (inner and outer) receives g_c of its coarse
|
|
parent in all directions; once per coarse step, no rescaling
|
|
4 fine stream, hop 1; targets are active cells and inner ghosts (inner ghosts
|
|
stream but never collide)
|
|
5 collide fine active cells only; outer ghosts are poisoned (NaN, invalid)
|
|
6 fine stream, hop 2; active cells now hold f_f(t+1)
|
|
7 coarse periodic stream; incoming populations of covered cells are invalid
|
|
8 coalesce: for each non-covered coarse cell K and direction i whose link
|
|
midpoint (centre(K) - c_i/2) lies in the CLOSED patch, replace the coarse
|
|
incoming population by the mean of K's four inner-ghost children's slot i
|
|
(includes the diagonal corner links, e.g. (4,3)->(3,4), i=8).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import numpy as np
|
|
|
|
API_VERSION = "CPUREF-0.2"
|
|
CONTRACT_SHA256 = "ee8010e763d38937b95c34dadaf97415cdb8b6c23e3c79a9f1f32bd5d4e112f6"
|
|
SNAPSHOT_SHA256 = "e45df47d0fabde7199d345ada0729bca9ee64a833f433cdd776a4774b6af0673"
|
|
|
|
NQ = 9
|
|
CX = np.array([0, 1, -1, 0, 0, 1, -1, 1, -1], dtype=np.int64)
|
|
CY = np.array([0, 0, 0, 1, -1, 1, -1, -1, 1], dtype=np.int64)
|
|
OPP = np.array([0, 2, 1, 4, 3, 6, 5, 8, 7], dtype=np.int64)
|
|
W = np.array([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4, dtype=np.float64)
|
|
FINE_VOLUME = 0.25
|
|
# Moment weights per direction: (mass, x-momentum, y-momentum).
|
|
MOMENTS = np.stack([np.ones(NQ), CX.astype(np.float64), CY.astype(np.float64)], axis=1)
|
|
|
|
ACTIVE, INNER_GHOST, OUTER_GHOST = 0, 1, 2
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Spec:
|
|
patch_id: str = "P0"
|
|
n_coarse: int = 16
|
|
patch_lo: int = 4
|
|
patch_hi: int = 12
|
|
halo: int = 4
|
|
dt_coarse: float = 1.0
|
|
dt_fine: float = 0.5
|
|
tau_coarse: float = 0.8
|
|
tau_fine: float = 1.1
|
|
collision: str = "identity"
|
|
|
|
@property
|
|
def n_fine(self) -> int:
|
|
return 2 * (self.patch_hi - self.patch_lo) + 2 * self.halo
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class State:
|
|
coarse_f: np.ndarray
|
|
fine_f: np.ndarray
|
|
phase: str = "arrival_f"
|
|
tick: int = 0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Maps:
|
|
coarse_active: np.ndarray # (n, n) bool [y, x]
|
|
coarse_covered: np.ndarray # (n, n) bool
|
|
fine_active: np.ndarray # (nf, nf) bool
|
|
fine_inner_ghost: np.ndarray # (nf, nf) bool
|
|
fine_outer_ghost: np.ndarray # (nf, nf) bool
|
|
fine_class: np.ndarray # (nf, nf) int: ACTIVE / INNER_GHOST / OUTER_GHOST
|
|
fine_parent: np.ndarray # (nf,) coarse parent index (same for x and y)
|
|
fine_centre: np.ndarray # (nf,) physical centre (same for x and y)
|
|
coalesce: np.ndarray # (M, 3) rows (i, ky, kx)
|
|
corner_links: tuple # 4 x (name, i, (sx, sy), (kx, ky))
|
|
|
|
|
|
def make_spec(*, collision: str = "identity") -> Spec:
|
|
if collision not in ("identity", "srt"):
|
|
raise ValueError("collision must be 'identity' or 'srt'")
|
|
spec = Spec(collision=collision)
|
|
if not np.isclose(spec.tau_fine - 0.5, 2.0 * (spec.tau_coarse - 0.5)):
|
|
raise ValueError("tau values violate acoustic 2:1 scaling")
|
|
if not np.isclose(spec.dt_fine, 0.5 * spec.dt_coarse):
|
|
raise ValueError("dt values violate 2:1 time refinement")
|
|
return spec
|
|
|
|
|
|
def _readonly(array) -> np.ndarray:
|
|
array = np.array(array)
|
|
array.setflags(write=False)
|
|
return array
|
|
|
|
|
|
def _children_start(spec: Spec, k: int) -> int:
|
|
"""First fine index of the two children of coarse index k."""
|
|
return spec.halo + 2 * (k - spec.patch_lo)
|
|
|
|
|
|
def build_maps(spec: Spec) -> Maps:
|
|
n, lo, hi, nf = spec.n_coarse, spec.patch_lo, spec.patch_hi, spec.n_fine
|
|
idx = np.arange(n)
|
|
inside = (idx >= lo) & (idx < hi)
|
|
covered = inside[:, None] & inside[None, :]
|
|
|
|
a = np.arange(nf)
|
|
parent = lo + (a - spec.halo) // 2
|
|
centre = lo + (a - spec.halo + 0.5) / 2
|
|
active_1d = (a >= spec.halo) & (a < nf - spec.halo)
|
|
ring_1d = (parent >= lo - 1) & (parent <= hi) # children of patch or first ring
|
|
fine_active = active_1d[:, None] & active_1d[None, :]
|
|
near = ring_1d[:, None] & ring_1d[None, :]
|
|
inner = near & ~fine_active
|
|
outer = ~near
|
|
fine_class = np.where(fine_active, ACTIVE, np.where(inner, INNER_GHOST, OUTER_GHOST))
|
|
|
|
rows = []
|
|
for ky in range(n):
|
|
for kx in range(n):
|
|
if covered[ky, kx]:
|
|
continue
|
|
for i in range(1, NQ):
|
|
mx = kx + 0.5 - CX[i] / 2.0
|
|
my = ky + 0.5 - CY[i] / 2.0
|
|
if lo <= mx <= hi and lo <= my <= hi:
|
|
rows.append((i, ky, kx))
|
|
coalesce = np.array(rows, dtype=np.int64).reshape(-1, 3)
|
|
for i, ky, kx in coalesce:
|
|
y0, x0 = _children_start(spec, ky), _children_start(spec, kx)
|
|
if not inner[y0:y0 + 2, x0:x0 + 2].all():
|
|
raise AssertionError("coalescence target children must be inner ghosts")
|
|
|
|
corners = (
|
|
("lower_left", 8, (lo, lo - 1), (lo - 1, lo)),
|
|
("lower_right", 5, (hi - 1, lo - 1), (hi, lo)),
|
|
("upper_left", 6, (lo, hi), (lo - 1, hi - 1)),
|
|
("upper_right", 7, (hi - 1, hi), (hi, hi - 1)),
|
|
)
|
|
listed = {tuple(r) for r in coalesce.tolist()}
|
|
for _, i, (sx, sy), (kx, ky) in corners:
|
|
if (kx - sx, ky - sy) != (CX[i], CY[i]) or (i, ky, kx) not in listed:
|
|
raise AssertionError("corner link inconsistent with coalescence map")
|
|
|
|
return Maps(
|
|
coarse_active=_readonly(~covered), coarse_covered=_readonly(covered),
|
|
fine_active=_readonly(fine_active), fine_inner_ghost=_readonly(inner),
|
|
fine_outer_ghost=_readonly(outer), fine_class=_readonly(fine_class),
|
|
fine_parent=_readonly(parent), fine_centre=_readonly(centre),
|
|
coalesce=_readonly(coalesce), corner_links=corners)
|
|
|
|
|
|
def _equilibrium(rho, ux, uy) -> np.ndarray:
|
|
"""D2Q9 second-order equilibrium; inputs are 1-D arrays of equal length."""
|
|
cu = CX[:, None] * ux[None, :] + CY[:, None] * uy[None, :]
|
|
usq = ux * ux + uy * uy
|
|
return W[:, None] * rho[None, :] * (1.0 + 3.0 * cu + 4.5 * cu * cu - 1.5 * usq[None, :])
|
|
|
|
|
|
def make_state(spec: Spec, rho: float = 1.0, velocity=(0.0, 0.0)) -> State:
|
|
maps = build_maps(spec)
|
|
ux, uy = velocity
|
|
feq = _equilibrium(np.array([rho], float), np.array([ux], float), np.array([uy], float))[:, 0]
|
|
n, nf = spec.n_coarse, spec.n_fine
|
|
coarse = np.full((NQ, n, n), np.nan)
|
|
fine = np.full((NQ, nf, nf), np.nan)
|
|
coarse[:, maps.coarse_active] = feq[:, None]
|
|
fine[:, maps.fine_active] = feq[:, None]
|
|
return State(coarse_f=coarse, fine_f=fine, phase="arrival_f", tick=0)
|
|
|
|
|
|
def _collide(spec: Spec, f: np.ndarray, valid: np.ndarray, tau: float,
|
|
mask: np.ndarray, counts: np.ndarray) -> np.ndarray:
|
|
"""Collision on masked cells (counted here); other cells copied unchanged."""
|
|
if not valid[:, mask].all():
|
|
raise RuntimeError("collision touches a cell with an invalid direction")
|
|
g = f.copy()
|
|
if spec.collision == "srt":
|
|
fm = f[:, mask]
|
|
rho = fm.sum(axis=0)
|
|
ux = (CX[:, None] * fm).sum(axis=0) / rho
|
|
uy = (CY[:, None] * fm).sum(axis=0) / rho
|
|
g[:, mask] = fm - (fm - _equilibrium(rho, ux, uy)) / tau
|
|
counts[mask] += 1
|
|
return g
|
|
|
|
|
|
def _explode(gc, coarse_valid, gf, fine_valid, maps: Maps, writes: np.ndarray) -> int:
|
|
"""Copy each ghost's coarse parent into it (all directions); counts writes."""
|
|
ghosts = maps.fine_inner_ghost | maps.fine_outer_ghost
|
|
gy, gx = np.nonzero(ghosts)
|
|
py, px = maps.fine_parent[gy], maps.fine_parent[gx]
|
|
if not coarse_valid[:, py, px].all():
|
|
raise RuntimeError("explosion source is not a valid coarse cell")
|
|
gf[:, gy, gx] = gc[:, py, px]
|
|
fine_valid[:, gy, gx] = True
|
|
np.add.at(writes, (gy, gx), 1)
|
|
return 1
|
|
|
|
|
|
def _stream_periodic(g: np.ndarray) -> np.ndarray:
|
|
out = np.empty_like(g)
|
|
for i in range(NQ):
|
|
out[i] = np.roll(g[i], (CY[i], CX[i]), axis=(0, 1)) # out[y,x] = g[y-cy, x-cx]
|
|
return out
|
|
|
|
|
|
def _shift(a: np.ndarray, dy: int, dx: int, fill) -> np.ndarray:
|
|
"""out[y, x] = a[y-dy, x-dx]; `fill` where the source lies outside the array."""
|
|
out = np.full(a.shape, fill, dtype=a.dtype)
|
|
ny, nx = a.shape
|
|
out[max(dy, 0):ny + min(dy, 0), max(dx, 0):nx + min(dx, 0)] = \
|
|
a[max(-dy, 0):ny + min(-dy, 0), max(-dx, 0):nx + min(-dx, 0)]
|
|
return out
|
|
|
|
|
|
def _fine_hop(g: np.ndarray, valid: np.ndarray, maps: Maps):
|
|
"""One fine pull-stream to active cells and inner ghosts, with exchange records."""
|
|
targets = maps.fine_active | maps.fine_inner_ghost
|
|
src_class = maps.fine_class.astype(np.float64)
|
|
out = np.full_like(g, np.nan)
|
|
out_valid = np.zeros_like(valid)
|
|
inflow = np.zeros(3) # into active cells from ghost sources (fine-volume units)
|
|
outflow = np.zeros(3) # from active sources into inner-ghost targets
|
|
sources = {"active": 0, "inner_ghost": 0, "outer_ghost": 0, "outside": 0}
|
|
with np.errstate(invalid="ignore"):
|
|
for i in range(NQ):
|
|
dy, dx = int(CY[i]), int(CX[i])
|
|
moved = _shift(g[i], dy, dx, np.nan)
|
|
cls = _shift(src_class, dy, dx, np.nan)
|
|
out[i] = moved
|
|
out_valid[i] = _shift(valid[i], dy, dx, False) & targets
|
|
from_ghost = maps.fine_active & (cls >= INNER_GHOST)
|
|
to_ghost = maps.fine_inner_ghost & (cls == ACTIVE)
|
|
inflow += MOMENTS[i] * moved[from_ghost].sum()
|
|
outflow += MOMENTS[i] * moved[to_ghost].sum()
|
|
act_cls = cls[maps.fine_active]
|
|
sources["active"] += int(np.sum(act_cls == ACTIVE))
|
|
sources["inner_ghost"] += int(np.sum(act_cls == INNER_GHOST))
|
|
sources["outer_ghost"] += int(np.sum(act_cls == OUTER_GHOST))
|
|
sources["outside"] += int(np.sum(np.isnan(act_cls)))
|
|
out[:, ~targets] = np.nan
|
|
out[~out_valid] = np.nan
|
|
return out, out_valid, FINE_VOLUME * inflow, FINE_VOLUME * outflow, sources
|
|
|
|
|
|
def _leaf_totals(coarse_f, fine_f, maps: Maps) -> np.ndarray:
|
|
mc = coarse_f[:, maps.coarse_active]
|
|
mf = fine_f[:, maps.fine_active]
|
|
return MOMENTS.T @ mc.sum(axis=1) + FINE_VOLUME * (MOMENTS.T @ mf.sum(axis=1))
|
|
|
|
|
|
def _moments_of_difference(after, before, mask) -> np.ndarray:
|
|
return MOMENTS.T @ (after[:, mask] - before[:, mask]).sum(axis=1)
|
|
|
|
|
|
def advance_coarse_once(spec: Spec, maps: Maps, state: State):
|
|
n, nf = spec.n_coarse, spec.n_fine
|
|
if state.phase != "arrival_f":
|
|
raise ValueError("state.phase must be 'arrival_f'")
|
|
if state.coarse_f.shape != (NQ, n, n) or state.fine_f.shape != (NQ, nf, nf):
|
|
raise ValueError("state arrays have the wrong shape")
|
|
fc = np.array(state.coarse_f, dtype=np.float64)
|
|
ff = np.array(state.fine_f, dtype=np.float64)
|
|
if not (np.isfinite(fc[:, maps.coarse_active]).all() and np.isfinite(ff[:, maps.fine_active]).all()):
|
|
raise ValueError("active populations must be finite")
|
|
fc[:, ~maps.coarse_active] = np.nan # invalid input entries are never trusted
|
|
ff[:, ~maps.fine_active] = np.nan
|
|
coarse_valid = np.broadcast_to(maps.coarse_active, (NQ, n, n)).copy()
|
|
fine_valid = np.broadcast_to(maps.fine_active, (NQ, nf, nf)).copy()
|
|
leaf_before = _leaf_totals(fc, ff, maps)
|
|
|
|
count_c = np.zeros((n, n), dtype=np.int64)
|
|
count_f = np.zeros((nf, nf), dtype=np.int64)
|
|
explosion_writes = np.zeros((nf, nf), dtype=np.int64)
|
|
|
|
# 1-2: collisions at t
|
|
gc = _collide(spec, fc, coarse_valid, spec.tau_coarse, maps.coarse_active, count_c)
|
|
gf = _collide(spec, ff, fine_valid, spec.tau_fine, maps.fine_active, count_f)
|
|
coarse_collision = _moments_of_difference(gc, fc, maps.coarse_active)
|
|
fine_collision = FINE_VOLUME * _moments_of_difference(gf, ff, maps.fine_active)
|
|
|
|
# 3: explosion, once per coarse step
|
|
explosion_count = _explode(gc, coarse_valid, gf, fine_valid, maps, explosion_writes)
|
|
origin_g = gf.copy() # hop-1 sources, kept for path certification
|
|
|
|
# 4: hop 1
|
|
f1, valid1, in1, out1, src1 = _fine_hop(gf, fine_valid, maps)
|
|
# 5: mid-step fine collision, active only; outer ghosts poisoned
|
|
g1 = _collide(spec, f1, valid1, spec.tau_fine, maps.fine_active, count_f)
|
|
fine_collision += FINE_VOLUME * _moments_of_difference(g1, f1, maps.fine_active)
|
|
valid1 = valid1.copy()
|
|
g1[:, maps.fine_outer_ghost] = np.nan
|
|
valid1[:, maps.fine_outer_ghost] = False
|
|
# 6: hop 2
|
|
f2, valid2, in2, out2, src2 = _fine_hop(g1, valid1, maps)
|
|
|
|
# 7: coarse stream
|
|
streamed = _stream_periodic(gc)
|
|
streamed_valid = np.stack([np.roll(coarse_valid[i], (CY[i], CX[i]), axis=(0, 1)) for i in range(NQ)])
|
|
to_covered = np.zeros(3)
|
|
for i in range(NQ):
|
|
to_covered += MOMENTS[i] * streamed[i][maps.coarse_covered & streamed_valid[i]].sum()
|
|
fc_new = streamed.copy()
|
|
new_valid = streamed_valid & maps.coarse_active[None, :, :]
|
|
fc_new[~new_valid] = np.nan
|
|
owner = np.zeros((NQ, n, n), dtype=np.int64)
|
|
owner[new_valid] = 1
|
|
|
|
# 8: coalescence of incoming directions from the fine region
|
|
replaced = np.zeros(3)
|
|
coalesced = np.zeros(3)
|
|
coalesce_writes = np.zeros((NQ, n, n), dtype=np.int64)
|
|
coalesce_reads = np.zeros((NQ, nf, nf), dtype=np.int64)
|
|
m = len(maps.coalesce)
|
|
path_class = np.zeros((m, 4, 2), dtype=np.int64) # (intermediate, origin) class
|
|
path_cells = np.zeros((m, 4, 2, 2), dtype=np.int64) # (intermediate, origin) (y, x)
|
|
path_valid = np.zeros((m, 4), dtype=bool)
|
|
for r, (i, ky, kx) in enumerate(maps.coalesce):
|
|
y0, x0 = _children_start(spec, ky), _children_start(spec, kx)
|
|
kids = [(y, x) for y in (y0, y0 + 1) for x in (x0, x0 + 1)]
|
|
for k, (y, x) in enumerate(kids):
|
|
ym, xm = y - CY[i], x - CX[i]
|
|
yo, xo = y - 2 * CY[i], x - 2 * CX[i]
|
|
path_cells[r, k] = ((ym, xm), (yo, xo))
|
|
path_class[r, k] = (maps.fine_class[ym, xm], maps.fine_class[yo, xo])
|
|
path_valid[r, k] = bool(valid2[i, y, x] and valid1[i, ym, xm]
|
|
and fine_valid[i, yo, xo] and np.isfinite(origin_g[i, yo, xo]))
|
|
if not path_valid[r, k]:
|
|
raise RuntimeError(f"invalid coalescence packet (i={i}, K=({kx},{ky}), child=({x},{y}))")
|
|
if path_class[r, k, 0] == OUTER_GHOST:
|
|
raise RuntimeError("coalescence packet passed through an outer ghost")
|
|
value = f2[i, y0:y0 + 2, x0:x0 + 2].mean()
|
|
if new_valid[i, ky, kx]:
|
|
replaced += MOMENTS[i] * fc_new[i, ky, kx]
|
|
fc_new[i, ky, kx] = value
|
|
new_valid[i, ky, kx] = True
|
|
coalesced += MOMENTS[i] * value
|
|
owner[i, ky, kx] = 2
|
|
coalesce_writes[i, ky, kx] += 1
|
|
coalesce_reads[i, y0:y0 + 2, x0:x0 + 2] += 1
|
|
|
|
if not new_valid[:, maps.coarse_active].all() or not np.isfinite(fc_new[:, maps.coarse_active]).all():
|
|
raise RuntimeError("coarse incoming population without a valid owner")
|
|
if not valid2[:, maps.fine_active].all() or not np.isfinite(f2[:, maps.fine_active]).all():
|
|
raise RuntimeError("fine active population received an invalid value")
|
|
ff_new = np.full((NQ, nf, nf), np.nan)
|
|
ff_new[:, maps.fine_active] = f2[:, maps.fine_active]
|
|
leaf_after = _leaf_totals(fc_new, ff_new, maps)
|
|
|
|
fine_inflow, fine_outflow = in1 + in2, out1 + out2
|
|
interface = (coalesced - to_covered - replaced) + (fine_inflow - fine_outflow)
|
|
budget = {
|
|
"coarse_collision": coarse_collision,
|
|
"fine_collision": fine_collision,
|
|
"coarse_to_covered": to_covered,
|
|
"coarse_replaced": replaced,
|
|
"coalesced": coalesced,
|
|
"fine_inflow": fine_inflow,
|
|
"fine_outflow": fine_outflow,
|
|
"interface_residual": interface,
|
|
# Bookkeeping self-check only; defined using interface_residual.
|
|
"closure_error": (leaf_after - leaf_before) - (coarse_collision + fine_collision + interface),
|
|
}
|
|
|
|
corner_packets = {}
|
|
for name, i, (sx, sy), (kx, ky) in maps.corner_links:
|
|
y0, x0 = _children_start(spec, ky), _children_start(spec, kx)
|
|
kids = [(y, x) for y in (y0, y0 + 1) for x in (x0, x0 + 1)]
|
|
corner_packets[name] = {
|
|
"direction": int(i),
|
|
"source_coarse_xy": (int(sx), int(sy)),
|
|
"target_coarse_xy": (int(kx), int(ky)),
|
|
"children_fine_yx": kids,
|
|
"children_centre_xy": [(float(maps.fine_centre[x]), float(maps.fine_centre[y])) for y, x in kids],
|
|
"packets": [float(f2[i, y, x]) for y, x in kids],
|
|
"weights": [0.25] * 4,
|
|
"coalesced": float(fc_new[i, ky, kx]),
|
|
}
|
|
|
|
diagnostics = {
|
|
"api_version": API_VERSION,
|
|
"collision": spec.collision,
|
|
"tick_before": int(state.tick),
|
|
"tick_after": int(state.tick) + 2,
|
|
"collision_count_coarse": count_c,
|
|
"collision_count_fine": count_f,
|
|
"explosion_count": explosion_count,
|
|
"explosion_writes": explosion_writes,
|
|
"coarse_incoming_owner": owner, # 0 invalid (covered), 1 coarse stream, 2 coalescence
|
|
"coalesce_writes": coalesce_writes,
|
|
"coalesce_reads": coalesce_reads,
|
|
"coalesce_paths": {"rows": np.array(maps.coalesce), "cells_yx": path_cells,
|
|
"classes": path_class, "valid": path_valid},
|
|
"fine_hop_sources": {"hop1": src1, "hop2": src2},
|
|
"fine_valid_after": valid2,
|
|
"coarse_valid_after": new_valid,
|
|
"leaf_before": leaf_before, # (mass, momentum_x, momentum_y)
|
|
"leaf_after": leaf_after,
|
|
"budget": budget,
|
|
"corner_packets": corner_packets,
|
|
}
|
|
new_state = State(coarse_f=fc_new, fine_f=ff_new, phase="arrival_f", tick=int(state.tick) + 2)
|
|
return new_state, diagnostics
|