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>
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
# OID_analysis/scripts/collect_target_cylinder.py
|
|
"""
|
|
Collect target cylinder data (q_tar) for illusion comparison.
|
|
Single cylinder at x=20*L0 with target diameter, plus 3 sensors at x=30*L0.
|
|
|
|
Usage:
|
|
conda run -n pycuda_3_10 python src/OID_analysis/scripts/collect_target_cylinder.py \
|
|
--diameter 1.0 --device 3 --steps 500
|
|
|
|
Output: data/illusion/illusion_{diam}L/ (shared with illusion scene)
|
|
sensors.npz, fields.npz, target.npz, target_harmonics.json
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
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)
|
|
_SRC = os.path.join(_REPO, "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from LegacyCelerisLab import FlowField # noqa: E402
|
|
from OID_analysis.utils.cfd_interface import ( # noqa: E402
|
|
load_legacy_configs, get_velocity_field, save_vorticity_png, vorticity_from_ddf,
|
|
analyze_harmonics,
|
|
)
|
|
from OID_analysis.configs import get_scene, data_dir_for_scene, LEGACY_CFG_DIR, FIFO_LEN # noqa: E402
|
|
|
|
DATA_TYPE = np.float32
|
|
L0 = 20.0
|
|
CENTER_Y = (512 - 1) / 2.0
|
|
|
|
|
|
def collect(diameter: float, device_id: int, n_steps: int) -> str:
|
|
# Find scene name for this diameter
|
|
scene_name = None
|
|
for cn in ["illusion_0.75L", "illusion_1.0L", "illusion_1.5L"]:
|
|
c = get_scene(cn)
|
|
if abs(c["target_diameter"] - diameter) < 0.01:
|
|
scene_name = cn
|
|
break
|
|
if scene_name is None:
|
|
raise ValueError(f"No scene for diameter={diameter}")
|
|
|
|
cfg = get_scene(scene_name)
|
|
u0 = cfg["u0"]
|
|
si = cfg["sample_interval"]
|
|
|
|
tgt_key = f"target_cylinder_{diameter}L"
|
|
if diameter == int(diameter):
|
|
tgt_key = f"target_cylinder_{diameter:.1f}L"
|
|
out_dir = data_dir_for_scene(tgt_key)
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR)
|
|
field_cfg = field_cfg._replace(viscosity=float(cfg["nu"]), velocity=float(u0))
|
|
|
|
ff = FlowField(field_cfg, cuda_cfg, device_id=device_id)
|
|
print(f" Target cylinder {diameter}L: {ff.FIELD_SHAPE}")
|
|
|
|
# Target cylinder at x=20*L0
|
|
tgt_radius = diameter * L0
|
|
ff.add_cylinder((20.0 * L0, CENTER_Y, 0.0), tgt_radius)
|
|
print(f" radius={tgt_radius}")
|
|
|
|
# 3 sensors at x=30*L0
|
|
for y_off in [2.0, 0.0, -2.0]:
|
|
ff.add_sensor((30.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0)
|
|
|
|
n_obj = 4
|
|
n_stab = int(4 * ff.FIELD_SHAPE[0] / u0)
|
|
print(f" Stabilising ({n_stab} steps)...")
|
|
ff.run(n_stab, np.zeros(n_obj, dtype=DATA_TYPE))
|
|
|
|
# Record target signals: obs[0:8] = cylinder force(2) + sensor(6)
|
|
target_states = np.empty((0, 8), dtype=DATA_TYPE)
|
|
for _ in range(FIFO_LEN):
|
|
ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE))
|
|
target_states = np.vstack((target_states, ff.obs.copy()[0:8]))
|
|
print(f" Target recorded: {target_states.shape}")
|
|
|
|
# Harmonics for force channels
|
|
target_harmonics = analyze_harmonics(target_states, n_harmonics=5)
|
|
harm_save = [{k: v for k, v in h.items()} for h in target_harmonics]
|
|
with open(os.path.join(out_dir, "target_harmonics.json"), "w") as f:
|
|
json.dump(harm_save, f, indent=2)
|
|
np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states)
|
|
|
|
# Full field rollout
|
|
ff.apply_ddf()
|
|
sens_list, ux_list, uy_list = [], [], []
|
|
for step in range(n_steps):
|
|
ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE))
|
|
obs_slice = ff.obs.copy()[0:6]
|
|
sens_list.append(obs_slice)
|
|
ux, uy = get_velocity_field(ff, u0=u0)
|
|
ux_list.append(ux)
|
|
uy_list.append(uy)
|
|
|
|
np.savez(os.path.join(out_dir, "sensors.npz"),
|
|
sensors=np.array(sens_list, dtype=np.float32))
|
|
np.savez_compressed(os.path.join(out_dir, "fields.npz"),
|
|
ux=np.stack(ux_list), uy=np.stack(uy_list))
|
|
|
|
omega = vorticity_from_ddf(ff, u0=u0)
|
|
save_vorticity_png(os.path.join(out_dir, "vorticity_target.png"),
|
|
omega, title=f"Target cylinder {diameter}L")
|
|
|
|
with open(os.path.join(out_dir, "config.json"), "w") as f:
|
|
json.dump({k: str(v) if not isinstance(v, (int, float, list, bool)) else v
|
|
for k, v in cfg.items()}, f, indent=2)
|
|
|
|
del ff
|
|
print(f" Saved {n_steps} snapshots to {out_dir}")
|
|
return out_dir
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--diameter", type=float, default=1.0,
|
|
help="Target cylinder diameter (0.75, 1.0, 1.5)")
|
|
ap.add_argument("--device", type=int, default=3)
|
|
ap.add_argument("--steps", type=int, default=500)
|
|
args = ap.parse_args()
|
|
|
|
t0 = time.time()
|
|
out = collect(args.diameter, args.device, args.steps)
|
|
print(f"Done in {time.time() - t0:.1f}s -> {out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|