astra/am3352-sbc
AM3352 single-board computer with DDR3, PMIC/USB‑C 5 V power, microSD, dual USB host, HDMI video, RGB indicators, and a PWM-driven buzzer.
- Version
- 0.1.18
- License
- unset
- Stars
- 0
scripts/open-local-signal-escapes.py
"""Search short CPU escapes on a fine local grid, preserving both reference layers.
Candidates require a fresh native build and every incremental acceptance audit.
"""
from pathlib import Path
exec(compile((Path(__file__).parent/'route-power-distribution.py').read_text().split("print('geometry loaded'")[0],str(Path(__file__).parent/'route-power-distribution.py'),'exec'))
from scipy.ndimage import label
from shapely import contains_xy
reference=json.loads((ROOT/'output/reference-baseline.audit.json').read_text());protected={}
for layer,outer in [('inner1','top'),('inner2','bottom')]:
planes=[];lines=[]
for e in reference:
if e['type']=='pcb_copper_pour' and e['layer']==outer:
b=e['brep_shape'];planes.append(Polygon([(p['x'],p['y']) for p in b['outer_ring']['vertices']], [[(p['x'],p['y']) for p in h['vertices']] for h in b['inner_rings']]))
if e['type']=='pcb_trace' and e['pcb_trace_id'].startswith('saved_phase_'):
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']==layer:lines.append(LineString([(a['x'],a['y']),(b['x'],b['y'])]))
protected[outer]=unary_union(planes).intersection(unary_union(lines).buffer(.1))
allpad=[]
for e in c:
if e['type']=='pcb_smtpad':
if e.get('points'):
allpad.append(Polygon([(v['x'],v['y']) for v in e['points']]).buffer(.16));continue
x,y=e['x'],e['y'];r=e.get('radius',0);w,h=e.get('width',2*r),e.get('height',2*r)
g=rotate(box(x-w/2,y-h/2,x+w/2,y+h/2),e.get('ccw_rotation',0),origin=(x,y));allpad.append(g.buffer(.16))
if e['type']=='pcb_via':allpad.append(Point(e['x'],e['y']).buffer(e['outer_diameter']/2+.253))
allpad=unary_union(allpad);routes=json.loads((ROOT/'design/remaining-routes.json').read_text());serial=max(int(r['name'].split('_')[-1]) for r in routes)+1;report=[]
for name in os.environ.get('ESCAPE_NETS','HDMI_HPD,MMC0_DAT0,MMC0_DAT1,USB1_VBUS').split(','):
t=next(t for t in terminals if labels.get(t['net'])==name and t['selector'].startswith('.U1 >'));n=t['net'];a=(t['x'],t['y']);area=Point(a).buffer(4)
foreign=[(g,ls) for g,ls,k in geoms if k!=n and g.intersects(area)]
top=unary_union([g for g,ls in foreign if 'top' in ls]).buffer(.153,quad_segs=32).union(protected['top'].buffer(.183,quad_segs=32));prepare(top)
drill=unary_union([g for g,ls in foreign]).buffer(.253,quad_segs=32).union(allpad).union(unary_union(list(protected.values())).buffer(.283,quad_segs=32));prepare(drill)
step=.05;radius=60;xx=a[0]+np.arange(-radius,radius+1)*step;yy=a[1]+np.arange(-radius,radius+1)*step
blocked=contains_xy(top,xx[None,:],yy[:,None]);via=contains_xy(drill,xx[None,:],yy[:,None]);regions,_=label(~blocked,np.ones((3,3)));reg=regions[radius,radius]
if not reg or not np.any((regions==reg)&~via):report.append({'net':name,'found':False,'reason':'No reachable via on local grid'});print(report[-1],flush=True);continue
def point(k):return (float(xx[k[0]]),float(yy[k[1]]))
start=(radius,radius);queue=[(0,start)];cost={start:0};parent={};seen=set();end=None
while queue:
d,k=heapq.heappop(queue)
if k in seen:continue
seen.add(k);p=point(k)
if d>4:break
if k!=start and not via[k[1],k[0]]:end=k;break
for dx,dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]:
q=(k[0]+dx,k[1]+dy);nd=d+step*math.hypot(dx,dy)
if not(0<=q[0]<=2*radius and 0<=q[1]<=2*radius) or blocked[q[1],q[0]] or nd>=cost.get(q,math.inf):continue
if top.intersects(LineString([p,point(q)])):continue
cost[q]=nd;parent[q]=k;heapq.heappush(queue,(nd,q))
if end is None:report.append({'net':name,'found':False,'reason':'Exact segment search failed'});print(report[-1],flush=True);continue
path=[end]
while path[-1]!=start:path.append(parent[path[-1]])
path=[point(k) for k in reversed(path)];simple=[path[0]];i=0
while i<len(path)-1:
j=len(path)-1
while j>i+1 and top.intersects(LineString([path[i],path[j]])):j-=1
simple.append(path[j]);i=j
b=simple[-1];world=[[x,y,0] for x,y in simple]+[[b[0],b[1],2]];angle=-math.radians(t['rotation']);origin=t['origin'];way=[]
def local(p):
x,y=p[0]-origin[0],p[1]-origin[1];return {'x':x*math.cos(angle)-y*math.sin(angle),'y':x*math.sin(angle)+y*math.cos(angle)}
for p in simple[1:]:way.append(local(p))
way.append({**local(b),'via':True,'fromLayer':'top','toLayer':'inner2'})
r={'name':f'REMAINING_{serial}','net':name,'from':t['selector'],'to':'net.'+name,'toPoint':local(b),'width':.1,'waypoints':way,'world':world};routes.append(r);serial+=1
for p,q in zip(simple,simple[1:]):geoms.append((LineString([p,q]).buffer(.05,quad_segs=32),['top'],n))
g=Point(b).buffer(.15,quad_segs=32);geoms.append((g,['top','inner1','inner2','bottom'],n));allpad=allpad.union(g.buffer(.253))
report.append({'net':name,'found':True,'via':b,'surfaceLengthMm':sum(math.dist(p,q) for p,q in zip(simple,simple[1:]))});print(report[-1],flush=True)
(ROOT/'design/remaining-routes.json').write_text(json.dumps(routes,indent=2)+'\n');(ROOT/'output/local-signal-escapes.json').write_text(json.dumps(report,indent=2)+'\n')