feat(reproduce): legacy-test framework + fixed-inlet reproduce pipeline
- Track A (legacy_test): systematic validation scripts for all trained PPO models using LegacyCelerisLab. Each test script rebuilds the exact legacy CFD environment, runs deterministic inference, and compares against SR_analysis reference data using DTW-based comparison. Verified: Karman re100/re50/re200, Vortex lamb/taylor all pass (DTW > 0.95). - Track B (reproduce): Phase 2 open-loop CFD validation + Phase 3 DRL inference using the legacy-compatible config (regularized inlet with neq_damp=1.0, matching the legacy NBB formula). The inlet scheme fix improves new-CFD Karman DTW from 0.916 to 0.943. - Fixes: action_wrapper sign convention docstring, model inventory duplicate entries and missing models, stale config paths in legacy run_all_cases.py/run_illusion_vortex.py, illusion label formatting - Add READMEs and run-all shell scripts for both tracks - Add .gitignore entries for runtime output directories Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# legacy_test/test_karman_cloak_crossre.py
|
||||
"""Karman Cloak Cross-Re — legacy test (re50, re200, re400).
|
||||
|
||||
Same procedure as test_karman_cloak_re100.py but for alternative
|
||||
Reynolds numbers. Each Re uses its own PPO model and SR_analysis
|
||||
reference data.
|
||||
|
||||
Usage: conda run -n pycuda_3_10 python test_karman_cloak_crossre.py --device 0
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
|
||||
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
_SRC = os.path.join(_REPO, "src")
|
||||
_DRL = os.path.join(_SRC, "drl_pinball")
|
||||
for p in [_REPO, _SRC, _DRL]:
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from legacy_test.core.legacy_env_builder import ( # noqa: E402
|
||||
build_karman_cloak, FIFO_LEN, CONV_LEN, U0, DATA_TYPE,
|
||||
)
|
||||
from legacy_test.core.model_loader import load_model # noqa: E402
|
||||
from legacy_test.core.comparator import compare_scene # noqa: E402
|
||||
from legacy_test.core.io_helpers import save_signals, save_target, save_norm # noqa: E402
|
||||
|
||||
SAMPLE_INTERVAL = 800
|
||||
ACTION_SCALE = 8.0
|
||||
ACTION_BIAS = (0.0, -4.0, 4.0)
|
||||
NUM_STEPS = 200
|
||||
OUT_BASE = os.path.join(os.path.dirname(__file__), "output")
|
||||
|
||||
|
||||
def log(msg): print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
|
||||
def run_crossre(device_id: int, re_code: float) -> dict:
|
||||
label = f"karman_re{int(re_code)}"
|
||||
log(f"=== {label}: Legacy Test ===")
|
||||
model_name = f"d1a3o12_re{int(re_code)}"
|
||||
ref_dir = os.path.join(_SRC, "SR_analysis", "data", "karman", label)
|
||||
out_dir = os.path.join(OUT_BASE, label)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
data = build_karman_cloak(device_id=device_id, re_code=re_code,
|
||||
action_bias=ACTION_BIAS, action_scale=ACTION_SCALE)
|
||||
ff = data["flow_field"]
|
||||
target_states = data["target_states"]
|
||||
norm = data["norm"]
|
||||
n_obj = norm.get("n_obj_total", 7)
|
||||
f_nf = float(norm["force_norm_fact"])
|
||||
s_dev = np.array(norm["sens_deviation"], dtype=np.float32)
|
||||
s_nf = np.array(norm["sens_norm_fact"], dtype=np.float32)
|
||||
|
||||
save_target(out_dir, target_states); save_norm(out_dir, norm)
|
||||
|
||||
model = load_model(model_name)
|
||||
log(f" Model: {model_name}")
|
||||
|
||||
# Restore + bias FIFO
|
||||
ff.restore_ddf(); ff.apply_ddf()
|
||||
bias_arr = np.zeros(n_obj, dtype=DATA_TYPE)
|
||||
bias_arr[4] = float(ACTION_BIAS[0] * U0)
|
||||
bias_arr[5] = float(ACTION_BIAS[1] * U0)
|
||||
bias_arr[6] = float(ACTION_BIAS[2] * U0)
|
||||
|
||||
fifo = deque(maxlen=FIFO_LEN)
|
||||
for _ in range(FIFO_LEN):
|
||||
ff.run(SAMPLE_INTERVAL, bias_arr)
|
||||
fifo.append(ff.obs.copy()[2:14])
|
||||
|
||||
# DRL inference
|
||||
sig_s = np.zeros((NUM_STEPS, 6), dtype=np.float32)
|
||||
sig_f = np.zeros((NUM_STEPS, 6), dtype=np.float32)
|
||||
sig_a = np.zeros((NUM_STEPS, 3), dtype=np.float32)
|
||||
|
||||
raw = ff.obs.copy()[2:14]
|
||||
forces_norm = raw[6:12] / f_nf
|
||||
sens_norm = (raw[0:6] - s_dev) / s_nf
|
||||
obs = np.clip(np.hstack([forces_norm, sens_norm]), -1.0, 1.0).astype(np.float32)
|
||||
|
||||
for step in range(NUM_STEPS):
|
||||
action, _ = model.predict(obs, deterministic=True)
|
||||
action = action.astype(np.float32).flatten()
|
||||
sig_a[step] = action.copy()
|
||||
|
||||
action_arr = np.zeros(n_obj, dtype=DATA_TYPE)
|
||||
action_arr[4:] = (action * ACTION_SCALE + np.array(ACTION_BIAS, dtype=np.float32)) * U0
|
||||
|
||||
ff.context.push()
|
||||
try:
|
||||
ff.run(SAMPLE_INTERVAL, action_arr)
|
||||
finally:
|
||||
ff.context.pop()
|
||||
|
||||
raw = ff.obs.copy()[2:14]
|
||||
fifo.append(raw)
|
||||
sig_s[step] = raw[0:6]
|
||||
sig_f[step] = raw[6:12]
|
||||
|
||||
forces_norm = raw[6:12] / f_nf
|
||||
sens_norm = (raw[0:6] - s_dev) / s_nf
|
||||
obs = np.clip(np.hstack([forces_norm, sens_norm]), -1.0, 1.0).astype(np.float32)
|
||||
|
||||
save_signals(out_dir, sig_s, sig_f, sig_a)
|
||||
np.savez_compressed(os.path.join(out_dir, "controlled.npz"),
|
||||
sensors=sig_s, forces=sig_f, actions=sig_a,
|
||||
rewards=np.zeros(NUM_STEPS, dtype=np.float32))
|
||||
|
||||
log(" Comparing against reference...")
|
||||
result = compare_scene(ref_dir, sig_s, sig_f, sig_a, conv_len=CONV_LEN, label=label)
|
||||
with open(os.path.join(out_dir, "result.json"), "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
log(f" {'PASS' if result['passed'] else 'FAIL'}")
|
||||
del ff
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--device", type=int, default=0)
|
||||
ap.add_argument("--re", type=str, default="50,200,400",
|
||||
help="Comma-separated Re values")
|
||||
args = ap.parse_args()
|
||||
|
||||
results = {}
|
||||
for re_str in args.re.split(","):
|
||||
re_val = float(re_str.strip())
|
||||
results[f"re{int(re_val)}"] = run_crossre(args.device, re_val)
|
||||
|
||||
log("\n=== Cross-Re Summary ===")
|
||||
for name, r in results.items():
|
||||
log(f" {name}: DTW={r['dtw_sim']:.4f}, act_corr={[f'{c:.3f}' for c in r['action_corr']]} -> {'PASS' if r['passed'] else 'FAIL'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user