Collect paired moving-pinball Fig2 research material
This commit is contained in:
1 parent
76873d911c
commit
63e5c3828b
8 files changed
+1284
No files matched your search
@@ -0,0 +1,94 @@
|
||||
"""Pure NumPy Fig2 primitives; no solver, phase selection, or probe DTW.
|
||||
|
||||
Adapted formula and inclusive ROI bounds from DynamisLab
|
||||
src/drl_pinball/eval/wake_l2.py::mean_field_metric, roi_slices, inspected
|
||||
2026-11-02 at HEAD 609116ec26f19766f5e9ce2b0df427d4e24faae8
|
||||
(working-tree source, not asserted clean). The explicit common-fluid/finite
|
||||
mask and motion summaries are additions. legacy_test/metrics.py::cycle_similarity
|
||||
was inspected only: its six probe channels are not a field recovery metric.
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
|
||||
def roi_slices(shape, *, D, xs, y0=None, downstream_solid_extent):
|
||||
"""Lab-fixed ROI: sensor-relative x/D=-6..14, y/D=-5..5, inclusive.
|
||||
|
||||
Equivalent to pinball-front-relative x/D=4..24 when xs=front+10D.
|
||||
Supply actual geometry, e.g. D=40, xs=1600, y0=510.5.
|
||||
"""
|
||||
ny, nx = shape
|
||||
y0 = (ny - 1) / 2 if y0 is None else float(y0)
|
||||
if D <= 0 or not all(np.isfinite(v) for v in (D, xs, y0, downstream_solid_extent)):
|
||||
raise ValueError("invalid geometry")
|
||||
x0, x1 = math.ceil(xs - 6 * D), math.floor(xs + 14 * D)
|
||||
lo, hi = math.ceil(y0 - 5 * D), math.floor(y0 + 5 * D)
|
||||
if x0 < 0 or lo < 0 or x1 >= nx or hi >= ny:
|
||||
raise ValueError("ROI outside grid")
|
||||
if x0 <= downstream_solid_extent:
|
||||
raise ValueError("ROI not downstream of solid extent")
|
||||
return slice(lo, hi + 1), slice(x0, x1 + 1)
|
||||
|
||||
|
||||
def common_fluid_roi_mask(fluid_masks, fields, roi):
|
||||
"""Intersect explicit boolean fluid masks, ROI, and finite 2-D fields.
|
||||
|
||||
Pass every compared role/component/sample to freeze a common support over
|
||||
time. Zero velocity never implies solid. Masks must share the lab grid.
|
||||
"""
|
||||
fields = [np.asarray(a, dtype=float) for a in fields]
|
||||
masks = [np.asarray(a) for a in fluid_masks]
|
||||
if not fields or not masks:
|
||||
raise ValueError("fields and explicit fluid masks required")
|
||||
shape = fields[0].shape
|
||||
if len(shape) != 2 or any(a.shape != shape for a in fields + masks):
|
||||
raise ValueError("equal-shape 2-D arrays required")
|
||||
if any(a.dtype != np.dtype(bool) for a in masks):
|
||||
raise ValueError("fluid masks must be boolean")
|
||||
support = np.zeros(shape, dtype=bool)
|
||||
ys, xs = roi
|
||||
for s, n in zip((ys, xs), shape):
|
||||
if not isinstance(s, slice) or s.step not in (None, 1) or not 0 <= (s.start or 0) < (n if s.stop is None else s.stop) <= n:
|
||||
raise ValueError("invalid ROI slices")
|
||||
support[ys, xs] = True
|
||||
for a in masks:
|
||||
support &= a
|
||||
for a in fields:
|
||||
support &= np.isfinite(a)
|
||||
if not support.any():
|
||||
raise ValueError("empty common finite fluid ROI")
|
||||
return support
|
||||
|
||||
|
||||
def field_error(rux, ruy, tux, tuy, u0, support):
|
||||
"""Eq1: sqrt(mean_support((dux**2 + duy**2)/U0**2)).
|
||||
|
||||
For E(mean field), pass arithmetic mean fields from matched windows.
|
||||
Mean instantaneous E requires separate calls per snapshot and then a mean;
|
||||
these operations are not interchangeable. No target-energy denominator.
|
||||
"""
|
||||
a = [np.asarray(v, dtype=float) for v in (rux, ruy, tux, tuy)]
|
||||
support = np.asarray(support)
|
||||
if a[0].ndim != 2 or any(v.shape != a[0].shape for v in a) or support.shape != a[0].shape or support.dtype != np.dtype(bool):
|
||||
raise ValueError("equal-shape 2-D fields and boolean support required")
|
||||
if not np.isfinite(u0) or u0 <= 0 or not support.any():
|
||||
raise ValueError("positive finite U0 and nonempty support required")
|
||||
if any(not np.isfinite(v[support]).all() for v in a):
|
||||
raise ValueError("nonfinite field on fixed support")
|
||||
du = (a[0][support] - a[2][support]) / u0
|
||||
dv = (a[1][support] - a[3][support]) / u0
|
||||
return float(np.sqrt(np.mean(du * du + dv * dv)))
|
||||
|
||||
|
||||
def motion_summary(displacement):
|
||||
"""Arithmetic mean, centered RMS, half peak-to-peak along time axis 0.
|
||||
|
||||
Input is displacement in caller-declared units; no implicit D scaling.
|
||||
Use the same retained window as field evaluation. No sinusoid assumption.
|
||||
"""
|
||||
a = np.asarray(displacement, dtype=float)
|
||||
if a.ndim == 0 or a.shape[0] == 0 or not np.isfinite(a).all():
|
||||
raise ValueError("nonempty finite time samples required")
|
||||
mean = a.mean(axis=0)
|
||||
return {"mean": mean, "centered_rms": np.sqrt(np.mean((a - mean) ** 2, axis=0)),
|
||||
"half_range": (a.max(axis=0) - a.min(axis=0)) / 2}
|
||||
Reference in new issue
Block a user