test: add bounded square shear orientation diagnostic
This commit is contained in:
1 parent
70c83b7233
commit
afa8f6aca1
4 files changed
+394
-14
No files matched your search
@@ -0,0 +1,136 @@
|
||||
"""Short axis-shear orientation diagnostic; no physical acceptance gates."""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
import shear_wave as sw
|
||||
from shear_probes import sample, validate_row, TRACE_MAX_BYTES
|
||||
from oracle import CX, CY, W, U, centers
|
||||
|
||||
ROOT_N = 128
|
||||
MODE, E, PHI, ANGLE_DEG = (2, 0), np.array([0., 1.]), np.pi / 4, 0.
|
||||
REFINEMENTS = [[(32, 32, 96, 96)]]
|
||||
CASES = ((100, 128, 1, "constant"), (100, 256, 1, "constant"),
|
||||
(100, 128, 2, "constant"), (100, 128, 2, "projected_linear"))
|
||||
STEPS = tuple(range(9))
|
||||
INITIALIZATION_ID = "postg_first_gradient_v1"
|
||||
INITIALIZATION = "physical FP64 axis-shear POST G=feq-(tau-1)A; first-gradient low-Mach CE"
|
||||
GROUPS = dict(inner_left=(32, 64), inner_right=(95, 63),
|
||||
inner_bottom=(64, 32), inner_top=(63, 95),
|
||||
outer_left=(31, 64), outer_right=(96, 63),
|
||||
outer_bottom=(64, 31), outer_top=(63, 96))
|
||||
|
||||
|
||||
def eps(n):
|
||||
return (4 * np.pi / n)**2
|
||||
|
||||
|
||||
def exact(x, t, length, viscosity):
|
||||
"""Transverse velocity and physical x derivative in the supplied length units."""
|
||||
k = 4 * np.pi / length
|
||||
amplitude = U * np.exp(-viscosity * k * k * t)
|
||||
return amplitude * np.sin(k * x + PHI), amplitude * k * np.cos(k * x + PHI)
|
||||
|
||||
|
||||
def initial(shape, level, length, viscosity):
|
||||
x, _ = centers(shape, level)
|
||||
uy, gradient = exact(x, 0, length, viscosity)
|
||||
tau = .5 + 3 * viscosity * 2**level
|
||||
a = (3 * W * CX * CY)[:, None, None] * gradient * 2.**-level
|
||||
return sw.equilibrium(np.ones_like(uy), np.zeros_like(uy), uy) - (tau - 1) * a
|
||||
|
||||
|
||||
def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
|
||||
rho, ux, uy, x, _, w, _ = sw._leaves(arrays, masks)
|
||||
if not np.isclose(w.sum(), length**2, rtol=0, atol=1e-10):
|
||||
raise ValueError("leaf partition does not cover domain once")
|
||||
ey, _ = exact(x, t, length, viscosity)
|
||||
p = (rho - 1) / 3
|
||||
p -= w @ p / w.sum() # one domain gauge, never the probe mean
|
||||
mass = float(w @ rho)
|
||||
return dict(tstar=t * U / length, mass=mass,
|
||||
mass_relative=0. if initial_mass is None else abs(mass-initial_mass)/initial_mass,
|
||||
velocity_l2=float(np.sqrt(w @ (ux**2 + (uy-ey)**2) / (w @ ey**2))),
|
||||
spurious_pressure_rms=float(np.sqrt(w @ p**2 / w.sum())),
|
||||
rho_min=float(rho.min()), rho_max=float(rho.max()),
|
||||
max_speed=float(np.hypot(ux, uy).max()), leaf_cells=len(rho))
|
||||
|
||||
|
||||
def case_steps(n):
|
||||
return [r * (n // ROOT_N) for r in STEPS]
|
||||
|
||||
|
||||
def select(masks, n=ROOT_N):
|
||||
"""Exact native cells filling eight unit-area groups; no interpolated samples."""
|
||||
if (n, len(masks)) not in ((128, 1), (256, 1), (128, 2)):
|
||||
raise ValueError("unsupported square probe layout")
|
||||
probes = []
|
||||
for name, (x, y) in GROUPS.items():
|
||||
level = int(len(masks) == 2 and name.startswith("inner"))
|
||||
factor = (n // ROOT_N) * 2**level
|
||||
for dy in range(factor):
|
||||
for dx in range(factor):
|
||||
i, j = factor*x + dx, factor*y + dy
|
||||
if not masks[level][j, i]:
|
||||
raise ValueError("square probe is not ACTIVE")
|
||||
probes.append(dict(group=name, level=level, i=i, j=j,
|
||||
center=[(i+.5)/factor, (j+.5)/factor],
|
||||
width=1/factor, area=1/factor**2, parity=[i % 2, j % 2]))
|
||||
if len({(p["level"], p["i"], p["j"]) for p in probes}) != len(probes):
|
||||
raise ValueError("duplicate square probes")
|
||||
return probes
|
||||
|
||||
|
||||
def new_trace(probes, case_viscosity, case):
|
||||
return dict(schema=1, kind="axis-square orientation 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/100, case_viscosity=case_viscosity,
|
||||
mode=list(MODE), phase=PHI, amplitude=U, fields=["rho", "ux", "uy"],
|
||||
probes=probes, steps=[], values=[])
|
||||
|
||||
|
||||
def group_moments(trace):
|
||||
"""Signed unit-area means; normalize by each group's mean actual-center derivative.
|
||||
|
||||
Momentum is rho*u. Raw density is ungauged. Per-cell parity stays in the trace.
|
||||
U128 samples one center, whereas U256/refined-inner use four different centers.
|
||||
"""
|
||||
out = []
|
||||
for step, row in zip(trace["steps"], trace["values"]):
|
||||
values, groups = np.asarray(row).reshape(-1, 3), {}
|
||||
for name in GROUPS:
|
||||
indices = [i for i, p in enumerate(trace["probes"]) if p["group"] == name]
|
||||
probes = [trace["probes"][i] for i in indices]
|
||||
weights = np.array([p["area"] for p in probes])
|
||||
if weights.sum() != 1:
|
||||
raise ValueError("probe group does not have unit area")
|
||||
rho, ux, uy = values[indices].T
|
||||
ey, gradient = exact(np.array([p["center"][0] for p in probes]), step,
|
||||
ROOT_N, trace["viscosity_root"])
|
||||
drho, djx, djy = (float(weights @ v) for v in (rho-1, rho*ux, rho*uy-ey))
|
||||
derivative = float(weights @ gradient)
|
||||
groups[name] = dict(drho=drho, djx=djx, djy=djy, mean_duy_dx=derivative,
|
||||
djx_over_duy_dx=djx/derivative, djy_over_duy_dx=djy/derivative)
|
||||
out.append(dict(root_step=step, groups=groups))
|
||||
return out
|
||||
|
||||
|
||||
def validate(trace):
|
||||
if trace["steps"] != list(STEPS) or len(trace["values"]) != len(STEPS):
|
||||
raise ValueError("square trace schedule mismatch")
|
||||
_, n, depth, scheme = trace["case"]
|
||||
if (100, n, depth, scheme) not in CASES or len(trace["probes"]) != (
|
||||
20 if depth == 2 else 8*(n//ROOT_N)**2):
|
||||
raise ValueError("square trace case/count mismatch")
|
||||
for row in trace["values"]:
|
||||
validate_row(row, len(trace["probes"]))
|
||||
trace["group_moments"] = group_moments(trace)
|
||||
size = 0
|
||||
while True:
|
||||
final = len(json.dumps(dict(trace, bytes=size), allow_nan=False).encode())
|
||||
if final == size:
|
||||
break
|
||||
size = final
|
||||
if size > TRACE_MAX_BYTES:
|
||||
raise ValueError("square trace exceeds byte cap")
|
||||
return size
|
||||
Reference in new issue
Block a user