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")