test: annulus mass ledger definition with synthetic seam/edge checks
Host-only stdlib definition of the per-hop identity dM + Q_exact = C + H with Q_exact from pre-staging owner populations and H = sum over every annulus cell/direction of (pulled - ideal owner input), split into seam (internal) and edge parts. Synthetic tests: ideal pull, collision source, interior-seam and edge perturbations, the double count of staged flux + full H, a late owner snapshot hiding staging, and a wrong pull direction. No solver change, no GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
750e8a483e
commit
106aa620b9
2 files changed
+176
No files matched your search
@@ -0,0 +1,75 @@
|
||||
"""Per-hop mass ledger of a fluid cell set A (host-only, stdlib, D2Q9 pull scheme).
|
||||
|
||||
Diagnostic definition only; no solver code, no GPU. Arrays are indexed [q][y][x] on a
|
||||
periodic nx x ny lattice; A is a set of (x, y) ACTIVE cells.
|
||||
|
||||
O owner populations before any staging (canonical owner slots, start of the hop)
|
||||
P populations each cell actually pulls: P[q](x) is what x reads for direction q
|
||||
(after halo copy, BFL/coalesce staging, ...)
|
||||
D populations written by the cell after collision
|
||||
|
||||
With M0 = sum_{A,q} O and M1 = sum_{A,q} D (both owner snapshots of A):
|
||||
|
||||
M1 - M0 + Q_exact = C + H (exact)
|
||||
|
||||
C = sum_{x in A, q} (D_q(x) - P_q(x)) collision source
|
||||
H = sum_{x in A, q} (P_q(x) - O_q(x - c_q)) actual minus ideal owner input,
|
||||
split into H_seam (x - c_q in A: internal pulls, e.g. tile seams) and
|
||||
H_edge (x - c_q not in A: inputs crossing the edge of A)
|
||||
Q_exact = sum_{y in A, y+c_q not in A} O_q(y) - sum_{y not in A, y+c_q in A} O_q(y)
|
||||
owner-based net outward flux (outgoing minus incoming)
|
||||
|
||||
Proof: sum_{A,q} O_q(x - c_q) - M0 = -Q_exact (relabel y = x - c_q per direction), and
|
||||
sum_{A,q} D = C + H + sum_{A,q} O_q(x - c_q). Q_exact must use O, not staged values: a
|
||||
flux built from staged inputs already contains H_edge, so adding the full H counts it
|
||||
twice (see staged_flux). The edge of A must not cross cut links, otherwise O at the solid
|
||||
end enters Q_exact.
|
||||
"""
|
||||
|
||||
CX = (0, 1, -1, 0, 0, 1, -1, 1, -1)
|
||||
CY = (0, 0, 0, 1, -1, 1, -1, -1, 1)
|
||||
|
||||
|
||||
def _src(x, y, q, nx, ny):
|
||||
return (x - CX[q]) % nx, (y - CY[q]) % ny
|
||||
|
||||
|
||||
def pull(O, nx, ny):
|
||||
"""Ideal pulled input: P[q](x) = O[q](x - c_q)."""
|
||||
return [[[O[q][_src(x, y, q, nx, ny)[1]][_src(x, y, q, nx, ny)[0]]
|
||||
for x in range(nx)] for y in range(ny)] for q in range(9)]
|
||||
|
||||
|
||||
def ledger(O, P, D, A, nx, ny):
|
||||
A = set(A)
|
||||
out = dict(dM=0.0, C=0.0, H_seam=0.0, H_edge=0.0, Q_exact=0.0)
|
||||
for (x, y) in A:
|
||||
for q in range(9):
|
||||
out["dM"] += D[q][y][x] - O[q][y][x]
|
||||
out["C"] += D[q][y][x] - P[q][y][x]
|
||||
sx, sy = _src(x, y, q, nx, ny)
|
||||
key = "H_seam" if (sx, sy) in A else "H_edge"
|
||||
out[key] += P[q][y][x] - O[q][sy][sx]
|
||||
if (sx, sy) not in A:
|
||||
out["Q_exact"] -= O[q][sy][sx] # incoming owner population
|
||||
tx, ty = (x + CX[q]) % nx, (y + CY[q]) % ny
|
||||
if (tx, ty) not in A:
|
||||
out["Q_exact"] += O[q][y][x] # outgoing owner population
|
||||
out["H"] = out["H_seam"] + out["H_edge"]
|
||||
out["residual"] = out["dM"] + out["Q_exact"] - out["C"] - out["H"]
|
||||
return out
|
||||
|
||||
|
||||
def staged_flux(O, P, A, nx, ny):
|
||||
"""Outward flux with incoming terms from the staged inputs P (the wrong combination)."""
|
||||
A = set(A)
|
||||
q_out = 0.0
|
||||
for (x, y) in A:
|
||||
for q in range(9):
|
||||
sx, sy = _src(x, y, q, nx, ny)
|
||||
if (sx, sy) not in A:
|
||||
q_out -= P[q][y][x]
|
||||
tx, ty = (x + CX[q]) % nx, (y + CY[q]) % ny
|
||||
if (tx, ty) not in A:
|
||||
q_out += O[q][y][x]
|
||||
return q_out
|
||||
Reference in new issue
Block a user