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/plane-tree.py

"""Represent ground-plane connectivity with explicit tracks entirely inside the pour.
Constrained triangulation keeps every added track inside the existing ground copper.
This also makes via/through-hole plane contacts visible to trace-only PCB checks.
"""
import json,math,heapq
from pathlib import Path
import shapely
from shapely.geometry import Point,Polygon,LineString
from shapely.ops import unary_union
from shapely.prepared import prep
from shapely.strtree import STRtree
c=json.loads(Path('dist/index/circuit.json').read_text());data=json.loads(Path('design/routed-copper.json').read_text())
import sys
name=sys.argv[1] if len(sys.argv)>1 else 'GND'
layer={'GND':'inner1','A3V3':'inner2','DDR_1V5':'inner2','VDD_CORE':'inner3','VDD_MPU':'inner4','V1V8':'inner5','V3V3':'bottom'}[name]
net=next(x['source_net_id'] for x in c if x['type']=='source_net' and x['name']==name)
def ring(vertices):
 result=[]
 for i,p in enumerate(vertices):
  q=vertices[(i+1)%len(vertices)];result.append((p['x'],p['y']));b=p.get('bulge',0)
  if abs(b)<1e-10:continue
  dx=q['x']-p['x'];dy=q['y']-p['y'];cx=(p['x']+q['x'])/2-dy*(1-b*b)/(4*b);cy=(p['y']+q['y'])/2+dx*(1-b*b)/(4*b)
  a=math.atan2(p['y']-cy,p['x']-cx);theta=4*math.atan(b);r=math.hypot(p['x']-cx,p['y']-cy);n=max(2,math.ceil(abs(theta)/(math.pi/64)))
  result.extend((cx+r*math.cos(a+theta*k/n),cy+r*math.sin(a+theta*k/n)) for k in range(1,n))
 return result
polys=[]
for p in c:
 if p['type']=='pcb_copper_pour' and p.get('source_net_id')==net and p['layer']==layer:
  b=p['brep_shape'];polys.append(Polygon(ring(b['outer_ring']['vertices']),[ring(h['vertices']) for h in b['inner_rings']]).buffer(0))
assert polys,'Build the board with the ground pour before constructing plane tracks'
if '--include-contacts' in sys.argv:
 import runpy
 ctx=runpy.run_path('scripts/audit-copper.py');canonical=ctx['find'](net)
 polys=[g for g,m in zip(ctx['shapes'],ctx['meta']) if m[0]==layer and m[1]==canonical]
 # Scratch checkpoints may contain pours from before a newly added via.
 # Clip actual foreign copper before deriving explicit centerlines.
 obstacles=unary_union([g for g,m in zip(ctx['shapes'],ctx['meta']) if m[0]==layer and m[1]!=canonical and not m[2].startswith('pcb_copper_pour')])
 safe=unary_union(polys).difference(obstacles.buffer(.10001)).buffer(-.05001)
else:
 safe=unary_union(polys).simplify(.002,preserve_topology=True).buffer(-.055)
prepared=prep(safe)
counts=data.setdefault('planeBaseRouteCounts',{})
base=data['nets'][net][:counts.get(net,len(data['nets'][net]))];counts[net]=len(base)
anchors={(round(p['x'],6),round(p['y'],6)) for r in base for p in r if p['route_type']=='via'}
sourcePorts={p for t in c if t['type']=='source_trace' and net in t.get('connected_source_net_ids',[]) for p in t['connected_source_port_ids']}
pcbPorts={p['pcb_port_id'] for p in c if p['type']=='pcb_port' and p['source_port_id'] in sourcePorts}
anchors.update((p['x'],p['y']) for p in c if (p['type']=='pcb_plated_hole' or (p['type']=='pcb_smtpad' and p['layer']==layer)) and p.get('pcb_port_id') in pcbPorts)
anchors=sorted(a for a in anchors if prepared.covers(Point(a)));assert anchors
triangles=list(shapely.constrained_delaunay_triangles(safe).geoms);tree=STRtree(triangles);centers=[tuple(t.centroid.coords[0]) for t in triangles]
edges={};adj=[[] for _ in triangles]
for i,t in enumerate(triangles):
 vs=list(t.exterior.coords)[:-1]
 for a,b in zip(vs,vs[1:]+vs[:1]):
  edge=tuple(sorted((a,b)))
  if edge in edges:
   j=edges[edge];mid=((a[0]+b[0])/2,(a[1]+b[1])/2);cost=math.dist(centers[i],mid)+math.dist(mid,centers[j]);adj[i].append((j,cost,mid));adj[j].append((i,cost,mid))
  else:edges[edge]=i
locations=[]
for a in anchors:
 hits=tree.query(Point(a),predicate='intersects')
 assert len(hits),f'Ground anchor outside usable plane: {a}'
 locations.append(int(hits[0]))
distance={};parent={};rootFor={};rootAnchor={}
for anchor,root in zip(anchors,locations):
 if root in distance:continue
 rootAnchor[root]=anchor;distance[root]=0.;rootFor[root]=root;queue=[(0.,root)]
 while queue:
  d,i=heapq.heappop(queue)
  if d!=distance[i]:continue
  for j,cost,mid in adj[i]:
   if d+cost<distance.get(j,float('inf')):distance[j]=d+cost;parent[j]=(i,mid);rootFor[j]=root;heapq.heappush(queue,(d+cost,j))
routes=[]
for a,i in zip(anchors,locations):
 root=rootFor[i]
 if a==rootAnchor[root]:continue
 assert i in distance,f'Ground plane island at {a}'
 points=[a,centers[i]]
 while i!=root:i,mid=parent[i];points.extend([mid,centers[i]])
 points.append(rootAnchor[root]);simple=[points[0]];j=0
 while j<len(points)-1:
  for k in range(len(points)-1,j,-1):
   if points[j]==points[k] or prepared.covers(LineString([points[j],points[k]])):break
  assert k>j and (points[j]==points[k] or prepared.covers(LineString([points[j],points[k]])))
  if points[k]!=simple[-1]:simple.append(points[k])
  j=k
 if len(simple)>1:routes.append([{'route_type':'wire','x':x,'y':y,'width':.1,'layer':layer} for x,y in simple])
data['nets'][net]=base+routes;Path('design/routed-copper.json').write_text(json.dumps(data,separators=(',',':'))+'\n')
print(name,'plane:',len(anchors),'contacts;',len(routes),'explicit paths contained within existing copper')