skill
EDA Skillpack
Public Made by Adomby adom
All skills for all EDA capabilities across the Adom ecosystem.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
from geometry import *
import numpy as np,shapely,subprocess,time
from collections import defaultdict
D=Path(__file__).parent
STEP=.05;X0=104.;Y0=58.;W=1281;H=1481;N=W*H
copper=[]; failures=[];plans=[]
pads=[p for p in pads if any(l.endswith('.Cu') for l in p['layers'])]
by=defaultdict(list)
for p in pads:
if p['net'] and not p['net'].startswith('unconnected-'):by[p['net']].append(p)
# board rounded corners, 0.25mm copper-to-edge conservative minimum
outline=box(107,61,165,129).buffer(3,quad_segs=16)
def ij(x,y):return round((x-X0)/STEP),round((y-Y0)/STEP)
def xy(i,j):return (round(X0+i*STEP,5),round(Y0+j*STEP,5))
def raster(arr,g,value=1):
if g.is_empty:return
x1,y1,x2,y2=g.bounds;i1=max(0,int((x1-X0)/STEP)-1);i2=min(W,int((x2-X0)/STEP)+2);j1=max(0,int((y1-Y0)/STEP)-1);j2=min(H,int((y2-Y0)/STEP)+2)
if i2<=i1 or j2<=j1:return
xx=X0+np.arange(i1,i2)[None,:]*STEP;yy=Y0+np.arange(j1,j2)[:,None]*STEP
hit=shapely.intersects_xy(g,xx,yy);a=arr[j1:j2,i1:i2];a[hit]=value
def masks(net,width):
a=np.ones((3,H,W),dtype=np.uint8)
for z,rad in [(0,width/2),(1,width/2),(2,.4)]:raster(a[z],outline.buffer(-.25-rad),0)
for p in pads:
if p['net']==net:continue
clearance=max(.2,p['clearance'])+.005
pg=p['g']; layers=p['layers']
for z,l in [(0,'F.Cu'),(1,'B.Cu')]:
if l in layers or '*.Cu' in layers:raster(a[z],pg.buffer(clearance+width/2))
raster(a[2],pg.buffer(clearance+.4))
for p in pads:
if p['hole']:raster(a[2],Point(p['x'],p['y']).buffer(p['hole']/2+.2+.25))
for c in copper:
if len(c['zs'])==2:raster(a[2],c['g'].centroid.buffer(.65+.02))
if c['net']==net:continue
for z in c['zs']:raster(a[z],c['g'].buffer(.205+width/2))
raster(a[2],c['g'].buffer(.205+.4))
return a
def register(net,points,width):
z=0
for p,q in zip(points,points[1:]):
oldz=z;zz=q.get('layer','F.Cu' if z==0 else 'B.Cu') if isinstance(q,dict) else ('F.Cu' if z==0 else 'B.Cu')
def pos(v):return (v['x'],v['y']) if isinstance(v,dict) else v
a,b=pos(p),pos(q)
if a!=b:copper.append(dict(net=net,g=LineString([a,b]).buffer(width/2),zs=[z]))
if isinstance(q,dict) and zz!=('F.Cu' if z==0 else 'B.Cu'):
copper.append(dict(net=net,g=Point(*b).buffer(.4),zs=[0,1]));z=1 if zz=='B.Cu' else 0
def compress(raw):
ps=[]
for n in raw:
z=n//N;j,i=divmod(n%N,W);ps.append((i,j,z))
keep=[ps[0]]
for a,b,c in zip(ps,ps[1:],ps[2:]):
if (b[0]-a[0],b[1]-a[1],b[2]-a[2])!=(c[0]-b[0],c[1]-b[1],c[2]-b[2]):keep.append(b)
keep.append(ps[-1]);return keep
def search(a,start,target,goalgeo,plane=False):
goal=np.zeros((2,H,W),dtype=np.uint8)
if plane:goal[0]=1-a[2]
else:
for z,g in goalgeo:raster(goal[z],g)
goal[a[:2]>0]=0
si,sj=ij(start['x'],start['y']);sx=si;sy=sj
if a[0,sj,si]:return None
a.tofile('mask.bin');goal.tofile('goal.bin');tx,ty=ij(*target)
r=subprocess.run(['./search'],input=f'{W} {H} {sj*W+si} {tx} {ty}',capture_output=True,text=True)
raw=json.loads(r.stdout)
if not raw:return None
ps=compress(raw);out=[[start['x'],start['y']]]
for i,j,z in ps:
x,y=xy(i,j)
if z!=(0 if len(out)==1 else curz):out.append(dict(x=x,y=y,layer='B.Cu' if z else 'F.Cu'))
elif [x,y]!=out[-1]:out.append([x,y])
curz=z
return out
def plan_planes():
for net,layer in [('GND','In1.Cu'),('+3V3','In2.Cu')]:
paths=[]
for p in sorted(by[net],key=lambda p:p['g'].area):
if p['type']=='thru_hole':continue
a=masks(net,.25);route=search(a,p,(p['x'],p['y']),[],True)
if route:
e=route[-1];x,y=(e['x'],e['y']) if isinstance(e,dict) else e
route.append(dict(x=x,y=y,layer=layer));register(net,route,.25);paths.append(route)
else:failures.append([net,p['name'],'plane stub blocked'])
plans.append(dict(net=net,width=.25,paths=paths));print(net,len(paths),'stubs',flush=True)
def plan_signals():
def span(n):
ps=by[n];return max(math.hypot(p['x']-q['x'],p['y']-q['y']) for p in ps for q in ps)
for net in sorted((n for n in by if n not in ['GND','+3V3'] and len(by[n])>1),key=span):
width=.5 if net in ['+VBAT','GND_OUT','/DRV_SHA','/DRV_SHB','/DRV_SHC'] else .25
ps=by[net];connected=[ps[0]];remaining=ps[1:];paths=[]
while remaining:
_,p,t=min((math.hypot(p['x']-q['x'],p['y']-q['y']),p,q) for p in remaining for q in connected)
remaining.remove(p)
goals=[]
for q in connected:
for z in ([0,1] if q['type']=='thru_hole' else [0]):goals.append((z,q['g'].buffer(-.02)))
for c in copper:
if c['net']==net:
for z in c['zs']:goals.append((z,c['g'].buffer(-width/2+.025)))
a=masks(net,width);route=search(a,p,(t['x'],t['y']),goals)
if route:register(net,route,width);paths.append(route);connected.append(p)
else:failures.append([net,p['name'],'no clearance-safe path']);print('FAIL',net,p['name'],flush=True)
plans.append(dict(net=net,width=width,paths=paths));print(net,len(paths),'paths',flush=True)
Path('plan.json').write_text(json.dumps(plans,indent=2));Path('failures.json').write_text(json.dumps(failures,indent=2))
if __name__=='__main__':
t=time.time();plan_planes();plan_signals();print('seconds',time.time()-t)