topology_variable_map.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. """Topology-aware Motor-CAD variable name mapping.
  2. Single source of truth for mapping logical parameter names to actual
  3. Motor-CAD variable names, per motor topology (AFM-SSSR, AFM-AFIR, RFM).
  4. Also provides cross-topology alias mapping: when a plan uses RFM-style
  5. variable names (e.g. Stator_Lam_Outer_Dia) on an AFM topology, this
  6. module maps them to the correct AFM variable name (Stator_Outer_Diameter).
  7. All variable names here are verified against the MARS-12S10P_SSSR .mot
  8. model and by pymotorcad get/set_variable probes (2026-09-03, TEST-037).
  9. """
  10. from typing import Dict, Optional, Set, Tuple, List
  11. # ---------------------------------------------------------------------------
  12. # Topology identifiers
  13. # ---------------------------------------------------------------------------
  14. TOPOLOGY_SSSR = "SSSR" # Single Stator Single Rotor axial flux
  15. TOPOLOGY_AFIR = "AFIR" # Axial Flux Integrated Rotor
  16. TOPOLOGY_RFM = "RFM" # Radial Flux Motor (legacy)
  17. ALL_TOPOLOGIES = (TOPOLOGY_SSSR, TOPOLOGY_AFIR, TOPOLOGY_RFM)
  18. # ---------------------------------------------------------------------------
  19. # Known Motor-CAD variable names per topology.
  20. # Verified against MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot and the
  21. # successful plan 21 run (2026-08-28).
  22. # ---------------------------------------------------------------------------
  23. _KNOWN_VARIABLES: Dict[str, Set[str]] = {
  24. TOPOLOGY_SSSR: {
  25. # --- Geometry ---
  26. "RotorOuterDiameter",
  27. "Stator_Lam_Dia",
  28. "Stator_Bore",
  29. "Back_Iron_Thickness",
  30. "Airgap",
  31. # --- Stator slots ---
  32. "Slot_Depth",
  33. "Slot_Width",
  34. "Tooth_Width",
  35. "Slot_Number",
  36. "Slot_Fill",
  37. "Slot_Type",
  38. # --- Rotor / magnets ---
  39. "Pole_Number",
  40. "Magnet_Length",
  41. "Magnet_Thickness",
  42. "Magnet_Arc_[ED]",
  43. "Material_Magnet",
  44. "Magnet_Br_at_RefTemp",
  45. "Magnet_Br_at_20",
  46. "InitialMagnetTemperature",
  47. # --- Materials ---
  48. # --- Electrical ---
  49. "DCBusVoltage",
  50. "RMSCurrent",
  51. "Shaft_Speed",
  52. "WindageGraph_MaxSpeed",
  53. "CurrentDefinition",
  54. "PhaseAdvance",
  55. # --- Winding ---
  56. "ConductorsPerSlot",
  57. "ParallelPaths",
  58. "Wire_Diameter",
  59. "WindingConnection",
  60. "Copper_Diameter",
  61. # --- Thermal ---
  62. "Ambient_Temperature",
  63. "Cooling_Type",
  64. # --- Simulation control ---
  65. "TorquePointsPerCycle",
  66. "AirgapMeshPoints_mesh",
  67. "AirgapMeshPoints_layers",
  68. "MessageDisplayState",
  69. # --- Axial force calculation (optional) ---
  70. "ElectromagneticForcesCalc_Load",
  71. "ElectromagneticForcesCalc_OC",
  72. },
  73. # AFIR shares most SSSR variables; AFIR-specific ones added below.
  74. TOPOLOGY_AFIR: set(),
  75. # RFM (radial flux) variable names -- legacy, kept for alias resolution.
  76. TOPOLOGY_RFM: {
  77. "Stator_Lam_Outer_Dia",
  78. "Stator_Lam_Inner_Dia",
  79. "Stator_Yoke_Width",
  80. "Rotor_Lam_Outer_Dia",
  81. "Rotor_Lam_Inner_Dia",
  82. "Airgap",
  83. "Slot_Depth",
  84. "Slot_Width",
  85. "Tooth_Width",
  86. "Slot_Number",
  87. "Pole_Number",
  88. "Magnet_Thickness",
  89. "Magnet_Width",
  90. "DCBusVoltage",
  91. "RMSCurrent",
  92. "Shaft_Speed",
  93. "ConductorsPerSlot",
  94. "ParallelPaths",
  95. "Wire_Diameter",
  96. "WindingConnection",
  97. "Material_Magnet",
  98. "Material_Stator_Lam_Yoke",
  99. "InitialMagnetTemperature",
  100. "Ambient_Temperature",
  101. "Cooling_Type",
  102. "TorquePointsPerCycle",
  103. "MessageDisplayState",
  104. "CurrentDefinition",
  105. "Current_Advance_Angle",
  106. "Slot_Fill",
  107. "Magnet_Br_at_RefTemp",
  108. "WindageGraph_MaxSpeed",
  109. },
  110. }
  111. # AFIR inherits all SSSR known variables.
  112. _KNOWN_VARIABLES[TOPOLOGY_AFIR] = set(_KNOWN_VARIABLES[TOPOLOGY_SSSR])
  113. # ---------------------------------------------------------------------------
  114. # Cross-topology alias mapping.
  115. # Key: alias name (logical template name OR RFM variable name).
  116. # Value: dict of topology -> actual Motor-CAD variable name.
  117. #
  118. # This serves two purposes:
  119. # 1. Maps template logical names (e.g. "Number_of_Slots") to Motor-CAD
  120. # actual names (e.g. "Slot_Number").
  121. # 2. Maps RFM variable names (e.g. "Stator_Lam_Outer_Dia") to AFM names
  122. # (e.g. "Stator_Outer_Diameter") when used on an AFM topology.
  123. # ---------------------------------------------------------------------------
  124. _ALIAS_MAP: Dict[str, Dict[str, str]] = {
  125. # --- RFM -> AFM geometry aliases (the root cause of plan 23 failure) ---
  126. "Stator_Lam_Outer_Dia": {
  127. TOPOLOGY_SSSR: "Stator_Lam_Dia",
  128. TOPOLOGY_AFIR: "Stator_Lam_Dia",
  129. TOPOLOGY_RFM: "Stator_Lam_Outer_Dia",
  130. },
  131. "Stator_Lam_Inner_Dia": {
  132. TOPOLOGY_SSSR: "Stator_Bore",
  133. TOPOLOGY_AFIR: "Stator_Bore",
  134. TOPOLOGY_RFM: "Stator_Lam_Inner_Dia",
  135. },
  136. "Stator_Yoke_Width": {
  137. TOPOLOGY_SSSR: "Stator_Yoke_Thickness",
  138. TOPOLOGY_AFIR: "Stator_Yoke_Thickness",
  139. TOPOLOGY_RFM: "Stator_Yoke_Width",
  140. },
  141. "Rotor_Lam_Outer_Dia": {
  142. TOPOLOGY_SSSR: "RotorOuterDiameter",
  143. TOPOLOGY_AFIR: "RotorOuterDiameter",
  144. TOPOLOGY_RFM: "Rotor_Lam_Outer_Dia",
  145. },
  146. "Rotor_Lam_Inner_Dia": {
  147. TOPOLOGY_SSSR: "Inner_Rotor_Diameter",
  148. TOPOLOGY_AFIR: "Inner_Rotor_Diameter",
  149. TOPOLOGY_RFM: "Rotor_Lam_Inner_Dia",
  150. },
  151. # --- Template logical name -> Motor-CAD actual name ---
  152. "Number_of_Slots": {
  153. TOPOLOGY_SSSR: "Slot_Number",
  154. TOPOLOGY_AFIR: "Slot_Number",
  155. TOPOLOGY_RFM: "Slot_Number",
  156. },
  157. "Number_of_Poles": {
  158. TOPOLOGY_SSSR: "Pole_Number",
  159. TOPOLOGY_AFIR: "Pole_Number",
  160. TOPOLOGY_RFM: "Pole_Number",
  161. },
  162. "DC_Link_Voltage": {
  163. TOPOLOGY_SSSR: "DCBusVoltage",
  164. TOPOLOGY_AFIR: "DCBusVoltage",
  165. TOPOLOGY_RFM: "DCBusVoltage",
  166. },
  167. "Turns_per_Coil": {
  168. TOPOLOGY_SSSR: "ConductorsPerSlot",
  169. TOPOLOGY_AFIR: "ConductorsPerSlot",
  170. TOPOLOGY_RFM: "ConductorsPerSlot",
  171. },
  172. "Parallel_Paths": {
  173. TOPOLOGY_SSSR: "ParallelPaths",
  174. TOPOLOGY_AFIR: "ParallelPaths",
  175. TOPOLOGY_RFM: "ParallelPaths",
  176. },
  177. "Copper_Fill_Factor": {
  178. TOPOLOGY_SSSR: "Slot_Fill",
  179. TOPOLOGY_AFIR: "Slot_Fill",
  180. TOPOLOGY_RFM: "Slot_Fill",
  181. },
  182. "Magnet_Material": {
  183. TOPOLOGY_SSSR: "Material_Magnet",
  184. TOPOLOGY_AFIR: "Material_Magnet",
  185. TOPOLOGY_RFM: "Material_Magnet",
  186. },
  187. "Steel_Grade": {
  188. TOPOLOGY_SSSR: "Material_Stator_Lam_Yoke",
  189. TOPOLOGY_AFIR: "Material_Stator_Lam_Yoke",
  190. TOPOLOGY_RFM: "Material_Stator_Lam_Yoke",
  191. },
  192. "Magnet_Temperature": {
  193. TOPOLOGY_SSSR: "InitialMagnetTemperature",
  194. TOPOLOGY_AFIR: "InitialMagnetTemperature",
  195. TOPOLOGY_RFM: "InitialMagnetTemperature",
  196. },
  197. "Magnet_Remanence": {
  198. TOPOLOGY_SSSR: "Magnet_Br_at_RefTemp",
  199. TOPOLOGY_AFIR: "Magnet_Br_at_RefTemp",
  200. TOPOLOGY_RFM: "Magnet_Br_at_RefTemp",
  201. },
  202. "Cooling_Method": {
  203. TOPOLOGY_SSSR: "Cooling_Type",
  204. TOPOLOGY_AFIR: "Cooling_Type",
  205. TOPOLOGY_RFM: "Cooling_Type",
  206. },
  207. "Max_Speed": {
  208. TOPOLOGY_SSSR: "WindageGraph_MaxSpeed",
  209. TOPOLOGY_AFIR: "WindageGraph_MaxSpeed",
  210. TOPOLOGY_RFM: "WindageGraph_MaxSpeed",
  211. },
  212. "Winding_Connection": {
  213. TOPOLOGY_SSSR: "WindingConnection",
  214. TOPOLOGY_AFIR: "WindingConnection",
  215. TOPOLOGY_RFM: "WindingConnection",
  216. },
  217. }
  218. # ---------------------------------------------------------------------------
  219. # Public API
  220. # ---------------------------------------------------------------------------
  221. def normalize_topology(topology: Optional[str]) -> str:
  222. """Normalize topology string to canonical form.
  223. Defaults to SSSR for None/empty/unknown values.
  224. Args:
  225. topology: Raw topology string from plan/project.
  226. Returns:
  227. Canonical topology identifier (SSSR / AFIR / RFM).
  228. """
  229. if not topology:
  230. return TOPOLOGY_SSSR
  231. t = topology.strip().upper()
  232. if t in ALL_TOPOLOGIES:
  233. return t
  234. # Common informal aliases
  235. if t in ("AFM", "AXIAL", "AXIAL_FLUX", "AXIALFLUX"):
  236. return TOPOLOGY_SSSR
  237. if t in ("RADIAL", "RADIAL_FLUX", "RADIALFLUX"):
  238. return TOPOLOGY_RFM
  239. return TOPOLOGY_SSSR
  240. def resolve_variable(
  241. name: str, topology: Optional[str] = None
  242. ) -> Tuple[str, bool]:
  243. """Resolve a logical/alias variable name to the actual Motor-CAD name.
  244. Checks the alias map first (covers both template logical names and
  245. RFM-to-AFM remapping). If not found, returns the name stripped.
  246. Args:
  247. name: Variable name as provided (logical, alias, or direct).
  248. topology: Motor topology. Defaults to SSSR.
  249. Returns:
  250. Tuple of (resolved_name, was_alias).
  251. was_alias=True means the input was remapped to a topology-specific
  252. name. was_alias=False means the input was used as-is (caller should
  253. verify with is_known_variable()).
  254. """
  255. topo = normalize_topology(topology)
  256. name_stripped = name.strip()
  257. if name_stripped in _ALIAS_MAP:
  258. mapped = _ALIAS_MAP[name_stripped].get(topo, name_stripped)
  259. return (mapped, mapped != name_stripped)
  260. return (name_stripped, False)
  261. def is_known_variable(name: str, topology: Optional[str] = None) -> bool:
  262. """Check if a (resolved) variable name is known for the given topology.
  263. Args:
  264. name: Actual Motor-CAD variable name (after resolve_variable).
  265. topology: Motor topology.
  266. Returns:
  267. True if the variable is in the verified known-variable set.
  268. """
  269. topo = normalize_topology(topology)
  270. return name.strip() in _KNOWN_VARIABLES.get(topo, set())
  271. def validate_parameters(
  272. params: Dict[str, float], topology: Optional[str] = None
  273. ) -> Dict[str, List]:
  274. """Validate a parameter dict against the topology's known variables.
  275. Resolves aliases, then classifies each parameter as valid or unknown.
  276. Args:
  277. params: Dict of variable_name -> value (as produced by plan expansion).
  278. topology: Motor topology.
  279. Returns:
  280. Dict with keys:
  281. - 'valid': list of (resolved_name, value) known to exist
  282. - 'unknown': list of (resolved_name, value) NOT in known set
  283. - 'aliases_resolved': list of (original_name, resolved_name) remapped
  284. - 'resolved_params': dict of resolved_name -> value (all parameters)
  285. """
  286. topo = normalize_topology(topology)
  287. result: Dict[str, List] = {
  288. "valid": [],
  289. "unknown": [],
  290. "aliases_resolved": [],
  291. "resolved_params": {},
  292. }
  293. for name, value in params.items():
  294. resolved, was_alias = resolve_variable(name, topo)
  295. if was_alias:
  296. result["aliases_resolved"].append((name, resolved))
  297. result["resolved_params"][resolved] = value
  298. if is_known_variable(resolved, topo):
  299. result["valid"].append((resolved, value))
  300. else:
  301. result["unknown"].append((resolved, value))
  302. return result
  303. def get_known_variables(topology: Optional[str] = None) -> Set[str]:
  304. """Return the verified set of Motor-CAD variable names for a topology."""
  305. topo = normalize_topology(topology)
  306. return set(_KNOWN_VARIABLES.get(topo, set()))
  307. def suggest_alternative(
  308. name: str, topology: Optional[str] = None
  309. ) -> Optional[str]:
  310. """Suggest a known alternative variable name for an unknown one.
  311. Uses character-overlap scoring to find the closest known variable.
  312. Returns None if no sufficiently close match is found.
  313. Args:
  314. name: Unknown variable name.
  315. topology: Motor topology.
  316. Returns:
  317. Best matching known variable name, or None.
  318. """
  319. topo = normalize_topology(topology)
  320. known = _KNOWN_VARIABLES.get(topo, set())
  321. name_lower = name.lower().replace("_", "").replace("-", "")
  322. best = None
  323. best_score = 0.0
  324. for candidate in known:
  325. cand_lower = candidate.lower().replace("_", "").replace("-", "")
  326. common = sum(1 for c in name_lower if c in cand_lower)
  327. score = common / max(len(name_lower), len(cand_lower))
  328. if score > best_score and score > 0.5:
  329. best_score = score
  330. best = candidate
  331. return best