소스 검색

feat(thermal): hardwire ambient_temperature into executor config (default 25C)

- RobustMotorCADSolver: add ambient_temperature param; override Ambient_Temperature (write+verify) before steady-state thermal solve
- Plumb through adapter -> executor -> entry script -> config (default 25.0, EXECUTOR_AMBIENT env)
- verify_enable_thermal.py now exercises the constructor param path (re-verified end-to-end PASS)
- Docs: CONVERSATION_LOG, TEST_RECORDS
carlin 20 시간 전
부모
커밋
553a04e22f

+ 14 - 0
docs/CONVERSATION_LOG.md

@@ -1099,3 +1099,17 @@ enable_thermal 改动(仅在 MotorCADTaskExecutor 子类)无关,为既有
 **修复 test_executor_m3**:失败根因是测试辅助 make_task 未设 `status="pending"`,导致
 execute_task 的 claim 逻辑(仅 status=="pending" 才 dispatch)被跳过、`dispatch_task=False`
 未生效。修复后 4 项全通过。属测试代码缺陷,非生产代码 bug。
+
+## 2026-09-04 — 环境温度固化到执行器配置(ambient_temperature 全链路)
+
+**背景**:执行器 enable_thermal 热求解时默认沿用模型 `Ambient_Temperature=125`(异常),
+需固化默认值 25°C 让执行器热仿真开箱即用且正确。
+
+**实现(7 处透传)**:RobustMotorCADSolver.__init__ 加 `ambient_temperature`(热求解前
+`_write_and_verify("Ambient_Temperature", ...)` 覆盖)→ MotorCADAdapter → MotorCADTaskExecutor
+→ run_task_executor.py → executor_config.py(默认 25.0 + 校验 + `EXECUTOR_AMBIENT` env)→
+executor_config.json(`ambient_temperature: 25.0`)。
+
+**验证**(verify_enable_thermal.py 改用构造参数复测):`RobustMotorCADSolver(enable_thermal=True,
+ambient_temperature=25.0)` 单点端到端 PASS——7 项热指标落盘,温升 +27.57、热阻 +5.657。
+测试 test_executor_config 20 + test_metrics_extension 20 全通过。

+ 6 - 0
docs/TEST_RECORDS.md

@@ -2213,3 +2213,9 @@ P5-M6 引入的 `enable_thermal` 开关使用了两个**不存在的 API**,从
 ### 结论
 
 执行器 enable_thermal 链路端到端 **PASS**。热仿真已完整接入生产链路(配置开关 → adapter → solver → 热指标落盘)。
+
+### 补充(环境温度固化)
+
+`ambient_temperature` 已固化到执行器配置(默认 25°C,`executor_config.json` + `EXECUTOR_AMBIENT` env 可调),
+`RobustMotorCADSolver(enable_thermal=True, ambient_temperature=25.0)` 构造参数方式复测端到端 PASS
+(温升 +27.57、热阻 +5.657,与 params 传参方式结果一致)。

+ 2 - 1
executor_config.json

@@ -7,5 +7,6 @@
   "log_level": "INFO",
   "tool": "motorcad",
   "enable_mock": false,
-  "enable_thermal": false
+  "enable_thermal": false,
+  "ambient_temperature": 25.0
 }

+ 6 - 1
scripts/executor_config.py

@@ -46,6 +46,7 @@ def default_config():
         "tool": DEFAULT_TOOL,
         "enable_mock": False,
         "enable_thermal": False,
+        "ambient_temperature": 25.0,
     }
 
 
@@ -138,6 +139,9 @@ def validate_config(cfg):
     thermal = cfg.get("enable_thermal")
     if not isinstance(thermal, bool):
         raise ValueError("enable_thermal must be a boolean")
+    ambient = cfg.get("ambient_temperature")
+    if ambient is not None and not isinstance(ambient, (int, float)):
+        raise ValueError("ambient_temperature must be a number or null")
     return cfg
 
 
@@ -180,12 +184,13 @@ def load_config(cli_path=None, env_overrides=True):
             "EXECUTOR_TOOL": "tool",
             "EXECUTOR_MOCK": "enable_mock",
             "EXECUTOR_THERMAL": "enable_thermal",
+            "EXECUTOR_AMBIENT": "ambient_temperature",
         }
         for env_key, cfg_key in env_map.items():
             raw = os.environ.get(env_key)
             if raw is None or raw == "":
                 continue
-            if cfg_key in ("instances", "poll_interval"):
+            if cfg_key in ("instances", "poll_interval", "ambient_temperature"):
                 try:
                     cfg[cfg_key] = float(raw) if "." in raw else int(raw)
                 except ValueError:

+ 15 - 1
scripts/robust_motorcad.py

@@ -296,11 +296,16 @@ class RobustMotorCADSolver:
     def __init__(self, model_path: str, output_dir: Optional[str] = None,
                  point_timeout: int = 300, max_retries: int = 3,
                  headless: bool = False, motorcad_version: Optional[str] = None,
-                 enable_thermal: bool = False):
+                 enable_thermal: bool = False,
+                 ambient_temperature: Optional[float] = None):
         self.model_path = model_path
         # P5-M6: optional thermal solve (requires model with thermal
         # network configured; OFF by default to preserve EM-only behavior)
         self.enable_thermal = bool(enable_thermal)
+        # P5-M6 thermal boundary: when set, Ambient_Temperature is overridden
+        # before the thermal solve. MARS ships 125 C (abnormal); use 25-40.
+        # None = leave the model value unchanged.
+        self.ambient_temperature = ambient_temperature
         self.output_dir = output_dir or os.path.join(
             os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
             "output", f"run_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
@@ -619,6 +624,15 @@ class RobustMotorCADSolver:
                             # do_thermal_calculation() method. The steady-state
                             # thermal solve is do_steady_state_analysis().
                             # Verified against ansys.motorcad.core sources.
+                            if self.ambient_temperature is not None:
+                                self._write_and_verify(
+                                    "Ambient_Temperature",
+                                    float(self.ambient_temperature),
+                                )
+                                self._log(
+                                    "Ambient_Temperature overridden to %s"
+                                    % self.ambient_temperature
+                                )
                             self.mc.do_steady_state_analysis()
                             self._log("Steady-state thermal calculation completed")
                         except Exception as _therr:  # noqa: BLE001

+ 1 - 0
scripts/run_task_executor.py

@@ -71,6 +71,7 @@ def build_executors(cfg):
             enable_mock=cfg["enable_mock"],
             tool=cfg["tool"],
             enable_thermal=cfg["enable_thermal"],
+            ambient_temperature=cfg["ambient_temperature"],
             executor_id=("motorcad-executor-%s" % i) if multi else None,
             on_progress=make_log_callback("progress"),
             on_complete=make_log_callback("complete"),

+ 6 - 1
scripts/task_executor.py

@@ -497,7 +497,9 @@ class MotorCADTaskExecutor(TaskExecutor):
 
     def __init__(self, *args, model_path: Optional[str] = None,
                          tool: str = "motorcad",
-                         enable_thermal: bool = False, **kwargs):
+                         enable_thermal: bool = False,
+                         ambient_temperature: Optional[float] = None,
+                         **kwargs):
         # Mock fallback is disabled by default for real solver adapter.
         kwargs.setdefault("enable_mock", False)
         super().__init__(*args, **kwargs)
@@ -506,6 +508,8 @@ class MotorCADTaskExecutor(TaskExecutor):
         # P5-M6: pass through to the adapter so each EM point can also run a
         # steady-state thermal solve and merge thermal metrics.
         self.enable_thermal = bool(enable_thermal)
+        # P5-M6 thermal boundary: Ambient_Temperature override (degC).
+        self.ambient_temperature = ambient_temperature
         self._adapter = None
 
     def _ensure_adapter(self):
@@ -534,6 +538,7 @@ class MotorCADTaskExecutor(TaskExecutor):
         self._adapter = get_adapter(
             self.tool, model_path=self.model_path, output_dir=output_dir,
             enable_thermal=self.enable_thermal,
+            ambient_temperature=self.ambient_temperature,
         )
         self._adapter.connect()
         return self._adapter

+ 3 - 3
scripts/verify_enable_thermal.py

@@ -42,13 +42,13 @@ def main() -> int:
         "verify_thermal_" + time.strftime("%Y%m%d_%H%M%S")
     ))
     solver = RobustMotorCADSolver(
-        model_path=MODEL, output_dir=output_dir, enable_thermal=True
+        model_path=MODEL, output_dir=output_dir, enable_thermal=True,
+        ambient_temperature=25.0,
     )
     solver.connect()
     try:
         result = solver.run_single_point(
-            {"RMSCurrent": 21.0, "Shaft_Speed": 5000.0,
-             "Ambient_Temperature": 25.0},
+            {"RMSCurrent": 21.0, "Shaft_Speed": 5000.0},
             point_index=0, point_label="verify",
         )
         print("status: %s" % result["status"], flush=True)

+ 5 - 0
src/afmcore/adapters/motorcad.py

@@ -37,6 +37,7 @@ class MotorCADAdapter(SimulationAdapter):
         max_retries: int = 3,
         headless: bool = False,
         enable_thermal: bool = False,
+        ambient_temperature: Optional[float] = None,
         log_cb=None,
         progress_cb=None,
         **kwargs: Any,
@@ -50,6 +51,9 @@ class MotorCADAdapter(SimulationAdapter):
         # P5-M6: after each EM solve, also run a steady-state thermal solve
         # and merge thermal metrics (OFF by default, preserves EM-only flow).
         self.enable_thermal = bool(enable_thermal)
+        # P5-M6 thermal boundary: Ambient_Temperature override (degC); None =
+        # leave model value. MARS ships 125 C, so pass 25-40 for valid results.
+        self.ambient_temperature = ambient_temperature
         self._solver = None  # lazy RobustMotorCADSolver
 
     # -- internal ----------------------------------------------------------
@@ -79,6 +83,7 @@ class MotorCADAdapter(SimulationAdapter):
                 max_retries=self.max_retries,
                 headless=self.headless,
                 enable_thermal=self.enable_thermal,
+                ambient_temperature=self.ambient_temperature,
             )
         return self._solver