seveibar/am3352-dev-board

A 324-ball TI AM3352 ARM processor with DDR3, LCD, USB, Ethernet, storage, serial, analog, clock, reset, power, and JTAG interfaces, paired with configurable JST-GH board connectors and constrained PCB routing.

Version
1.0.2
License
unset
Stars
0

scripts/optimize-decoupling.py

"""Optimize bottom capacitor placement against fixed BGA escape copper and courtyards.
Distances are planar power-pad-to-ball projections, not an inductance simulation.
"""
import json,runpy,math,random
from pathlib import Path
import numpy as np,shapely
from shapely.geometry import Point,box
from shapely.ops import unary_union
x=runpy.run_path('scripts/audit-copper.py');c=x['c'];data=json.loads(Path('design/routed-copper.json').read_text())
src={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'};components={src[e['source_component_id']]:e for e in c if e['type']=='pcb_component'};ps={e['source_port_id']:e for e in c if e['type']=='pcb_port'}
sourcePorts=[e for e in c if e['type']=='source_port'];powerNames={'GND','A3V3','DDR_1V5','RTC_1V8','V1V8','V3V3','VDD_CORE','VDD_MPU'}
movable={n for n in components if n.startswith(('C_U1_','C_DDR'))};movableIds={components[n]['pcb_component_id'] for n in movable};padIds={e['pcb_smtpad_id'] for e in c if e['type']=='pcb_smtpad' and e['pcb_component_id'] in movableIds}
removeTraceIds={f'fr_{sid}_{i}' for sid,rs in data['nets'].items() for i,r in enumerate(rs) if x['netnames'].get(x['find'](sid)) in powerNames and all(p['route_type']=='wire' and p['layer']=='bottom' for p in r)}
fixed=[(g,m) for g,m in zip(x['shapes'],x['meta']) if m[0]=='bottom' and m[2] not in padIds and m[2] not in removeTraceIds and not m[2].startswith('pcb_copper_pour')]
netGeometry={}
for name in powerNames:
 sid=next(e['source_net_id'] for e in c if e['type']=='source_net' and e['name']==name);net=x['find'](sid)
 obstacle=unary_union([g for g,m in fixed if m[1]!=net]).buffer(.105)
 # Mechanical through holes constrain component bodies too.
 netGeometry[name]=obstacle;shapely.prepare(obstacle)
vias={name:np.array([(p['x'],p['y']) for sid,rs in data['nets'].items() if x['netnames'].get(x['find'](sid))==name for r in rs for p in r if p['route_type']=='via']) for name in powerNames}
netForPort={p:n['name'] for t in c if t['type']=='source_trace' for n in c if n['type']=='source_net' and n['source_net_id'] in t.get('connected_source_net_ids',[]) for p in t.get('connected_source_port_ids',[])}
items=[]
for n in sorted(movable):
 if not n.startswith('C_U1_'):continue
 ball=n.removeprefix('C_U1_');p=next(p for p in sourcePorts if src[p['source_component_id']]=='U1' and p['name']==ball);q=ps[p['source_port_id']]
 items.append(dict(name=n,chip='U1',ball=ball,targetX=q['x'],targetY=q['y'],net=netForPort[p['source_port_id']]))
ram=[p for p in sourcePorts if src[p['source_component_id']]=='U3' and any(h.startswith('signal_VDD') for h in p['port_hints'])]
for i,p in enumerate(ram):
 q=ps[p['source_port_id']];num=p['pin_number'];ball='ABCDEFGHJKLMNPRT'[(num-1)%16]+str([1,2,3,7,8,9][(num-1)//16])
 items.append(dict(name=f'C_DDR{i}',chip='U3',ball=ball,targetX=q['x'],targetY=q['y'],net='DDR_1V5'))
print('Optimizing',len(items),'capacitors; preserving all vias and bottom signal traces',flush=True)
dx,dy=np.meshgrid(np.arange(-7,7.001,.2),np.arange(-7,7.001,.2));offsets=np.column_stack([dx.ravel(),dy.ravel()]);candidates=[]
for item in items:
 tx,ty=item['targetX'],item['targetY'];points=offsets+np.array([tx,ty]);v=vias[item['net']]
 if len(v):points=np.vstack([points,v[np.linalg.norm(v-[tx,ty],axis=1)<6]])
 found=[]
 for angle in [0,90,180,270]:
  ux,uy=round(math.cos(math.radians(angle))),round(math.sin(math.radians(angle)));px,py=points[:,0],points[:,1];cx,cy=px-.51*ux,py-.51*uy;gx,gy=px-1.02*ux,py-1.02*uy
  pw,ph=(.54,.64) if angle%180==0 else (.64,.54);cw,ch=(1.86,.94) if angle%180==0 else (.94,1.86)
  a=shapely.box(px-pw/2,py-ph/2,px+pw/2,py+ph/2);b=shapely.box(gx-pw/2,gy-ph/2,gx+pw/2,gy+ph/2)
  valid=~shapely.intersects(a,netGeometry[item['net']]) & ~shapely.intersects(b,netGeometry['GND']) & (abs(cx)+cw/2<34.5)&(abs(cy)+ch/2<29.5)
  px,py,cx,cy,gx,gy=[a[valid] for a in [px,py,cx,cy,gx,gy]]
  if not len(px):continue
  pinDist=np.hypot(px-tx,py-ty)
  nearPower=np.min(np.hypot(px[:,None]-v[:,0],py[:,None]-v[:,1]),axis=1) if len(v) else pinDist
  gv=vias['GND'];nearGround=np.min(np.hypot(gx[:,None]-gv[:,0],gy[:,None]-gv[:,1]),axis=1)
  score=pinDist+.25*nearPower+.25*nearGround+.15*pinDist**2
  found.extend(zip(score,cx,cy,np.full(len(px),angle),pinDist,nearPower,nearGround,cx-cw/2-.03,cy-ch/2-.03,cx+cw/2+.03,cy+ch/2+.03))
 a=np.array(sorted(found,key=lambda v:v[0])[:10000]);assert len(a),item['name'];candidates.append(a)
 print(item['name'],len(a),'sites; closest',round(float(a[:,4].min()),2),'mm',flush=True)
# Different greedy orders followed by one-at-a-time improvements avoid a single grid bias.
rng=random.Random(3352);best=None;bestScore=float('inf');N=len(items)
def pick(i,selected):
 a=candidates[i];others=np.array([candidates[j][k,7:11] for j,k in selected.items() if j!=i]);valid=np.ones(len(a),bool)
 if len(others):
  for loX,loY,hiX,hiY in others:valid &= (a[:,9]<loX)|(a[:,7]>hiX)|(a[:,10]<loY)|(a[:,8]>hiY)
 hits=np.flatnonzero(valid);return int(hits[0]) if len(hits) else None
scarcity=[int(sum(a[:,4]<2)) for a in candidates]
for trial in range(20):
 order=sorted(range(N),key=lambda i:scarcity[i]*(1 if trial==0 else rng.uniform(.4,1.6)))
 selected={}
 for i in order:
  k=pick(i,selected)
  if k is not None:selected[i]=k
 if len(selected)!=N:
  pending=[i for i in order if i not in selected];tabu={}
  for step in range(1800):
   if not pending:break
   i=pending.pop(0);a=candidates[i];conflicts=np.zeros(len(a));colliders={}
   for j,k in selected.items():
    b=candidates[j][k];hit=~((a[:,9]<b[7])|(a[:,7]>b[9])|(a[:,10]<b[8])|(a[:,8]>b[10]));conflicts+=hit
   merit=conflicts*30+a[:,0]
   for (ti,tk),penalty in tabu.items():
    if ti==i:merit[tk]+=penalty
   top=np.argsort(merit)[:min(12,len(a))];k=int(top[0] if step%7 else rng.choice(top[:5]))
   b=a[k];evicted=[j for j,v in selected.items() if not (b[9]<candidates[j][v,7] or b[7]>candidates[j][v,9] or b[10]<candidates[j][v,8] or b[8]>candidates[j][v,10])]
   for j in evicted:
    oldk=selected.pop(j);tabu[(j,oldk)]=tabu.get((j,oldk),0)+5;pending.append(j)
   selected[i]=k
  if len(selected)!=N:
   print('Trial',trial,'placed',len(selected),'/',N,flush=True);continue
 for repeat in range(3):
  for i in sorted(range(N),key=lambda i:-candidates[i][selected[i],0]):selected[i]=pick(i,selected)
 score=sum(candidates[i][k,0] for i,k in selected.items())
 if score<bestScore:bestScore=score;best=selected;print('Trial',trial,'score',round(score,2),'max projected distance',round(max(candidates[i][k,4] for i,k in selected.items()),2),flush=True)
assert best is not None,'No complete non-overlapping placement found'
report=[];placements={}
for i,k in best.items():
 item=items[i];a=candidates[i][k];name=item['name'];placements[name]={'x':round(float(a[1]),6),'y':round(float(a[2]),6),'rotation':int(a[3])}
 old=components.get(name);oldDist=None
 if old:
  sp=next(p for p in sourcePorts if src[p['source_component_id']]==name and p['pin_number']==1);q=ps[sp['source_port_id']];oldDist=math.hypot(q['x']-item['targetX'],q['y']-item['targetY'])
 report.append({**item,**placements[name],'oldPadToBallMm':oldDist,'padToBallMm':round(float(a[4]),4),'nearestPowerViaMm':round(float(a[5]),4),'nearestGroundViaMm':round(float(a[6]),4)})
Path('design/decoupling-placement.json').write_text(json.dumps(placements,indent=2)+'\n');Path('design/decoupling-report.json').write_text(json.dumps({'method':'Multi-start discrete placement, hard courtyard/via/signal-copper constraints; objective weights pad-to-ball distance and existing power/ground via proximity. Not a global optimality or inductance proof.','capacitors':report},indent=2)+'\n')
print('Saved placement: mean',round(np.mean([r['padToBallMm'] for r in report]),3),'mm; max',round(max(r['padToBallMm'] for r in report),3),'mm',flush=True)