Files
claude-skills/pcb-ai-engineer/scripts/bom_check.py
T
2026-03-21 19:36:11 +03:00

135 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Check BOM output for common issues.
Usage:
python bom_check.py path/to/bom.csv
Checks:
- All components have footprint assigned
- All components have value assigned (resistors, capacitors, inductors)
- No duplicate reference designators
- Passive values are standard E-series (basic check)
- Flags DNP components
"""
from __future__ import annotations
import csv
import re
import sys
from pathlib import Path
# Standard E12 values (covers most common passives)
E12_VALUES = {1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2}
PASSIVE_PREFIXES = {"R", "C", "L"}
def normalize_value(value_str: str) -> float | None:
"""Try to extract numeric value from component value string."""
match = re.match(r"^(\d+\.?\d*)\s*([kKmMuUnNpP]?)", value_str.strip())
if not match:
return None
num = float(match.group(1))
suffix = match.group(2).lower()
multipliers = {"k": 1e3, "m": 1e6, "u": 1e-6, "n": 1e-9, "p": 1e-12}
return num * multipliers.get(suffix, 1.0)
def is_e12_compatible(value: float) -> bool:
"""Check if a value falls on an E12 series value (within 5% tolerance)."""
if value <= 0:
return False
# Normalize to 1.0-9.99 range
normalized = value
while normalized >= 10:
normalized /= 10
while normalized < 1:
normalized *= 10
for e12 in E12_VALUES:
if abs(normalized - e12) / e12 < 0.05:
return True
return False
def check_bom(filepath: Path) -> tuple[list[str], list[str]]:
errors: list[str] = []
warnings: list[str] = []
refs_seen: set[str] = set()
with filepath.open(newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if reader.fieldnames is None:
errors.append("BOM file has no headers")
return errors, warnings
# Try common column name patterns
ref_col = next((c for c in reader.fieldnames if c.lower() in ("ref", "reference", "designator")), None)
val_col = next((c for c in reader.fieldnames if c.lower() in ("value", "val")), None)
fp_col = next((c for c in reader.fieldnames if c.lower() in ("footprint", "package", "fp")), None)
if ref_col is None:
errors.append("Cannot find reference designator column in BOM")
return errors, warnings
for row_num, row in enumerate(reader, start=2):
ref = row.get(ref_col, "").strip()
if not ref:
continue
# Duplicate check
if ref in refs_seen:
errors.append(f"Row {row_num}: duplicate reference '{ref}'")
refs_seen.add(ref)
# Footprint check
footprint = row.get(fp_col, "").strip() if fp_col else ""
if not footprint:
errors.append(f"{ref}: missing footprint")
# Value check for passives
value = row.get(val_col, "").strip() if val_col else ""
prefix = re.match(r"^[A-Z]+", ref)
if prefix and prefix.group() in PASSIVE_PREFIXES:
if not value or value.upper() == "DNP":
if value.upper() == "DNP":
warnings.append(f"{ref}: marked as DNP — verify this is intentional")
else:
errors.append(f"{ref}: passive component missing value")
else:
numeric = normalize_value(value)
if numeric and not is_e12_compatible(numeric):
warnings.append(f"{ref}: value '{value}' may not be standard E12 series")
return errors, warnings
def main() -> int:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <path/to/bom.csv>")
return 1
path = Path(sys.argv[1])
if not path.exists():
print(f"File not found: {path}")
return 1
errors, warnings = check_bom(path)
for w in warnings:
print(f" WARN: {w}")
for e in errors:
print(f" ERROR: {e}")
print(f"\n {len(errors)} errors, {len(warnings)} warnings")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())