test: signed interface-probe trace diagnostic (--probe-trace)

Test-only mechanism diagnostic, no pass gates. New pinned shear_probes.py:
24 fixed root-physical points on vertical/horizontal interface-normal
transects (>=8 root cells from corners), ACTIVE-leaf selection storing
level/i/j/actual centers, root-sync rho/ux/uy at root steps 0-128 and
1920-2048 (U512 every 4 lattice steps), analytic residuals at actual
centers, schedule/finite/600 kB validation. run_case: --probe-trace for
nested U128/U512/projected d3 and twin projected d2 (not with
--final-snapshot), shear_probes.py pin, probe-only steps skip metrics,
five qualification samples unchanged. shear_wave/shear_nested/shear_twin/
oracle byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-19 03:11:48 +00:00
1 parent 02bde0f751
commit 1e188e7419
4 files changed
+377 -3

No files matched your search

@@ -0,0 +1,113 @@
"""Signed interface-probe traces for the nested/twin shear presets (mechanism diagnostic).
No pass gates. 24 fixed physical points on two interface-normal transects, away
from corners, crossing L0->L1 and (nested only) L1->L2:
V: y = 64.1, across the vertical faces x = 16 (L0/L1) and x = 22 (L1/L2) of box 1;
H: x = 90.1, across the horizontal faces y = 24 (L0/L1) and y = 30 (L1/L2) of box 2.
Each point is sampled at the ACTIVE leaf cell containing it, so cases with
different grids (U128, U512, nested, twin) give different cell centers: the
actual center and level are stored and analytic fields are evaluated there.
Samples are POST-collision G at root synchronization only (root-step units);
no substep content is observed, and grid-scale alternation can alias.
"""
import math
import numpy as np
import shear_wave as sw
from oracle import CX, CY, U
ROOT_N = 128
OFFSETS = (-3.9, -1.9, -0.4, 0.1, 0.6, 2.1, 4.1, 5.6, 6.1, 6.6, 8.1, 10.1) # from outer face
TRANSECTS = {"V": dict(axis=0, face=16.0, other=64.1), # x varies, y fixed
"H": dict(axis=1, face=24.0, other=90.1)} # y varies, x fixed
STEPS = tuple(range(0, 129)) + tuple(range(1920, 2049)) # root steps, both inclusive
TRACE_MAX_BYTES = 600_000
def points():
out = []
for name, t in TRANSECTS.items():
for offset in OFFSETS:
along = t["face"] + offset
xy = (along, t["other"]) if t["axis"] == 0 else (t["other"], along)
out.append((name, offset, xy))
return out
def case_steps(n):
"""Case lattice steps at the fixed root-physical sample times (U512: 4 per root step)."""
return [s * (n // ROOT_N) for s in STEPS]
def select(masks, n=ROOT_N):
"""Leaf cell containing each point: finest level whose ACTIVE mask covers it.
Points and stored centers are in root-physical units (root cell = 1); a case
with root N = n has cells of size ROOT_N / (n * 2**level) there.
"""
if n % ROOT_N:
raise ValueError("case root N must be a multiple of 128")
probes = []
for name, offset, (x, y) in points():
for level in reversed(range(len(masks))):
scale = (n // ROOT_N) * 2 ** level
i, j = int(math.floor(x * scale)), int(math.floor(y * scale))
if masks[level][j, i]:
h = 1.0 / scale
probes.append(dict(transect=name, offset=offset, point=[x, y], level=level,
i=i, j=j, center=[h * (i + 0.5), h * (j + 0.5)]))
break
else:
raise ValueError(f"point {(x, y)} is not covered by an ACTIVE leaf")
return probes
def sample(arrays, probes):
"""Flat [rho, ux, uy] per probe from FP64 physical populations."""
row = []
for p in probes:
f = np.asarray(arrays[p["level"]][:, p["j"], p["i"]], dtype=float)
rho = float(f.sum())
row += [rho, float(CX @ f) / rho, float(CY @ f) / rho]
return row
def new_trace(probes, case_viscosity, case):
"""Times/coordinates are root-physical (N=128); nu_root = U*128/Re for all cases."""
return dict(schema=1, kind="mechanism diagnostic, no pass gates", case=case,
time_unit="root step (N=128)", sample_phase="postcollision G at root synchronization",
root_n=ROOT_N, viscosity_root=U * ROOT_N / sw.RE, case_viscosity=case_viscosity,
mode=list(sw.MODE), phase=sw.PHI,
amplitude=U, fields=["rho", "ux", "uy"], probes=probes, steps=[], values=[])
def residuals(trace):
"""Signed (drho, dux, duy) per step and probe, analytic field at each actual center."""
out = []
for step, row in zip(trace["steps"], trace["values"]):
res = []
for n, p in enumerate(trace["probes"]):
x, y = p["center"]
ux, uy, _ = sw.exact(x, y, step, trace["root_n"], trace["viscosity_root"])
rho, vx, vy = row[3 * n: 3 * n + 3]
res.append((rho - 1.0, vx - float(ux), vy - float(uy)))
out.append(res)
return out
def validate(trace):
"""Exact schedule, finite values, fixed probe count and byte cap."""
import json
if trace.get("steps") != list(STEPS) or len(trace["values"]) != len(STEPS):
raise ValueError("trace steps differ from the fixed schedule")
if len(trace["probes"]) != len(OFFSETS) * len(TRANSECTS):
raise ValueError("unexpected probe count")
width = 3 * len(trace["probes"])
if any(len(r) != width or not all(math.isfinite(v) for v in r) for r in trace["values"]):
raise ValueError("malformed or nonfinite trace row")
size = len(json.dumps(trace, allow_nan=False).encode())
if size > TRACE_MAX_BYTES:
raise ValueError(f"trace exceeds {TRACE_MAX_BYTES} bytes ({size})")
return size