test: integrate pinned Re100 and jet control-volume validation
This commit is contained in:
1 parent
d79e3f6f0a
commit
69e9b78de9
12 files changed
+2418
No files matched your search
@@ -0,0 +1,130 @@
|
||||
"""CPU-only cycle extraction. All criteria frozen before any Re100 GPU result."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
GATES = dict(
|
||||
min_tstar=250.0,
|
||||
cycles=5,
|
||||
min_period=2.0,
|
||||
max_period=5.0,
|
||||
min_lift_amplitude=0.2,
|
||||
period_relative_range=0.005,
|
||||
extrema_absolute_range=0.005,
|
||||
mass_relative_range=1e-4,
|
||||
cycle_mean_flux_relative=0.005,
|
||||
)
|
||||
REFERENCE_GATES = dict(
|
||||
cd_relative=0.02, cl_absolute=0.03, dp_relative=0.02, st_relative=0.02
|
||||
)
|
||||
|
||||
|
||||
def minima(t, y):
|
||||
out = []
|
||||
for i in range(1, len(y) - 1):
|
||||
if y[i] < y[i - 1] and y[i] <= y[i + 1]:
|
||||
# Quadratic sub-sample location; only separated physical cycles accepted.
|
||||
coef = np.polyfit(t[i - 1 : i + 2] - t[i], y[i - 1 : i + 2], 2)
|
||||
v = t[i] - coef[1] / (2 * coef[0]) if coef[0] > 0 else t[i]
|
||||
if t[i - 1] <= v <= t[i + 1] and (not out or v - out[-1] > 1.0):
|
||||
out.append(float(v))
|
||||
return out
|
||||
|
||||
|
||||
def extract(rows, min_time=250.0):
|
||||
if len(rows) < 3:
|
||||
return []
|
||||
t = np.array([r["tstar_force"] for r in rows])
|
||||
cl = np.array([r["cl"] for r in rows])
|
||||
ends = minima(t, cl)
|
||||
cycles = []
|
||||
for a, b in zip(ends, ends[1:]):
|
||||
if a < min_time or not GATES["min_period"] < b - a < GATES["max_period"]:
|
||||
continue
|
||||
grid = np.linspace(a, b, 257)
|
||||
c = dict(start=a, end=b, period=b - a, st=1 / (b - a))
|
||||
for key in ("cd", "cl", "dp", "mass", "flux_in", "flux_out"):
|
||||
times = np.array(
|
||||
[r["tstar" if key not in ("cd", "cl") else "tstar_force"] for r in rows]
|
||||
)
|
||||
# No endpoint extrapolation: pressure/mass must cover the whole cycle.
|
||||
if a < times[0] or b > times[-1]:
|
||||
break
|
||||
v = np.interp(grid, times, [r[key] for r in rows])
|
||||
c[key + "_min"] = float(v.min())
|
||||
c[key + "_max"] = float(v.max())
|
||||
c[key + "_midrange"] = float((v.min() + v.max()) / 2)
|
||||
c[key + "_amplitude"] = float(v.max() - v.min())
|
||||
c[key + "_time_mean"] = float(
|
||||
np.sum((v[1:] + v[:-1]) * np.diff(grid) / 2) / (b - a)
|
||||
)
|
||||
if key in ("cd", "cl", "dp"):
|
||||
c[key + "_waveform"] = v.tolist()
|
||||
else:
|
||||
c["flux_relative"] = abs(
|
||||
c["flux_in_time_mean"] - c["flux_out_time_mean"]
|
||||
) / max(abs(c["flux_in_time_mean"]), 1e-30)
|
||||
cycles.append(c)
|
||||
return cycles
|
||||
|
||||
|
||||
def assess(rows):
|
||||
cycles = extract(rows, min_time=GATES["min_tstar"])
|
||||
tail = cycles[-GATES["cycles"] :]
|
||||
checks = {}
|
||||
checks["enough_cycles"] = len(tail) == GATES["cycles"]
|
||||
if checks["enough_cycles"]:
|
||||
checks["consecutive"] = all(
|
||||
abs(a["end"] - b["start"]) < 1e-8 for a, b in zip(tail, tail[1:])
|
||||
)
|
||||
checks["recent"] = (
|
||||
0 <= rows[-1]["tstar_force"] - tail[-1]["end"] <= GATES["max_period"]
|
||||
)
|
||||
checks["shedding"] = (
|
||||
min(c["cl_amplitude"] for c in tail) >= GATES["min_lift_amplitude"]
|
||||
)
|
||||
periods = [c["period"] for c in tail]
|
||||
checks["period"] = (
|
||||
np.ptp(periods) / np.mean(periods) <= GATES["period_relative_range"]
|
||||
)
|
||||
checks["extrema"] = all(
|
||||
np.ptp([c[k + "_" + e] for c in tail]) <= GATES["extrema_absolute_range"]
|
||||
for k in ("cd", "cl", "dp")
|
||||
for e in ("min", "max")
|
||||
)
|
||||
mass = [c["mass_time_mean"] for c in tail]
|
||||
checks["mass"] = (
|
||||
np.ptp(mass) / abs(np.mean(mass)) <= GATES["mass_relative_range"]
|
||||
)
|
||||
checks["flux"] = (
|
||||
max(c["flux_relative"] for c in tail) <= GATES["cycle_mean_flux_relative"]
|
||||
)
|
||||
return dict(
|
||||
periodic=bool(checks["enough_cycles"] and all(checks.values())),
|
||||
checks={k: bool(v) for k, v in checks.items()},
|
||||
cycles=cycles,
|
||||
)
|
||||
|
||||
|
||||
def compare(cycle, ref):
|
||||
errors = {}
|
||||
passed = True
|
||||
for key in ("cd", "cl", "dp"):
|
||||
for ext in ("min", "max"):
|
||||
k = key + "_" + ext
|
||||
e = abs(cycle[k] - ref[k])
|
||||
e = e if key == "cl" else e / abs(ref[k])
|
||||
errors[k] = e
|
||||
passed &= (
|
||||
e
|
||||
<= REFERENCE_GATES[key + ("_absolute" if key == "cl" else "_relative")]
|
||||
)
|
||||
errors[key + "_waveform_rms"] = float(
|
||||
np.sqrt(
|
||||
np.mean(
|
||||
(np.array(cycle[key + "_waveform"]) - ref[key + "_waveform"]) ** 2
|
||||
)
|
||||
)
|
||||
)
|
||||
errors["st_relative"] = abs(cycle["st"] - ref["st"]) / ref["st"]
|
||||
passed &= errors["st_relative"] <= REFERENCE_GATES["st_relative"]
|
||||
return dict(reference_agreement=bool(passed), errors=errors)
|
||||
Reference in new issue
Block a user