fix(oid): confirm FIFO bias bug has no structural impact

- Fixed bias_arr[4] (front 0), bias_arr[5] (bottom -4U0), bias_arr[6] (top +4U0)
- Re-ran full karman pipeline: force-sig overlap unchanged (-0.034)
- Force-OID still beats POD (0.295 vs 0.068, was 0.750 vs 0.418)
- Absolute R2 shifted because corrected FIFO changed PPO trajectory start
- Structural conclusion (force-sig near-orthogonal) is robust

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-06-30 16:26:54 +08:00
co-authored by Cursor
parent 5c55c5bdf7
commit 2ae248421d
282 changed files with 27668 additions and 368 deletions
@@ -0,0 +1,241 @@
"""All scene parameters in one place.
Single source of truth for geometry, action scaling, norm formulas, and DRL settings.
Every scene family needed for reproduction is defined here.
Re convention:
- "re_code" uses reference length 2*D (=40 lattice units), matching model file naming.
- nu = U0 * (2*D) / re_code
- Physical Re_D = re_code / 2 (uses single cylinder diameter D=20)
"""
from __future__ import annotations
from typing import Any, Dict
import numpy as np
# ---------------------------------------------------------------------------
# Physics constants (must match config_lbm_pinball.json)
# ---------------------------------------------------------------------------
U0 = 0.01 # inlet centre velocity (lattice units)
L0 = 20.0 # base length unit = 1 cylinder diameter in lattice
D_CYL = 20.0 # single cylinder diameter
D_REF = 40.0 # reference length for code Re = 2*D
NX = 1280
NY = 512
CENTER_Y = (NY - 1) / 2.0 # 255.5
CFG_PATH = "configs/config_lbm_pinball.json"
def nu_from_re(re_code: float) -> float:
"""Kinematic viscosity from code Reynolds number."""
return U0 * D_REF / re_code
# ---------------------------------------------------------------------------
# Scene definitions
# ---------------------------------------------------------------------------
SCENES: Dict[str, Dict[str, Any]] = {}
# -- Steady Cloak (clean inflow, no disturbance cylinder) --------------------
# Also serves as the base pinball-only env for Illusion inference and Vortex.
SCENES["steady_cloak_re100"] = {
"scene_id": "steady_cloak",
"model": "d1a3o12_re100",
"model_subdir": "old",
"re_code": 100,
"nu": nu_from_re(100),
"s_dim": 12,
"a_dim": 3,
"has_disturbance": False,
"pinball_front_x": 30.0 * L0, # 600
"pinball_rear_x": 31.3 * L0, # 626
"pinball_y_span": 0.75 * L0, # 15
"sensor_x": 40.0 * L0, # 800
"sensor_y_span": 2.0 * L0, # 40
"sensor_radius": L0 / 4, # 5
"pinball_radius": L0 / 2, # 10
"sample_interval": 800,
"action_scale": 8.0,
"action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32),
"u0": U0,
"fifo_len": 150,
"conv_len": 30,
"max_steps": 500,
"n_objects": 6,
"obs_slice": (0, 12),
"force_norm_formula": "6*max",
"sens_norm_factor": 5,
"target_type": "steady", # mean of clean channel
"warmup_steps_init": int(4 * NX / U0),
"warmup_steps_pinball": int(4 * NX / U0),
}
# -- Karman Cloak (upstream disturbance cylinder) ----------------------------
SCENES["karman_cloak_re100"] = {
"scene_id": "karman_cloak",
"model": "d1a3o12_re100",
"model_subdir": "old",
"re_code": 100,
"nu": nu_from_re(100),
"s_dim": 12,
"a_dim": 3,
"has_disturbance": True,
"dist_center_x": 10.0 * L0, # 200
"dist_radius": 1.0 * L0, # 20
"pinball_front_x": 30.0 * L0, # 600
"pinball_rear_x": 31.3 * L0, # 626
"pinball_y_span": 0.75 * L0, # 15
"sensor_x": 40.0 * L0, # 800
"sensor_y_span": 2.0 * L0, # 40
"sensor_radius": L0 / 4, # 5
"pinball_radius": L0 / 2, # 10
"sample_interval": 800,
"action_scale": 8.0,
"action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32),
"u0": U0,
"fifo_len": 150,
"conv_len": 30,
"max_steps": 500,
"n_objects": 7,
"obs_slice": (2, 14), # skip dist_cyl forces
"force_norm_formula": "6*max",
"sens_norm_factor": 5,
"target_type": "periodic",
"warmup_steps_dist": int(4 * NX / U0),
"warmup_steps_pinball": int(4 * NX / U0),
}
# Also define the karman scenes at other Re for completeness
for re_code, name in [(50, "re50"), (200, "re200"), (400, "re400")]:
key = f"karman_cloak_{name}"
SCENES[key] = dict(SCENES["karman_cloak_re100"])
SCENES[key].update({
"model": f"d1a3o12_{name}",
"model_subdir": "old",
"re_code": re_code,
"nu": nu_from_re(re_code),
"scene_id": f"karman_cloak_{name}",
})
# -- Illusion (three target diameters) ---------------------------------------
# Inference geometry (matching uni_test): target at x=31*L0, sensors at x=40*L0
# Pinball uses standard geometry (front x=30, rear x=31.3, sensors x=40)
def _illusion_base() -> Dict[str, Any]:
return {
"scene_id": "illusion",
"re_code": 100,
"nu": nu_from_re(100),
"s_dim": 14,
"a_dim": 3,
"has_disturbance": False,
"target_center_x": 31.0 * L0, # 620 (inference position)
"pinball_front_x": 30.0 * L0, # 600 (standard inference pinball)
"pinball_rear_x": 31.3 * L0, # 626
"pinball_y_span": 0.75 * L0,
"sensor_x": 40.0 * L0, # 800 (inference, not training!)
"sensor_y_span": 2.0 * L0,
"sensor_radius": L0 / 4,
"pinball_radius": L0 / 2,
"action_scale": 8.0,
"action_bias": np.array([0.0, -2.0, 2.0], dtype=np.float32),
"u0": U0,
"fifo_len": 150,
"conv_len": 36, # illusion uses CONV_LEN=36
"max_steps": 500,
"n_objects": 6,
"obs_slice": (0, 12),
"force_norm_formula": "6*max",
"sens_norm_factor": 5,
"target_type": "harmonics",
"n_harmonics": 5,
"warmup_steps_target": int(4 * NX / U0),
"warmup_steps_pinball": int(4 * NX / U0),
}
illusion_entries = [
("illusion_075L", {
"model": "d1a3o14_250525_imit_075L_2U_400S",
"model_subdir": "250525",
"target_diameter": 0.75 * L0, # 15
"sample_interval": 400,
}),
("illusion_1L", {
"model": "d1a3o14_250525_imit_1L_2U_600S",
"model_subdir": "250525",
"target_diameter": 1.0 * L0, # 20
"sample_interval": 800, # uni_test used 800 for inference
}),
("illusion_15L", {
"model": "d1a3o14_250525_imit_15L_2U",
"model_subdir": "250525",
"target_diameter": 1.5 * L0, # 30
"sample_interval": 800,
}),
]
for key, overrides in illusion_entries:
base = _illusion_base()
base.update(overrides)
SCENES[key] = base
# -- Vortex (Lamb dipole and Taylor monopole) --------------------------------
def _vortex_base() -> Dict[str, Any]:
return {
"scene_id": "vortex",
"re_code": 100,
"nu": nu_from_re(100),
"s_dim": 12,
"a_dim": 3,
"has_disturbance": False,
"pinball_front_x": 30.0 * L0,
"pinball_rear_x": 31.3 * L0,
"pinball_y_span": 0.75 * L0,
"sensor_x": 40.0 * L0,
"sensor_y_span": 2.0 * L0,
"sensor_radius": L0 / 4,
"pinball_radius": L0 / 2,
"sample_interval": 800,
"action_scale": 4.0, # NOTE: scale=4 not 8
"action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32),
"u0": U0,
"fifo_len": 150,
"conv_len": 30,
"max_steps": 150, # transient!
"n_objects": 6,
"obs_slice": (0, 12),
"force_norm_formula": "6*max",
"sens_norm_factor": 5,
"target_type": "transient",
"vortex_center_x": 10.0 * L0, # target phase
"vortex_pinball_center_x": 15.0 * L0, # pinball phase
"vortex_radius": 2.0 * L0,
}
vortex_entries = [
("vortex_lamb", {
"model": "vortex_lamb",
"model_subdir": "old",
"vortex_type": "lamb",
"vortex_strength": 0.5 * U0,
}),
("vortex_taylor", {
"model": "vortex_taylor",
"model_subdir": "old",
"vortex_type": "taylor",
"vortex_strength": 0.03 * U0,
}),
]
for key, overrides in vortex_entries:
base = _vortex_base()
base.update(overrides)
SCENES[key] = base
def get_scene(name: str) -> Dict[str, Any]:
"""Look up a scene by name. Raises KeyError if not found."""
if name not in SCENES:
available = ", ".join(sorted(SCENES.keys()))
raise KeyError(f"Unknown scene '{name}'. Available: {available}")
return dict(SCENES[name]) # return a copy