run_thermal.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. """Standalone Motor-CAD steady-state thermal simulation for the MARS model.
  2. Purpose: validate that the Motor-CAD thermal solver runs end-to-end on the
  3. MARS PCB axial flux motor, and capture the REAL thermal result field names
  4. so the metric aliases in src/afmcore/metrics.py can be tuned to match.
  5. Flow (mode=steady, default):
  6. 1. Ensure Motor-CAD environment (license + exe path fallback).
  7. 2. Connect to a new, visible Motor-CAD instance.
  8. 3. Load the MARS baseline model.
  9. 4. Run the electromagnetic calculation (losses are the thermal source).
  10. 5. Run the steady-state thermal analysis.
  11. 6. Export EM + thermal results (solution_type="SteadyState") and parse.
  12. Flow (mode=coupled):
  13. Steps 1-3, then do_magnetic_thermal_calculation (EM + thermal in one
  14. coupled call), then export and parse both EM and thermal results.
  15. Run with the venv python that has ansys-motorcad-core installed, e.g.:
  16. <venv>/Scripts/python.exe scripts/run_thermal.py --mode steady
  17. <venv>/Scripts/python.exe scripts/run_thermal.py --mode coupled
  18. All source is ASCII only.
  19. """
  20. from __future__ import annotations
  21. import argparse
  22. import math
  23. import os
  24. import sys
  25. import time
  26. import traceback
  27. from datetime import datetime
  28. from pathlib import Path
  29. # Make the platform core importable (src/afmcore/metrics.py).
  30. _ROOT = Path(__file__).resolve().parent.parent
  31. _SRC = _ROOT / "src"
  32. if str(_SRC) not in sys.path:
  33. sys.path.insert(0, str(_SRC))
  34. from afmcore.metrics import parse_export, extract_all_metrics # noqa: E402
  35. MODEL_PATH = _ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
  36. _MOTORCAD_EXE_CANDIDATES = [
  37. r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
  38. r"E:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
  39. r"C:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
  40. ]
  41. # Workload for the EM run (matches the MARS baseline documented in
  42. # KNOWLEDGE_BASE section 3): RMS phase current 21 A, shaft speed 5000 rpm.
  43. WORKLOAD = {
  44. "RMSCurrent": 21.0,
  45. "Shaft_Speed": 5000.0,
  46. }
  47. def log(msg: str) -> None:
  48. print(msg, flush=True)
  49. def ensure_environment() -> None:
  50. """Set Motor-CAD env vars (non-login shell trap, see KNOWLEDGE_BASE 1)."""
  51. if not os.environ.get("MOTORCAD_ACTIVEX"):
  52. try:
  53. from ansys.motorcad.core import set_motorcad_exe
  54. for candidate in _MOTORCAD_EXE_CANDIDATES:
  55. if os.path.exists(candidate):
  56. set_motorcad_exe(candidate)
  57. log("MOTORCAD_ACTIVEX unset; fallback to %s" % candidate)
  58. break
  59. except Exception:
  60. pass
  61. if not os.environ.get("ANSYSLMD_LICENSE_FILE"):
  62. os.environ["ANSYSLMD_LICENSE_FILE"] = "1055@localhost"
  63. def write_and_verify(mc, variable: str, value: float) -> None:
  64. """Write a variable and read it back; raise on mismatch (AGENTS rule 4)."""
  65. mc.set_variable(variable, value)
  66. applied = float(mc.get_variable(variable))
  67. if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
  68. raise RuntimeError(
  69. "Write verification failed for %s: wrote %s, read %s"
  70. % (variable, value, applied)
  71. )
  72. def main(mode: str = "steady") -> int:
  73. ensure_environment()
  74. import ansys.motorcad.core as pymotorcad
  75. output_dir = _ROOT / "output" / (
  76. "thermal_validation_" + datetime.now().strftime("%Y%m%d_%H%M%S")
  77. )
  78. raw_dir = output_dir / "raw"
  79. raw_dir.mkdir(parents=True, exist_ok=True)
  80. log("Mode: %s" % mode)
  81. log("Output dir: %s" % output_dir)
  82. log("Connecting to a new visible Motor-CAD instance ...")
  83. mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
  84. mc.set_visible(True)
  85. mc.set_variable("MessageDisplayState", 2)
  86. mc.display_screen("Scripting")
  87. time.sleep(2)
  88. log("Connected.")
  89. try:
  90. log("Loading model: %s" % MODEL_PATH)
  91. mc.load_from_file(str(MODEL_PATH))
  92. for var, val in WORKLOAD.items():
  93. try:
  94. write_and_verify(mc, var, val)
  95. log(" %s = %s (verified)" % (var, val))
  96. except Exception as exc: # noqa: BLE001
  97. log(" WARNING: %s write failed: %s" % (var, exc))
  98. if mode == "coupled":
  99. # Magnetic-thermal coupled solve: EM + thermal in one call.
  100. log("Running magnetic-thermal coupled calculation ...")
  101. t0 = time.time()
  102. mc.do_magnetic_thermal_calculation()
  103. log("Coupled solve done in %.1f s" % (time.time() - t0))
  104. else:
  105. log("Running electromagnetic calculation (losses = thermal source) ...")
  106. t0 = time.time()
  107. mc.do_magnetic_calculation()
  108. log("EM done in %.1f s" % (time.time() - t0))
  109. log("Running steady-state thermal analysis ...")
  110. t0 = time.time()
  111. mc.do_steady_state_analysis()
  112. log("Thermal steady-state done in %.1f s" % (time.time() - t0))
  113. em_raw = raw_dir / "emagnetic.csv"
  114. mc.export_results("EMagnetic", str(em_raw))
  115. log("EM results exported: %s" % em_raw)
  116. thermal_raw = raw_dir / "thermal_steadystate.csv"
  117. mc.export_results("SteadyState", str(thermal_raw))
  118. log("Thermal results exported: %s" % thermal_raw)
  119. parsed = parse_export(thermal_raw)
  120. metrics = extract_all_metrics(parsed)
  121. # Print the key thermal metrics (not the full field dump, which is
  122. # only needed when tuning aliases; keep output compact).
  123. log("")
  124. log("=== Extracted thermal metrics ===")
  125. thermal_keys = [
  126. "winding_temp_c", "winding_hotspot_temp_c", "magnet_temp_c",
  127. "stator_temp_c", "bearing_temp_c", "temp_rise_c",
  128. "thermal_resistance_k_w",
  129. ]
  130. for key in thermal_keys:
  131. if key in metrics:
  132. log(" %s = %s" % (key, metrics[key]))
  133. # Also report the EM metrics for coupled-vs-steady comparison.
  134. em_parsed = parse_export(em_raw)
  135. em_metrics = extract_all_metrics(em_parsed)
  136. log("")
  137. log("=== Extracted EM metrics (for comparison) ===")
  138. for key in ["tavg_nm", "ripple_pct", "total_losses_w", "efficiency_pct"]:
  139. if key in em_metrics:
  140. log(" %s = %s" % (key, em_metrics[key]))
  141. log("")
  142. log("Thermal validation finished. Output dir: %s" % output_dir)
  143. return 0
  144. finally:
  145. try:
  146. mc.load_from_file(str(MODEL_PATH))
  147. except Exception: # noqa: BLE001
  148. pass
  149. try:
  150. mc.quit()
  151. except Exception: # noqa: BLE001
  152. pass
  153. log("Motor-CAD instance closed.")
  154. if __name__ == "__main__":
  155. parser = argparse.ArgumentParser(
  156. description="Motor-CAD thermal validation for the MARS model."
  157. )
  158. parser.add_argument(
  159. "--mode",
  160. choices=["steady", "coupled"],
  161. default="steady",
  162. help="steady = EM then steady-state thermal (default); "
  163. "coupled = do_magnetic_thermal_calculation (EM+thermal in one).",
  164. )
  165. args = parser.parse_args()
  166. try:
  167. sys.exit(main(mode=args.mode))
  168. except Exception:
  169. traceback.print_exc()
  170. sys.exit(1)