| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- """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.:
- <venv>/Scripts/python.exe scripts/run_thermal.py --mode steady
- <venv>/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)
|