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-route-shape.py

"""Copper-aware self spacing and bend audit, independent of router metadata.
The local exclusion is arclength, not segment count: resampling cannot hide defects.
"""
import json,math,sys,hashlib
import numpy as np
from shapely.geometry import LineString
from shapely.strtree import STRtree

def constrained_distance(a,b,c,d,required):
 u=b-a;v=d-c;lu=np.linalg.norm(u);lv=np.linalg.norm(v)
 if lu+lv<required:return math.inf
 u=u/lu;v=v/lv;origin=b-c
 # Parameter polygon [0,lu] x [0,lv], clipped to s+t >= required.
 poly=[(0.,0.),(lu,0.),(lu,lv),(0.,lv)];clipped=[]
 for x,y in zip(poly,poly[1:]+poly[:1]):
  fx=sum(x)-required;fy=sum(y)-required
  if fx>=0:clipped.append(np.array(x))
  if (fx>=0)!=(fy>=0):clipped.append(np.array(x)+(np.array(y)-x)*fx/(fx-fy))
 if not clipped:return math.inf
 def delta(p):return origin-u*p[0]-v*p[1]
 best=math.inf
 for x,y in zip(clipped,clipped[1:]+clipped[:1]):
  p=delta(x);q=delta(y)-p;t=np.clip(-np.dot(p,q)/(np.dot(q,q) or 1),0,1);best=min(best,float(np.linalg.norm(p+t*q)))
 matrix=np.column_stack([u,v])
 if abs(np.linalg.det(matrix))>1e-12:
  s,t=np.linalg.solve(matrix,origin)
  if 0<=s<=lu and 0<=t<=lv and s+t>=required:return 0.
 return best

def audit(c):
 findings=[];short_radius=[];sharp=[];off_axis=[]
 for r in c:
  if r['type']!='pcb_trace':continue
  name=r['source_trace_id'];ps=np.array([(p['x'],p['y']) for p in r['route'] if p['route_type']=='wire' and p['layer']!='top'])
  ps=ps[np.r_[True,np.linalg.norm(np.diff(ps,axis=0),axis=1)>1e-8]]
  lengths=np.linalg.norm(np.diff(ps,axis=0),axis=1);prefix=np.r_[0,np.cumsum(lengths)]
  lines=[LineString([a,b]) for a,b in zip(ps,ps[1:])];tree=STRtree(lines);overlap=spacing=0;worst=1e9
  for i,l in enumerate(lines):
   for j in tree.query(l.buffer(.20001)):
    if j<=i:continue
    gap=prefix[j]-prefix[i+1]
    distance=(l.distance(lines[j]) if gap>=.3 else constrained_distance(ps[i],ps[i+1],ps[j],ps[j+1],.3-gap))
    worst=min(worst,distance)
    overlap+=distance<.0999;spacing+=distance<.1999
  if spacing:findings.append(dict(net=name,overlapPairs=int(overlap),spacingPairs=int(spacing),minimumCenterDistanceMm=worst))
  headings=np.degrees(np.arctan2(np.diff(ps,axis=0)[:,1],np.diff(ps,axis=0)[:,0]));turns=abs((np.diff(headings)+180)%360-180)
  for i,t in enumerate(turns):
   if t>45.1:sharp.append(dict(net=name,point=ps[i+1].tolist(),turnDegrees=float(t)))
   if .05<t<10:
    a,b,c=ps[i:i+3];ab=b-a;ac=c-a;cross=abs(ab[0]*ac[1]-ab[1]*ac[0]);radius=np.linalg.norm(b-a)*np.linalg.norm(c-b)*np.linalg.norm(c-a)/(2*cross) if cross>1e-12 else math.inf
    if radius<.1195:short_radius.append(dict(net=name,point=b.tolist(),radiusMm=float(radius)))
  # A non-octilinear long edge cannot be disguised as a tessellated curve.
  for i,(h,l) in enumerate(zip(headings,lengths)):
   if abs((h+22.5)%45-22.5)>.2 and l>.06:off_axis.append(dict(net=name,lengthMm=float(l),headingDegrees=float(h)))
 return dict(netsWithSelfContact=sum(x['overlapPairs']>0 for x in findings),netsWithSelfClearanceViolations=len(findings),selfSpacing=findings,sharpCornerCount=len(sharp),sharpCorners=sharp,undersizedCurveVertexCount=len(short_radius),undersizedCurves=short_radius,nonOctilinearLongSegmentCount=len(off_axis),nonOctilinearLongSegments=off_axis)
if __name__=='__main__':
 raw=open(sys.argv[1],'rb').read();result=audit(json.loads(raw));result['circuitSha256']=hashlib.sha256(raw).hexdigest();result['status']='PASS' if not any(result[k] for k in ['netsWithSelfClearanceViolations','sharpCornerCount','undersizedCurveVertexCount','nonOctilinearLongSegmentCount']) else 'FAIL';print(json.dumps(result,indent=2))
 if '--strict' in sys.argv:sys.exit(result['status']!='PASS')