master
John Lauer Preserve validated CRR thermal routing recipe and narrated demonstration 92d2870 1mo ago
"""BQ-specific estimate of bottom copper physically connected to footprint thermal pads."""
import sys,json,xml.etree.ElementTree as ET
from pathlib import Path
from collections import defaultdict
from shapely.geometry import Polygon,Point,LineString
from shapely.ops import unary_union
from shapely import make_valid
P=Path(__file__).resolve().parent;sys.path.insert(0,str(P.parent));from geometry import Geometry
m=Geometry()
def ring(e):
 vs=e.findall('vertex');assert vs and not any(v.get('curve') for v in vs)
 return [(float(v.get('x')),float(v.get('y'))) for v in vs]
def measure(file):
 b=ET.parse(file).find('drawing/board');cu=defaultdict(list)
 holes=[Point(at).buffer(r,quad_segs=24) for at,r in m.holes]
 for p in m.pads.values():
  if 16 in p['layers']:cu[p['net']].append(p['shape'])
 for s in b.findall('signals/signal'):
  n=s.get('name')
  for v in s.findall('via'):
   at=Point(float(v.get('x')),float(v.get('y')));dr=float(v.get('drill'));dia=float(v.get('diameter'))
   holes.append(at.buffer(dr/2,quad_segs=24))
   if v.get('extent')=='1-16':cu[n].append(at.buffer(dia/2,quad_segs=24))
  for w in s.findall('wire'):
   if w.get('layer')!='16':continue
   if float(w.get('curve','0')):raise ValueError('Arc needs tessellation')
   cu[n].append(LineString([(float(w.get('x1')),float(w.get('y1'))),(float(w.get('x2')),float(w.get('y2')))]).buffer(float(w.get('width'))/2,quad_segs=24))
  for p in s.findall('polygonpour'):
   if p.get('layer')!='16':continue
   shapes=p.findall('polygonfilldetails/polygonshape');assert shapes,'Native fill contours missing: refill and export before measuring'
   for shape in shapes:
    outer=shape.find('polygonoutlinesegments')
    if outer is None:outer=shape.find('polygonoutlineobjects')
    if outer is None:continue
    holes=[ring(h) for h in shape.findall('polygonholelist/polygonholesegments')]
    cu[n].append(make_valid(Polygon(ring(outer),holes)))
 drill=unary_union(holes)
 result=[]
 for q in ['Q1','Q2','Q3','Q4','Q5']:
  pads=[p for p in m.pads.values() if p['name'].startswith(q+'.P$')];net=pads[0]['net'];u=unary_union(cu[net]).difference(drill);parts=list(u.geoms) if hasattr(u,'geoms') else [u];connected=[g for g in parts if any(g.intersects(p['shape']) for p in pads)];area=unary_union(connected).area
  result.append({'component':q,'thermalNet':net,'footprintThermalHoles':len(pads),'bottomConnectedCopperEstimateMm2':area,'connectedRegions':len(connected)})
 return result
if __name__=='__main__':
 files=[P.parent/'ablation/take-02/bq25792-ablation-routed.brd',P/'recording-02/bq25792-astra-thermal-routed.brd'];r={str(f):measure(f) for f in files};(P/'thermal-area-comparison.json').write_text(json.dumps(r,indent=2));print(json.dumps(r,indent=2))