diff --git a/docs/validation/duetsch_re100_kc5_protocol.md b/docs/validation/duetsch_re100_kc5_protocol.md index 85f1851..8421d15 100644 --- a/docs/validation/duetsch_re100_kc5_protocol.md +++ b/docs/validation/duetsch_re100_kc5_protocol.md @@ -134,12 +134,19 @@ ledger; verdict PASS or FAIL-CLOSED. Scales: `F_ref = 0.5 rho U_max² D`, `U_ref = U_max`; never a near-zero mean force as denominator. -- Settling metrics, cycle k−1 → k, phase-aligned by step within the cycle: - - waveform `RMS(dFx) / F_ref ≤ .02`; - - `|d mean|`, `|d RMS|`, `|d peak|` of Fx, each `/ F_ref ≤ .01`; - - scored profiles (fluid-only) `|du| / U_max`, `|dv| / U_max ≤ .03` at the +- Settling metrics, cycle k−1 → k, phase-aligned by step within the cycle + (error budget: one fifth of the tightest comparison band of the same + observable, so residual drift cannot obscure a domain or resolution pass): + - waveform `RMS(dFx) / F_ref ≤ .002`; + - `|d mean|`, `|d RMS|`, `|d peak|` of Fx, each `/ F_ref ≤ .002`; + - scored profiles (fluid-only) `|du| / U_max`, `|dv| / U_max ≤ .004` at the three phases × four stations; - - Morison `|dc_d| ≤ .02`, `|dc_i| ≤ .02`. + - Morison `|dc_d| ≤ .004`, `|dc_i| ≤ .004`. +- Conservative comparison: every domain and cross-resolution criterion is + applied to `|difference| + drift_a + drift_b`, where drift is the largest + pair metric of that case inside the scored window. +- These constants (protocol.py SETTLE, DOMAIN, CROSS) are frozen before any + physical result; no post-result loosening. - Rule: all metrics for N = 3 consecutive whole paper cycles → settled. Cycle indexing (worked example, binding). Paper cycle k spans diff --git a/research/duetsch_re100_kc5/analyze.py b/research/duetsch_re100_kc5/analyze.py index 40f55a3..c479f54 100644 --- a/research/duetsch_re100_kc5/analyze.py +++ b/research/duetsch_re100_kc5/analyze.py @@ -117,7 +117,9 @@ def main(argv): entry.update(duration_T=case["steps"] / case["T"], K_case=k_case, pairs=pairs, coverage={k: coverage(case, k) for k in sorted(cycle_slices(case)) if k > p.DISCARD}, no_donor=float(case["diag"][:, 13].sum()), eq_only=float(case["diag"][:, 12].sum())) - if window: + if window: # residual drift inside the scored window (pairs K..K+2) + entry["window_drift"] = {key: max(pairs[k][key] for k in window) + for key in ("wave", "profile", "morison")} entry["morison_obs"] = cycle_morison(case, window) entry["morison"] = {name: cycle_morison(case, window, fx) for name, fx in candidates(case).items()} @@ -146,6 +148,19 @@ def main(argv): d = max(max(abs(prof[key][0] - fine_p[key][0]), abs(prof[key][1] - fine_p[key][1])) for key in keys) / case["u_max"] entry.setdefault("profile_max_vs_finest", {})[k] = float(d) + ref_entry = report["cases"][cases.index(fine)] + for entry in report["cases"]: # conservative: difference + both drifts + if entry is ref_entry: + continue + drift = {key: entry["window_drift"][key] + ref_entry["window_drift"][key] + for key in ("wave", "profile", "morison")} + dm = max(abs(entry["morison"]["cme"][0] - ref_entry["morison"]["cme"][0]), + abs(entry["morison"]["cme"][1] - ref_entry["morison"]["cme"][1])) + entry["conservative"] = dict( + wave=entry["wave_rms_vs_finest"] + drift["wave"], + profile=max(entry.get("profile_max_vs_finest", {}).values(), default=math.inf) + + drift["profile"], + morison_cme=dm + drift["morison"]) print(json.dumps(report, indent=1, default=float)) diff --git a/research/duetsch_re100_kc5/protocol.py b/research/duetsch_re100_kc5/protocol.py index ecd3ced..be17314 100644 --- a/research/duetsch_re100_kc5/protocol.py +++ b/research/duetsch_re100_kc5/protocol.py @@ -16,8 +16,12 @@ STATIONS = (-0.6, 0.0, 0.6, 1.2) # lab-fixed x/D from the mean position PHASES = (180.0, 210.0, 330.0) # degrees; bins [p, p + 1) X2 = tuple(np.round(np.arange(-1.5, 1.5001, 0.05), 10)) # declared x2/D points DISCARD, LAST_SCORED, N_PAIRS = 2, 11, 3 # cycles 0..2 discarded, cap 12 T -SETTLE = dict(wave=.02, scalar=.01, profile=.03, morison=.02) -DOMAIN = dict(wave=.01, morison=.02, profile=.02) # a-priori, vs the 60 D reference +# A-priori bands. Settling is one fifth of the tightest comparison band of the +# same observable, and comparisons add both cases' residual window drift +# (conservative: |difference| + drift_a + drift_b <= band). +DOMAIN = dict(wave=.01, morison=.02, profile=.02) # vs the 60 D reference at D = 40 +CROSS = dict(wave=.02, profile=.02) # D40 vs D80 +SETTLE = dict(wave=.002, scalar=.002, profile=.004, morison=.004) def amplitude(D):