test: apply shear-wave review corrections

Modal magnitude R=hypot(a,b) with amplitude error and separate phase;
free-intercept ln R rate fit with per-interval rates, log-fit residual,
t* units and finite-positive check; spurious-pressure naming with raw rms;
INVALID_CONTROL vs CONTROL_SCREEN_FAIL (refined results kept), mixed-source
check, raw ratios to both controls; docs narrow the cubic-term scope,
startup and placement limitations. oracle.py still 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 00:18:20 +00:00
1 parent ffff742b69
commit 10069feadb
3 files changed
+197 -101

No files matched your search

+85 -69
View File
@@ -164,82 +164,98 @@ The four Python harness files remain together under tests/validation/taylor_gree
`shear_wave.py` reuses the frozen oracle's constants, cell centers and
geometry. `oracle.py` is byte-identical, and `test_shear_wave.py` pins its
eec4677 SHA-256. `run_case.py --flow shear_wave` selects it. The default flow
is still the frozen Taylor–Green oracle, and the default receipt is unchanged.
eec4677 SHA-256. `run_case.py --flow shear_wave` selects the shear wave;
the default flow and its receipt are the unchanged Taylor–Green path.
Convention: integer mode (2,1) on the L=N periodic root domain, so
`k = (2*pi/L)*(2,1)` points 26.565° from +x. Mode (1,2) would point 63.435°.
A wrong-mode test catches a swap between the two. Polarization is
`E = (-1,2)/sqrt(5)`, perpendicular to k:
### Convention
Integer mode (2,1) on the L=N periodic root domain:
- the wavevector `k = (2*pi/L)*(2,1)` points 26.565° from +x;
- mode (1,2) would point 63.435°, and a wrong-mode test catches a swap.
Polarization is `E = (-1,2)/sqrt(5)`, perpendicular to k:
u = U*E*sin(k.x + phi)*exp(-nu*|k|^2*t), phi = 0, p = const, rho0 = 1
Here `nu = U*N/100`, the Taylor–Green Re100 viscosity at the same N. The
initial condition is FP64 second-order equilibrium at the actual leaf
centers, with no non-equilibrium part.
`nu = U*N/100`, the Taylor–Green Re100 viscosity at the same N. This is an
exact incompressible Navier–Stokes solution. The initial condition is FP64
second-order equilibrium at the actual leaf centers, with no
non-equilibrium part.
Physical caveats:
1. *Finite-Mach error.* The second-order D2Q9 equilibrium has third moment
`sum_i f_eq c_a c_b c_g = rho*cs^2*(u_a d_bg + u_b d_ag + u_g d_ab)`. The
`(c.u)^2` term contributes only even moments. The continuum target also
has `rho*u_a*u_b*u_g`. At Navier–Stokes (first Chapman–Enskog) order,
the resulting stress error is proportional to `d_g(rho u_a u_b u_g)`.
For this field with uniform density, `u.grad = 0` and `div u = 0`, so
that particular term vanishes.
### Physical caveats
This does *not* show that all finite-Mach or cubic error vanishes. It
does not cover:
- higher Chapman–Enskog orders;
- lattice truncation for the oblique wavevector;
- FP32 storage;
- any density nonuniformity, which interfaces or anisotropy can create.
2. *Startup.* Equilibrium initialization omits the non-equilibrium stress.
The resulting kinetic transient lasts O(tau) steps and may shift the
in-phase amplitude. The decay rate is fitted from `ln(a_hat)` using
only samples with t* >= 0.12, which is about 1.9N root steps and much
longer than tau. This is intended to reduce startup sensitivity. It
has not been shown to cancel the bias, and that remains unverified
until measured.
1. *Scope of the cubic-term argument.* The second-order D2Q9 equilibrium
lacks the cubic third moment `rho*u_a*u_b*u_g` of the Maxwellian (Dellar,
https://people.maths.ox.ac.uk/dellar/papers/Galilean.pdf, eq. 3). At
Navier–Stokes order this produces a stress error proportional to
`d_g(rho u_a u_b u_g)`.
Metrics are computed on ACTIVE leaf cells only, each weighted by its area
`4**-level` in root-cell units. The partition must cover the domain exactly
once. One global pressure gauge is used, never one per level.
For the ideal constant-density, zero-background transverse wave, k.E = 0
makes that contracted derivative vanish. That is the only claim made.
It does not remove other discrete or finite-Mach errors. It is not
guaranteed once AMR introduces density, longitudinal or higher-mode
contamination. The analytic NS field itself is exact.
2. *Startup.* Equilibrium initialization is not the discrete shear
eigenmode (Mei et al., https://doi.org/10.1016/j.compfluid.2005.08.008).
- The decay rate is a free-intercept least-squares fit of ln R over the
samples with t* >= 0.12, where `R = hypot(a_hat, b_hat)`.
- The intercept cancels only a constant amplitude prefactor, and only
once a single exponential dominates.
- Per-interval rates and the maximum log-fit residual are kept, so a
lingering transient stays visible.
- Late sampling alone does not show that transients have gone.
- `a_hat`, `b_hat`: weighted least-squares fit of `u.E` onto the sin and cos
modes, solved with the 2x2 Gram matrix of the actual composite quadrature.
- `phase_drift = atan2(b_hat, a_hat)`, in radians.
- `rate_error = |rate_fit/(nu|k|^2) - 1|`, dimensionless. Rates are per root
step.
- `velocity_l2`: error relative to the exact field.
- `residual_l2`: the velocity left after subtracting the fitted mode,
relative to the exact-field norm.
- `longitudinal_l2`: the `u.k_hat` part of the velocity, relative to the
exact-field norm.
- `pressure_rel = p_rms/(A(t)^2/4)`: the gauged composite rms of
`p = (rho-1)/3`, divided by a *fixed analytic scale*.
- A²/4 is the continuum rms of the Taylor–Green exact pressure at the
same amplitude. That makes the number comparable in magnitude with TG
`pressure_l2`, but it is not that norm. TG normalizes by the discrete,
globally gauged exact-pressure norm on the composite grid.
- Exact pressure here is constant, so no relative-to-exact claim is made.
- Max and per-level values are reported for localization.
### Metrics
Preregistered screens (`shear_wave.assess`):
- Run set: uniform N, uniform 2N, and refined depth-2 runs at root N, with
constant and/or projected_linear explosion.
- `eps(N) = (|k| h_root)^2 = 5*(2*pi/N)^2`, dimensionless, coefficient one.
It is 0.01205 at N=128.
- Absolute screens: rate_error, max |phase_drift| (radians) and max
residual_l2 must each be <= eps. This is a preregistered diagnostic
choice of the second-order truncation scale, *not* a proven error bound.
- Existing ceilings also apply: velocity_l2 <= 0.05, mass <= 1e-5 and
pressure_rel <= 0.02.
- Budget form, as in `assess.py`:
`refined <= max(2*control_N, floor)`, with floors 0.002 for rate and
residual and 0.02 for pressure.
- The ratio to the 2N control is reported, not gated.
- A control failing an absolute screen makes the set INVALID_CONTROL. The
screens are never retuned.
Computed on ACTIVE leaf cells, each weighted by its area `4**-level` in
root-cell units. The partition must cover the domain exactly once, and one
global pressure gauge is used.
Scope: one flow, one rectangle, SRT only. Two resolutions show a trend,
not an order. This does not establish placement robustness.
- `a_hat`, `b_hat`: weighted least-squares fit of `u.E` onto sin/cos, using
the 2x2 Gram matrix of the actual composite quadrature. A wrong Fourier
mode need not project to exactly zero on this quadrature.
- `amplitude_error = R/A_exact - 1`.
- `phase_drift = atan2(b_hat, a_hat)`, in radians, reported separately.
- Rates are positive (minus the fitted slope).
- Per root step they are compared with `nu*|k|^2`. Uniform 2N uses its
own `nu = 0.0768` at N=256 and doubled step counts.
- In t* they are compared with `5*(2*pi)^2/Re = 1.97392088`.
- `rate_error = |rate/exact - 1|`.
- `velocity_l2`, `residual_l2` (velocity left after removing the fitted
mode) and `longitudinal_l2` (the `u.k_hat` part), all relative to the
exact-field norm. A longitudinal perturbation also raises the
full-vector residual and the velocity L2.
- `spurious_pressure_rms`: the raw gauged rms of `p = (rho-1)/3`.
- `spurious_pressure_scaled`: that rms divided by `A_exact(t)^2/4`, a
continuum reference scale. It is *not* the discrete, globally gauged
Taylor–Green norm on the composite grid. Exact pressure is constant, so
no relative-to-exact claim is made. Max and per-level values are kept.
### Preregistered screens (`shear_wave.assess`)
Run set: uniform N, uniform 2N, and refined depth-2 runs at root N.
- `eps(N) = (|k| h_root)^2 = 5*(2*pi/N)^2`: dimensionless, coefficient
one, 0.01205 at N=128. This is a chosen screen, not a proven error bound.
- Absolute screens:
- rate_error, max |phase_drift| (radians) and max residual_l2 must each
be <= eps;
- velocity_l2 <= 0.05, mass <= 1e-5, spurious_pressure_scaled <= 0.02.
- Budgets keep the `assess.py` form, `refined <= max(2*control_N, floor)`:
- floors are 0.002 for rate and residual, 0.02 for pressure;
- because the pressure ceiling and floor are both 0.02, the comparative
pressure budget adds no constraint unless the root control exceeds 0.01;
- raw ratios to both the root and finest controls, including raw
pressure rms, are always reported.
- Outcomes are kept distinct:
- INVALID_CONTROL: a control is missing, incomplete or nonfinite, or the
records come from mixed sources;
- CONTROL_SCREEN_FAIL: complete, finite controls that fail a screen.
Refined results are still computed and retained.
- Screens are never retuned.
### Limitations
One flow, one phase and one rectangle, SRT only. The rectangle's vertical
faces span 0.625 tangential periods and its horizontal faces 1.5, so this
is not placement robustness. Two resolutions show a trend, not an order.
+66 -18
View File
@@ -22,8 +22,10 @@ INITIALIZATION = (
)
# Per-row screens. phase_drift_abs, residual_l2 and the run-level rate screen
# use eps(n) and are preregistered diagnostic choices, not error bounds.
LIMITS = dict(velocity_l2=0.05, pressure_rel=0.02, mass_relative=1e-5)
FLOORS = dict(rate_error=0.002, residual_l2=0.002, pressure_rel=0.02)
LIMITS = dict(velocity_l2=0.05, spurious_pressure_scaled=0.02, mass_relative=1e-5)
# With equal absolute ceiling and floor (0.02) the comparative pressure budget
# adds no constraint unless the root control exceeds 0.01; raw ratios are kept.
FLOORS = dict(rate_error=0.002, residual_l2=0.002, spurious_pressure_scaled=0.02)
BASELINE_FACTOR = 2.0
@@ -103,7 +105,8 @@ def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
fit = a_hat * s + b_hat * c
longitudinal = (ux * k[0] + uy * k[1]) / math.sqrt(k @ k)
amplitude = U * math.exp(-decay_rate(length, viscosity) * t)
scale = amplitude * amplitude / 4 # fixed pressure scale, see docs
scale = amplitude * amplitude / 4 # continuum reference scale, not the TG norm
modal = math.hypot(a_hat, b_hat)
p = (rho - 1) / 3
p = p - w @ p / volume # one global gauge
mass = float(w @ rho)
@@ -117,6 +120,8 @@ def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
b_hat=float(b_hat),
amplitude_exact=amplitude,
amplitude_ratio=float(a_hat / amplitude),
modal_amplitude=modal,
amplitude_error=modal / amplitude - 1,
phase_drift=float(math.atan2(b_hat, a_hat)),
phase_drift_abs=abs(float(math.atan2(b_hat, a_hat))),
velocity_l2=float(
@@ -126,9 +131,10 @@ def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
np.sqrt(w @ ((ux - fit * E[0]) ** 2 + (uy - fit * E[1]) ** 2) / exact_norm)
),
longitudinal_l2=float(np.sqrt(w @ (longitudinal**2) / exact_norm)),
pressure_rel=float(np.sqrt(w @ (p * p) / volume) / scale),
pressure_max_rel=float(np.max(np.abs(p)) / scale),
pressure_rel_by_level=[
spurious_pressure_rms=float(np.sqrt(w @ (p * p) / volume)),
spurious_pressure_scaled=float(np.sqrt(w @ (p * p) / volume) / scale),
spurious_pressure_max_scaled=float(np.max(np.abs(p)) / scale),
spurious_pressure_scaled_by_level=[
float(np.sqrt(w[lev == i] @ (p[lev == i] ** 2) / w[lev == i].sum()) / scale)
for i in range(len(masks))
],
@@ -140,15 +146,33 @@ def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
def fit_rate(rows, length, viscosity):
"""Least-squares decay rate of ln(a_hat) over late samples."""
"""Free-intercept least squares of ln R, R = hypot(a_hat, b_hat), late samples.
The intercept absorbs only a constant amplitude prefactor once a single
exponential dominates; per-interval rates and the fit residual are kept so
a lingering transient stays visible. Rates are per root step of the record
(uniform 2N uses its own nu and doubled step counts).
"""
late = [r for r in rows if r["tstar"] >= FIT_FROM_TSTAR - 1e-12]
if len(late) < 2 or any(r["a_hat"] <= 0 for r in late):
raise ValueError("need >= 2 late samples with positive amplitude")
amp = np.array([math.hypot(r["a_hat"], r["b_hat"]) for r in late])
if len(late) < 3 or not (np.isfinite(amp).all() and (amp > 0).all()):
raise ValueError("need >= 3 late samples with finite positive R")
t = np.array([r["step"] for r in late], dtype=float)
slope = np.polyfit(t, np.log([r["a_hat"] for r in late]), 1)[0]
(slope, intercept) = np.polyfit(t, np.log(amp), 1)
residual = np.log(amp) - (slope * t + intercept)
exact_rate = decay_rate(length, viscosity)
return dict(rate=float(-slope), exact_rate=exact_rate,
rate_error=abs(-slope / exact_rate - 1))
to_tstar = length / U # root steps per unit t*
return dict(
rate=float(-slope), exact_rate=exact_rate,
rate_tstar=float(-slope * to_tstar),
exact_rate_tstar=exact_rate * to_tstar, # = 5 (2 pi)^2 / Re
rate_error=abs(-slope / exact_rate - 1),
interval_rate_errors=[
float(-np.log(amp[i + 1] / amp[i]) / (t[i + 1] - t[i]) / exact_rate - 1)
for i in range(len(t) - 1)
],
log_fit_residual_max=float(np.max(np.abs(residual))),
)
def summarize(record):
@@ -160,7 +184,9 @@ def summarize(record):
complete=record["completed"] and len(rows) == len(TIMES),
residual_l2=max(r["residual_l2"] for r in rows),
phase_drift_abs=max(r["phase_drift_abs"] for r in rows),
pressure_rel=max(r["pressure_rel"] for r in rows),
spurious_pressure_scaled=max(r["spurious_pressure_scaled"] for r in rows),
spurious_pressure_rms=max(r["spurious_pressure_rms"] for r in rows),
amplitude_error_abs=max(abs(r["amplitude_error"]) for r in rows),
velocity_l2=max(r["velocity_l2"] for r in rows),
mass_relative=max(r["mass_relative"] for r in rows),
)
@@ -170,25 +196,47 @@ def summarize(record):
return out
def _usable(record):
try:
out = summarize(record)
except (KeyError, ValueError):
return None
finite = all(math.isfinite(v) for v in out.values() if isinstance(v, float))
return out if out["complete"] and finite else None
def assess(base_control, fine_control, refined):
"""Uniform root-spacing and finest-spacing controls versus refined runs."""
"""Uniform root-spacing and finest-spacing controls versus refined runs.
INVALID_CONTROL: missing, incomplete, nonfinite or mixed-source controls.
CONTROL_SCREEN_FAIL: complete finite controls that fail a screen; refined
results are still computed and kept.
"""
if base_control is None or fine_control is None:
return dict(status="INVALID_CONTROL", reason="missing control")
n = base_control["n"]
if (base_control["depth"], fine_control["depth"], fine_control["n"]) != (1, 1, 2 * n):
raise ValueError("controls must be uniform N and uniform 2N")
if any(r["depth"] != 2 or r["n"] != n for r in refined):
raise ValueError("refined runs must be depth 2 at root N")
base, fine = summarize(base_control), summarize(fine_control)
sources = {repr(r.get("source")) for r in [base_control, fine_control, *refined]}
base, fine = _usable(base_control), _usable(fine_control)
if base is None or fine is None or len(sources) != 1:
return dict(status="INVALID_CONTROL",
reason="incomplete/nonfinite control or mixed source")
report = dict(base_control=base, fine_control=fine, refined={}, status="PASS")
if not (base["absolute_pass"] and fine["absolute_pass"]):
report["status"] = "INVALID_CONTROL"
report["status"] = "CONTROL_SCREEN_FAIL"
for r in refined:
s = summarize(r)
s["budget"] = {
key: max(BASELINE_FACTOR * base[key], floor) for key, floor in FLOORS.items()
}
s["budget_pass"] = all(s[key] <= s["budget"][key] for key in FLOORS)
s["fine_control_ratio"] = {
key: (s[key] / fine[key] if fine[key] > 0 else None) for key in FLOORS
s["control_ratios"] = {
name: {key: (s[key] / c[key] if c[key] > 0 else None)
for key in (*FLOORS, "spurious_pressure_rms")}
for name, c in (("root", base), ("finest", fine))
}
s["pass"] = s["absolute_pass"] and s["budget_pass"]
report["refined"][r["explosion"]] = s
@@ -120,13 +120,15 @@ class MetricTests(unittest.TestCase):
self.assertLess(r["longitudinal_l2"], 1e-12)
self.assertLess(r["phase_drift_abs"], 1e-12)
self.assertAlmostEqual(r["amplitude_ratio"], 1.0, places=12)
self.assertLess(r["pressure_rel"], 1e-10)
self.assertEqual(len(r["pressure_rel_by_level"]), 2)
self.assertLess(abs(r["amplitude_error"]), 1e-12)
self.assertLess(r["spurious_pressure_scaled"], 1e-10)
self.assertEqual(len(r["spurious_pressure_scaled_by_level"]), 2)
self.assertEqual(r["leaf_cells"], int(sum(m.sum() for m in self.masks)))
def test_phase_control(self):
r = self.sample(exact_field(phase=0.01), 0.0)
self.assertAlmostEqual(r["phase_drift"], 0.01, delta=1e-9)
self.assertLess(abs(r["amplitude_error"]), 1e-12) # R = hypot(a, b) is phase-free
self.assertLess(r["residual_l2"], 1e-12) # a pure phase shift stays modal
def test_rate_control_and_fit(self):
@@ -138,16 +140,24 @@ class MetricTests(unittest.TestCase):
rows.append(dict(self.sample(field, step), step=step))
fit = sw.fit_rate(rows, N, NU)
self.assertAlmostEqual(fit["rate_error"], expected, delta=1e-9)
np.testing.assert_allclose(fit["interval_rate_errors"], expected, atol=1e-9)
self.assertLess(fit["log_fit_residual_max"], 1e-9)
self.assertAlmostEqual(fit["exact_rate_tstar"], 5 * (2 * math.pi) ** 2 / sw.RE)
self.assertAlmostEqual(fit["exact_rate_tstar"], 1.97392088, places=8)
def test_fit_ignores_startup_sample(self):
rows = [dict(tstar=0.0, step=0, a_hat=2 * U)]
rows += [dict(tstar=s, step=round(s * N / U),
rows = [dict(tstar=0.0, step=0, a_hat=2 * U, b_hat=0.0)]
rows += [dict(tstar=s, step=round(s * N / U), b_hat=0.0,
a_hat=U * math.exp(-sw.decay_rate(N, NU) * round(s * N / U)))
for s in TIMES[1:]]
self.assertLess(sw.fit_rate(rows, N, NU)["rate_error"], 1e-12)
rows[-1]["a_hat"] = float("nan")
with self.assertRaises(ValueError):
sw.fit_rate(rows, N, NU)
def test_wrong_mode_control(self):
# Swapping (2,1) for (1,2), i.e. 26.6 vs 63.4 deg, is not silently modal.
# Composite quadrature need not give an exactly zero projection.
r = self.sample(exact_field(mode=(1, 2)), 0.0)
self.assertLess(abs(r["amplitude_ratio"]), 0.05)
self.assertGreater(r["residual_l2"], 0.95)
@@ -155,18 +165,23 @@ class MetricTests(unittest.TestCase):
def test_longitudinal_control(self):
r = self.sample(exact_field(longitudinal=1e-3), 0.0)
self.assertAlmostEqual(r["longitudinal_l2"], 1e-3 / U, delta=1e-9)
# The full-vector residual and velocity error see it too.
self.assertAlmostEqual(r["residual_l2"], 1e-3 / U, delta=1e-9)
self.assertAlmostEqual(r["velocity_l2"], 1e-3 / U, delta=1e-9)
self.assertLess(r["phase_drift_abs"], 1e-12)
self.assertAlmostEqual(r["amplitude_ratio"], 1.0, places=12)
def test_pressure_gauge_and_ripple(self):
r = self.sample(exact_field(offset=1e-4), 0.0)
self.assertLess(r["pressure_rel"], 1e-10) # one global gauge removes offsets
self.assertLess(r["spurious_pressure_scaled"], 1e-10) # one global gauge
ripple = 3e-5
r = self.sample(exact_field(ripple=ripple), 0.0)
# p = ripple/3 cos(theta); rms over a whole number of periods = ripple/(3 sqrt 2).
# Composite midpoint quadrature is not exactly periodic: 3% tolerance.
expected = ripple / (3 * math.sqrt(2)) / (U * U / 4)
self.assertAlmostEqual(r["pressure_rel"], expected, delta=0.03 * expected)
self.assertAlmostEqual(r["spurious_pressure_scaled"], expected, delta=0.03 * expected)
self.assertAlmostEqual(r["spurious_pressure_rms"], expected * U * U / 4,
delta=0.03 * expected * U * U / 4)
def test_volume_weighting_and_double_count(self):
bad = [np.ones_like(m) for m in self.masks]
@@ -177,9 +192,9 @@ class MetricTests(unittest.TestCase):
self.assertEqual(r["leaf_cells"], N * N - self.masks[1].sum() // 4 + self.masks[1].sum())
def record(n, depth, rows, explosion="constant", completed=True):
def record(n, depth, rows, explosion="constant", completed=True, source="s"):
return dict(n=n, depth=depth, viscosity=U * n / sw.RE, rows=rows, completed=completed,
explosion=explosion)
explosion=explosion, source=source)
def rows_for(n, rate_error=0.0, residual=1e-4, pressure=1e-3, phase=0.0):
@@ -187,8 +202,10 @@ def rows_for(n, rate_error=0.0, residual=1e-4, pressure=1e-3, phase=0.0):
out = []
for tstar in TIMES:
step = round(tstar * n / U)
out.append(dict(tstar=tstar, step=step, a_hat=U * math.exp(-rate * step),
residual_l2=residual, phase_drift_abs=phase, pressure_rel=pressure,
out.append(dict(tstar=tstar, step=step, a_hat=U * math.exp(-rate * step), b_hat=0.0,
amplitude_error=0.0, residual_l2=residual, phase_drift_abs=phase,
spurious_pressure_scaled=pressure,
spurious_pressure_rms=pressure * U * U / 4,
velocity_l2=residual, mass_relative=0.0))
return out
@@ -203,15 +220,30 @@ class AssessTests(unittest.TestCase):
self.assertEqual(report["status"], "FAIL")
self.assertAlmostEqual(report["refined"]["constant"]["budget"]["rate_error"], 0.002)
# pressure budget = max(2*0.015, 0.02) = 0.03 (control-dominated).
self.assertAlmostEqual(report["refined"]["constant"]["budget"]["pressure_rel"], 0.03)
self.assertAlmostEqual(
report["refined"]["constant"]["budget"]["spurious_pressure_scaled"], 0.03)
self.assertIn("finest", report["refined"]["constant"]["control_ratios"])
ok = record(128, 2, rows_for(128, rate_error=0.0015), explosion="projected_linear")
self.assertEqual(sw.assess(base, fine, [ok])["status"], "PASS")
def test_invalid_control_and_wrong_shapes(self):
base = record(128, 1, rows_for(128, residual=0.5)) # > eps(128)
def test_control_outcomes_are_distinct(self):
fine = record(256, 1, rows_for(256))
refined = record(128, 2, rows_for(128))
failing = record(128, 1, rows_for(128, residual=0.5)) # complete, > eps(128)
report = sw.assess(failing, fine, [refined])
self.assertEqual(report["status"], "CONTROL_SCREEN_FAIL")
self.assertIn("constant", report["refined"]) # refined results preserved
good = record(128, 1, rows_for(128))
self.assertEqual(sw.assess(None, fine, [refined])["status"], "INVALID_CONTROL")
incomplete = record(128, 1, rows_for(128), completed=False)
self.assertEqual(sw.assess(incomplete, fine, [refined])["status"], "INVALID_CONTROL")
mixed = record(256, 1, rows_for(256), source="other")
self.assertEqual(sw.assess(good, mixed, [refined])["status"], "INVALID_CONTROL")
def test_wrong_shapes(self):
base = record(128, 1, rows_for(128))
fine = record(256, 1, rows_for(256))
refined = record(128, 2, rows_for(128))
self.assertEqual(sw.assess(base, fine, [refined])["status"], "INVALID_CONTROL")
with self.assertRaises(ValueError):
sw.assess(base, record(128, 1, rows_for(128)), [refined])
with self.assertRaises(ValueError):