research: Dütsch driver — continuous settling, domain criteria, profile coverage

common_window requires every pair from each case's own K_case to K+2;
a-priori domain criteria against the 60 D reference (separate from Table 1);
bilinear sampling returns the original fluid corner weight sum; analysis
time-averages bin samples, reports coverage/missing points and fails closed
on an empty common profile set; cases run to the end of cycle 11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-30 10:58:05 +00:00
1 parent 6f95c34c0b
commit b31a6141b3
5 files changed
+79 -32

No files matched your search

+36 -2
View File
@@ -36,9 +36,30 @@ def coarse_wave(case, k, r=1):
def profiles(case, k):
"""{(phase, station, x2): (u, v)} of cycle k, time-averaged over the bin steps
(equal weights per step) from valid samples only."""
s = case["samples"]
acc = {}
for _, _, ph, xs, x2, u, v, _ in s[s[:, 1] == k]:
if not (math.isnan(u) or math.isnan(v)):
acc.setdefault((ph, xs, x2), []).append((u, v))
return {key: tuple(np.mean(vals, axis=0)) for key, vals in acc.items()}
def coverage(case, k):
s = case["samples"]
rows = s[s[:, 1] == k]
return {(ph, xs, x2): (u, v) for _, _, ph, xs, x2, u, v in rows}
missing = int(np.isnan(rows[:, 5]).sum())
return dict(samples=len(rows), missing=missing, bin_steps=len(set(rows[:, 0])),
min_fluid_weight=float(np.nanmin(np.where(rows[:, 7] > 0, rows[:, 7], np.nan)))
if len(rows) else math.nan)
def common_points(dicts):
keys = set(dicts[0])
for d in dicts[1:]:
keys &= set(d)
return sorted(keys)
def cycle_morison(case, ks):
@@ -51,7 +72,7 @@ def cycle_morison(case, ks):
def pair_metrics(case, k):
a, b = coarse_wave(case, k - 1), coarse_wave(case, k)
pa, pb = profiles(case, k - 1), profiles(case, k)
common = [key for key in pa if key in pb and not any(map(math.isnan, pa[key] + pb[key]))]
common = common_points([pa, pb]) # empty set -> inf -> pair fails (FAIL-CLOSED)
prof = max((max(abs(pa[c][0] - pb[c][0]), abs(pa[c][1] - pb[c][1])) for c in common),
default=math.inf) / case["u_max"]
(cd0, ci0), (cd1, ci1) = cycle_morison(case, [k - 1]), cycle_morison(case, [k])
@@ -80,6 +101,7 @@ def main(paths):
entry = {key: case[key] for key in ("path", "D", "T", "box", "n", "u_max", "nu", "tau", "A",
"steps", "cycles", "F_ref")}
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:
entry["morison"] = cycle_morison(case, window)
@@ -94,6 +116,18 @@ def main(paths):
ref_c = ref.reshape(-1, r).mean(axis=1)
own = np.concatenate([coarse_wave(case, k) for k in window])
entry["wave_rms_vs_finest"] = float(np.sqrt(np.mean((own - ref_c) ** 2)))
for k in window: # profiles on the common valid point set of all compared cases
per_case = [profiles(c, k) for c in cases]
keys = common_points(per_case)
report.setdefault("common_profile_points", {})[k] = len(keys)
if not keys:
report["verdict"] = "FAIL-CLOSED: empty common profile set"
continue
fine_p = per_case[cases.index(fine)]
for case, entry, prof in zip(cases, report["cases"], per_case):
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)
print(json.dumps(report, indent=1, default=float))
+10 -7
View File
@@ -17,6 +17,7 @@ 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
def amplitude(D):
@@ -65,8 +66,9 @@ def morison_fit(f_norm, b1, b2):
def bilinear_fluid(field, fluid, x, y):
"""Bilinear value at (x, y) in cell coordinates from fluid cells only.
Returns (value, ((ix, iy, weight), ...)) with weights renormalized over the
fluid corners, or (None, ()) when no corner is fluid (MISSING, never zero).
Returns (value, ((ix, iy, weight), ...), fluid_weight_sum) with weights
renormalized over the fluid corners and the original sum of their bilinear
weights, or (None, (), 0.0) when no corner is fluid (MISSING, never zero).
"""
x0, y0 = int(math.floor(x)), int(math.floor(y))
fx, fy = x - x0, y - y0
@@ -75,9 +77,9 @@ def bilinear_fluid(field, fluid, x, y):
used = [(i, j, w) for i, j, w in corners if w > 0.0 and fluid[j, i]]
total = sum(w for _, _, w in used)
if total == 0.0:
return None, ()
return None, (), 0.0
used = tuple((i, j, w / total) for i, j, w in used)
return float(sum(w * field[j, i] for i, j, w in used)), used
return float(sum(w * field[j, i] for i, j, w in used)), used, float(total)
def settled_start(pairs_pass):
@@ -95,13 +97,14 @@ def settled_start(pairs_pass):
def common_window(k_cases, pairs_pass_by_case):
"""Common scored cycles K..K+2 with K = max K_case, or None (FAIL-CLOSED) when a
case never settled, K + 2 exceeds the cap, or a case fails a window pair."""
case never settled, K + 2 exceeds the cap, or a case fails any pair p_j for
j = its own K_case .. K + 2 (continuous settling, including the wait for K)."""
if any(k is None for k in k_cases):
return None
K = max(k_cases)
if K + 2 > LAST_SCORED:
return None
for pairs in pairs_pass_by_case:
if not all(pairs.get(k, False) for k in (K, K + 1, K + 2)):
for k_case, pairs in zip(k_cases, pairs_pass_by_case):
if not all(pairs.get(j, False) for j in range(k_case, K + 3)):
return None
return K, K + 1, K + 2
+5 -5
View File
@@ -73,18 +73,18 @@ def main():
for xs in p.STATIONS:
for x2 in p.X2:
x, y = xm + xs * D, ym + x2 * D
u, wu = p.bilinear_fluid(ux, fluid, x, y)
v, _ = p.bilinear_fluid(uy, fluid, x, y)
u, wu, wsum = p.bilinear_fluid(ux, fluid, x, y)
v, _, _ = p.bilinear_fluid(uy, fluid, x, y)
samples.append((s, p.cycle(s, T), ph, xs, x2,
math.nan if u is None else u, math.nan if v is None else v,
json.dumps(wu)))
wsum, json.dumps(wu)))
np.savez(os.path.join(args.out, "raw.npz"), force=force, diag=np.asarray(diag),
samples=np.asarray([r[:7] for r in samples], float))
samples=np.asarray([r[:8] for r in samples], float))
with open(os.path.join(args.out, "case.json"), "w") as fh:
json.dump(dict(D=D, T=T, box=args.box, n=n, u_max=u_max, nu=cfg["physics"]["viscosity"],
tau=3 * cfg["physics"]["viscosity"] + 0.5, A=A, steps=steps,
cycles=args.cycles, F_ref=0.5 * u_max ** 2 * D,
weights=[r[7] for r in samples]), fh)
weights=[r[8] for r in samples]), fh)
finally:
sim.close()