main
John Lauer Independent end-to-end layout workflow and explicitly scoped Astra ESC source e17f9f3 23d ago
from bridge import *
import math,re
s=json.load(open(D/'evidence/state.json'));F={p['ref']:p for p in s['footprints']};P=json.load(open(D/'evidence/local-pads.json'))
poses={r:(p['x'],p['y'],p['rotation']) for r,p in F.items() if p['locked']};boxes={r:tuple(p['courtyard'][k] for k in ['minX','minY','maxX','maxY']) for r,p in F.items() if p['locked']};order=[];groups={};targets={}
def rot(x,y,a):
 c=round(math.cos(math.radians(a)),9);t=round(math.sin(math.radians(a)),9);return c*x+t*y,-t*x+c*y
def box(r,x,y,a):
 p=F[r];b=p['courtyard'];corners=[rot(xx-p['x'],yy-p['y'],a) for xx in [b['minX'],b['maxX']] for yy in [b['minY'],b['maxY']]]
 return (x+min(v[0] for v in corners),y+min(v[1] for v in corners),x+max(v[0] for v in corners),y+max(v[1] for v in corners))
def fits(b,skip=None,gap=.25):
 if b[0]<104.5 or b[1]<58.5 or b[2]>167.5 or b[3]>131.5:return False
 return not any(r!=skip and b[0]<q[2]+gap and b[2]>q[0]-gap and b[1]<q[3]+gap and b[3]>q[1]-gap for r,q in boxes.items())
def setp(r,x,y,a,g):
 b=box(r,x,y,a)
 assert fits(b,r),(r,b,[(r1,b1) for r1,b1 in boxes.items() if b[0]<b1[2]+.25 and b[2]>b1[0]-.25 and b[1]<b1[3]+.25 and b[3]>b1[1]-.25])
 poses[r]=(x,y,a);boxes[r]=b;order.append(r);groups[r]=g
anchors=[('C2',140.7,76,0,'battery'),('C3',154.4,76,0,'battery'),('D1',141,68,0,'battery'),('R32',128,70,0,'sense'),('U6',127,75,90,'sense'),('U2',116,74,0,'supplies'),('U3',118,82,0,'supplies'),('U1',131,84,0,'supplies'),('U5',123,97,0,'control'),('U4',138.5,99,0,'driver'),('J1',113,113,0,'debug'),('SW1',108,101.5,0,'debug'),('Y1',115.3,96.5,0,'crystal')]
for phase,y,hi,lo in [('C',85,'Q5','Q6'),('B',99,'Q3','Q4'),('A',113,'Q1','Q2')]:anchors.extend([(hi,147.5,y,180,'phase'+phase),(lo,156,y,0,'phase'+phase)])
for r,x,y,a,g in anchors:setp(r,x,y,a,g)
def pad(r,n):
 p=next(p for p in P[r] if p['pad']==str(n));x,y,a=poses[r];dx,dy=rot(p['x'],p['y'],a);return x+dx,y+dy
# Explicit electrical placement targets. Smaller parts are oriented using their actual pad nets.
def want(r,g,xy,links=[]):targets[r]=dict(group=g,xy=xy,links=links)
def near(r,g,owner,pn):want(r,g,pad(owner,pn),[(owner,str(pn),8)])
for r,pn in [('C24',1),('C25',24),('C26',36),('C27',48)]:near(r,'control','U5',pn)
for r in ['C28','C29','FB1']:near(r,'analog','U5',20)
near('C30','control','U5',48);near('C44','sense','U6',6)
for r,pn in [('C35',1),('C36',3)]:near(r,'crystal','Y1',pn)
for r,pn in [('C39',7),('R20',7),('R19',45),('R17',43),('R18',44),('R21',26),('R22',26),('C40',26)]:near(r,'control','U5',pn)
for r in ['C6','C23']:near(r,'supplies','U3',5)
near('C9','supplies','U3',1)
for r in ['C4','C5','R1']:near(r,'supplies','U2',3)
near('C1','supplies','U2',8)
for r in ['C10','C11']:near(r,'supplies','U2',4)
want('D2','supplies',(111.5,69));near('JP1','supplies','U2',2)
near('C7','supplies','U1',2);near('C8','supplies','U1',3)
for r in ['C20','C22']:near(r,'driver','U4',7)
for r,pn in [('C12',20),('C13',17),('C21',14)]:near(r,'driver','U4',pn)
for phase,hi,lo,rs,caps in [('A','Q1','Q2',[2,3,4,5],[14,15,31,32]),('B','Q3','Q4',[7,8,9,10],[16,17,33,34]),('C','Q5','Q6',[12,13,14,15],[18,19,37,38])]:
 for n in rs[:2]:near('R'+str(n),'phase'+phase,hi,4)
 for n in rs[2:]:near('R'+str(n),'phase'+phase,lo,4)
 for n in caps:near('C'+str(n),'phase'+phase,hi,5)
for r in ['R23','R25','C41']:near(r,'analog','U5',14)
for rs,pn in [(['R24','R26','R27','C42'],13),(['R28','R29','R30','C43'],12),(['R31','R33','R34','C45'],8)]:
 for r in rs:near(r,'analog','U5',pn)
for led,res,xy in [('LED1','R6',(130,89)),('LED2','R11',(115,87)),('LED3','R16',(123,88))]:
 want(led,'indicators',xy);want(res,'indicators',(xy[0]+2,xy[1]))
for r in F:
 if r.startswith('TP'):
  net=P[r][0]['net'];refs=[q for q in poses if not F[q]['locked'] and any(p['net']==net for p in P[q])]
  if refs:
   q=min(refs,key=lambda q:0 if q.startswith('U') else 1);pn=next(p['pad'] for p in P[q] if p['net']==net);near(r,'test',q,pn)
  else:want(r,'test',(125,108))
# Supply test pads adjacent to accessible power groups; phase test pads beside fixed phase contacts.
for r,xy in [('TP7',(143,122)),('TP8',(146,68)),('TP12',(160,117)),('TP13',(160,103)),('TP14',(160,89)),('TP10',(113,77)),('TP15',(113,80)),('TP16',(113,90)),('TP17',(113,87))]:want(r,'test',xy)
assert set(F)==set(poses)|set(targets),set(F)-set(poses)-set(targets)
# Prioritize capacitors and pin-critical components over testpoints/indicators.
def prio(r):
 return (3 if r.startswith('TP') else 2 if r.startswith('LED') else 0 if r.startswith('C') or r in ['FB1','D2'] else 1,-(F[r]['courtyard']['maxX']-F[r]['courtyard']['minX'])*(F[r]['courtyard']['maxY']-F[r]['courtyard']['minY']))
for r in sorted(targets,key=prio):
 t=targets[r];tx,ty=t['xy'];best=None
 for a in [0,90,180,270]:
  for ix in range(-40,41):
   x=round(tx*2)/2+ix*.5
   for iy in range(-40,41):
    y=round(ty*2)/2+iy*.5
    dist=(x-tx)**2+(y-ty)**2
    if best and dist>best[0]:continue
    b=box(r,x,y,a)
    if not fits(b):continue
    # Match pad polarity and signal pads to the intended IC or transistor pin.
    score=dist
    for owner,pn,w in t['links']:
     op=next(p for p in P[owner] if p['pad']==pn);pts=[p for p in P[r] if p['net']==op['net']]
     if pts:
      ox,oy=pad(owner,pn);score+=w*min((x+rot(p['x'],p['y'],a)[0]-ox)**2+(y+rot(p['x'],p['y'],a)[1]-oy)**2 for p in pts)
    if best is None or score<best[0]:best=(score,x,y,a)
 if best is None:raise RuntimeError(('no fit',r))
 _,x,y,a=best;setp(r,x,y,a,t['group'])
plan=[dict(ref=r,x=poses[r][0],y=poses[r][1],rotation=poses[r][2],group=groups[r]) for r in order]
(D/'evidence/plan.json').write_text(json.dumps(plan,indent=2));(D/'evidence/planned-boxes.json').write_text(json.dumps(boxes));print('planned',len(plan),'fixed',len(F)-len(plan))
# Local plot, not a claim of editor validation.
import matplotlib;matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
fig,ax=plt.subplots(figsize=(10,11));colors={'battery':'orange','sense':'cyan','supplies':'gold','control':'green','driver':'purple','phaseA':'red','phaseB':'red','phaseC':'red','analog':'lightgreen','test':'gray','crystal':'blue','debug':'teal','indicators':'gray'}
for r,(x1,y1,x2,y2) in boxes.items():
 ax.add_patch(Rectangle((x1,y1),x2-x1,y2-y1,facecolor=colors.get(groups.get(r),'silver'),edgecolor='black',alpha=.6));ax.text((x1+x2)/2,(y1+y2)/2,r,ha='center',va='center',fontsize=7)
ax.set_xlim(103,169);ax.set_ylim(133,57);ax.set_aspect('equal');ax.grid(alpha=.2);fig.savefig(D/'evidence/plan.png',dpi=160)