molecule
Steven's Nameplate: three revisions
Public Made by Adomby adom
A USB-C desk nameplate for Steven Moss (Astera Institute), built three ways and published as open experiments: a 3x5 LED board, a 5x7 LED board, and a laser-ablated silkscreen plate. Source, fab files, negative results, reproduce steps.
main
John Lauer
Three revisions with per-experiment write-ups, inline 3D viewers, sources, fab files, open-science scorecard
091a7c9
12d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
#!/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)