Complete implementation of Observable-Inferred Decomposition (OID) for the fluidic pinball project. Covers Phases 0-7 for all 5 scenes (steady cloak, Karman cloak, illusion 0.75L/1.0L/1.5L). Key deliverables: - Full analysis pipeline: configs, utils, 11 collection scripts, 7 phase scripts, robustness analysis, figure generator, batch runner - Data collected: 500 snapshots per scene, separate illusion-position q_blk - 7 publication-quality figures: force-sig overlap, rank sensitivity, OID vs POD comparison, tau_c sensitivity, POD energy, steady metrics, white-box chain - Comprehensive report at docs/OID_analysis_results.md (292 lines) - Handover document at docs/OID_handover.md - Updated knowledge base and notes with all Phase 2 results Core finding: force-relevant and signature-relevant correction structures systematically separate across control tasks (steady: +0.763 -> Karman: -0.034 -> illusion: -0.082 to -0.932), with OID consistently outperforming POD. Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
7.5 KiB
Python
209 lines
7.5 KiB
Python
# OID_analysis/analysis/steady_reanalysis.py
|
|
"""
|
|
Steady cloak re-analysis with suppression/restoration metrics.
|
|
Replaces R^2 with physically meaningful metrics:
|
|
- RMS reduction per zone
|
|
- Recirculation length/area collapse
|
|
- Enstrophy reduction
|
|
- Force RMS
|
|
|
|
Usage:
|
|
conda run -n sr_env python3 src/OID_analysis/analysis/steady_reanalysis.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
if _REPO not in sys.path:
|
|
sys.path.insert(0, _REPO)
|
|
|
|
from OID_analysis.configs import DATA_DIR, data_dir_for_scene # noqa: E402
|
|
|
|
|
|
def compute_rms_reduction(ux_ctl, uy_ctl, ux_blk, uy_blk, zone_mask=None):
|
|
"""Compute RMS reduction ratio: 1 - RMS(ctl)/RMS(blk)"""
|
|
if zone_mask is not None:
|
|
ux_ctl = ux_ctl[:, zone_mask]
|
|
uy_ctl = uy_ctl[:, zone_mask]
|
|
ux_blk = ux_blk[:, zone_mask]
|
|
uy_blk = uy_blk[:, zone_mask]
|
|
|
|
rms_ctl_u = np.std(ux_ctl, axis=0)
|
|
rms_ctl_v = np.std(uy_ctl, axis=0)
|
|
rms_blk_u = np.std(ux_blk, axis=0)
|
|
rms_blk_v = np.std(uy_blk, axis=0)
|
|
|
|
rms_ctl = np.sqrt(np.mean(rms_ctl_u**2 + rms_ctl_v**2))
|
|
rms_blk = np.sqrt(np.mean(rms_blk_u**2 + rms_blk_v**2))
|
|
|
|
reduction = 1.0 - rms_ctl / (rms_blk + 1e-30)
|
|
return float(reduction), float(rms_ctl), float(rms_blk)
|
|
|
|
|
|
def compute_enstrophy_reduction(ux_ctl, uy_ctl, ux_blk, uy_blk, zone_mask=None):
|
|
"""Compute enstrophy reduction ratio."""
|
|
def zonal_enstrophy(ux, uy):
|
|
omega = np.zeros((ux.shape[0], ux.shape[1], ux.shape[2]), dtype=np.float64)
|
|
for t in range(min(ux.shape[0], 100)): # subsample for speed
|
|
omega[t] = np.gradient(uy[t].astype(np.float64), axis=1) - \
|
|
np.gradient(ux[t].astype(np.float64), axis=0)
|
|
if zone_mask is not None:
|
|
area = max(np.sum(zone_mask), 1)
|
|
return np.mean(0.5 * omega[:, zone_mask]**2)
|
|
return np.mean(0.5 * omega**2)
|
|
|
|
ens_ctl = zonal_enstrophy(ux_ctl, uy_ctl)
|
|
ens_blk = zonal_enstrophy(ux_blk, uy_blk)
|
|
reduction = 1.0 - ens_ctl / (ens_blk + 1e-30)
|
|
return float(reduction), float(ens_ctl), float(ens_blk)
|
|
|
|
|
|
def compute_recirculation_metrics(mean_u):
|
|
"""Compute Lr and Ar from mean u field."""
|
|
ny, nx = mean_u.shape
|
|
center_y = (ny - 1) / 2.0
|
|
cl_y = int(center_y)
|
|
|
|
# Lr: furthest downstream x on centerline where mean_u < 0
|
|
u_cl = mean_u[cl_y, :]
|
|
neg_idx = np.where(u_cl < 0)[0]
|
|
Lr = float(neg_idx[-1]) if len(neg_idx) > 0 else 0.0
|
|
|
|
# Ar: pixels where mean_u < 0
|
|
Ar = float(np.sum(mean_u < 0))
|
|
|
|
return Lr, Ar
|
|
|
|
|
|
def analyze_steady():
|
|
print("=== Steady Cloak Re-Analysis ===")
|
|
|
|
# Load fields
|
|
blk_dir = data_dir_for_scene("pinball_baseline")
|
|
ctl_dir = data_dir_for_scene("steady_cloak")
|
|
|
|
f_blk = np.load(os.path.join(blk_dir, "fields.npz"))
|
|
ux_blk, uy_blk = f_blk["ux"], f_blk["uy"]
|
|
|
|
f_ctl = np.load(os.path.join(ctl_dir, "fields.npz"))
|
|
ux_ctl, uy_ctl = f_ctl["ux"], f_ctl["uy"]
|
|
|
|
# Equalize lengths
|
|
N = min(ux_blk.shape[0], ux_ctl.shape[0])
|
|
ux_blk, uy_blk = ux_blk[:N], uy_blk[:N]
|
|
ux_ctl, uy_ctl = ux_ctl[:N], uy_ctl[:N]
|
|
|
|
print(f"\nN snapshots: {N} (min of blk={ux_blk.shape[0]}, ctl={ux_ctl.shape[0]})")
|
|
|
|
# Zone masks
|
|
ny, nx = ux_blk.shape[1], ux_blk.shape[2]
|
|
|
|
# Near-body: x=[580,660], y=[200,310] in lattice
|
|
nb_mask = np.zeros((ny, nx), dtype=bool)
|
|
nb_mask[200:310, 580:660] = True
|
|
|
|
# Near-wake: x=[660,800], y=[180,330]
|
|
nw_mask = np.zeros((ny, nx), dtype=bool)
|
|
nw_mask[180:330, 660:800] = True
|
|
|
|
# Downstream sensor zone: x=[790,810]
|
|
ds_mask = np.zeros((ny, nx), dtype=bool)
|
|
for sy in [215, 255, 295]:
|
|
y0, y1 = max(0, sy-10), min(ny, sy+10)
|
|
ds_mask[y0:y1, 790:810] = True
|
|
|
|
zones = [
|
|
("near-body", nb_mask),
|
|
("near-wake", nw_mask),
|
|
("downstream", ds_mask),
|
|
("full-field", None),
|
|
]
|
|
|
|
results = {}
|
|
for zname, zmask in zones:
|
|
print(f"\n Zone: {zname}")
|
|
rms_red, rms_c, rms_b = compute_rms_reduction(
|
|
ux_ctl, uy_ctl, ux_blk, uy_blk, zmask)
|
|
ens_red, ens_c, ens_b = compute_enstrophy_reduction(
|
|
ux_ctl, uy_ctl, ux_blk, uy_blk, zmask)
|
|
results[zname] = {
|
|
"rms_reduction": rms_red,
|
|
"rms_ctl": rms_c,
|
|
"rms_blk": rms_b,
|
|
"enstrophy_reduction": ens_red,
|
|
"enstrophy_ctl": ens_c,
|
|
"enstrophy_blk": ens_b,
|
|
}
|
|
print(f" RMS reduction: {rms_red:.4f} (ctl={rms_c:.6f}, blk={rms_b:.6f})")
|
|
print(f" Enstrophy reduction: {ens_red:.4f} (ctl={ens_c:.6f}, blk={ens_b:.6f})")
|
|
|
|
# Recirculation metrics for full field
|
|
mean_u_ctl = np.mean(ux_ctl, axis=0)
|
|
mean_u_blk = np.mean(ux_blk, axis=0)
|
|
Lr_ctl, Ar_ctl = compute_recirculation_metrics(mean_u_ctl)
|
|
Lr_blk, Ar_blk = compute_recirculation_metrics(mean_u_blk)
|
|
results["recirculation"] = {
|
|
"Lr_ctl_lattice": Lr_ctl,
|
|
"Lr_blk_lattice": Lr_blk,
|
|
"Lr_collapse": Lr_ctl / (Lr_blk + 1e-30),
|
|
"Ar_ctl": Ar_ctl,
|
|
"Ar_blk": Ar_blk,
|
|
"Ar_collapse": Ar_ctl / (Ar_blk + 1e-30),
|
|
}
|
|
print(f"\n Recirculation:")
|
|
print(f" Lr: ctl={Lr_ctl:.0f}, blk={Lr_blk:.0f}, collapse={Lr_ctl/(Lr_blk+1e-30):.4f}")
|
|
print(f" Ar: ctl={Ar_ctl:.0f}, blk={Ar_blk:.0f}, collapse={Ar_ctl/(Ar_blk+1e-30):.4f}")
|
|
|
|
# Force metrics
|
|
fp = os.path.join(blk_dir, "forces.npz")
|
|
forces_blk = np.load(fp)["forces"][:N]
|
|
fp_ctl = os.path.join(ctl_dir, "forces.npz")
|
|
forces_ctl = np.load(fp_ctl)["forces"][:N]
|
|
|
|
Fx_blk_rms = np.std(np.sum(forces_blk[:, 0::2], axis=1))
|
|
Fy_blk_rms = np.std(np.sum(forces_blk[:, 1::2], axis=1))
|
|
Fx_ctl_rms = np.std(np.sum(forces_ctl[:, 0::2], axis=1))
|
|
Fy_ctl_rms = np.std(np.sum(forces_ctl[:, 1::2], axis=1))
|
|
|
|
results["force"] = {
|
|
"Fx_rms_blk": float(Fx_blk_rms),
|
|
"Fx_rms_ctl": float(Fx_ctl_rms),
|
|
"Fx_reduction": float(1.0 - Fx_ctl_rms / (Fx_blk_rms + 1e-30)),
|
|
"Fy_rms_blk": float(Fy_blk_rms),
|
|
"Fy_rms_ctl": float(Fy_ctl_rms),
|
|
"Fy_reduction": float(1.0 - Fy_ctl_rms / (Fy_blk_rms + 1e-30)),
|
|
}
|
|
print(f"\n Force:")
|
|
print(f" Fx RMS: blk={Fx_blk_rms:.6f}, ctl={Fx_ctl_rms:.6f}, reduction={results['force']['Fx_reduction']:.4f}")
|
|
print(f" Fy RMS: blk={Fy_blk_rms:.6f}, ctl={Fy_ctl_rms:.6f}, reduction={results['force']['Fy_reduction']:.4f}")
|
|
|
|
# Save
|
|
out_dir = os.path.join(DATA_DIR, "derived", "steady_metrics")
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
with open(os.path.join(out_dir, "steady_reanalysis.json"), "w") as f:
|
|
json.dump(results, f, indent=2)
|
|
print(f"\nSaved to {out_dir}/steady_reanalysis.json")
|
|
|
|
# Summary table
|
|
print(f"\n{'='*60}")
|
|
print(f"STEADY CLOAK RE-ANALYSIS SUMMARY")
|
|
print(f"{'='*60}")
|
|
print(f"{'Metric':<30s} {'Uncontrolled':>12s} {'Controlled':>12s} {'Reduction':>12s}")
|
|
print(f"{'-'*66}")
|
|
for zname in ["near-body", "near-wake", "downstream"]:
|
|
zr = results[zname]
|
|
print(f"RMS ({zname}){'':<12s} {zr['rms_blk']:12.6f} {zr['rms_ctl']:12.6f} {zr['rms_reduction']:12.4f}")
|
|
print(f"Lr (recirc length) {Lr_blk:12.0f} {Lr_ctl:12.0f} {results['recirculation']['Lr_collapse']:12.4f}")
|
|
print(f"Ar (recirc area) {Ar_blk:12.0f} {Ar_ctl:12.0f} {results['recirculation']['Ar_collapse']:12.4f}")
|
|
print(f"Fx RMS {Fx_blk_rms:12.6f} {Fx_ctl_rms:12.6f} {results['force']['Fx_reduction']:12.4f}")
|
|
print(f"Fy RMS {Fy_blk_rms:12.6f} {Fy_ctl_rms:12.6f} {results['force']['Fy_reduction']:12.4f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
analyze_steady()
|