main
John Lauer Three revisions with per-experiment write-ups, inline 3D viewers, sources, fab files, open-science scorecard 091a7c9 12d ago
#!/usr/bin/env python3
"""Geometric connectivity check on dist/lib/index/circuit.json.

Builds a graph of SMT pads, plated holes, vias and pcb_trace polylines,
joining anything whose endpoint lies inside a pad / on a via / coincides with
another endpoint (per layer), then reports islands per source net.
"""
import json, sys, collections, math
c = json.load(open('dist/lib/index/circuit.json'))
TOL = 0.05

parent = {}
def find(a):
    while parent.setdefault(a, a) != a:
        parent[a] = parent[parent[a]]; a = parent[a]
    return a
def union(a, b): parent[find(a)] = find(b)

# net lookup: source_port -> subcircuit net name
port_net = {}
for n in c:
    if n['type'] == 'source_net':
        pass
net_of_port = {}
for t in c:
    if t['type'] == 'source_trace':
        pass
# use pcb_port -> source_port -> source_trace membership
sp_by_id = {e['source_port_id']: e for e in c if e['type'] == 'source_port'}
sc_by_id = {e['source_component_id']: e for e in c if e['type'] == 'source_component'}
pp_by_id = {e['pcb_port_id']: e for e in c if e['type'] == 'pcb_port'}
# subcircuit_connectivity_map_key is on source_port / pcb_port in current tscircuit
def key_of_pp(pp):
    sp = sp_by_id.get(pp['source_port_id'], {})
    return sp.get('subcircuit_connectivity_map_key') or pp.get('subcircuit_connectivity_map_key')

pads = [e for e in c if e['type'] == 'pcb_smtpad']
holes = [e for e in c if e['type'] == 'pcb_plated_hole']
vias = [e for e in c if e['type'] == 'pcb_via']
traces = [e for e in c if e['type'] == 'pcb_trace']

def pad_contains(p, x, y):
    if p.get('shape') == 'rect':
        return abs(x-p['x']) <= p['width']/2+TOL and abs(y-p['y']) <= p['height']/2+TOL
    r = p.get('radius') or max(p.get('width',0), p.get('height',0))/2
    return math.hypot(x-p['x'], y-p['y']) <= r+TOL
def hole_contains(h, x, y):
    r = (h.get('outer_diameter') or max(h.get('outer_width',0), h.get('outer_height',0)))/2
    return math.hypot(x-h['x'], y-h['y']) <= r+TOL

# nodes: ('pad',id) ('hole',id) ('via',id) ('pt',layer,x,y)
def pt(layer, x, y): return ('pt', layer, round(x/TOL), round(y/TOL))

for t in traces:
    r = t['route']
    tid = t['pcb_trace_id']
    for i, seg in enumerate(r):
        if seg['route_type'] == 'wire':
            union(('tr', tid), pt(seg['layer'], seg['x'], seg['y']))
        else:  # via
            union(('tr', tid), ('via_pt', round(seg['x']/TOL), round(seg['y']/TOL)))
            for L in ('top', 'bottom'):
                union(('tr', tid), pt(L, seg['x'], seg['y']))
# endpoints (first/last wire point) may land in pads/holes
for t in traces:
    for seg in (t['route'][0], t['route'][-1]):
        if seg['route_type'] != 'wire': continue
        x, y, L = seg['x'], seg['y'], seg['layer']
        for p in pads:
            if p['layer'] == L and pad_contains(p, x, y): union(('pad', p['pcb_smtpad_id']), ('tr', t['pcb_trace_id']))
        for h in holes:
            if hole_contains(h, x, y): union(('hole', h['pcb_plated_hole_id']), ('tr', t['pcb_trace_id']))
for v in vias:
    for L in ('top', 'bottom'): union(('via', v['pcb_via_id']), pt(L, v['x'], v['y']))
    union(('via', v['pcb_via_id']), ('via_pt', round(v['x']/TOL), round(v['y']/TOL)))

# group pads/holes by net
by_net = collections.defaultdict(list)
for p in pads + holes:
    ppid = p.get('pcb_port_id')
    if not ppid or ppid not in pp_by_id: continue
    k = key_of_pp(pp_by_id[ppid])
    if not k: continue
    node = ('pad', p['pcb_smtpad_id']) if p['type']=='pcb_smtpad' else ('hole', p['pcb_plated_hole_id'])
    sc = sc_by_id.get(sp_by_id[pp_by_id[ppid]['source_port_id']]['source_component_id'], {})
    by_net[k].append((sc.get('name','?') + '.' + sp_by_id[pp_by_id[ppid]['source_port_id']].get('name','?'), node))

bad = 0
for k, members in sorted(by_net.items()):
    roots = collections.defaultdict(list)
    for label, node in members: roots[find(node)].append(label)
    if len(roots) > 1:
        bad += 1
        print(f"NET {k}: {len(members)} pads in {len(roots)} islands")
        for r, labs in sorted(roots.items(), key=lambda kv: -len(kv[1])):
            print('   ', len(labs), sorted(set(labs))[:12], '…' if len(labs)>12 else '')
print(f"{len(by_net)} nets checked, {bad} split")
sys.exit(1 if bad else 0)