This commit is contained in:
David Rice
2026-04-13 14:12:21 +01:00
parent 2da2d2e10a
commit 95b700d6eb
2 changed files with 45 additions and 24 deletions

View File

@@ -252,9 +252,40 @@ def normalise(s: str) -> str:
return re.sub(r"[^a-z0-9]", "", s.lower())
def _prefix_match(a: str, b: str) -> bool:
"""True if normalised strings match or one is a prefix of the other."""
na, nb = normalise(a), normalise(b)
_MFR_ALIASES = {
"ti": "texasinstruments",
"texas": "texasinstruments",
"texasinstruments": "texasinstruments",
"onsemi": "onsemi",
"onsemiconductor": "onsemi",
"onsemin": "onsemi",
"nxp": "nxp",
"nxpsemiconductors": "nxp",
"st": "stmicro",
"stmicro": "stmicro",
"stmicroelectronics": "stmicro",
"abracon": "abracon",
"abraconcorporation": "abracon",
"ck": "ck",
"ckcomponents": "ck",
"murata": "murata",
"muratamfg": "murata",
"muratamanufacturing": "murata",
"kingbright": "kingbright",
"kingbight": "kingbright", # known Nexar typo
}
def _norm_mfr(s: str) -> str:
"""Normalise a manufacturer name, resolving known abbreviations and aliases."""
n = normalise(s)
return _MFR_ALIASES.get(n, n)
def _mfr_match(a: str, b: str) -> bool:
"""True if two manufacturer names refer to the same company (alias-aware, prefix-tolerant)."""
na, nb = _norm_mfr(a), _norm_mfr(b)
return bool(na and nb and (na == nb or na.startswith(nb) or nb.startswith(na)))
@@ -290,6 +321,8 @@ def _elec_match(a: str, b: str) -> bool:
return na == nb or nb.startswith(na) or na.startswith(nb)
def load_bom_data(bom_path: str) -> list | None:
"""Load BOM xlsx and return a list of entry dicts.
Returns None if no 'bom' tab is found."""
@@ -350,27 +383,29 @@ def _find_bom_match(bom_data: list, mfr_name: str, mpn: str) -> dict | None:
"""Return the highest unit cost BOM entry that prefix-matches both manufacturer and MPN."""
candidates = [
e for e in bom_data
if _prefix_match(e["manufacturer"], mfr_name) and _mpn_match(e["mpn"], mpn)
if _mfr_match(e["manufacturer"], mfr_name) and _mpn_match(e["mpn"], mpn)
]
return max(candidates, key=lambda e: e["unit_cost"]) if candidates else None
def _extract_r_vals(s: str) -> tuple:
"""Extract (resistance_norm, footprint) from a description string using word boundaries."""
s = re.sub(r'(\d),(\d)', r'\1.\2', s) # normalise European decimal comma
rm = re.search(r'\b(\d+\.?\d*\s*(?:kohms?|mohms?|k[\s\-]?ohms?|ohms?|k|m|r))\b', s, re.IGNORECASE)
resistance = _norm_elec(rm.group(1)) if rm else ""
fm = re.search(r'\b(0\d{3})\b', s)
fm = re.search(r'\b(\d{4})\b', s)
footprint = fm.group(1) if fm else ""
return resistance, footprint
def _extract_c_vals(s: str) -> tuple:
"""Extract (capacitance_norm, voltage_norm, case) from a description string."""
s = re.sub(r'(\d),(\d)', r'\1.\2', s) # normalise European decimal comma
cm = re.search(r'\b(\d+\.?\d*\s*(?:p|n|u|µ|μ|m)\s*f)\b', s, re.IGNORECASE)
capacitance = _norm_elec(cm.group(1)) if cm else ""
vm = re.search(r'\b(\d+\.?\d*\s*v)\b', s, re.IGNORECASE)
voltage = _norm_elec(vm.group(1)) if vm else ""
fm = re.search(r'\b(0\d{3})\b', s)
fm = re.search(r'\b(\d{4})\b', s)
case = fm.group(1) if fm else ""
return capacitance, voltage, case
@@ -435,13 +470,11 @@ def _find_passive_bom_match(bom_data: list, designator: str, nexar_params: dict,
desc = e.get("description", "")
if not desc or "cap" not in desc.lower():
continue
bom_c, bom_v, bom_case = _extract_c_vals(desc)
bom_c, _, bom_case = _extract_c_vals(desc)
if not bom_c or not _elec_match(bom_c, capacitance):
continue
if not bom_case or bom_case.lower() != case.lower().strip():
continue
if voltage and bom_v and not _elec_match(bom_v, voltage):
continue
candidates.append(e)
return max(candidates, key=lambda e: e["unit_cost"]) if candidates else None
@@ -528,22 +561,10 @@ def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_d
f"(BOM: '{mpn_in_bom['manufacturer']}' vs Nexar: '{valid_pairs[0][0]}')"
)
else:
# Check if BOM has the manufacturer but with a different MPN
mfr_in_bom = next(
(e for e in bom_data for mfr, _ in valid_pairs if _prefix_match(e["manufacturer"], mfr)),
None
print(
f" NO MATCH [{designator}] {ref}: "
f"Not on Manufacturer's BoM"
)
if mfr_in_bom:
print(
f" NO MATCH [{designator}] {ref}: "
f"manufacturer '{mfr_in_bom['manufacturer']}' found in BOM "
f"but MPN differs (BOM: '{mfr_in_bom['mpn']}' vs Nexar: {tried_mpns})"
)
else:
print(
f" NO MATCH [{designator}] {ref}: "
f"Nexar MPN(s) {tried_mpns} not found in BOM at all"
)
if match:
row += [match["manufacturer"], match["mpn"], match["unit_cost"]]