muse/book-reading-clip-lamp

A tscircuit package created by muse.

Version
0.4.6
License
unset
Stars
0

verify-manufacturing.py

"""Independent prototype fabrication checks; supplements core DRC and Gerber shorts.

Uses JLCPCB's published rigid-board drilling rules. Does not replace CAM review,
controlled-impedance design, current-capacity analysis or hardware qualification.
"""
import argparse, json, math, sys
from pathlib import Path

root = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument('circuit', nargs='?', default=str(root / 'dist/index/circuit.json'))
parser.add_argument('--report')
args = parser.parse_args()
data = json.loads(Path(args.circuit).read_text())
issues = []
board = next(e for e in data if e['type'] == 'pcb_board')
vias = [e for e in data if e['type'] == 'pcb_via']
if board.get('num_layers') != 4:
    issues.append('Expected a four-layer through-via board')
if board.get('min_via_hole_edge_to_via_hole_edge_clearance', 0) < .20 - 1e-9:
    issues.append('Board rule must require >=0.20 mm via drill-edge clearance')
closest = None
for i, a in enumerate(vias):
    if not (abs(a['hole_diameter'] - .30) < 1e-9 and abs(a['outer_diameter'] - .45) < 1e-9):
        issues.append(f"{a['pcb_via_id']}: expected 0.30 mm drill / 0.45 mm copper")
    if set(a['layers']) != {'top', 'inner1', 'inner2', 'bottom'}:
        issues.append(f"{a['pcb_via_id']}: blind/buried via is not permitted")
    for b in vias[i + 1:]:
        gap = math.hypot(a['x'] - b['x'], a['y'] - b['y']) - (a['hole_diameter'] + b['hole_diameter']) / 2
        pair = {'viaIds': [a['pcb_via_id'], b['pcb_via_id']], 'drillEdgeGapMm': gap}
        if closest is None or gap < closest['drillEdgeGapMm']:
            closest = pair
        if gap < .20 - 1e-6:
            issues.append(f"{pair['viaIds']}: drill edge gap {gap:.6f} mm < 0.20 mm")
# Multilayer 1 oz plated component lands: JLCPCB absolute minimum annular ring 0.15 mm.
for hole in (e for e in data if e['type'] == 'pcb_plated_hole'):
    if 'hole_diameter' in hole and 'outer_diameter' in hole:
        ring = (hole['outer_diameter'] - hole['hole_diameter']) / 2
    elif all(k in hole for k in ('rect_pad_width', 'rect_pad_height', 'hole_diameter')):
        ring = (min(hole['rect_pad_width'], hole['rect_pad_height']) - hole['hole_diameter']) / 2
    elif all(k in hole for k in ('outer_width', 'outer_height', 'hole_width', 'hole_height')):
        ring = min(hole['outer_width'] - hole['hole_width'], hole['outer_height'] - hole['hole_height']) / 2
    else:
        issues.append(f"{hole.get('pcb_plated_hole_id')}: unsupported drill land geometry")
        continue
    if ring < .15 - 1e-5:
        issues.append(f"{hole['pcb_plated_hole_id']}: component annular ring {ring:.6f} mm < 0.15 mm")
# Verify the actual exposed-pad spur widths; core's net-wide GND warning can
# incorrectly attribute other ground branches to these short thermal connections.
components = {e['source_component_id']: e['name'] for e in data if e['type'] == 'source_component'}
ports = {e['source_port_id']: (components.get(e['source_component_id'], e['source_component_id']), e.get('pin_number')) for e in data if e['type'] == 'source_port'}
for ref, pin in [('U2', 17), ('U3', 15), ('U5', 17)]:
    matches = [e for e in data if e['type'] == 'source_trace' and len(e.get('connected_source_port_ids', [])) == 2 and any(ports.get(p) == (ref, pin) for p in e['connected_source_port_ids'])]
    if len(matches) != 1:
        issues.append(f'{ref}: cannot uniquely identify exposed-pad spur')
        continue
    copper = [e for e in data if e['type'] == 'pcb_trace' and e.get('source_trace_id') == matches[0]['source_trace_id']]
    if not copper or any(point.get('width', .3) < .3 - 1e-6 for e in copper for point in e['route'] if point['route_type'] == 'wire'):
        issues.append(f'{ref}: thermal spur must contain >=0.30 mm copper throughout')
errors = [e for e in data if e['type'].endswith('_error') or str(e.get('error_type', '')).endswith('_error')]
if errors:
    issues.append(f'{len(errors)} core circuit/DRC error records remain')
result = {'check': 'JLCPCB prototype manufacturing geometry', 'status': 'FAIL' if issues else 'PASS', 'requiredViaDrillEdgeGapMm': .2, 'viaCount': len(vias), 'closestViaPair': closest, 'coreErrorCount': len(errors), 'issues': issues, 'sources': ['https://jlcpcb.com/capabilities/pcb-capabilities'], 'limitations': 'Selected geometry rules only. Separate Gerber shorts, assembler/CAM review and physical qualification are required.'}
if args.report:
    Path(args.report).write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(result, indent=2))
sys.exit(bool(issues))