DynamisLab/src/OID_analysis/analysis/steady_reanalysis.py
Frank14f 6614f18248 OID Analysis: correction-field structure diagnosis pipeline
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>
2026-06-22 17:18:19 +08:00

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