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