"""Standalone Motor-CAD steady-state thermal simulation for the MARS model. Purpose: validate that the Motor-CAD thermal solver runs end-to-end on the MARS PCB axial flux motor, and capture the REAL thermal result field names so the metric aliases in src/afmcore/metrics.py can be tuned to match. Flow (mode=steady, default): 1. Ensure Motor-CAD environment (license + exe path fallback). 2. Connect to a new, visible Motor-CAD instance. 3. Load the MARS baseline model. 4. Run the electromagnetic calculation (losses are the thermal source). 5. Run the steady-state thermal analysis. 6. Export EM + thermal results (solution_type="SteadyState") and parse. Flow (mode=coupled): Steps 1-3, then do_magnetic_thermal_calculation (EM + thermal in one coupled call), then export and parse both EM and thermal results. Run with the venv python that has ansys-motorcad-core installed, e.g.: /Scripts/python.exe scripts/run_thermal.py --mode steady /Scripts/python.exe scripts/run_thermal.py --mode coupled All source is ASCII only. """ from __future__ import annotations import argparse import math import os import sys import time import traceback from datetime import datetime from pathlib import Path # Make the platform core importable (src/afmcore/metrics.py). _ROOT = Path(__file__).resolve().parent.parent _SRC = _ROOT / "src" if str(_SRC) not in sys.path: sys.path.insert(0, str(_SRC)) from afmcore.metrics import parse_export, extract_all_metrics # noqa: E402 MODEL_PATH = _ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot" _MOTORCAD_EXE_CANDIDATES = [ r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe", r"E:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe", r"C:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe", ] # Workload for the EM run (matches the MARS baseline documented in # KNOWLEDGE_BASE section 3): RMS phase current 21 A, shaft speed 5000 rpm. WORKLOAD = { "RMSCurrent": 21.0, "Shaft_Speed": 5000.0, } def log(msg: str) -> None: print(msg, flush=True) def ensure_environment() -> None: """Set Motor-CAD env vars (non-login shell trap, see KNOWLEDGE_BASE 1).""" if not os.environ.get("MOTORCAD_ACTIVEX"): try: from ansys.motorcad.core import set_motorcad_exe for candidate in _MOTORCAD_EXE_CANDIDATES: if os.path.exists(candidate): set_motorcad_exe(candidate) log("MOTORCAD_ACTIVEX unset; fallback to %s" % candidate) break except Exception: pass if not os.environ.get("ANSYSLMD_LICENSE_FILE"): os.environ["ANSYSLMD_LICENSE_FILE"] = "1055@localhost" def write_and_verify(mc, variable: str, value: float) -> None: """Write a variable and read it back; raise on mismatch (AGENTS rule 4).""" mc.set_variable(variable, value) applied = float(mc.get_variable(variable)) if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7): raise RuntimeError( "Write verification failed for %s: wrote %s, read %s" % (variable, value, applied) ) def main(mode: str = "steady") -> int: ensure_environment() import ansys.motorcad.core as pymotorcad output_dir = _ROOT / "output" / ( "thermal_validation_" + datetime.now().strftime("%Y%m%d_%H%M%S") ) raw_dir = output_dir / "raw" raw_dir.mkdir(parents=True, exist_ok=True) log("Mode: %s" % mode) log("Output dir: %s" % output_dir) log("Connecting to a new visible Motor-CAD instance ...") mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False) mc.set_visible(True) mc.set_variable("MessageDisplayState", 2) mc.display_screen("Scripting") time.sleep(2) log("Connected.") try: log("Loading model: %s" % MODEL_PATH) mc.load_from_file(str(MODEL_PATH)) for var, val in WORKLOAD.items(): try: write_and_verify(mc, var, val) log(" %s = %s (verified)" % (var, val)) except Exception as exc: # noqa: BLE001 log(" WARNING: %s write failed: %s" % (var, exc)) if mode == "coupled": # Magnetic-thermal coupled solve: EM + thermal in one call. log("Running magnetic-thermal coupled calculation ...") t0 = time.time() mc.do_magnetic_thermal_calculation() log("Coupled solve done in %.1f s" % (time.time() - t0)) else: log("Running electromagnetic calculation (losses = thermal source) ...") t0 = time.time() mc.do_magnetic_calculation() log("EM done in %.1f s" % (time.time() - t0)) log("Running steady-state thermal analysis ...") t0 = time.time() mc.do_steady_state_analysis() log("Thermal steady-state done in %.1f s" % (time.time() - t0)) em_raw = raw_dir / "emagnetic.csv" mc.export_results("EMagnetic", str(em_raw)) log("EM results exported: %s" % em_raw) thermal_raw = raw_dir / "thermal_steadystate.csv" mc.export_results("SteadyState", str(thermal_raw)) log("Thermal results exported: %s" % thermal_raw) parsed = parse_export(thermal_raw) metrics = extract_all_metrics(parsed) # Print the key thermal metrics (not the full field dump, which is # only needed when tuning aliases; keep output compact). log("") log("=== Extracted thermal metrics ===") thermal_keys = [ "winding_temp_c", "winding_hotspot_temp_c", "magnet_temp_c", "stator_temp_c", "bearing_temp_c", "temp_rise_c", "thermal_resistance_k_w", ] for key in thermal_keys: if key in metrics: log(" %s = %s" % (key, metrics[key])) # Also report the EM metrics for coupled-vs-steady comparison. em_parsed = parse_export(em_raw) em_metrics = extract_all_metrics(em_parsed) log("") log("=== Extracted EM metrics (for comparison) ===") for key in ["tavg_nm", "ripple_pct", "total_losses_w", "efficiency_pct"]: if key in em_metrics: log(" %s = %s" % (key, em_metrics[key])) log("") log("Thermal validation finished. Output dir: %s" % output_dir) return 0 finally: try: mc.load_from_file(str(MODEL_PATH)) except Exception: # noqa: BLE001 pass try: mc.quit() except Exception: # noqa: BLE001 pass log("Motor-CAD instance closed.") if __name__ == "__main__": parser = argparse.ArgumentParser( description="Motor-CAD thermal validation for the MARS model." ) parser.add_argument( "--mode", choices=["steady", "coupled"], default="steady", help="steady = EM then steady-state thermal (default); " "coupled = do_magnetic_thermal_calculation (EM+thermal in one).", ) args = parser.parse_args() try: sys.exit(main(mode=args.mode)) except Exception: traceback.print_exc() sys.exit(1)