"""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) --------------------------------------- # NOTE: The positions below (sensors at 40*L0, pinball at 30/31.3*L0) are the # "unified" inference geometry used by CCD_analysis. The actual training # geometry (legacy_env_imit.py) used sensors at 30*L0 and pinball at 19/20.3*L0. # phase3_reproduce.py and legacy_test scripts use the TRAINING positions. 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": 600, }), ("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