solver.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # -*- coding: utf-8 -*-
  2. """
  3. 轴向磁拉力求解核心 — 从 axial_force_final.py / axial_compare.py 提取,
  4. 封装为可被 GUI 调用的 Solver 类。支持单点仿真与气隙扫描两种模式。
  5. 所有日志通过 log_cb 回调输出到 GUI。
  6. """
  7. import json
  8. import math
  9. import os
  10. import time
  11. MU0 = 4e-7 * math.pi
  12. MAX_TSTEPS = 64
  13. MAX_NODES = 40
  14. # 全局 keepalive: keep_open=True 时保存 MotorCAD 对象引用, 防止 solver 被 GC
  15. # 时连带销毁 MotorCAD → 关闭 Motor-CAD 进程 (GUI 模式下必须保持)
  16. _KEEPALIVE_MC = None
  17. def _stats(ys):
  18. if not ys:
  19. return None
  20. return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
  21. "pk2pk": max(ys) - min(ys), "n": len(ys)}
  22. def _read_nodes(mc, graph, sec, tstep):
  23. xs, ys = [], []
  24. for i in range(MAX_NODES):
  25. try:
  26. x, y = mc.get_magnetic_3d_graph_point(graph, sec, i, tstep)
  27. except Exception:
  28. break
  29. xs.append(x)
  30. ys.append(y)
  31. return xs, ys
  32. def _net_force_series(mc, graph):
  33. series = []
  34. meta = {"sections": {}}
  35. for tstep in range(MAX_TSTEPS):
  36. total, got = 0.0, False
  37. for sec in (1, 2):
  38. xs, ys = _read_nodes(mc, graph, sec, tstep)
  39. if not ys:
  40. continue
  41. got = True
  42. nu = len(ys) - 1 if (len(xs) > 1 and
  43. abs(xs[-1] - xs[0] - 360.0) < 1e-6) else len(ys)
  44. total += sum(ys[:nu])
  45. if tstep == 0:
  46. meta["sections"][sec] = {"n_points": len(ys), "n_unique": nu}
  47. if not got:
  48. break
  49. series.append(total)
  50. return series, meta
  51. def _torque_from_ft(mc, sec_radii_mm):
  52. tq = 0.0
  53. for sec, r_mm in zip((1, 2), sec_radii_mm):
  54. xs, ys = _read_nodes(mc, "Ft_Rotor_OL_Lumped", sec, 0)
  55. if not ys:
  56. return None
  57. nu = len(ys) - 1 if (len(xs) > 1 and
  58. abs(xs[-1] - xs[0] - 360.0) < 1e-6) else len(ys)
  59. tq += sum(ys[:nu]) * (r_mm * 1e-3)
  60. return tq
  61. def _read_2d(mc, graph, maxpts=64):
  62. ys = []
  63. for i in range(maxpts):
  64. try:
  65. _, y = mc.get_magnetic_graph_point(graph, i)
  66. except Exception:
  67. break
  68. ys.append(y)
  69. return ys
  70. class Solver:
  71. """轴向磁拉力求解器。参数通过构造函数传入, run_single() / run_sweep() 执行。"""
  72. def __init__(self, mot_path, out_dir, rms_current=21.0, speed_rpm=5000.0,
  73. airgap_mm=1.0, magnet_temp_c=100.0, sec_radii_mm=None,
  74. motorcad_exe=None, keep_open=True, log_cb=None):
  75. self.mot_path = mot_path
  76. self.out_dir = out_dir
  77. self.rms_current = rms_current
  78. self.speed_rpm = speed_rpm
  79. self.airgap_mm = airgap_mm
  80. self.magnet_temp_c = magnet_temp_c
  81. self.sec_radii_mm = sec_radii_mm or [28.25, 34.75]
  82. self.motorcad_exe = motorcad_exe
  83. self.keep_open = keep_open
  84. self.log = log_cb or (lambda msg: None)
  85. self.mc = None
  86. def _start_motorcad(self):
  87. from ansys.motorcad.core import MotorCAD, set_motorcad_exe
  88. if not os.environ.get("MOTORCAD_ACTIVEX") and self.motorcad_exe:
  89. if os.path.isfile(self.motorcad_exe):
  90. set_motorcad_exe(self.motorcad_exe)
  91. self.log(" 未检测到 MOTORCAD_ACTIVEX, 已显式定位 exe")
  92. self.log("启动 Motor-CAD (前台) ...")
  93. self.mc = MotorCAD()
  94. try:
  95. self.mc.set_visible(True)
  96. except Exception as e:
  97. self.log(" [提示] set_visible 失败 (不影响计算): %s" % e)
  98. def _load_and_configure(self, tag):
  99. os.makedirs(self.out_dir, exist_ok=True)
  100. ts = time.strftime("%m%d_%H%M%S")
  101. self.mc.load_from_file(self.mot_path)
  102. out_mot = os.path.join(self.out_dir, "MARS_SSSR_%s_%s.mot" % (tag, ts))
  103. self.mc.save_to_file(out_mot)
  104. # 设置工况参数
  105. self.mc.set_variable("RMSCurrent", self.rms_current)
  106. self.mc.set_variable("ShaftSpeed", self.speed_rpm)
  107. self.mc.set_variable("Airgap", self.airgap_mm)
  108. self.mc.set_variable("Magnet_Temperature", self.magnet_temp_c)
  109. # 开力计算开关
  110. self.mc.set_variable("ElectromagneticForcesCalc_Load", True)
  111. self.mc.set_variable("ElectromagneticForcesCalc_OC", True)
  112. # 回读确认
  113. params = {}
  114. for var in ["RMSCurrent", "ShaftSpeed", "Airgap", "Magnet_Temperature",
  115. "Stator_Lam_Dia", "Stator_Bore"]:
  116. try:
  117. params[var] = self.mc.get_variable(var)
  118. except Exception:
  119. params[var] = None
  120. self.log(" 工况: RMS=%.1fA, %drpm, 气隙=%.2fmm, 磁钢%.0f°C"
  121. % (params["RMSCurrent"], params["ShaftSpeed"],
  122. params["Airgap"], params["Magnet_Temperature"]))
  123. return out_mot, params, ts
  124. def _solve_once(self, tag="axialF"):
  125. """执行一次求解, 返回 (results_dict, csv_path, json_path)"""
  126. out_mot, params, ts = self._load_and_configure(tag)
  127. self.log("求解中 (OC+OL 力同算) ...")
  128. t0 = time.time()
  129. self.mc.do_magnetic_calculation()
  130. dt = time.time() - t0
  131. self.log(" 耗时 %.1f s" % dt)
  132. results = {"when": ts, "source_mot": os.path.basename(self.mot_path),
  133. "work_mot": out_mot, "solve_seconds": dt,
  134. "params": {"rms_current": params["RMSCurrent"],
  135. "speed_rpm": params["ShaftSpeed"],
  136. "airgap_mm": params["Airgap"],
  137. "magnet_temp_c": params["Magnet_Temperature"]},
  138. "convention_note": "AFM 2.5D 中 Fr(法向)=轴向力; OL=负载, OC=空载"}
  139. # 净轴向力
  140. forces = {}
  141. for graph in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
  142. "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
  143. series, meta = _net_force_series(self.mc, graph)
  144. if series:
  145. forces[graph] = {"series_N": series, "stats": _stats(series),
  146. "meta": meta}
  147. s = _stats(series)
  148. self.log(" %s: 均值 %.1f N, 纹波 %.2f N"
  149. % (graph, s["mean"], s["pk2pk"]))
  150. else:
  151. forces[graph] = None
  152. self.log(" [警告] %s 无数据" % graph)
  153. results["axial_forces"] = forces
  154. # 三判据
  155. checks = {}
  156. for case in ("OL", "OC"):
  157. fr = forces.get("Fr_Rotor_%s_Lumped" % case)
  158. fs = forces.get("Fr_Stator_%s_Lumped" % case)
  159. if fr and fs:
  160. mr, ms = fr["stats"]["mean"], fs["stats"]["mean"]
  161. checks["action_reaction_%s" % case] = {
  162. "rotor_mean_N": mr, "stator_mean_N": ms,
  163. "imbalance_pct": abs(mr + ms) / max(abs(mr), 1e-9) * 100}
  164. tq_ft = _torque_from_ft(self.mc, self.sec_radii_mm)
  165. tys = _read_2d(self.mc, 17)
  166. tq_graph = _stats(tys)["mean"] if tys else None
  167. checks["torque_crosscheck"] = {"sum_Ft_x_r_Nm_t0": tq_ft,
  168. "torque_graph_mean_Nm": tq_graph}
  169. bys = _read_2d(self.mc, "FluxDensityAirgap")
  170. if bys and params.get("Stator_Lam_Dia") and params.get("Stator_Bore"):
  171. b2 = sum(b * b for b in bys) / len(bys)
  172. d_out = float(params["Stator_Lam_Dia"]) * 1e-3
  173. d_in = float(params["Stator_Bore"]) * 1e-3
  174. area = math.pi / 4.0 * (d_out ** 2 - d_in ** 2)
  175. checks["analytic"] = {"mean_B2_T2": b2, "area_m2": area,
  176. "F_est_N": area / (2 * MU0) * b2}
  177. results["checks"] = checks
  178. self.log("校核: %s" % json.dumps(checks, ensure_ascii=False))
  179. # CSV
  180. csv_path = os.path.join(self.out_dir, "axial_force_%s_%s.csv" % (tag, ts))
  181. with open(csv_path, "w", encoding="utf-8") as f:
  182. f.write("tstep,Fr_Rotor_OL_N,Fr_Stator_OL_N,"
  183. "Fr_Rotor_OC_N,Fr_Stator_OC_N\n")
  184. nmax = max(len(v["series_N"]) if v else 0 for v in forces.values())
  185. for i in range(nmax):
  186. row = [str(i)]
  187. for g in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
  188. "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
  189. v = forces.get(g)
  190. row.append("%.4f" % v["series_N"][i]
  191. if v and i < len(v["series_N"]) else "")
  192. f.write(",".join(row) + "\n")
  193. results["csv"] = csv_path
  194. # JSON
  195. json_path = os.path.join(self.out_dir, "axialforce_%s_%s.json" % (tag, ts))
  196. with open(json_path, "w", encoding="utf-8") as f:
  197. json.dump(results, f, ensure_ascii=False, indent=2)
  198. # 结论摘要
  199. for case, label in (("OL", "负载(RMS %.0fA)" % self.rms_current),
  200. ("OC", "空载")):
  201. v = forces.get("Fr_Rotor_%s_Lumped" % case)
  202. if v:
  203. s = v["stats"]
  204. self.log("[结论] %s 转子净轴向力: 均值 %.1f N, 纹波峰峰 %.2f N"
  205. % (label, s["mean"], s["pk2pk"]))
  206. return results, csv_path, json_path
  207. def run_single(self):
  208. """单点仿真。返回 results dict。"""
  209. self._start_motorcad()
  210. try:
  211. results, csv_path, json_path = self._solve_once("single")
  212. self.log("结果文件: %s" % json_path)
  213. return results
  214. finally:
  215. if not self.keep_open:
  216. try:
  217. self.mc.quit()
  218. except Exception:
  219. pass
  220. else:
  221. global _KEEPALIVE_MC
  222. _KEEPALIVE_MC = self.mc # 保持引用, 防止 GC 关闭 Motor-CAD
  223. self.log("[提示] Motor-CAD 保持前台打开供检查。")
  224. def run_sweep(self, gaps):
  225. """气隙扫描。gaps: 气隙列表(mm)。返回汇总 dict。"""
  226. self._start_motorcad()
  227. try:
  228. os.makedirs(self.out_dir, exist_ok=True)
  229. ts = time.strftime("%m%d_%H%M%S")
  230. self.mc.load_from_file(self.mot_path)
  231. out_mot = os.path.join(self.out_dir, "MARS_SSSR_sweep_%s.mot" % ts)
  232. self.mc.save_to_file(out_mot)
  233. self.mc.set_variable("RMSCurrent", self.rms_current)
  234. self.mc.set_variable("ShaftSpeed", self.speed_rpm)
  235. self.mc.set_variable("Magnet_Temperature", self.magnet_temp_c)
  236. self.mc.set_variable("ElectromagneticForcesCalc_Load", True)
  237. self.mc.set_variable("ElectromagneticForcesCalc_OC", True)
  238. sweep = {"when": ts, "magnet_temp_C": self.magnet_temp_c,
  239. "rms_current_A": self.rms_current, "work_mot": out_mot,
  240. "cases": []}
  241. for g in gaps:
  242. self.mc.set_variable("Airgap", g)
  243. back = self.mc.get_variable("Airgap")
  244. self.log("== 气隙 %.2f mm (回读 %s), 求解 ..." % (g, back))
  245. t0 = time.time()
  246. self.mc.do_magnetic_calculation()
  247. dt = time.time() - t0
  248. case = {"airgap_mm": back, "solve_seconds": dt}
  249. for graph, key in [("Fr_Rotor_OC_Lumped", "F_OC"),
  250. ("Fr_Rotor_OL_Lumped", "F_OL")]:
  251. s, _ = _net_force_series(self.mc, graph)
  252. case[key] = _stats(s)
  253. bys = _read_2d(self.mc, "FluxDensityAirgap")
  254. if bys:
  255. case["B2_mean_T2"] = sum(b * b for b in bys) / len(bys)
  256. self.log(" F_OC=%.1f N, F_OL=%.1f N (耗时 %.0fs)"
  257. % (case["F_OC"]["mean"], case["F_OL"]["mean"], dt))
  258. sweep["cases"].append(case)
  259. # 磁负刚度 (有限差分, OC 口径)
  260. cs = sweep["cases"]
  261. if len(cs) >= 2:
  262. f = [c["F_OC"]["mean"] for c in cs]
  263. g = [c["airgap_mm"] for c in cs]
  264. kneg = {}
  265. for i in range(len(cs) - 1):
  266. kneg["seg_%.2f_%.2f" % (g[i], g[i + 1])] = \
  267. -(f[i + 1] - f[i]) / (g[i + 1] - g[i])
  268. if len(cs) >= 3:
  269. kneg["central"] = -(f[-1] - f[0]) / (g[-1] - g[0])
  270. sweep["kneg_N_per_mm"] = kneg
  271. self.log("磁负刚度: %s N/mm" % json.dumps(kneg, ensure_ascii=False))
  272. json_path = os.path.join(self.out_dir, "sweep_results_%s.json" % ts)
  273. with open(json_path, "w", encoding="utf-8") as f:
  274. json.dump(sweep, f, ensure_ascii=False, indent=2)
  275. self.log("扫描结果: %s" % json_path)
  276. return sweep
  277. finally:
  278. if not self.keep_open:
  279. try:
  280. self.mc.quit()
  281. except Exception:
  282. pass
  283. else:
  284. global _KEEPALIVE_MC
  285. _KEEPALIVE_MC = self.mc # 保持引用, 防止 GC 关闭 Motor-CAD
  286. self.log("[提示] Motor-CAD 保持前台打开供检查。")