| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759 |
- """Metrics single source of truth + normalized result parser.
- This module is THE authoritative definition of simulation output metrics
- for the whole platform. Consumers MUST import from here instead of
- maintaining their own copies:
- - src/solver_core.py (legacy local solver)
- - scripts/robust_motorcad.py (robust solver / executor)
- - web/backend/app/metrics_constants.py (Web backend)
- Design:
- - METRIC_DEFINITIONS: complete list of {key, label, aliases, unit, direction, required}
- - Normalized matching: full-width -> half-width brackets, strip ALL whitespace
- (incl. full-width space U+3000), lowercase. This fixes the historical bug
- where "Average torque (virtual work)" / "\\u5e73\\u5747\\u8f6c\\u77e9 (virtual work)"
- could not be matched due to invisible full-width chars.
- - parse_export(): section-aware semicolon CSV parser with tolerant numeric scan.
- - pick_metric()/extract_all_metrics(): section-priority exact match ->
- all-section exact match -> prefix fuzzy match.
- All source is ASCII only.
- """
- from __future__ import annotations
- import os
- from pathlib import Path
- from typing import Dict, List, Optional, Tuple, Any
- # ---------------------------------------------------------------------------
- # Single source of truth: metric definitions
- # ---------------------------------------------------------------------------
- # unit: SI display unit. direction: higher/lower/neutral (for optimization).
- # required: True means a valid result MUST contain this metric.
- METRIC_DEFINITIONS: List[Dict[str, Any]] = [
- # --- Torque ---
- {
- "key": "tavg_nm",
- "label": "Average Torque [Nm]",
- "unit": "Nm",
- "direction": "higher",
- "required": True,
- "aliases": [
- "Average torque (virtual work)",
- "Average Torque (Virtual Work)",
- "Average torque (DQ)",
- "Average torque (loop torque)",
- "Torque (average)",
- "Average Torque",
- "\u5e73\u5747\u8f6c\u77e9 (virtual work)",
- "\u5e73\u5747\u8f6c\u77e9(virtual work)",
- "\u5e73\u5747\u8f6c\u77e9 (DQ)",
- "\u5e73\u5747\u8f6c\u77e9(DQ)",
- "\u5e73\u5747\u8f6c\u77e9 (loop torque)",
- "\u5e73\u5747\u8f6c\u77e9(loop torque)",
- "\u5e73\u5747\u8f6c\u77e9",
- ],
- },
- {
- "key": "tmax_nm",
- "label": "Max Torque [Nm]",
- "unit": "Nm",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Maximum torque (virtual work)",
- "Max Torque",
- "\u6700\u5927\u8f6c\u77e9 (virtual work)",
- "\u6700\u5927\u8f6c\u77e9(virtual work)",
- "\u6700\u5927\u8f6c\u77e9",
- ],
- },
- {
- "key": "tmin_nm",
- "label": "Min Torque [Nm]",
- "unit": "Nm",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Minimum torque (virtual work)",
- "Min Torque",
- "\u6700\u5c0f\u8f6c\u77e9 (virtual work)",
- "\u6700\u5c0f\u8f6c\u77e9(virtual work)",
- "\u6700\u5c0f\u8f6c\u77e9",
- ],
- },
- {
- "key": "ripple_pct",
- "label": "Torque Ripple [%]",
- "unit": "%",
- "direction": "lower",
- "required": True,
- "aliases": [
- "Torque Ripple (VW) [%]",
- "Torque Ripple (VW)[%]",
- "Torque Ripple (VW) %",
- "Torque Ripple (VW) (%)",
- "Torque Ripple [%]",
- "Torque Ripple (%)",
- "Torque Ripple %",
- "\u8f6c\u77e9\u8109\u52a8 (VW) [%]",
- "\u8f6c\u77e9\u8109\u52a8(VW)[%]",
- "\u8f6c\u77e9\u8109\u52a8 [%]",
- "\u8f6c\u77e9\u8109\u52a8[%]",
- "\u8f6c\u77e9\u8109\u52a8",
- ],
- },
- {
- "key": "ripple_nm",
- "label": "Torque Ripple [Nm]",
- "unit": "Nm",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Torque Ripple (VW)",
- "Torque Ripple",
- "\u8f6c\u77e9\u8109\u52a8 (VW)",
- "\u8f6c\u77e9\u8109\u52a8(VW)",
- ],
- },
- {
- "key": "ripple_abs_nm",
- "label": "Torque Ripple (abs) [Nm]",
- "unit": "Nm",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Torque Ripple (VW) (abs)",
- "\u8f6c\u77e9\u8109\u52a8\u7edd\u5bf9\u503c",
- ],
- },
- {
- "key": "shaft_torque_nm",
- "label": "Shaft Torque [Nm]",
- "unit": "Nm",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Shaft Torque",
- "Shaft Torque (Nm)",
- "\u8f74\u8f6c\u77e9",
- "\u8f74\u8f6c\u77e9(Nm)",
- ],
- },
- {
- "key": "stall_torque_nm",
- "label": "Stall Torque [Nm]",
- "unit": "Nm",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Stall Torque",
- "Stall Torque (Nm)",
- "\u5835\u8f6c\u8f6c\u77e9",
- "\u5835\u8f6c\u8f6c\u77e9(Nm)",
- ],
- },
- {
- "key": "torque_constant",
- "label": "Torque Constant [Nm/A]",
- "unit": "Nm/A",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Torque Constant (Kt)",
- "Torque Constant",
- "Torque Constant [Nm/A]",
- "\u8f6c\u77e9\u5e38\u6570(Kt)",
- "\u8f6c\u77e9\u5e38\u6570\uff08Kt\uff09",
- ],
- },
- # --- Efficiency / power ---
- {
- "key": "efficiency_pct",
- "label": "Efficiency [%]",
- "unit": "%",
- "direction": "higher",
- "required": True,
- "aliases": [
- "System Efficiency",
- "Efficiency",
- "\u7cfb\u7edf\u6548\u7387",
- "\u6548\u7387",
- ],
- },
- {
- "key": "input_power_w",
- "label": "Input Power [W]",
- "unit": "W",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "Input Power",
- "\u8f93\u5165\u529f\u7387",
- ],
- },
- {
- "key": "output_power_w",
- "label": "Output Power [W]",
- "unit": "W",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Output Power",
- "\u8f93\u51fa\u529f\u7387",
- "\u8f93\u51fa\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
- ],
- },
- {
- "key": "em_power_w",
- "label": "EM Power [W]",
- "unit": "W",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Electromagnetic Power",
- "EM Power",
- "\u7535\u78c1\u529f\u7387",
- "\u7535\u78c1\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
- ],
- },
- {
- "key": "power_factor",
- "label": "Power Factor",
- "unit": "",
- "direction": "higher",
- "required": False,
- "aliases": [
- "Power Factor",
- "\u529f\u7387\u56e0\u6570",
- ],
- },
- # --- Losses ---
- {
- "key": "total_losses_w",
- "label": "Total Losses [W]",
- "unit": "W",
- "direction": "lower",
- "required": True,
- "aliases": [
- "Total Losses (on load)",
- "Total Losses",
- "\u603b\u635f\u8017(\u989d\u5b9a)",
- "\u603b\u635f\u8017 (\u989d\u5b9a)",
- "\u603b\u635f\u8017(\u7a7a\u8f7d)",
- "\u603b\u635f\u8017",
- ],
- },
- {
- "key": "copper_loss_w",
- "label": "DC Copper Loss [W]",
- "unit": "W",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Armature DC Copper Loss (on load)",
- "Armature Copper Loss (on load)",
- "DC Copper Loss (on load)",
- "\u7535\u67a2\u76f4\u6d41\u94dc\u8017(\u5e26\u8f7d)",
- "\u7535\u67a2\u76f4\u6d41\u94dc\u8017 (\u5e26\u8f7d)",
- "\u7535\u67a2\u76f4\u6d41\u94dc\u8017(\u7a7a\u8f7d)",
- "\u7535\u67a2\u76f4\u6d41\u94dc\u8017",
- "\u94dc\u8017",
- ],
- },
- {
- "key": "iron_loss_w",
- "label": "Stator Iron Loss [W]",
- "unit": "W",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Stator iron Loss [total] (on load)",
- "Stator Iron Loss [total] (on load)",
- "Stator Iron Loss",
- "Iron Loss",
- "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017](\u989d\u5b9a)",
- "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017] (\u989d\u5b9a)",
- "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017](\u7a7a\u8f7d)",
- "\u5b9a\u5b50\u94c1\u8017",
- "\u94c1\u635f",
- "\u94c1\u8017",
- ],
- },
- {
- "key": "magnet_loss_w",
- "label": "Magnet Loss [W]",
- "unit": "W",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Magnet Loss (on load)",
- "Magnet Loss",
- "\u6c38\u78c1\u4f53\u635f\u8017(\u989d\u5b9a)",
- "\u6c38\u78c1\u4f53\u635f\u8017 (\u989d\u5b9a)",
- "\u6c38\u78c1\u4f53\u635f\u8017(\u7a7a\u8f7d)",
- "\u78c1\u94a2\u635f\u8017",
- ],
- },
- # --- Electrical ---
- {
- "key": "back_emf_v",
- "label": "Back EMF LL rms [V]",
- "unit": "V",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "Back EMF Line-Line Voltage (rms)",
- "Back EMF Line-Line Voltage",
- "Back EMF Line-Line Voltage (peak)",
- "Back EMF (rms)",
- "\u7ebf\u95f4\u53cd\u5411\u7535\u52a8\u52bf\u6709\u6548\u503c",
- "\u7ebf\u95f4\u53cd\u5411\u7535\u52a8\u52bf\u5e45\u503c",
- "\u53cd\u7535\u52a8\u52bf",
- ],
- },
- {
- "key": "back_emf_thd_pct",
- "label": "Back EMF THD [%]",
- "unit": "%",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Harmonic Distortion Back EMF Line-Line Voltage",
- "Back EMF THD",
- "\u7ebf\u53cd\u5411\u7535\u52a8\u52bf\u8c10\u6ce2",
- "\u7ebf\u7535\u538b\u8c10\u6ce2",
- ],
- },
- {
- "key": "phase_current_peak_a",
- "label": "Phase Current (peak) [A]",
- "unit": "A",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "Peak Phase Current",
- "Phase Current (peak)",
- "\u76f8\u7535\u6d41\u5cf0\u503c",
- ],
- },
- {
- "key": "line_current_rms_a",
- "label": "Line Current (rms) [A]",
- "unit": "A",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "Line Current (rms)",
- "Line Current",
- "\u7ebf\u7535\u6d41 (\u6709\u6548\u503c)",
- "\u7ebf\u7535\u6d41(\u6709\u6548\u503c)",
- "\u76f8\u7535\u6d41\u6709\u6548\u503c",
- ],
- },
- # --- Speed ---
- {
- "key": "shaft_speed_rpm",
- "label": "Shaft Speed [rpm]",
- "unit": "rpm",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "Shaft Speed",
- "Speed",
- "\u8f6c\u901f[RPM]",
- "\u8f6c\u901f [RPM]",
- "\u8f6c\u901f{RPM}",
- "\u8f6c\u901f",
- ],
- },
- {
- "key": "no_load_speed_rpm",
- "label": "No-load Speed [rpm]",
- "unit": "rpm",
- "direction": "neutral",
- "required": False,
- "aliases": [
- "No load speed",
- "No-Load Speed",
- "\u7a7a\u8f7d\u8f6c\u901f",
- ],
- },
- # --- Thermal ---
- {
- "key": "winding_temp_c",
- "label": "Winding Temp [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "aliases": [
- "Winding Temperature",
- "\u7ed5\u7ec4\u6e29\u5ea6",
- "T[\u7ed5\u7ec4\u5e73\u5747]",
- "T [Winding (A) Average]",
- ],
- },
- {
- "key": "winding_hotspot_temp_c",
- "label": "Winding Hotspot Temp [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Winding Hotspot Temperature",
- "Winding Hotspot Temp",
- "Hotspot Temperature",
- "\u7ed5\u7ec4\u70ed\u70b9\u6e29\u5ea6",
- "\u70ed\u70b9\u6e29\u5ea6",
- "T[\u7ed5\u7ec4\u6700\u9ad8]",
- "T [Winding (A) Maximum]",
- "T [EWdg (Outer) Maximum]",
- ],
- },
- {
- "key": "magnet_temp_c",
- "label": "Magnet Temp [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Magnet Temperature",
- "Magnet Temp",
- "Permanent Magnet Temperature",
- "\u6c38\u78c1\u4f53\u6e29\u5ea6",
- "\u78c1\u94a2\u6e29\u5ea6",
- "T [Magnet Average]",
- "T [Magnet Active]",
- "T [Magnet Maximum]",
- ],
- },
- {
- "key": "stator_temp_c",
- "label": "Stator Temp [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Stator Temperature",
- "Stator Temp",
- "Stator Winding Temperature",
- "\u5b9a\u5b50\u6e29\u5ea6",
- "T[\u5b9a\u5b50\u5916\u8868\u9762]",
- "T[\u5b9a\u5b50\u8f6d]",
- ],
- },
- {
- "key": "bearing_temp_c",
- "label": "Bearing Temp [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Bearing Temperature",
- "Bearing Temp",
- "\u8f74\u627f\u6e29\u5ea6",
- "T[\u540e\u8f74\u627f]",
- ],
- },
- {
- "key": "temp_rise_c",
- "label": "Temperature Rise [C]",
- "unit": "C",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Temperature Rise",
- "Temp Rise",
- "Winding Temperature Rise",
- "\u6e29\u5347",
- "\u7ed5\u7ec4\u6e29\u5347",
- "dT [Winding (Maximum) - Ambient]",
- "dT [Winding (Average) - Ambient]",
- ],
- },
- {
- "key": "thermal_resistance_k_w",
- "label": "Thermal Resistance [K/W]",
- "unit": "K/W",
- "direction": "lower",
- "required": False,
- "domain": "thermal",
- "aliases": [
- "Thermal Resistance",
- "Thermal Resistance (total)",
- "\u70ed\u963b",
- "\u603b\u70ed\u963b",
- "Rt [Winding (Maximum) - Ambient]",
- "Rt [Winding (Average) - Ambient]",
- ],
- },
- # --- Structural / mechanical ---
- {
- "key": "axial_force_n",
- "label": "Axial Force [N]",
- "unit": "N",
- "direction": "lower",
- "required": False,
- "domain": "structural",
- "aliases": [
- "Axial Force",
- "Axial Magnetic Force",
- "Axial Pull Force",
- "\u8f74\u5411\u529b",
- "\u8f74\u5411\u78c1\u5438\u529b",
- ],
- },
- {
- "key": "radial_force_n",
- "label": "Radial Force [N]",
- "unit": "N",
- "direction": "lower",
- "required": False,
- "domain": "structural",
- "aliases": [
- "Radial Force",
- "Radial Magnetic Force",
- "\u5f84\u5411\u529b",
- "\u5f84\u5411\u78c1\u5438\u529b",
- ],
- },
- {
- "key": "max_stress_mpa",
- "label": "Max Stress [MPa]",
- "unit": "MPa",
- "direction": "lower",
- "required": False,
- "domain": "structural",
- "aliases": [
- "Maximum Stress",
- "Max Stress",
- "Von Mises Stress (max)",
- "\u6700\u5927\u5e94\u529b",
- "\u5bc6\u5e94\u529b\u6700\u5927\u503c",
- ],
- },
- {
- "key": "deformation_mm",
- "label": "Max Deformation [mm]",
- "unit": "mm",
- "direction": "lower",
- "required": False,
- "domain": "structural",
- "aliases": [
- "Maximum Deformation",
- "Max Deformation",
- "Total Deformation (max)",
- "\u6700\u5927\u53d8\u5f62",
- "\u603b\u53d8\u5f62\u6700\u5927\u503c",
- ],
- },
- ]
- # ---------------------------------------------------------------------------
- # Derived lookup structures
- # ---------------------------------------------------------------------------
- METRIC_KEYS: frozenset = frozenset(m["key"] for m in METRIC_DEFINITIONS)
- METRIC_LABELS: Dict[str, str] = {m["key"]: m["label"] for m in METRIC_DEFINITIONS}
- METRIC_UNITS: Dict[str, str] = {m["key"]: m["unit"] for m in METRIC_DEFINITIONS}
- METRIC_DIRECTIONS: Dict[str, str] = {m["key"]: m["direction"] for m in METRIC_DEFINITIONS}
- REQUIRED_METRICS: frozenset = frozenset(m["key"] for m in METRIC_DEFINITIONS if m["required"])
- # ---------------------------------------------------------------------------
- # Name normalization (the historical tavg/ripple parsing fix)
- # ---------------------------------------------------------------------------
- # Full-width -> half-width char map
- _FULL_TO_HALF = {
- "\uff08": "(", # full-width (
- "\uff09": ")", # full-width )
- "\uff0c": ",", # full-width ,
- "\uff1b": ";", # full-width ;
- "\uff1a": ":", # full-width :
- "\uff3b": "[", # full-width [
- "\uff3d": "]", # full-width ]
- "\u3000": " ", # full-width space (ideographic space)
- }
- def normalize_name(name: str) -> str:
- """Normalize a Motor-CAD field name for matching.
- - Full-width brackets/punct -> half-width
- - Strip ALL whitespace (incl. full-width space U+3000)
- - Lowercase
- Example: "\\u5e73\\u5747\\u8f6c\\u77e9 (virtual work)" and
- "Average torque (virtual work)" both normalize to the same token space.
- """
- s = name or ""
- for full, half in _FULL_TO_HALF.items():
- s = s.replace(full, half)
- s = "".join(s.split())
- return s.lower()
- # Pre-normalized alias map: normalized_alias -> metric_key
- _METRIC_ALIAS_MAP: Dict[str, str] = {}
- for _m in METRIC_DEFINITIONS:
- for _alias in _m["aliases"]:
- _METRIC_ALIAS_MAP[normalize_name(_alias)] = _m["key"]
- # Section name priority for exact matching (English + Chinese).
- SECTION_PRIORITY = [
- "E-Magnetics", "\u7535\u78c1",
- "Drive", "\u9a71\u52a8",
- "Losses", "\u635f\u8017",
- "Materials", "\u6750\u6599",
- "Miscellaneous", "\u6742\u9879",
- ]
- # ---------------------------------------------------------------------------
- # Parsing
- # ---------------------------------------------------------------------------
- def parse_export(path: str | Path) -> Dict[str, Dict[str, float]]:
- """Parse a Motor-CAD semicolon-delimited export file into
- {section_name: {field_name: value}}.
- - Section header = a line WITHOUT ';'.
- - Data line = "field;value[;...]" -> field is parts[0], value is the
- first numeric token in parts[1:] (tolerates thousands separators).
- - Multi-encoding fallback: utf-8-sig, utf-8, gbk, cp1252, latin-1.
- """
- text: Optional[str] = None
- for encoding in ("utf-8-sig", "utf-8", "gbk", "cp1252", "latin-1"):
- try:
- text = Path(path).read_text(encoding=encoding)
- break
- except (UnicodeDecodeError, OSError):
- continue
- if text is None:
- return {}
- result: Dict[str, Dict[str, float]] = {}
- section = "(root)"
- result.setdefault(section, {})
- for raw in text.splitlines():
- line = raw.strip()
- if not line:
- continue
- if ";" not in line:
- section = line.strip()
- result.setdefault(section, {})
- continue
- parts = line.split(";")
- field = parts[0].strip().strip('"')
- if not field:
- continue
- value: Optional[float] = None
- for part in parts[1:]:
- part = part.strip()
- if not part:
- continue
- try:
- value = float(part.replace(",", "."))
- break
- except ValueError:
- continue
- if value is None:
- continue
- result.setdefault(section, {})[field] = value
- return result
- def _exact_match_in_sections(
- parsed: Dict[str, Dict[str, float]],
- wanted: set,
- section_order: Optional[List[str]] = None,
- ) -> Optional[float]:
- """Exact normalized match. If section_order given, scan sections in that
- order first; then fall back to all sections."""
- if section_order:
- for section_name in section_order:
- section = parsed.get(section_name)
- if not section:
- continue
- for field, value in section.items():
- if normalize_name(field) in wanted:
- return value
- for section in parsed.values():
- for field, value in section.items():
- if normalize_name(field) in wanted:
- return value
- return None
- def pick_metric(parsed: Dict[str, Dict[str, float]], metric_key: str) -> Optional[float]:
- """Extract a single metric from parsed export results.
- Strategy (reference: KNOWLEDGE_BASE section 4.3):
- 1. Exact normalized match, E-Magnetics section priority.
- 2. Exact normalized match across all sections.
- 3. Prefix fuzzy match across all sections (len > 3 to avoid noise).
- """
- wanted = {
- norm for alias, key in _METRIC_ALIAS_MAP.items()
- if key == metric_key for norm in [alias]
- }
- if not wanted:
- return None
- value = _exact_match_in_sections(parsed, wanted, SECTION_PRIORITY)
- if value is not None:
- return value
- # Phase 3: prefix fuzzy match - ONLY when the exported field name is
- # LONGER than / starts with a known alias. The reverse direction
- # (alias longer than field) is intentionally dropped because it caused
- # false positives, e.g. alias "Torque Ripple (VW) (abs)" matched the
- # plain "Torque Ripple (VW)" field and polluted ripple_abs_nm.
- for alias_norm in wanted:
- if len(alias_norm) <= 3:
- continue
- for section in parsed.values():
- for field, value in section.items():
- field_norm = normalize_name(field)
- if len(field_norm) <= 3:
- continue
- if not field_norm.startswith(alias_norm):
- continue
- # Guard: a "%" field (e.g. "Torque Ripple (VW) [%]") must
- # never satisfy a non-percent alias (e.g. "Torque Ripple (VW)")
- # or it would pollute the Nm-valued metric with a % value.
- if "%" in field_norm and "%" not in alias_norm:
- continue
- return value
- return None
- def extract_all_metrics(parsed: Dict[str, Dict[str, float]]) -> Dict[str, float]:
- """Extract every defined metric from parsed results."""
- out: Dict[str, float] = {}
- for m in METRIC_DEFINITIONS:
- val = pick_metric(parsed, m["key"])
- if val is not None:
- out[m["key"]] = val
- return out
- def check_required_metrics(metrics: Dict[str, float]) -> Tuple[bool, List[str]]:
- """Check that all required metrics are present."""
- missing = [k for k in REQUIRED_METRICS if k not in metrics]
- return (len(missing) == 0, missing)
- def extract_metrics_from_file(path: str | Path) -> Dict[str, float]:
- """One-shot: parse an export file and extract all metrics."""
- return extract_all_metrics(parse_export(path))
|