|
|
@@ -297,7 +297,8 @@ class RobustMotorCADSolver:
|
|
|
point_timeout: int = 300, max_retries: int = 3,
|
|
|
headless: bool = False, motorcad_version: Optional[str] = None,
|
|
|
enable_thermal: bool = False,
|
|
|
- ambient_temperature: Optional[float] = None):
|
|
|
+ ambient_temperature: Optional[float] = None,
|
|
|
+ thermal_mode: Optional[str] = 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)
|
|
|
@@ -306,6 +307,10 @@ class RobustMotorCADSolver:
|
|
|
# before the thermal solve. MARS ships 125 C (abnormal); use 25-40.
|
|
|
# None = leave the model value unchanged.
|
|
|
self.ambient_temperature = ambient_temperature
|
|
|
+ # P6 thermal modes (task-level): "off" (EM only) / "steady" (EM +
|
|
|
+ # steady-state thermal) / "coupled" (magnetic-thermal coupled). None
|
|
|
+ # = derive from enable_thermal at run time (backwards compatible).
|
|
|
+ self.thermal_mode = thermal_mode
|
|
|
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')}"
|
|
|
@@ -530,7 +535,8 @@ class RobustMotorCADSolver:
|
|
|
|
|
|
def run_single_point(self, params: Dict[str, Any], point_index: int = 0,
|
|
|
point_label: str = "",
|
|
|
- enable_thermal: Optional[bool] = None) -> Dict[str, Any]:
|
|
|
+ enable_thermal: Optional[bool] = None,
|
|
|
+ thermal_mode: Optional[str] = None) -> Dict[str, Any]:
|
|
|
"""Run a single simulation point with full robustness protocol.
|
|
|
|
|
|
Protocol:
|
|
|
@@ -604,26 +610,26 @@ class RobustMotorCADSolver:
|
|
|
copper_w = compute_copper_width(float(params["Slot_Opening"]))
|
|
|
self._write_and_verify("Copper_Width", copper_w)
|
|
|
|
|
|
- # Step 5: Run magnetic calculation
|
|
|
- try:
|
|
|
- self.mc.do_magnetic_calculation()
|
|
|
- except MotorCADError as e:
|
|
|
- raise RuntimeError(f"Magnetic calculation failed: {e}")
|
|
|
-
|
|
|
- # Step 5b: Optional thermal calculation (P5-M6)
|
|
|
- # Best-effort: thermal solve requires a model with thermal
|
|
|
- # network configured; failures are warnings, EM results
|
|
|
- # remain valid. enable_thermal param overrides instance default.
|
|
|
- _thermal_on = (
|
|
|
- enable_thermal if enable_thermal is not None
|
|
|
- else self.enable_thermal
|
|
|
- )
|
|
|
- if _thermal_on:
|
|
|
+ # Step 5: Resolve thermal mode, then run EM / thermal solve.
|
|
|
+ # Precedence: per-call thermal_mode > instance thermal_mode
|
|
|
+ # > legacy enable_thermal boolean. Modes:
|
|
|
+ # "off" = EM only
|
|
|
+ # "steady" = EM then steady-state thermal (losses as source)
|
|
|
+ # "coupled" = do_magnetic_thermal_calculation (EM<->thermal iterate)
|
|
|
+ _mode = thermal_mode
|
|
|
+ if _mode is None:
|
|
|
+ _mode = self.thermal_mode
|
|
|
+ if _mode is None:
|
|
|
+ _legacy = (
|
|
|
+ enable_thermal if enable_thermal is not None
|
|
|
+ else self.enable_thermal
|
|
|
+ )
|
|
|
+ _mode = "steady" if _legacy else "off"
|
|
|
+ self._log("Thermal mode: %s" % _mode)
|
|
|
+
|
|
|
+ if _mode == "coupled":
|
|
|
+ # Coupled solve runs EM + thermal iteration in one call.
|
|
|
try:
|
|
|
- # P5-M6 fix (2026-09-04): pymotorcad has NO
|
|
|
- # 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",
|
|
|
@@ -633,13 +639,35 @@ class RobustMotorCADSolver:
|
|
|
"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
|
|
|
- self._log(
|
|
|
- f"WARNING: thermal calculation failed "
|
|
|
- f"(model may lack thermal network): {_therr}"
|
|
|
- )
|
|
|
+ self.mc.do_magnetic_thermal_calculation()
|
|
|
+ self._log("Magnetic-thermal coupled solve completed")
|
|
|
+ except MotorCADError as e:
|
|
|
+ raise RuntimeError(f"Coupled solve failed: {e}")
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ self.mc.do_magnetic_calculation()
|
|
|
+ except MotorCADError as e:
|
|
|
+ raise RuntimeError(f"Magnetic calculation failed: {e}")
|
|
|
+ # Optional steady-state thermal (P5-M6). Best-effort:
|
|
|
+ # failures are warnings, EM results remain valid.
|
|
|
+ if _mode == "steady":
|
|
|
+ try:
|
|
|
+ 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
|
|
|
+ self._log(
|
|
|
+ f"WARNING: thermal calculation failed "
|
|
|
+ f"(model may lack thermal network): {_therr}"
|
|
|
+ )
|
|
|
|
|
|
# Step 6: Export and parse
|
|
|
raw_file = os.path.join(
|
|
|
@@ -657,14 +685,12 @@ class RobustMotorCADSolver:
|
|
|
|
|
|
metrics = self._parse_export(raw_file)
|
|
|
|
|
|
- # Step 6b: Optional thermal export and metric merge (P5-M6)
|
|
|
- # Best-effort: thermal export section name may vary by
|
|
|
- # Motor-CAD version; failures do not invalidate EM metrics.
|
|
|
- if _thermal_on:
|
|
|
+ # Step 6b: thermal export and metric merge for steady/coupled.
|
|
|
+ # solution_type is "SteadyState" (NOT "Thermal").
|
|
|
+ # Valid values: EMagnetic / Lab / SteadyState / Transient.
|
|
|
+ if _mode in ("steady", "coupled"):
|
|
|
try:
|
|
|
_thermal_file = raw_file.replace(".csv", "_thermal.csv")
|
|
|
- # solution_type is "SteadyState" (NOT "Thermal").
|
|
|
- # Valid values: EMagnetic / Lab / SteadyState / Transient.
|
|
|
self.mc.export_results("SteadyState", _thermal_file)
|
|
|
if os.path.exists(_thermal_file):
|
|
|
_thermal_metrics = self._parse_export(_thermal_file)
|