"""Boundary-condition (BC) field catalog - single source of truth. The BC key naming had drifted across three places: 1. the project boundary form / rule_engine -> current_a, speed_rpm, slots, ... 2. the fixed-param template (build_default_fixed_params) -> rated_current_a, rated_speed_rpm, ... 3. the plan-detail display template -> rated_* (a third set) This module is the single source for the canonical BC field keys, their Chinese labels, units, grouping, and the legacy/variant alias keys accepted on read. Canonical keys match the project boundary form and rule_engine (the data-entry and primary consumers). `aliases` keeps backward compatibility with the template-era rated_* names and other historical variants. All source is ASCII; Chinese labels are written as unicode escapes (project hard rule). """ from __future__ import annotations from typing import Any, Dict, List # Category labels (unicode-escaped). _CAT_ELEC = "\u7535\u6c14" _CAT_GEOM = "\u51e0\u4f55\u5c3a\u5bf8" _CAT_SLOT = "\u69fd\u6781" _CAT_MAT = "\u6750\u6599\u5c5e\u6027" _CAT_GOAL = "\u6027\u80fd\u76ee\u6807" _CAT_THERM = "\u70ed\u4e0e\u73af\u5883" # Canonical BC field catalog. Order defines display order in the UI. BC_FIELD_CATALOG: List[Dict[str, Any]] = [ # --- Electrical --- {"key": "power_w", "label": "\u989d\u5b9a\u529f\u7387", "unit": "W", "category": _CAT_ELEC, "aliases": ["rated_power_w"]}, {"key": "voltage_v", "label": "\u989d\u5b9a\u7535\u538b", "unit": "V", "category": _CAT_ELEC, "aliases": ["dc_link_voltage_v"]}, {"key": "phase_count", "label": "\u76f8\u6570", "unit": "", "category": _CAT_ELEC, "aliases": []}, {"key": "speed_rpm", "label": "\u989d\u5b9a\u8f6c\u901f", "unit": "rpm", "category": _CAT_ELEC, "aliases": ["rated_speed_rpm"]}, {"key": "max_speed_rpm", "label": "\u6700\u9ad8\u8f6c\u901f", "unit": "rpm", "category": _CAT_ELEC, "aliases": []}, {"key": "current_a", "label": "\u989d\u5b9a\u7535\u6d41", "unit": "A", "category": _CAT_ELEC, "aliases": ["rated_current_a"]}, # --- Geometry --- {"key": "outer_diameter_mm", "label": "\u5916\u5f84", "unit": "mm", "category": _CAT_GEOM, "aliases": []}, {"key": "max_outer_diameter_mm", "label": "\u6700\u5927\u5916\u5f84", "unit": "mm", "category": _CAT_GEOM, "aliases": []}, {"key": "inner_diameter_mm", "label": "\u5185\u5f84", "unit": "mm", "category": _CAT_GEOM, "aliases": []}, {"key": "axial_length_mm", "label": "\u8f74\u5411\u957f\u5ea6", "unit": "mm", "category": _CAT_GEOM, "aliases": []}, # --- Slots & poles --- {"key": "slots", "label": "\u69fd\u6570", "unit": "", "category": _CAT_SLOT, "aliases": ["slot_count"]}, {"key": "poles", "label": "\u6781\u6570", "unit": "", "category": _CAT_SLOT, "aliases": []}, {"key": "pole_pairs", "label": "\u6781\u5bf9\u6570", "unit": "", "category": _CAT_SLOT, "aliases": []}, # --- Material --- {"key": "magnet_grade", "label": "\u78c1\u94a2\u724c\u53f7", "unit": "", "category": _CAT_MAT, "aliases": []}, {"key": "magnet_temp_c", "label": "\u78c1\u94a2\u6e29\u5ea6", "unit": "\xb0C", "category": _CAT_MAT, "aliases": []}, # --- Performance goals --- {"key": "target_torque_nm", "label": "\u76ee\u6807\u8f6c\u77e9", "unit": "Nm", "category": _CAT_GOAL, "aliases": ["rated_torque_nm"]}, {"key": "target_efficiency_pct", "label": "\u76ee\u6807\u6548\u7387", "unit": "%", "category": _CAT_GOAL, "aliases": ["efficiency_min_pct"]}, {"key": "max_losses_w", "label": "\u6700\u5927\u635f\u8017", "unit": "W", "category": _CAT_GOAL, "aliases": []}, {"key": "target_ripple_pct", "label": "\u76ee\u6807\u8109\u52a8", "unit": "%", "category": _CAT_GOAL, "aliases": ["max_torque_ripple_pct"]}, {"key": "max_axial_force_n", "label": "\u6700\u5927\u8f74\u5411\u529b", "unit": "N", "category": _CAT_GOAL, "aliases": []}, {"key": "weight_kg", "label": "\u76ee\u6807\u91cd\u91cf", "unit": "kg", "category": _CAT_GOAL, "aliases": []}, # --- Thermal & environment --- {"key": "cooling_type", "label": "\u51b7\u5374\u65b9\u5f0f", "unit": "", "category": _CAT_THERM, "aliases": ["cooling_method"]}, {"key": "insulation_class", "label": "\u7edd\u7f18\u7b49\u7ea7", "unit": "", "category": _CAT_THERM, "aliases": []}, {"key": "duty_cycle", "label": "\u5de5\u4f5c\u5236", "unit": "", "category": _CAT_THERM, "aliases": []}, {"key": "ambient_temp_c", "label": "\u73af\u5883\u6e29\u5ea6", "unit": "\xb0C", "category": _CAT_THERM, "aliases": []}, ] def get_bc_field_catalog() -> List[Dict[str, Any]]: """Return the BC field catalog as a list of dicts (for API / UI rendering). Each field is enriched with form-rendering metadata: `type` (number / int / enum) and `options` for enum fields, so the frontend can render the full catalog as a form without hard-coding any field list. """ out = [dict(f) for f in BC_FIELD_CATALOG] for f in out: meta = _FIELD_FORM_META.get(f["key"], {"type": "number"}) f["type"] = meta["type"] if meta.get("options"): f["options"] = list(meta["options"]) return out # Form-rendering metadata keyed by canonical BC key. Enum options are # engineering-standard choices (IEC insulation classes / duty cycles, common # NdFeB grades; Cooling_Method verified against Motor-CAD via the MARS run). _FIELD_FORM_META: Dict[str, Dict[str, Any]] = { "cooling_type": {"type": "enum", "options": ["Natural", "Forced Air", "Water", "Oil"]}, "insulation_class": {"type": "enum", "options": ["A", "E", "B", "F", "H"]}, "duty_cycle": {"type": "enum", "options": ["S1", "S2", "S3", "S6"]}, "magnet_grade": {"type": "enum", "options": ["N35", "N38", "N42", "N42SH", "N42UH", "N48", "N52"]}, "phase_count": {"type": "int"}, "slots": {"type": "int"}, "poles": {"type": "int"}, "pole_pairs": {"type": "int"}, } def _alias_to_canonical() -> Dict[str, str]: """Build alias -> canonical key lookup from the catalog.""" m: Dict[str, str] = {} for f in BC_FIELD_CATALOG: for a in f.get("aliases", []): m[a] = f["key"] return m def normalize_bc(data: Dict[str, Any]) -> Dict[str, Any]: """Normalize a boundary-conditions dict to canonical keys. Alias keys are mapped to their canonical key; a canonical key already present always wins over its aliases. Unknown keys are passed through unchanged so forward-compatible extras are preserved. Returns a new dict (the input is not mutated). """ if not data: return {} amap = _alias_to_canonical() out: Dict[str, Any] = {} for k, v in data.items(): canon = amap.get(k, k) # Canonical already set (explicitly or by an earlier alias): keep the # first non-empty value, preferring an explicit canonical entry. if canon in out and out[canon] not in (None, ""): if k == canon and v not in (None, ""): out[canon] = v # explicit canonical overrides alias-filled continue out[canon] = v return out