"""Topology-aware Motor-CAD variable name mapping. Single source of truth for mapping logical parameter names to actual Motor-CAD variable names, per motor topology (AFM-SSSR, AFM-AFIR, RFM). Also provides cross-topology alias mapping: when a plan uses RFM-style variable names (e.g. Stator_Lam_Outer_Dia) on an AFM topology, this module maps them to the correct AFM variable name (Stator_Outer_Diameter). All variable names here are verified against the MARS-12S10P_SSSR .mot model and by pymotorcad get/set_variable probes (2026-09-03, TEST-037). """ from typing import Dict, Optional, Set, Tuple, List # --------------------------------------------------------------------------- # Topology identifiers # --------------------------------------------------------------------------- TOPOLOGY_SSSR = "SSSR" # Single Stator Single Rotor axial flux TOPOLOGY_AFIR = "AFIR" # Axial Flux Integrated Rotor TOPOLOGY_RFM = "RFM" # Radial Flux Motor (legacy) ALL_TOPOLOGIES = (TOPOLOGY_SSSR, TOPOLOGY_AFIR, TOPOLOGY_RFM) # --------------------------------------------------------------------------- # Known Motor-CAD variable names per topology. # Verified against MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot and the # successful plan 21 run (2026-08-28). # --------------------------------------------------------------------------- _KNOWN_VARIABLES: Dict[str, Set[str]] = { TOPOLOGY_SSSR: { # --- Geometry --- "RotorOuterDiameter", "Stator_Lam_Dia", "Stator_Bore", "Back_Iron_Thickness", "Airgap", # --- Stator slots --- "Slot_Depth", "Slot_Width", "Tooth_Width", "Slot_Number", "Slot_Fill", "Slot_Type", # --- Rotor / magnets --- "Pole_Number", "Magnet_Length", "Magnet_Thickness", "Magnet_Arc_[ED]", "Material_Magnet", "Magnet_Br_at_RefTemp", "Magnet_Br_at_20", "InitialMagnetTemperature", # --- Materials --- # --- Electrical --- "DCBusVoltage", "RMSCurrent", "Shaft_Speed", "WindageGraph_MaxSpeed", "CurrentDefinition", "PhaseAdvance", # --- Winding --- "ConductorsPerSlot", "ParallelPaths", "Wire_Diameter", "WindingConnection", "Copper_Diameter", # --- Thermal --- "Ambient_Temperature", "Cooling_Type", # --- Simulation control --- "TorquePointsPerCycle", "AirgapMeshPoints_mesh", "AirgapMeshPoints_layers", "MessageDisplayState", # --- Axial force calculation (optional) --- "ElectromagneticForcesCalc_Load", "ElectromagneticForcesCalc_OC", }, # AFIR shares most SSSR variables; AFIR-specific ones added below. TOPOLOGY_AFIR: set(), # RFM (radial flux) variable names -- legacy, kept for alias resolution. TOPOLOGY_RFM: { "Stator_Lam_Outer_Dia", "Stator_Lam_Inner_Dia", "Stator_Yoke_Width", "Rotor_Lam_Outer_Dia", "Rotor_Lam_Inner_Dia", "Airgap", "Slot_Depth", "Slot_Width", "Tooth_Width", "Slot_Number", "Pole_Number", "Magnet_Thickness", "Magnet_Width", "DCBusVoltage", "RMSCurrent", "Shaft_Speed", "ConductorsPerSlot", "ParallelPaths", "Wire_Diameter", "WindingConnection", "Material_Magnet", "Material_Stator_Lam_Yoke", "InitialMagnetTemperature", "Ambient_Temperature", "Cooling_Type", "TorquePointsPerCycle", "MessageDisplayState", "CurrentDefinition", "Current_Advance_Angle", "Slot_Fill", "Magnet_Br_at_RefTemp", "WindageGraph_MaxSpeed", }, } # AFIR inherits all SSSR known variables. _KNOWN_VARIABLES[TOPOLOGY_AFIR] = set(_KNOWN_VARIABLES[TOPOLOGY_SSSR]) # --------------------------------------------------------------------------- # Cross-topology alias mapping. # Key: alias name (logical template name OR RFM variable name). # Value: dict of topology -> actual Motor-CAD variable name. # # This serves two purposes: # 1. Maps template logical names (e.g. "Number_of_Slots") to Motor-CAD # actual names (e.g. "Slot_Number"). # 2. Maps RFM variable names (e.g. "Stator_Lam_Outer_Dia") to AFM names # (e.g. "Stator_Outer_Diameter") when used on an AFM topology. # --------------------------------------------------------------------------- _ALIAS_MAP: Dict[str, Dict[str, str]] = { # --- RFM -> AFM geometry aliases (the root cause of plan 23 failure) --- "Stator_Lam_Outer_Dia": { TOPOLOGY_SSSR: "Stator_Lam_Dia", TOPOLOGY_AFIR: "Stator_Lam_Dia", TOPOLOGY_RFM: "Stator_Lam_Outer_Dia", }, "Stator_Lam_Inner_Dia": { TOPOLOGY_SSSR: "Stator_Bore", TOPOLOGY_AFIR: "Stator_Bore", TOPOLOGY_RFM: "Stator_Lam_Inner_Dia", }, "Stator_Yoke_Width": { TOPOLOGY_SSSR: "Stator_Yoke_Thickness", TOPOLOGY_AFIR: "Stator_Yoke_Thickness", TOPOLOGY_RFM: "Stator_Yoke_Width", }, "Rotor_Lam_Outer_Dia": { TOPOLOGY_SSSR: "RotorOuterDiameter", TOPOLOGY_AFIR: "RotorOuterDiameter", TOPOLOGY_RFM: "Rotor_Lam_Outer_Dia", }, "Rotor_Lam_Inner_Dia": { TOPOLOGY_SSSR: "Inner_Rotor_Diameter", TOPOLOGY_AFIR: "Inner_Rotor_Diameter", TOPOLOGY_RFM: "Rotor_Lam_Inner_Dia", }, # --- Template logical name -> Motor-CAD actual name --- "Number_of_Slots": { TOPOLOGY_SSSR: "Slot_Number", TOPOLOGY_AFIR: "Slot_Number", TOPOLOGY_RFM: "Slot_Number", }, "Number_of_Poles": { TOPOLOGY_SSSR: "Pole_Number", TOPOLOGY_AFIR: "Pole_Number", TOPOLOGY_RFM: "Pole_Number", }, "DC_Link_Voltage": { TOPOLOGY_SSSR: "DCBusVoltage", TOPOLOGY_AFIR: "DCBusVoltage", TOPOLOGY_RFM: "DCBusVoltage", }, "Turns_per_Coil": { TOPOLOGY_SSSR: "ConductorsPerSlot", TOPOLOGY_AFIR: "ConductorsPerSlot", TOPOLOGY_RFM: "ConductorsPerSlot", }, "Parallel_Paths": { TOPOLOGY_SSSR: "ParallelPaths", TOPOLOGY_AFIR: "ParallelPaths", TOPOLOGY_RFM: "ParallelPaths", }, "Copper_Fill_Factor": { TOPOLOGY_SSSR: "Slot_Fill", TOPOLOGY_AFIR: "Slot_Fill", TOPOLOGY_RFM: "Slot_Fill", }, "Magnet_Material": { TOPOLOGY_SSSR: "Material_Magnet", TOPOLOGY_AFIR: "Material_Magnet", TOPOLOGY_RFM: "Material_Magnet", }, "Steel_Grade": { TOPOLOGY_SSSR: "Material_Stator_Lam_Yoke", TOPOLOGY_AFIR: "Material_Stator_Lam_Yoke", TOPOLOGY_RFM: "Material_Stator_Lam_Yoke", }, "Magnet_Temperature": { TOPOLOGY_SSSR: "InitialMagnetTemperature", TOPOLOGY_AFIR: "InitialMagnetTemperature", TOPOLOGY_RFM: "InitialMagnetTemperature", }, "Magnet_Remanence": { TOPOLOGY_SSSR: "Magnet_Br_at_RefTemp", TOPOLOGY_AFIR: "Magnet_Br_at_RefTemp", TOPOLOGY_RFM: "Magnet_Br_at_RefTemp", }, "Cooling_Method": { TOPOLOGY_SSSR: "Cooling_Type", TOPOLOGY_AFIR: "Cooling_Type", TOPOLOGY_RFM: "Cooling_Type", }, "Max_Speed": { TOPOLOGY_SSSR: "WindageGraph_MaxSpeed", TOPOLOGY_AFIR: "WindageGraph_MaxSpeed", TOPOLOGY_RFM: "WindageGraph_MaxSpeed", }, "Winding_Connection": { TOPOLOGY_SSSR: "WindingConnection", TOPOLOGY_AFIR: "WindingConnection", TOPOLOGY_RFM: "WindingConnection", }, } # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def normalize_topology(topology: Optional[str]) -> str: """Normalize topology string to canonical form. Defaults to SSSR for None/empty/unknown values. Args: topology: Raw topology string from plan/project. Returns: Canonical topology identifier (SSSR / AFIR / RFM). """ if not topology: return TOPOLOGY_SSSR t = topology.strip().upper() if t in ALL_TOPOLOGIES: return t # Common informal aliases if t in ("AFM", "AXIAL", "AXIAL_FLUX", "AXIALFLUX"): return TOPOLOGY_SSSR if t in ("RADIAL", "RADIAL_FLUX", "RADIALFLUX"): return TOPOLOGY_RFM return TOPOLOGY_SSSR def resolve_variable( name: str, topology: Optional[str] = None ) -> Tuple[str, bool]: """Resolve a logical/alias variable name to the actual Motor-CAD name. Checks the alias map first (covers both template logical names and RFM-to-AFM remapping). If not found, returns the name stripped. Args: name: Variable name as provided (logical, alias, or direct). topology: Motor topology. Defaults to SSSR. Returns: Tuple of (resolved_name, was_alias). was_alias=True means the input was remapped to a topology-specific name. was_alias=False means the input was used as-is (caller should verify with is_known_variable()). """ topo = normalize_topology(topology) name_stripped = name.strip() if name_stripped in _ALIAS_MAP: mapped = _ALIAS_MAP[name_stripped].get(topo, name_stripped) return (mapped, mapped != name_stripped) return (name_stripped, False) def is_known_variable(name: str, topology: Optional[str] = None) -> bool: """Check if a (resolved) variable name is known for the given topology. Args: name: Actual Motor-CAD variable name (after resolve_variable). topology: Motor topology. Returns: True if the variable is in the verified known-variable set. """ topo = normalize_topology(topology) return name.strip() in _KNOWN_VARIABLES.get(topo, set()) def validate_parameters( params: Dict[str, float], topology: Optional[str] = None ) -> Dict[str, List]: """Validate a parameter dict against the topology's known variables. Resolves aliases, then classifies each parameter as valid or unknown. Args: params: Dict of variable_name -> value (as produced by plan expansion). topology: Motor topology. Returns: Dict with keys: - 'valid': list of (resolved_name, value) known to exist - 'unknown': list of (resolved_name, value) NOT in known set - 'aliases_resolved': list of (original_name, resolved_name) remapped - 'resolved_params': dict of resolved_name -> value (all parameters) """ topo = normalize_topology(topology) result: Dict[str, List] = { "valid": [], "unknown": [], "aliases_resolved": [], "resolved_params": {}, } for name, value in params.items(): resolved, was_alias = resolve_variable(name, topo) if was_alias: result["aliases_resolved"].append((name, resolved)) result["resolved_params"][resolved] = value if is_known_variable(resolved, topo): result["valid"].append((resolved, value)) else: result["unknown"].append((resolved, value)) return result def get_known_variables(topology: Optional[str] = None) -> Set[str]: """Return the verified set of Motor-CAD variable names for a topology.""" topo = normalize_topology(topology) return set(_KNOWN_VARIABLES.get(topo, set())) def suggest_alternative( name: str, topology: Optional[str] = None ) -> Optional[str]: """Suggest a known alternative variable name for an unknown one. Uses character-overlap scoring to find the closest known variable. Returns None if no sufficiently close match is found. Args: name: Unknown variable name. topology: Motor topology. Returns: Best matching known variable name, or None. """ topo = normalize_topology(topology) known = _KNOWN_VARIABLES.get(topo, set()) name_lower = name.lower().replace("_", "").replace("-", "") best = None best_score = 0.0 for candidate in known: cand_lower = candidate.lower().replace("_", "").replace("-", "") common = sum(1 for c in name_lower if c in cand_lower) score = common / max(len(name_lower), len(cand_lower)) if score > best_score and score > 0.5: best_score = score best = candidate return best