From f04036e4e45b513e38ada081855ae997e18750b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 10:10:08 +0000 Subject: [PATCH] research: replay frozen Legacy karman_re100 PPO on current CelerisLab Legacy legacy_test interface contract: A = (8a + b) U0 with b = (0,-4,4), omega = +A/R, per-lattice-step EMA reset to zero every 800-step interval, zero first observation, frozen norm.json, deterministic policy, Legacy warm-up order. Reuses the DynamisLab model loader and observation normalizer read-only; writes the f2f88c phase3 signals.npz layout. Research only; src/ unchanged. Not executed here (no GPU/numpy). Co-Authored-By: Claude Opus 5.5 --- research/karman_reproduce/run.py | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 research/karman_reproduce/run.py diff --git a/research/karman_reproduce/run.py b/research/karman_reproduce/run.py new file mode 100644 index 0000000..a667557 --- /dev/null +++ b/research/karman_reproduce/run.py @@ -0,0 +1,108 @@ +"""Replay the frozen Legacy karman_re100 PPO policy on current CelerisLab. + +Research only; nothing in src/ is changed. Plant: Legacy Karman cloak scene +(1280x512, D=20, parabolic inlet, mean U0 .01, nu .004, Re_D 50; upstream +cylinder r=20 at x=200; pinball front (600, 255.5), rear (626, 255.5+-15); +probes r=5 at x=800, y=255.5+40/0/-40). Interface contract = DynamisLab +legacy_test (the executor behind the Legacy rerun), not f2f88c phase3: + +- command per interval: A = (8 a + b) U0, b = (0, -4, 4), a = deterministic + policy output in [-1, 1]^3; wall speed command, omega = +A / R (both + solvers impose u_w = omega x r, CCW positive); +- smoothing per LATTICE step, A_eff <- .9 A_eff + .1 A, with A_eff = 0 at the + start of every 800-step interval (legacy_test.runtime.run_historical_interval); +- observation: interval means of the three pinball forces (front, top, bottom) + and the three probe (ux, uy) sums, normalized by the frozen norm.json with + legacy_test.runtime.policy_observation; the first observation is zero. + +Output (--out, outside the checkout): signals.npz with sensors (N,6), forces +(N,6), actions (N,3) in the f2f88c phase3 layout, so its comparator applies. + + PYTHONPATH=src python research/karman_reproduce/run.py --dynamis /path/to/DynamisLab \\ + --config /path/to/config_lbm_pinball_legacy_compat.json --out /tmp/... [--device 0] +""" +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys + +import numpy as np + +L0, U0, NX, NY = 20.0, 0.01, 1280, 512 +R, YC = L0 / 2, (NY - 1) / 2 +SI, INTERVALS, WARMUP = 800, 200, int(4 * NX / U0) +BIAS = np.array([0.0, -4.0, 4.0]) + + +def git_head(path): + return subprocess.run(["git", "-C", path, "rev-parse", "HEAD"], capture_output=True, + text=True).stdout.strip() + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dynamis", required=True, help="DynamisLab checkout (read-only)") + ap.add_argument("--config", required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--device", type=int, default=0) + ap.add_argument("--intervals", type=int, default=INTERVALS) + args = ap.parse_args() + here = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) + if os.path.realpath(args.out).startswith(here): + raise SystemExit("--out must be outside the checkout") + os.makedirs(args.out, exist_ok=True) + + sys.path.insert(0, os.path.join(args.dynamis, "src")) + from drl_pinball.legacy_test.core.model_loader import load_model, model_path + from drl_pinball.legacy_test.runtime import load_policy_norm, policy_observation + norm_path = os.path.join(args.dynamis, "src", "SR_analysis", "data", "karman", "karman_re100", + "norm.json") + model, norm = load_model("d1a3o12_re100"), load_policy_norm(norm_path) + + from CelerisLab.simulation import Simulation + sim = Simulation(args.config, device_id=args.device) + try: + sim.add_body("circle", center=(10 * L0, YC, 0.0), radius=L0) + probes = [sim.add_body("sensor", center=(40 * L0, YC + dy, 0.0), radius=5.0) + for dy in (40.0, 0.0, -40.0)] + sim.initialize() + sim.run(WARMUP) # Legacy order: disturbance + probes first, then the pinball + pinball = [sim.add_body("circle", center=c, radius=R) + for c in ((30 * L0, YC, 0.0), (626.0, YC + 15, 0.0), (626.0, YC - 15, 0.0))] + sim.sync_bodies() + sim.run(WARMUP) + cells = float(len(sim.bodies.get(probes[0]).get_sensor_list(NX, NY)[0])) # probe sum = mean * cells + + obs = np.zeros(12, np.float32) + sensors, forces, actions = (np.zeros((args.intervals, n), np.float32) for n in (6, 6, 3)) + for n in range(args.intervals): + a = np.asarray(model.predict(obs, deterministic=True)[0], np.float32).reshape(3) + target, eff = (8.0 * a + BIAS) * U0, np.zeros(3) + for k in range(SI): + eff = 0.9 * eff + 0.1 * target + for pid, w in zip(pinball, eff / R): + sim.set_body(pid, omega=float(w)) + sim.run(1, zero_obs=(k == 0), sync_obs=(k == SI - 1)) + s = [v * cells for pid in probes for v in sim.read_sensor(pid)[:2]] + f = [v for pid in pinball for v in sim.read_force(pid)[:2]] + raw = np.asarray(s + f, np.float32) + if not np.all(np.isfinite(raw)): + raise RuntimeError(f"non-finite observation at interval {n}") + sensors[n], forces[n], actions[n] = raw[:6], raw[6:], a + obs = policy_observation(raw, norm) + np.savez_compressed(os.path.join(args.out, "signals.npz"), sensors=sensors, + forces=forces, actions=actions) + with open(os.path.join(args.out, "case.json"), "w") as fh: + json.dump(dict(celeris=git_head(here), dynamis=git_head(args.dynamis), + config=os.path.realpath(args.config), model=model_path("d1a3o12_re100"), + norm=norm_path, warmup=WARMUP, interval=SI, intervals=args.intervals, + probe_cells=cells), fh, indent=1) + finally: + sim.close() + + +if __name__ == "__main__": + main()