"""Unified scene configuration for CCD_analysis. All scene metadata in one place. Each scene dict contains all parameters needed for data collection, resampling, POD, and CCD. Re convention: - "re_code" uses reference length 2*D (matching model file naming). - Re_D = re_code / 2 is the true physical Reynolds number. """ from __future__ import annotations import os from typing import Any, Dict, List, Optional # -- Root paths --------------------------------------------------------------- _PROJ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) MODEL_DIR = os.path.join(_PROJ, "..", "..", "models") LEGACY_CFG_DIR = os.path.join(os.path.dirname(__file__), "configs") DATA_DIR = os.path.join(os.path.dirname(__file__), "data") # -- Physics constants ------------------------------------------------------- U0 = 0.01 D_CYL = 20.0 D_REF = 40.0 L0 = 20.0 NX = 1280 NY = 512 CENTER_Y = (NY - 1) / 2.0 FIFO_LEN = 150 CONV_LEN = 30 def nu_from_re(re_code: float, u0: float = U0) -> float: """Viscosity from code Reynolds number (reference length = 2*D).""" return u0 * D_REF / re_code # -- Scene definitions ------------------------------------------------------- SCENES: Dict[str, Any] = {} # -- Pure Pinball (uncontrolled baseline, 6 objects, no disturbance) --------- SCENES["pinball"] = { "scene_id": "pinball", "re_code": 100, "has_disturbance": False, "sample_interval": 800, "source": "open_loop", "model_name": None, "n_objects_env": 6, "obs_slice": (0, 12), "sensor_x": 40.0, "pinball_front_x": 30.0, "pinball_rear_x": 31.3, "target_type": "periodic", "s_dim": 12, "u0": U0, "nu": nu_from_re(100), } # -- Steady Cloak (open-loop constant rotation, 6 objects) -------------------- SCENES["steady_cloak"] = { "scene_id": "steady_cloak", "re_code": 100, "has_disturbance": False, "sample_interval": 800, "source": "open_loop", "model_name": None, "n_objects_env": 6, "obs_slice": (0, 12), "sensor_x": 40.0, "pinball_front_x": 30.0, "pinball_rear_x": 31.3, "target_type": "steady", "s_dim": 12, "u0": U0, "nu": nu_from_re(100), "omega_front": 0.0, "omega_rear_scale": 5.1, } # -- Karman Cloak (PPO, 7 objects, disturbance cylinder) --------------------- SCENES["karman_re100"] = { "scene_id": "karman", "re_code": 100, "has_disturbance": True, "sample_interval": 800, "action_scale": 8.0, "action_bias": (0.0, -4.0, 4.0), "source": "PPO_inference", "model_name": "d1a3o12_re100", "model_subdir": "old", "n_objects_env": 7, "obs_slice": (2, 14), "sensor_x": 40.0, "pinball_front_x": 30.0, "pinball_rear_x": 31.3, "target_type": "periodic", "s_dim": 12, "u0": U0, "nu": nu_from_re(100), } # -- 1L Illusion (PPO, 6 objects, 2U=0.02) ---------------------------------- SCENES["illusion_1L"] = { "scene_id": "illusion", "re_code": 100, "target_diameter": 1.0, "has_disturbance": False, "sample_interval": 600, "action_scale": 8.0, "action_bias": (0.0, -2.0, 2.0), "source": "PPO_inference", "model_name": "d1a3o14_250525_imit_1L_2U_600S", "model_subdir": "250525", "n_objects_env": 6, "obs_slice": (0, 12), "sensor_x": 30.0, "pinball_front_x": 19.0, "pinball_rear_x": 20.3, "target_type": "periodic", "s_dim": 14, "u0": 0.02, "nu": nu_from_re(100, u0=0.02), } # -- Utility helpers --------------------------------------------------------- def get_scene(name: str) -> dict: """Return scene config dict by name. Raises KeyError if not found.""" if name not in SCENES: raise KeyError(f"Unknown scene: {name}. Available: {list(SCENES.keys())}") return dict(SCENES[name]) def get_scene_list(scene_id: Optional[str] = None) -> List[str]: """Return list of scene names, optionally filtered by scene_id.""" if scene_id is None: return list(SCENES.keys()) return [k for k, v in SCENES.items() if v["scene_id"] == scene_id] def model_path_for_scene(scene_name: str) -> Optional[str]: """Return absolute path to PPO model .zip file, or None.""" cfg = get_scene(scene_name) mn = cfg.get("model_name") if mn is None: return None subdir = cfg.get("model_subdir", "old") p = os.path.join(MODEL_DIR, subdir, f"{mn}.zip") return p if os.path.isfile(p) else None def data_dir_for_scene(scene_name: str) -> str: """Return the data directory for a scene, creating it if needed.""" cfg = get_scene(scene_name) scene_id = cfg["scene_id"] d = os.path.join(DATA_DIR, scene_id, scene_name) os.makedirs(d, exist_ok=True) return d