seveibar/am3352-ram-dogbone-and-single-layer-route-test
AM3352BZCZ100 processor connected to a W631GG6MB-12 DDR3 memory chip through a routed, length-matched 16-bit DDR interface with clock/data-strobe pairs and control/address signals.
- Version
- 0.0.9
- License
- unset
- Stars
- 0
scripts/audit-copper.py
"""Independent geometric connectivity/clearance audit of generated circuit JSON.
Requires shapely. Uses real pads, wires, through vias and rendered pour polygons.
"""
import json, math, collections, hashlib, os
from pathlib import Path
from shapely.geometry import Point, Polygon, LineString
from shapely.strtree import STRtree
from shapely.affinity import rotate, translate
raw=Path(os.environ.get('AUDIT_CIRCUIT','dist/index/circuit.json')).read_bytes();c=json.loads(raw)
layers=['top',*[f'inner{i}' for i in range(1,next(x for x in c if x['type']=='pcb_board')['num_layers']-1)],'bottom'];root={}
def find(x):
root.setdefault(x,x)
if root[x]!=x:root[x]=find(root[x])
return root[x]
def union(a,b):root[find(a)]=find(b)
for t in c:
if t['type']=='source_trace':
ids=[t['source_trace_id']]+t.get('connected_source_port_ids',[])+t.get('connected_source_net_ids',[])
for p in ids[1:]:union(ids[0],p)
for element in c:
if element['type']=='source_component':
for ids in element.get('internally_connected_source_port_ids',[]):
for p in ids[1:]:union(ids[0],p)
netnames={find(n['source_net_id']):n['name'] for n in c if n['type']=='source_net'}
ports={p['pcb_port_id']:p for p in c if p['type']=='pcb_port'}
shapes=[];meta=[];vertical=[];padnodes=collections.defaultdict(list)
def add(g,l,net,ident,port=None):
i=len(shapes);shapes.append(g);meta.append((l,net,ident,port))
if port:padnodes[port].append(i)
return i
def padshape(p):
shape=p.get('shape',p.get('hole_shape','circle'));w=p.get('width',p.get('outer_width',p.get('outer_diameter',p.get('rect_pad_width',p.get('hole_diameter',2*p.get('radius',0))))));h=p.get('height',p.get('outer_height',p.get('rect_pad_height',w)))
if shape=='circle':return Point(p['x'],p['y']).buffer(w/2,quad_segs=64)
if shape=='pill':
g=(LineString([(-(w-h)/2,0),((w-h)/2,0)]).buffer(h/2,quad_segs=64) if w>h else LineString([(0,-(h-w)/2),(0,(h-w)/2)]).buffer(w/2,quad_segs=64))
return translate(rotate(g,p.get('ccw_rotation',0),origin=(0,0)),p['x'],p['y'])
a=math.radians(p.get('ccw_rotation',p.get('rect_ccw_rotation',0)))
return Polygon([(p['x']+x*math.cos(a)-y*math.sin(a),p['y']+x*math.sin(a)+y*math.cos(a)) for x,y in [(-w/2,-h/2),(w/2,-h/2),(w/2,h/2),(-w/2,h/2)]])
for p in c:
if p['type'] not in ['pcb_smtpad','pcb_plated_hole']:continue
port=ports.get(p.get('pcb_port_id'));net=find(port['source_port_id']) if port else 'NC_'+p[p['type']+'_id'];g=padshape(p)
ids=[add(g,l,net,p[p['type']+'_id'],p.get('pcb_port_id')) for l in ([p['layer']] if p['type']=='pcb_smtpad' else layers)]
vertical.append(ids)
for t in c:
if t['type']!='pcb_trace':continue
net=find(t.get('source_trace_id',t.get('connection_name',t['pcb_trace_id'])));r=t['route']
for i,p in enumerate(r):
if p['route_type']=='via':
g=Point(p['x'],p['y']).buffer(p.get('via_diameter',.4)/2,quad_segs=64)
vertical.append([add(g,l,net,t['pcb_trace_id']+'_via') for l in layers])
elif p['route_type']=='wire':
q=r[i+1] if i+1<len(r) else None
if q and q['route_type']=='wire' and q['layer']==p['layer'] and (p['x'],p['y'])!=(q['x'],q['y']):g=LineString([(p['x'],p['y']),(q['x'],q['y'])]).buffer(p['width']/2,quad_segs=32)
else:g=Point(p['x'],p['y']).buffer(p['width']/2,quad_segs=32)
add(g,p['layer'],net,t['pcb_trace_id'])
def ring(vertices):
result=[]
for i,p in enumerate(vertices):
q=vertices[(i+1)%len(vertices)];result.append((p['x'],p['y']));b=p.get('bulge',0)
if abs(b)<1e-10:continue
dx=q['x']-p['x'];dy=q['y']-p['y'];cx=(p['x']+q['x'])/2-dy*(1-b*b)/(4*b);cy=(p['y']+q['y'])/2+dx*(1-b*b)/(4*b)
a=math.atan2(p['y']-cy,p['x']-cx);theta=4*math.atan(b);r=math.hypot(p['x']-cx,p['y']-cy);n=max(2,math.ceil(abs(theta)/(math.pi/64)))
result.extend((cx+r*math.cos(a+theta*k/n),cy+r*math.sin(a+theta*k/n)) for k in range(1,n))
return result
for p in c:
if p['type']!='pcb_copper_pour':continue
if p['shape']=='brep':b=p['brep_shape'];g=Polygon(ring(b['outer_ring']['vertices']),[ring(h['vertices']) for h in b['inner_rings']]).buffer(0)
elif p['shape']=='polygon':g=Polygon([(v['x'],v['y']) for v in p['points']])
else:g=padshape(dict(p,x=p['center']['x'],y=p['center']['y']))
for poly in ([g] if g.geom_type=='Polygon' else g.geoms):add(poly,p['layer'],find(p['source_net_id']),p['pcb_copper_pour_id'])
parent=list(range(len(shapes)))
def component(i):
while parent[i]!=i:parent[i]=parent[parent[i]];i=parent[i]
return i
def join(i,j):parent[component(i)]=component(j)
for ids in vertical:
for i in ids[1:]:join(ids[0],i)
violations=[]
for layer in layers:
indexes=[i for i,m in enumerate(meta) if m[0]==layer];geoms=[shapes[i] for i in indexes];tree=STRtree(geoms)
for a,g in enumerate(geoms):
i=indexes[a]
for b in tree.query(g.buffer(.101)):
j=indexes[int(b)]
if j<=i:continue
distance=g.distance(shapes[j])
if meta[i][1]==meta[j][1]:
if distance<.001:join(i,j)
elif distance<.097:violations.append({'layer':layer,'a':meta[i][2],'b':meta[j][2],'netA':netnames.get(meta[i][1],meta[i][1]),'netB':netnames.get(meta[j][1],meta[j][1]),'clearance':round(distance,5)})
netports=collections.defaultdict(list)
for t in c:
if t['type']=='source_trace':
for sp in t.get('connected_source_port_ids',[]):
for p in ports.values():
if p['source_port_id']==sp:netports[find(sp)].append(p['pcb_port_id'])
incomplete={}
for net,ps in netports.items():
groups=collections.defaultdict(list)
for p in set(ps):
nodes=padnodes[p]
groups[component(nodes[0]) if nodes else 'missing_'+p].append(p)
if len(groups)>1:incomplete[netnames.get(net,net)]=list(groups.values())
report={'circuitSha256':hashlib.sha256(raw).hexdigest(),'audit':'Physical pad/trace/via/pour geometry; 1 um connectivity tolerance, 3 um clearance tolerance','nets':len(netports),'incompleteNets':incomplete,'clearanceViolations':violations,'copperGeometryItems':len(shapes)}
Path(os.environ.get('AUDIT_REPORT_PATH','design/copper-audit.json')).write_text(json.dumps(report,indent=2)+'\n')
print('Geometric audit:',len(netports),'nets;',len(incomplete),'incomplete;',len(violations),'clearance violations')
if __name__ == "__main__":
import sys
if "--strict" in sys.argv and (incomplete or violations):
raise SystemExit("Copper audit failed: disconnected nets or clearance violations")