astra/iphone-encoder-counter
A CR2450-powered MSP430 FRAM counter combines an always-on dual-channel TMR rotation sensor, pushbutton input, and ST25DV NFC tag with an integrated tuned printed antenna and I²C/GPIO interface.
- Version
- 1.0.3
- License
- unset
- Stars
- 0
scripts/check_geometry.py
"""Independent copper clearance audit of Circuit JSON; requires shapely 2.x.
This does not use KiCad. Printed-coil terminal contacts are explicitly modeled.
"""
from pathlib import Path
import json,math
from shapely.geometry import Point,LineString,box
from shapely.ops import unary_union
from shapely.affinity import rotate
root=Path(__file__).resolve().parents[1]
d=json.loads((root/'dist/index/circuit.json').read_text())
bytype={}
for e in d:bytype.setdefault(e['type'],[]).append(e)
sourceports={e['source_port_id']:e for e in bytype['source_port']}
ports={e['pcb_port_id']:e for e in bytype['pcb_port']}
nets={e['source_net_id']:e for e in bytype['source_net']}
traces={e['source_trace_id']:e for e in bytype['source_trace']}
netnames={e['subcircuit_connectivity_map_key']:e['name'] for e in bytype['source_net']}
sc={e['source_component_id']:e['name'] for e in bytype['source_component']}
pc={e['pcb_component_id']:sc[e['source_component_id']] for e in bytype['pcb_component']}
shapes={};issues=[];terminals={};nshapes=0
def keyport(id):
p=ports[id];s=sourceports[p['source_port_id']]
return s.get('subcircuit_connectivity_map_key',s['source_port_id'])
def add(layer,key,g):
global nshapes
shapes.setdefault((layer,key),[]).append(g);nshapes+=1
if not box(-29.7,-49.7,29.7,49.7).buffer(1e-6).covers(g):issues.append({'kind':'board_edge','net':key,'layer':layer})
for e in bytype['pcb_smtpad']+bytype.get('pcb_plated_hole',[]):
k=keyport(e['pcb_port_id']) if e.get('pcb_port_id') else e.get('pcb_smtpad_id',e.get('pcb_plated_hole_id'))
x,y=e['x'],e['y'];shape=e['shape']
if shape=='circle':g=Point(x,y).buffer(e.get('outer_diameter',e.get('radius',0)*2)/2,quad_segs=64)
elif shape=='rect':g=box(x-e['width']/2,y-e['height']/2,x+e['width']/2,y+e['height']/2)
elif shape=='rotated_rect':
g=rotate(box(x-e['width']/2,y-e['height']/2,x+e['width']/2,y+e['height']/2),e['ccw_rotation'],origin=(x,y))
else:raise ValueError('Unhandled pad shape '+shape)
for layer in e.get('layers',[e.get('layer','top')]):add(layer,k,g)
if pc.get(e.get('pcb_component_id'))=='L1':terminals[k]=g.buffer(.151)
for e in bytype['pcb_trace']:
if e.get('pcb_component_id') and pc.get(e['pcb_component_id'])=='L1':k='PRINTED_COIL'
elif e.get('connection_name') in nets:k=nets[e['connection_name']]['subcircuit_connectivity_map_key']
else:
st=traces.get(e.get('source_trace_id'));assert st,e
k=st['subcircuit_connectivity_map_key']
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']!='wire' or b['route_type']!='wire' or a['layer']!=b['layer']:continue
assert a['width']>=.15-1e-7, a
add(a['layer'],k,LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(a['width']/2,quad_segs=32))
for e in bytype.get('pcb_via',[]):
assert e['hole_diameter']>=.3-1e-7 and (e['outer_diameter']-e['hole_diameter'])/2>=.15-1e-7
k=e['subcircuit_connectivity_map_key'];g=Point(e['x'],e['y']).buffer(e['outer_diameter']/2,quad_segs=64)
for layer in e['layers']:add(layer,k,g)
merged={k:unary_union(v) for k,v in shapes.items()}
checks=0
for i,((layer,a),ga) in enumerate(merged.items()):
for (lb,b),gb in list(merged.items())[i+1:]:
if lb!=layer or a==b:continue
g1,g2=ga,gb
if a=='PRINTED_COIL' and b in terminals:g1=ga.difference(terminals[b]);g2=gb.difference(terminals[b])
if b=='PRINTED_COIL' and a in terminals:g1=ga.difference(terminals[a]);g2=gb.difference(terminals[a])
if g1.is_empty or g2.is_empty:continue
gap=g1.distance(g2);checks+=1
if gap<.1499:issues.append({'kind':'copper_clearance','layer':layer,'a':netnames.get(a,a),'b':netnames.get(b,b),'gap_mm':round(gap,6)})
report={'engine':'Shapely independent Circuit JSON geometry audit','minimum_clearance_mm':.15,'board_edge_mm':.3,'shape_count':nshapes,'net_pair_checks':checks,'issues':issues}
(root/'validation').mkdir(exist_ok=True)
(root/'validation/geometry.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2));raise SystemExit(bool(issues))