master
John Lauer Publish thermal bottleneck planner, accepted candidate, analysis and native recording scripts 0049da5 28d ago
"""Evaluate layer relocations against fixed pads/tracks; native refill/DRC is authoritative."""
import sys,copy,json,xml.etree.ElementTree as ET
from pathlib import Path
from shapely.geometry import LineString,Point
P=Path(__file__).resolve().parent;sys.path.insert(0,str(P.parent));from geometry import Geometry,LAYERS
BASE=P.parent/'thermal-expansion/recording/bq25792-expanded-thermal.brd'
t=ET.parse(BASE);b=t.find('drawing/board');m=Geometry()
for s in b.findall('signals/signal'):
 n=s.get('name')
 for w in s.findall('wire'):
  a=[float(w.get('x1')),float(w.get('y1'))];z=[float(w.get('x2')),float(w.get('y2'))]
  m.copper.append((n,int(w.get('layer')),LineString([a,z]).buffer(float(w.get('width'))/2)))
 for v in s.findall('via'):
  a=[float(v.get('x')),float(v.get('y'))];dia=float(v.get('diameter'))
  for l in LAYERS:m.copper.append((n,l,Point(a).buffer(dia/2)))
  m.holes.append((a,float(v.get('drill'))/2))
for n in ['N$1','SDA','VUSB','PGND']:
 s=next(s for s in b.findall('signals/signal') if s.get('name')==n)
 print(n,'pads',[c.attrib for c in s.findall('contactref')])
 for w in s.findall('wire'):
  if w.get('layer')!='16':continue
  a=[float(w.get('x1')),float(w.get('y1'))];z=[float(w.get('x2')),float(w.get('y2'))];width=float(w.get('width'))
  print(a,z,'clear layers',[l for l in [1,2,15] if m.clear([a,z],n,l,width)])
if __name__=='__main__':
 sys.path.insert(0,str(P.parent/'thermal'));from plan_priority import detour
 tasks=[('N$1',(14.88575625,11.00863125),(15.2,25.2),.15),('SDA',(25.2,1.2),(19.2,15.2),.15),('VUSB',(19.2,25.2),(17.2,21.2),.15)]
 routes=[]
 for net,a,z,width in tasks:
  options=[]
  for layer in [15,2,1]:
   pts=detour(m,a,z,net,layer,width,limit=18000,step=.2)
   print('planned',net,layer,pts,flush=True)
   if pts:
    options.append((LineString(pts).length,layer,pts));break
  if not options:
   print('RETAINED: no acceptable single-layer detour',net,flush=True);continue
  # Prefer inner 15 when available: keep thermal exterior clear and use layer2 as reference.
  options.sort(key=lambda q:(q[1]!=15,q[1]==1,q[0]));length,layer,pts=options[0]
  s=next(s for s in b.findall('signals/signal') if s.get('name')==net)
  removed=[dict(w.attrib) for w in s.findall('wire') if w.get('layer')=='16']
  for w in list(s.findall('wire')):
   if w.get('layer')=='16':s.remove(w)
  for a,z in zip(pts,pts[1:]):ET.SubElement(s,'wire',{'x1':str(a[0]),'y1':str(a[1]),'x2':str(z[0]),'y2':str(z[1]),'width':str(width),'layer':str(layer)})
  routes.append({'net':net,'layer':layer,'points':pts,'width':width,'removed':removed,'newVias':[],'lengthMm':length})
  m.add_route(net,[(layer,pts,width)],[])
 # Selected connector ground stubs: relocate only left/bottom sections to inner2, same width.
 s=next(s for s in b.findall('signals/signal') if s.get('name')=='PGND')
 for w in list(s.findall('wire')):
  if w.get('layer')!='16':continue
  a=[float(w.get('x1')),float(w.get('y1'))];z=[float(w.get('x2')),float(w.get('y2'))];width=float(w.get('width'))
  if max(a[0],z[0])>18 or max(a[1],z[1])>6:continue
  assert m.clear([a,z],'PGND',2,width)
  removed=dict(w.attrib);w.set('layer','2');routes.append({'net':'PGND','layer':2,'points':[a,z],'width':width,'removed':[removed],'newVias':[],'lengthMm':LineString([a,z]).length})
 for p in b.findall('signals/signal/polygonpour'):
  for fill in p.findall('polygonfilldetails'):p.remove(fill)
 (P/'candidate-01').mkdir(exist_ok=True);t.write(P/'candidate-01/bq25792-thermal-reroute.brd',encoding='utf-8',xml_declaration=True)
 (P/'candidate-01/plan.json').write_text(json.dumps(routes,indent=2))