Files
CelerisLab/tests/validation/cycle_v2/periodic_v2.py
T

103 lines
4.0 KiB
Python

"""Versioned ascending-zero DFG measurement; original data and gates unchanged."""
import numpy as np
from periodic import GATES, REFERENCE_GATES, compare
VERSION = "dfg-ascending-zero-v2"
def ascending_crossings(t, y):
t = np.asarray(t, dtype=float); y = np.asarray(y, dtype=float)
if t.ndim != 1 or y.shape != t.shape or not np.isfinite(t).all() or not np.isfinite(y).all():
raise ValueError("Finite one-dimensional time/signal arrays required")
if np.any(np.diff(t) <= 0):
raise ValueError("Strictly increasing times required")
i = np.flatnonzero((y[:-1] < 0) & (y[1:] >= 0))
return (t[i] - y[i] * (t[i+1] - t[i]) / (y[i+1] - y[i])).tolist()
def validate_rows(rows):
keys = ("tstar", "tstar_force", "cd", "cl", "dp", "mass", "flux_in", "flux_out")
values = np.asarray([[row[k] for k in keys] for row in rows], dtype=float)
if not np.isfinite(values).all():
raise ValueError("Nonfinite observation")
if len(rows) and (np.any(np.diff(values[:, 0]) <= 0) or np.any(np.diff(values[:, 1]) <= 0)):
raise ValueError("Strictly increasing observation times required")
def extract(rows, min_time=250.0):
validate_rows(rows)
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 = ascending_crossings(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,
)