# 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()