From 10069feadb12f320380c8e52caa134393f89c579 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 00:18:20 +0000 Subject: [PATCH] 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 --- docs/validation/taylor_green.md | 154 ++++++++++-------- tests/validation/taylor_green/shear_wave.py | 84 ++++++++-- .../taylor_green/test_shear_wave.py | 60 +++++-- 3 files changed, 197 insertions(+), 101 deletions(-) diff --git a/docs/validation/taylor_green.md b/docs/validation/taylor_green.md index f48a817..5309ebb 100644 --- a/docs/validation/taylor_green.md +++ b/docs/validation/taylor_green.md @@ -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. diff --git a/tests/validation/taylor_green/shear_wave.py b/tests/validation/taylor_green/shear_wave.py index 4864223..1e2a5e9 100644 --- a/tests/validation/taylor_green/shear_wave.py +++ b/tests/validation/taylor_green/shear_wave.py @@ -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 diff --git a/tests/validation/taylor_green/test_shear_wave.py b/tests/validation/taylor_green/test_shear_wave.py index bf4d3d0..53e6a20 100644 --- a/tests/validation/taylor_green/test_shear_wave.py +++ b/tests/validation/taylor_green/test_shear_wave.py @@ -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):