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:
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
|
||||
Reference in new issue
Block a user