Updates
This commit is contained in:
140
nexarxclaude.py
140
nexarxclaude.py
@@ -271,6 +271,25 @@ def _mpn_match(a: str, b: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_elec(s: str) -> str:
|
||||||
|
"""Normalise electrical value: lowercase, µ→u, strip non-alphanumeric."""
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
s = s.strip().lower()
|
||||||
|
for frm, to in [('\u00b5', 'u'), ('\u03bc', 'u'), ('µ', 'u'), ('μ', 'u'),
|
||||||
|
('\u03a9', 'ohm'), ('\u2126', 'ohm'), ('mf', 'uf')]:
|
||||||
|
s = s.replace(frm, to)
|
||||||
|
return re.sub(r'[^a-z0-9]', '', s)
|
||||||
|
|
||||||
|
|
||||||
|
def _elec_match(a: str, b: str) -> bool:
|
||||||
|
"""True if two electrical values match (prefix-tolerant after normalisation)."""
|
||||||
|
na, nb = _norm_elec(a), _norm_elec(b)
|
||||||
|
if not na or not nb:
|
||||||
|
return False
|
||||||
|
return na == nb or nb.startswith(na) or na.startswith(nb)
|
||||||
|
|
||||||
|
|
||||||
def load_bom_data(bom_path: str) -> list | None:
|
def load_bom_data(bom_path: str) -> list | None:
|
||||||
"""Load BOM xlsx and return a list of entry dicts.
|
"""Load BOM xlsx and return a list of entry dicts.
|
||||||
Returns None if no 'bom' tab is found."""
|
Returns None if no 'bom' tab is found."""
|
||||||
@@ -289,21 +308,40 @@ def load_bom_data(bom_path: str) -> list | None:
|
|||||||
return headers[c]
|
return headers[c]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def cell(row, col):
|
||||||
|
return str(row[col]).strip() if col is not None and len(row) > col and row[col] is not None else ""
|
||||||
|
|
||||||
mfr_col = find_col("manufacturer", "mfr", "manufacturer name")
|
mfr_col = find_col("manufacturer", "mfr", "manufacturer name")
|
||||||
mpn_col = find_col("mpn", "manufacturer part number", "manufacturer part no", "part number")
|
mpn_col = find_col("mpn", "manufacturer part number", "manufacturer part no", "part number")
|
||||||
cost_col = find_col("unit cost", "unit price", "cost", "price")
|
cost_col = find_col("unit cost", "unit price", "cost", "price")
|
||||||
|
desc_col = find_col("description", "desc", "component description")
|
||||||
|
res_col = find_col("resistance", "resistance (ohms)")
|
||||||
|
tol_col = find_col("tolerance", "tol")
|
||||||
|
cap_col = find_col("capacitance", "capacitance (farads)")
|
||||||
|
volt_col = find_col("voltage", "voltage rating", "voltage - rated", "rated voltage")
|
||||||
|
fp_col = find_col("footprint", "case", "package", "package / case", "case - imperial", "case - metric")
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for row in bom_sheet.iter_rows(min_row=2, values_only=True):
|
for row in bom_sheet.iter_rows(min_row=2, values_only=True):
|
||||||
mfr = str(row[mfr_col]).strip() if mfr_col is not None and len(row) > mfr_col and row[mfr_col] is not None else ""
|
mfr = cell(row, mfr_col)
|
||||||
mpn = str(row[mpn_col]).strip() if mpn_col is not None and len(row) > mpn_col and row[mpn_col] is not None else ""
|
mpn = cell(row, mpn_col)
|
||||||
if not mfr and not mpn:
|
if not mfr and not mpn:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
cost = float(row[cost_col]) if cost_col is not None and len(row) > cost_col and row[cost_col] is not None else 0.0
|
cost = float(row[cost_col]) if cost_col is not None and len(row) > cost_col and row[cost_col] is not None else 0.0
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
cost = 0.0
|
cost = 0.0
|
||||||
entries.append({"manufacturer": mfr, "mpn": mpn, "unit_cost": cost})
|
entries.append({
|
||||||
|
"manufacturer": mfr,
|
||||||
|
"mpn": mpn,
|
||||||
|
"unit_cost": cost,
|
||||||
|
"description": cell(row, desc_col),
|
||||||
|
"resistance": cell(row, res_col),
|
||||||
|
"tolerance": cell(row, tol_col),
|
||||||
|
"capacitance": cell(row, cap_col),
|
||||||
|
"voltage": cell(row, volt_col),
|
||||||
|
"footprint": cell(row, fp_col),
|
||||||
|
})
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
@@ -317,6 +355,98 @@ def _find_bom_match(bom_data: list, mfr_name: str, mpn: str) -> dict | None:
|
|||||||
return max(candidates, key=lambda e: e["unit_cost"]) if candidates else None
|
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."""
|
||||||
|
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)
|
||||||
|
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."""
|
||||||
|
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)
|
||||||
|
case = fm.group(1) if fm else ""
|
||||||
|
return capacitance, voltage, case
|
||||||
|
|
||||||
|
|
||||||
|
def _find_passive_bom_match(bom_data: list, designator: str, nexar_params: dict, nexar_desc: str) -> dict | None:
|
||||||
|
"""Fallback matcher for R/C: extract values from Nexar params/description, match against BOM description."""
|
||||||
|
# Take only leading letters from designator e.g. C38 → C, R102 → R
|
||||||
|
prefix_match = re.match(r'^([a-zA-Z]+)', designator)
|
||||||
|
prefix = prefix_match.group(1).upper() if prefix_match else ""
|
||||||
|
if prefix not in ("R", "C"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
pl = {k.lower(): v.strip() for k, v in nexar_params.items()
|
||||||
|
if v and str(v).strip() and str(v).strip() != "-"}
|
||||||
|
|
||||||
|
def gp(*keys):
|
||||||
|
for k in keys:
|
||||||
|
v = pl.get(k.lower(), "")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
return ""
|
||||||
|
|
||||||
|
pkg_keys = ("package / case", "package/case", "case - imperial",
|
||||||
|
"case - metric", "supplier device package", "footprint", "case")
|
||||||
|
|
||||||
|
if prefix == "R":
|
||||||
|
resistance = gp("resistance")
|
||||||
|
footprint = gp(*pkg_keys)
|
||||||
|
if not resistance or not footprint:
|
||||||
|
r_d, fp_d = _extract_r_vals(nexar_desc)
|
||||||
|
if not resistance: resistance = r_d
|
||||||
|
if not footprint: footprint = fp_d
|
||||||
|
if not resistance or not footprint:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for e in bom_data:
|
||||||
|
desc = e.get("description", "")
|
||||||
|
if not desc or "res" not in desc.lower():
|
||||||
|
continue
|
||||||
|
bom_r, bom_fp = _extract_r_vals(desc)
|
||||||
|
if not bom_r or not _elec_match(bom_r, resistance):
|
||||||
|
continue
|
||||||
|
if not bom_fp or bom_fp.lower() != footprint.lower().strip():
|
||||||
|
continue
|
||||||
|
candidates.append(e)
|
||||||
|
|
||||||
|
else: # C
|
||||||
|
capacitance = gp("capacitance")
|
||||||
|
voltage = gp("voltage - rated", "voltage rating", "rated voltage", "voltage")
|
||||||
|
case = gp(*pkg_keys)
|
||||||
|
if not capacitance or not case:
|
||||||
|
c_d, v_d, cs_d = _extract_c_vals(nexar_desc)
|
||||||
|
if not capacitance: capacitance = c_d
|
||||||
|
if not voltage: voltage = v_d
|
||||||
|
if not case: case = cs_d
|
||||||
|
if not capacitance or not case:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for e in bom_data:
|
||||||
|
desc = e.get("description", "")
|
||||||
|
if not desc or "cap" not in desc.lower():
|
||||||
|
continue
|
||||||
|
bom_c, bom_v, 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
|
||||||
|
|
||||||
|
|
||||||
MAX_MFR_PARTS = 5
|
MAX_MFR_PARTS = 5
|
||||||
|
|
||||||
def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_data: dict | None) -> str:
|
def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_data: dict | None) -> str:
|
||||||
@@ -370,6 +500,10 @@ def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_d
|
|||||||
match = _find_bom_match(bom_data, mfr_name, mpn)
|
match = _find_bom_match(bom_data, mfr_name, mpn)
|
||||||
if match:
|
if match:
|
||||||
break
|
break
|
||||||
|
if not match:
|
||||||
|
match = _find_passive_bom_match(
|
||||||
|
bom_data, node.get("designator") or "", params, comp.get("description") or ""
|
||||||
|
)
|
||||||
if match:
|
if match:
|
||||||
row += [match["manufacturer"], match["mpn"], match["unit_cost"]]
|
row += [match["manufacturer"], match["mpn"], match["unit_cost"]]
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user