- Add crossre_transfer.sh: calibrate → transfer-train for re60→re200→re400 - Add re60 config (ν=0.006667, SI=800, uniform+free-slip, very weak shedding) - Calibrate re60, re200, re400: FORCE_SCALE, SENS_SCALE, dtw_norm_scale, SIM_BP - Fix all paths: use DynamisLab submodule CelerisLab, remove external ~/CelerisLab - Remove _clean_cache() from envs/calibrate — CelerisLab handles internally - Move V4 backups to old/: env_karman_2000x600, train_karman_2000x600, etc. - train_karman.py: save model + vecnormalize every episode (non-optional) - Update TRAIN_KNOWLEDGE.md: file structure, calibration table, cross-re guide - All 3 Re verified: 5-episode transfer test passed (re60: 0.64, re200: 0.43, re400: 0.49) Co-authored-by: Cursor <cursoragent@cursor.com>
518 lines
21 KiB
Python
518 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase 0: Baseline measurement for 2000x600 Karman Cloak.
|
|
|
|
Collects Stage0 (zero rotation) and Stage1 (bias [0,-4,4]*U0) data to inform
|
|
reward design. Records per-step Cd/Cl/Sim and obs norm health.
|
|
|
|
Outputs to output/stage_baseline_2000x600/:
|
|
- stage_data.pkl : raw obs + forces + sim per stage
|
|
- norm_health.json : obs distribution stats
|
|
- summary.txt : human-readable comparison table
|
|
|
|
Usage:
|
|
conda run -n pycuda_3_10 python -u phase0_baseline_measure.py --device-id 0
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import pickle
|
|
import sys
|
|
import time
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pycuda.driver as cuda
|
|
|
|
cuda.init()
|
|
|
|
_REPO = Path(__file__).resolve().parents[3]
|
|
if str(_REPO) not in sys.path:
|
|
sys.path.insert(0, str(_REPO))
|
|
|
|
from CelerisLab import Simulation
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hard-coded CelerisLab paths for cache cleaning
|
|
# ---------------------------------------------------------------------------
|
|
_CELERIS = _REPO / "CelerisLab"
|
|
_CONFIG_H = _CELERIS / "src/CelerisLab/lbm/kernels/config/config_objects.h"
|
|
_PTX = _CELERIS / "src/CelerisLab/lbm/kernels/kernel.ptx"
|
|
|
|
|
|
def _clean_cache():
|
|
for p in [_CONFIG_H, _PTX]:
|
|
if p.exists():
|
|
p.unlink()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Physics / geometry constants (match env_karman_2000x600.py)
|
|
# ---------------------------------------------------------------------------
|
|
L0 = 20.0
|
|
D_CYL = L0
|
|
U0 = 0.01
|
|
RADIUS = L0 / 2.0
|
|
|
|
NX = 2000
|
|
NY = 600
|
|
CENTER_Y = float(NY - 1) / 2.0
|
|
|
|
DIST_X = 600.0
|
|
PINBALL_FRONT_X = 1000.0
|
|
PINBALL_REAR_X = 1026.0
|
|
SENSOR_X = 1200.0
|
|
|
|
SI = 800
|
|
FIFO_LEN = 150
|
|
CONV_LEN = 30
|
|
ACTION_SCALE = 8.0
|
|
ACTION_BIAS = np.array([0.0, -4.0, 4.0], dtype=np.float32)
|
|
|
|
# Current env norm constants (to assess health)
|
|
FORCE_SCALE = np.float32(0.005)
|
|
SENS_SCALE = np.float32(U0)
|
|
|
|
WARMUP_STEPS = int(4.0 * NX / U0)
|
|
CFG_PATH = str(_REPO / "configs" / "config_lbm_karman_2000x600.json")
|
|
SENSOR_CC = 78.0
|
|
|
|
N_MEASURE = 100 # steps to measure per stage (after FIFO filled)
|
|
|
|
OUT_DIR = Path(__file__).resolve().parent / "output" / "stage_baseline_2000x600"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DTW utilities (match env_karman_2000x600.py exactly)
|
|
# ---------------------------------------------------------------------------
|
|
def calc_lag(target: np.ndarray, state: np.ndarray) -> int:
|
|
t_mean = np.mean(target)
|
|
s_mean = np.mean(state)
|
|
corr = np.correlate(target - t_mean, state - s_mean, mode="full")
|
|
lags = np.arange(-len(target) + 1, len(target))
|
|
return int(lags[np.argmax(corr)])
|
|
|
|
|
|
def calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float:
|
|
n, m = len(target), len(state)
|
|
dtw = np.full((n + 1, m + 1), np.inf)
|
|
dtw[0, 0] = 0.0
|
|
for i in range(1, n + 1):
|
|
for j in range(1, m + 1):
|
|
cost = abs(float(target[i - 1]) - float(state[j - 1]))
|
|
last_min = min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1])
|
|
dtw[i, j] = cost + last_min
|
|
return float(1.0 - dtw[n, m] / float(n))
|
|
|
|
|
|
def compute_similarity(target_states: np.ndarray, fifo_states: np.ndarray,
|
|
conv_len: int = CONV_LEN) -> float:
|
|
target = np.asarray(target_states, dtype=np.float64)
|
|
state = np.asarray(fifo_states, dtype=np.float64)
|
|
id_sens = 3 # s1_uy (center sensor y-velocity) — matches 2000x600 env
|
|
target_seq = target[conv_len:2 * conv_len, id_sens]
|
|
state_seq = state[-conv_len:, id_sens]
|
|
lag = calc_lag(target_seq, state_seq)
|
|
sim = 0.0
|
|
for i in range(6):
|
|
t_seq = np.roll(target[:, i], -lag)[conv_len:2 * conv_len]
|
|
s_seq = state[-conv_len:, i]
|
|
sim += calc_dtw_sim(t_seq, s_seq)
|
|
return float(sim / 6.0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context guard
|
|
# ---------------------------------------------------------------------------
|
|
class CtxGuard:
|
|
def __init__(self, sim):
|
|
self.sim = sim
|
|
|
|
def __enter__(self):
|
|
if self.sim is not None:
|
|
self.sim.ctx._ctx.push()
|
|
|
|
def __exit__(self, *exc):
|
|
if self.sim is not None:
|
|
self.sim.ctx._ctx.pop()
|
|
return False
|
|
|
|
|
|
def gpu_block(sim, fn):
|
|
with CtxGuard(sim):
|
|
fn()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
def action_to_omega(action_norm: np.ndarray) -> np.ndarray:
|
|
"""[-1,1] action -> lattice omega (new kernel sign convention)."""
|
|
sv = (np.asarray(action_norm, dtype=np.float32) * ACTION_SCALE + ACTION_BIAS) * U0
|
|
return -sv / RADIUS
|
|
|
|
|
|
def read_obs(sim, dist_id, sensor_ids, pinball_ids) -> np.ndarray:
|
|
"""14-dim: [dist_fx,fy, 6x raw_sensor, 6x raw_force]."""
|
|
obs = list(sim.read_force(dist_id, normalize=True))
|
|
for sid in sensor_ids:
|
|
s = sim.read_sensor(sid, normalize=True)
|
|
obs.extend([float(s[0]), float(s[1])])
|
|
for pid in pinball_ids:
|
|
obs.extend(sim.read_force(pid, normalize=True))
|
|
return np.array(obs, dtype=np.float32)
|
|
|
|
|
|
def set_omega(sim, pinball_ids, omega):
|
|
for pid, w in zip(pinball_ids, omega):
|
|
sim.set_body(pid, omega=float(w))
|
|
|
|
|
|
def cd_cl_from_obs(obs_slice, force_scale=FORCE_SCALE):
|
|
"""obs_slice = [6 sensor (legacy-equiv), 6 force (raw)].
|
|
Returns (cd, cl) normalized by force_scale.
|
|
"""
|
|
forces = obs_slice[6:12] / force_scale
|
|
cd = (forces[0] + forces[2] + forces[4]) / 3.0
|
|
cl = (forces[1] + forces[3] + forces[5]) / 3.0
|
|
return float(cd), float(cl)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage runner
|
|
# ---------------------------------------------------------------------------
|
|
def run_stage(sim, dist_id, sensor_ids, pinball_ids, target_states,
|
|
omega_vec, stage_name, n_measure=N_MEASURE):
|
|
"""Run one stage: fill FIFO with given omega, then measure n_measure steps.
|
|
|
|
Returns dict with per-step obs_slice, cd, cl, sim, and FIFO.
|
|
"""
|
|
print(f" [{stage_name}] Filling FIFO ({FIFO_LEN} x SI)...", end=" ", flush=True)
|
|
t0 = time.perf_counter()
|
|
fifo = deque(maxlen=FIFO_LEN)
|
|
with CtxGuard(sim):
|
|
set_omega(sim, pinball_ids, omega_vec)
|
|
for _ in range(FIFO_LEN):
|
|
sim.run(SI, zero_obs=True)
|
|
obs = read_obs(sim, dist_id, sensor_ids, pinball_ids)
|
|
sl = obs[2:14].copy()
|
|
sl[0:6] *= SENSOR_CC # legacy-equiv for DTW
|
|
fifo.append(sl)
|
|
print(f"done ({time.perf_counter()-t0:.0f}s).")
|
|
|
|
print(f" [{stage_name}] Measuring ({n_measure} steps)...", end=" ", flush=True)
|
|
t0 = time.perf_counter()
|
|
obs_slices = []
|
|
cds, cls, sims = [], [], []
|
|
with CtxGuard(sim):
|
|
set_omega(sim, pinball_ids, omega_vec)
|
|
for _ in range(n_measure):
|
|
sim.run(SI, zero_obs=True)
|
|
obs = read_obs(sim, dist_id, sensor_ids, pinball_ids)
|
|
sl = obs[2:14].copy()
|
|
sl[0:6] *= SENSOR_CC
|
|
fifo.append(sl)
|
|
obs_slices.append(sl.copy())
|
|
cd, cl = cd_cl_from_obs(sl)
|
|
cds.append(cd)
|
|
cls.append(cl)
|
|
# Sim requires >= CONV_LEN*2 samples in fifo
|
|
sim_val = compute_similarity(target_states, np.array(list(fifo)))
|
|
sims.append(sim_val)
|
|
print(f"done ({time.perf_counter()-t0:.0f}s).")
|
|
|
|
return {
|
|
"obs_slices": np.array(obs_slices, dtype=np.float32), # (N, 12)
|
|
"cds": np.array(cds, dtype=np.float32),
|
|
"cls": np.array(cls, dtype=np.float32),
|
|
"sims": np.array(sims, dtype=np.float32),
|
|
"fifo": np.array(list(fifo), dtype=np.float32), # (FIFO_LEN, 12)
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Norm health
|
|
# ---------------------------------------------------------------------------
|
|
def norm_health(obs_slices, force_scale=FORCE_SCALE, sens_scale=SENS_SCALE):
|
|
"""Assess obs normalization health.
|
|
|
|
obs_slices: (N, 12) where [0:6]=sensor (legacy-equiv), [6:12]=force (raw).
|
|
Returns dict with per-dim stats after normalization.
|
|
"""
|
|
# Normalize the same way env does (but env uses raw sensor, not legacy-equiv;
|
|
# for norm health we check the RAW values that env actually normalizes)
|
|
# Note: env _normalize_obs uses obs_slice[0:6] (raw sensor) / SENS_SCALE
|
|
# and obs_slice[6:12] (raw force) / FORCE_SCALE.
|
|
# But our obs_slices here have sensor already *CC. For norm health we need RAW.
|
|
# We stored sensor*CC for DTW; for norm health we divide back.
|
|
raw = obs_slices.copy()
|
|
raw[:, 0:6] = raw[:, 0:6] / SENSOR_CC # back to raw (area-normalized) sensor
|
|
|
|
forces_n = raw[:, 6:12] / force_scale
|
|
sens_n = raw[:, 0:6] / sens_scale
|
|
normed = np.hstack([forces_n, sens_n]) # (N, 12) [forces(6), sens(6)] — env order
|
|
|
|
names = ["f_front_fx", "f_front_fy", "f_top_fx", "f_top_fy", "f_bot_fx", "f_bot_fy",
|
|
"s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"]
|
|
stats = {}
|
|
clip_count = 0
|
|
total = normed.size
|
|
for i, name in enumerate(names):
|
|
col = normed[:, i]
|
|
clipped = np.sum(np.abs(col) > 1.0)
|
|
clip_count += clipped
|
|
stats[name] = {
|
|
"mean": float(np.mean(col)),
|
|
"std": float(np.std(col)),
|
|
"min": float(np.min(col)),
|
|
"max": float(np.max(col)),
|
|
"clip_ratio": float(clipped / len(col)),
|
|
}
|
|
stats["_overall"] = {
|
|
"clip_ratio_total": float(clip_count / total),
|
|
"force_clip": float(np.sum(np.abs(forces_n) > 1.0) / forces_n.size),
|
|
"sens_clip": float(np.sum(np.abs(sens_n) > 1.0) / sens_n.size),
|
|
}
|
|
return stats, normed
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Phase 0 Baseline Measure")
|
|
parser.add_argument("--device-id", type=int, default=0)
|
|
args = parser.parse_args()
|
|
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
log_path = OUT_DIR / "phase0.log"
|
|
|
|
def log(msg, **kwargs):
|
|
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
|
|
print(line, **kwargs)
|
|
with open(log_path, "a") as f:
|
|
f.write(line + "\n")
|
|
f.flush()
|
|
|
|
log(f"=== Phase 0 Baseline Measure (2000x600, device={args.device_id}) ===")
|
|
log(f" Output: {OUT_DIR}")
|
|
|
|
# ---- Step 1: Record target (dist_cyl + 3 sensors, no pinball) ----
|
|
log(" Step 1: Recording target signal...")
|
|
_clean_cache()
|
|
sim = Simulation(lbm_config_path=CFG_PATH, device_id=args.device_id)
|
|
sim._assert_object_count_contract = lambda *a, **kw: None
|
|
dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
|
s0 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
|
s1 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
|
s2 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
|
sensor_ids = [s0, s1, s2]
|
|
sim.initialize()
|
|
|
|
log(f" Warmup target sim ({WARMUP_STEPS} steps)...", end=" ", flush=True)
|
|
t0 = time.perf_counter()
|
|
with CtxGuard(sim):
|
|
sim.run(WARMUP_STEPS, zero_obs=True)
|
|
log(f"done ({time.perf_counter()-t0:.0f}s).")
|
|
|
|
target = np.zeros((FIFO_LEN, 6), dtype=np.float32)
|
|
with CtxGuard(sim):
|
|
for i in range(FIFO_LEN):
|
|
sim.run(SI, zero_obs=True)
|
|
target[i] = [
|
|
sim.read_sensor(s0, normalize=True)[0] * SENSOR_CC,
|
|
sim.read_sensor(s0, normalize=True)[1] * SENSOR_CC,
|
|
sim.read_sensor(s1, normalize=True)[0] * SENSOR_CC,
|
|
sim.read_sensor(s1, normalize=True)[1] * SENSOR_CC,
|
|
sim.read_sensor(s2, normalize=True)[0] * SENSOR_CC,
|
|
sim.read_sensor(s2, normalize=True)[1] * SENSOR_CC,
|
|
]
|
|
sim.close()
|
|
log(f" Target recorded. s1_uy range: [{target[:,3].min():.4f}, {target[:,3].max():.4f}], std={target[:,3].std():.4f}")
|
|
|
|
# ---- Step 2: Create training sim with ALL objects ----
|
|
log(" Step 2: Creating training sim (all 7 objects)...")
|
|
_clean_cache()
|
|
sim = Simulation(lbm_config_path=CFG_PATH, device_id=args.device_id)
|
|
sim._assert_object_count_contract = lambda *a, **kw: None
|
|
dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
|
sensor_ids = [
|
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0),
|
|
]
|
|
sim.add_body("circle", center=(PINBALL_FRONT_X, CENTER_Y, 0.0), radius=RADIUS)
|
|
sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS)
|
|
sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS)
|
|
sim.initialize()
|
|
pinball_ids = [4, 5, 6]
|
|
|
|
log(f" Warmup pinball sim ({WARMUP_STEPS} steps)...", end=" ", flush=True)
|
|
t0 = time.perf_counter()
|
|
with CtxGuard(sim):
|
|
sim.run(WARMUP_STEPS, zero_obs=True)
|
|
log(f"done ({time.perf_counter()-t0:.0f}s).")
|
|
|
|
# Snapshot at zero-action state (for restoring between stages)
|
|
with CtxGuard(sim):
|
|
sim.snapshot()
|
|
log(" Snapshot saved (zero-action pinball state).")
|
|
|
|
# ---- Step 3: Stage 0 — zero rotation ----
|
|
log(" Step 3: Stage 0 (zero rotation)...")
|
|
with CtxGuard(sim):
|
|
sim.restore()
|
|
zero_omega = np.zeros(3, dtype=np.float32)
|
|
stage0 = run_stage(sim, dist_id, sensor_ids, pinball_ids, target,
|
|
zero_omega, "Stage0")
|
|
|
|
# ---- Step 4: Stage 1 — bias [0,-4,4]*U0 ----
|
|
log(" Step 4: Stage 1 (bias [0,-4,4]*U0)...")
|
|
with CtxGuard(sim):
|
|
sim.restore()
|
|
# bias action_norm = [0,0,0] -> omega = -(0*8 + [0,-4,4])*U0 / R
|
|
bias_omega = action_to_omega(np.zeros(3, dtype=np.float32))
|
|
log(f" bias_omega = {bias_omega}")
|
|
stage1 = run_stage(sim, dist_id, sensor_ids, pinball_ids, target,
|
|
bias_omega, "Stage1")
|
|
|
|
sim.close()
|
|
|
|
# ---- Step 5: Analysis ----
|
|
log(" Step 5: Analysis...")
|
|
|
|
# 5a. Cd/Cl/Sim summary per stage
|
|
def stage_summary(stage, name):
|
|
cd_mean, cd_std = float(np.mean(stage["cds"])), float(np.std(stage["cds"]))
|
|
cl_mean, cl_std = float(np.mean(stage["cls"])), float(np.std(stage["cls"]))
|
|
sim_mean, sim_std = float(np.mean(stage["sims"])), float(np.std(stage["sims"]))
|
|
# Current reward formula
|
|
r_cd = float(np.mean(np.exp(-np.abs(stage["cds"] * 20))))
|
|
r_cl = float(np.mean(np.exp(-np.abs(stage["cls"] * 80))))
|
|
r_sim = float(np.mean(np.exp(-10 * np.abs(stage["sims"] - 1))))
|
|
reward = float(np.mean(np.minimum(0.3 * np.exp(-np.abs(stage["cds"] * 20)) +
|
|
0.4 * np.exp(-np.abs(stage["cls"] * 80)) +
|
|
0.3 * np.exp(-10 * np.abs(stage["sims"] - 1)), 1.0)))
|
|
return {
|
|
"cd_mean": cd_mean, "cd_std": cd_std,
|
|
"cl_mean": cl_mean, "cl_std": cl_std,
|
|
"sim_mean": sim_mean, "sim_std": sim_std,
|
|
"r_cd": r_cd, "r_cl": r_cl, "r_sim": r_sim, "reward": reward,
|
|
}
|
|
|
|
s0_sum = stage_summary(stage0, "Stage0")
|
|
s1_sum = stage_summary(stage1, "Stage1")
|
|
|
|
# 5b. Force range (for "combined range" normalization design)
|
|
def force_range(stage):
|
|
f = stage["obs_slices"][:, 6:12] # raw forces
|
|
return {
|
|
"max_abs_per_dim": [float(np.max(np.abs(f[:, i]))) for i in range(6)],
|
|
"max_abs_combined": float(np.max(np.abs(f))),
|
|
"mean_abs_combined": float(np.mean(np.abs(f))),
|
|
"std_combined": float(np.std(f)),
|
|
}
|
|
|
|
def sensor_range(stage):
|
|
s = stage["obs_slices"][:, 0:6] # legacy-equiv sensors
|
|
return {
|
|
"max_abs_per_dim": [float(np.max(np.abs(s[:, i]))) for i in range(6)],
|
|
"max_abs_combined": float(np.max(np.abs(s))),
|
|
"mean_abs_combined": float(np.mean(np.abs(s))),
|
|
"std_combined": float(np.std(s)),
|
|
}
|
|
|
|
fr0, fr1 = force_range(stage0), force_range(stage1)
|
|
sr0, sr1 = sensor_range(stage0), sensor_range(stage1)
|
|
|
|
# 5c. Norm health (using current FORCE_SCALE=0.005, SENS_SCALE=U0)
|
|
nh0, normed0 = norm_health(stage0["obs_slices"])
|
|
nh1, normed1 = norm_health(stage1["obs_slices"])
|
|
|
|
# ---- Print summary ----
|
|
lines = []
|
|
lines.append("=" * 100)
|
|
lines.append("PHASE 0 BASELINE MEASURE SUMMARY (2000x600)")
|
|
lines.append("=" * 100)
|
|
|
|
lines.append("\n--- Target signal stats ---")
|
|
names_s = ["s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"]
|
|
for i, n in enumerate(names_s):
|
|
lines.append(f" {n:>8}: mean={target[:,i].mean():.4f}, std={target[:,i].std():.4f}, "
|
|
f"range=[{target[:,i].min():.4f}, {target[:,i].max():.4f}]")
|
|
|
|
lines.append("\n--- Stage comparison: Cd / Cl / Sim / Reward ---")
|
|
lines.append(f" {'Stage':>8} {'Cd_mean':>10} {'Cd_std':>10} {'Cl_mean':>10} {'Cl_std':>10} "
|
|
f"{'Sim_mean':>10} {'Sim_std':>10} {'r_cd':>8} {'r_cl':>8} {'r_sim':>8} {'reward':>8}")
|
|
for name, s in [("Stage0", s0_sum), ("Stage1", s1_sum)]:
|
|
lines.append(f" {name:>8} {s['cd_mean']:>10.4f} {s['cd_std']:>10.4f} {s['cl_mean']:>10.4f} "
|
|
f"{s['cl_std']:>10.4f} {s['sim_mean']:>10.4f} {s['sim_std']:>10.4f} "
|
|
f"{s['r_cd']:>8.4f} {s['r_cl']:>8.4f} {s['r_sim']:>8.4f} {s['reward']:>8.4f}")
|
|
|
|
lines.append("\n--- Force range (raw, for combined-range norm design) ---")
|
|
fnames = ["front_fx", "front_fy", "top_fx", "top_fy", "bot_fx", "bot_fy"]
|
|
for name, fr in [("Stage0", fr0), ("Stage1", fr1)]:
|
|
lines.append(f" {name}: max_abs_combined={fr['max_abs_combined']:.6f}, "
|
|
f"mean_abs={fr['mean_abs_combined']:.6f}, std={fr['std_combined']:.6f}")
|
|
for i, fn in enumerate(fnames):
|
|
lines.append(f" {fn:>10}: max_abs={fr['max_abs_per_dim'][i]:.6f}")
|
|
|
|
lines.append("\n--- Sensor range (legacy-equiv, for combined-range norm design) ---")
|
|
for name, sr in [("Stage0", sr0), ("Stage1", sr1)]:
|
|
lines.append(f" {name}: max_abs_combined={sr['max_abs_combined']:.4f}, "
|
|
f"mean_abs={sr['mean_abs_combined']:.4f}, std={sr['std_combined']:.4f}")
|
|
for i, sn in enumerate(names_s):
|
|
lines.append(f" {sn:>8}: max_abs={sr['max_abs_per_dim'][i]:.4f}")
|
|
|
|
lines.append("\n--- Obs norm health (current: FORCE_SCALE=0.005, SENS_SCALE=U0=0.01) ---")
|
|
nh_names = ["f_front_fx", "f_front_fy", "f_top_fx", "f_top_fy", "f_bot_fx", "f_bot_fy",
|
|
"s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"]
|
|
for name, nh in [("Stage0", nh0), ("Stage1", nh1)]:
|
|
lines.append(f"\n {name} (env order: forces[6], sensors[6]):")
|
|
lines.append(f" {'dim':>14} {'mean':>10} {'std':>10} {'min':>10} {'max':>10} {'clip%':>8}")
|
|
for i, n in enumerate(nh_names):
|
|
d = nh[n]
|
|
lines.append(f" {n:>14} {d['mean']:>10.4f} {d['std']:>10.4f} {d['min']:>10.4f} "
|
|
f"{d['max']:>10.4f} {d['clip_ratio']*100:>7.1f}%")
|
|
ov = nh["_overall"]
|
|
lines.append(f" {'OVERALL':>14} clip_ratio_total={ov['clip_ratio_total']*100:.1f}%, "
|
|
f"force_clip={ov['force_clip']*100:.1f}%, sens_clip={ov['sens_clip']*100:.1f}%")
|
|
|
|
lines.append("\n--- Gradient assessment ---")
|
|
lines.append(f" Cd: Stage0={s0_sum['cd_mean']:.4f} -> Stage1={s1_sum['cd_mean']:.4f} "
|
|
f"(Δ={s1_sum['cd_mean']-s0_sum['cd_mean']:+.4f})")
|
|
lines.append(f" Cl: Stage0={s0_sum['cl_mean']:.4f} -> Stage1={s1_sum['cl_mean']:.4f} "
|
|
f"(Δ={s1_sum['cl_mean']-s0_sum['cl_mean']:+.4f})")
|
|
lines.append(f" Sim: Stage0={s0_sum['sim_mean']:.4f} -> Stage1={s1_sum['sim_mean']:.4f} "
|
|
f"(Δ={s1_sum['sim_mean']-s0_sum['sim_mean']:+.4f})")
|
|
lines.append(f" Reward: Stage0={s0_sum['reward']:.4f} -> Stage1={s1_sum['reward']:.4f} "
|
|
f"(Δ={s1_sum['reward']-s0_sum['reward']:+.4f})")
|
|
|
|
summary = "\n".join(lines)
|
|
print("\n" + summary)
|
|
with open(OUT_DIR / "summary.txt", "w") as f:
|
|
f.write(summary + "\n")
|
|
|
|
# Save raw data
|
|
with open(OUT_DIR / "stage_data.pkl", "wb") as f:
|
|
pickle.dump({
|
|
"target": target,
|
|
"stage0": stage0, "stage1": stage1,
|
|
"s0_summary": s0_sum, "s1_summary": s1_sum,
|
|
"force_range": {"stage0": fr0, "stage1": fr1},
|
|
"sensor_range": {"stage0": sr0, "stage1": sr1},
|
|
}, f)
|
|
|
|
with open(OUT_DIR / "norm_health.json", "w") as f:
|
|
json.dump({"stage0": nh0, "stage1": nh1}, f, indent=2)
|
|
|
|
log(f"\nPhase 0 complete. Files in {OUT_DIR}:")
|
|
log(f" summary.txt, stage_data.pkl, norm_health.json, phase0.log")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|