AnasSarkiz/NEMA14_CH32X035G8U6
A NEMA stepper motor is shown with proposed M3 screws/spacers, four wired motor leads and splices, and a JST SHR-04V-S connector.
- Version
- 1.12.2
- License
- ISC
- Stars
- 0
scripts/repair-routing.py
"""Finish explicitly selected connections on saved copper.
A collision-aware grid assists the selected manual repairs; unchanged routes are retained.
All results must pass the independent tscircuit DRC and Gerber short checks.
"""
import json,math,pathlib,heapq,time
import numpy as np
from PIL import Image,ImageDraw
ROOT=pathlib.Path(__file__).resolve().parents[1];path=ROOT/'artifacts/board.circuit.json';j=json.loads(path.read_text())
STEP=.05;LOW=-17.5;N=701
src={e['source_component_id']:e for e in j if e['type']=='source_component'}
sp={e['source_port_id']:e for e in j if e['type']=='source_port'}
ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'}
comp={e['pcb_component_id']:e for e in j if e['type']=='pcb_component'}
nets={e['subcircuit_connectivity_map_key']:e for e in j if e['type']=='source_net'}
traces={e['subcircuit_connectivity_map_key']:e['source_trace_id'] for e in j if e['type']=='source_trace'}
pads=[e for e in j if e['type'] in ['pcb_smtpad','pcb_plated_hole']]
def xy(x,y):return (round((x-LOW)/STEP),round((y-LOW)/STEP))
def world(p):return (LOW+p[0]*STEP,LOW+p[1]*STEP)
def key(e):
if e['type'] in ['pcb_smtpad','pcb_plated_hole']:
p=ports.get(e.get('pcb_port_id'),{});return sp.get(p.get('source_port_id'),{}).get('subcircuit_connectivity_map_key')
return e.get('subcircuit_connectivity_map_key')
def findport(cname,pname):
return next(p for p in ports.values() if src[comp[p['pcb_component_id']]['source_component_id']]['name']==cname and sp[p['source_port_id']]['name']==pname)
def drawrect(draw,x,y,w,h,r=0):draw.rectangle([xy(x-w/2-r,y-h/2-r),xy(x+w/2+r,y+h/2+r)],fill=1)
def circle(draw,x,y,r):draw.ellipse([xy(x-r,y-r),xy(x+r,y+r)],fill=1)
def obstacle(net,width,via=False):
imgs=[Image.new('L',(N,N)) for _ in range(2)];ds=[ImageDraw.Draw(i) for i in imgs]
margin=.15+(.25 if via else width/2)+.018
for e in j:
typ=e['type']
if typ in ['pcb_smtpad','pcb_plated_hole']:
if key(e)==net and not via:continue
w=e.get('width',e.get('outer_width',e.get('outer_diameter',e.get('radius',0)*2)));h=e.get('height',e.get('outer_height',e.get('outer_diameter',e.get('radius',0)*2)))
if abs(e.get('ccw_rotation',0)%180-90)<.01:w,h=h,w
ls=[0 if l=='top' else 1 for l in e.get('layers',[e.get('layer','top')])]
for l in ls:drawrect(ds[l],e['x'],e['y'],w,h,margin)
elif typ=='pcb_trace' and key(e)!=net:
rr=e['route']
for a,b in zip(rr,rr[1:]):
if a['route_type']!='wire' or b['route_type']!='wire' or a['layer']!=b['layer']:continue
r=margin+max(a['width'],b['width'])/2;d=ds[0 if a['layer']=='top' else 1]
d.line([xy(a['x'],a['y']),xy(b['x'],b['y'])],fill=1,width=math.ceil(2*r/STEP))
circle(d,a['x'],a['y'],r);circle(d,b['x'],b['y'],r)
elif typ=='pcb_via':
if key(e)==net and not via:continue
r=.72 if via else e['outer_diameter']/2+margin
for d in ds:circle(d,e['x'],e['y'],r)
elif typ=='pcb_keepout':
c=e['center']
for d in ds:drawrect(d,c['x'],c['y'],e.get('width',e.get('radius',0)*2),e.get('height',e.get('radius',0)*2),.15+(.25 if via else width/2)+.018)
elif typ=='pcb_hole':
for d in ds:circle(d,e['x'],e['y'],e['hole_diameter']/2+.2+(.25 if via else width/2)+.018)
border=.3+(.25 if via else width/2)
for d in ds:
b=math.ceil(border/STEP);d.rectangle([0,0,N-1,b],fill=1);d.rectangle([0,N-1-b,N-1,N-1],fill=1);d.rectangle([0,0,b,N-1],fill=1);d.rectangle([N-1-b,0,N-1,N-1],fill=1)
return [np.asarray(im,dtype=np.bool_) for im in imgs]
# Move the VM_SENSE bottom-layer diagonal away from the PD_GOOD escape corridor.
removed=next(e for e in j if e.get('pcb_trace_id')=='freerouted_trace_227' and e['type']=='pcb_trace')
assert removed.get('source_net_id')==next(e['source_net_id'] for e in j if e['type']=='source_net' and e['name']=='VM_SENSE')
j.remove(removed)
j[:]=[e for e in j if e.get('pcb_trace_id')!='freerouted_trace_226' and not(e['type']=='pcb_via' and math.hypot(e['x']-3.804,e['y']+1.2898)<.01)]
# Shift the A+ fanout via right/up to open a compliant B+ escape between the two vias.
for e in j:
if e['type']=='pcb_via' and math.hypot(e['x']-.0359,e['y']+4.5644)<.01:e['x'],e['y']=.45,-4.35
if e['type']=='pcb_trace':
for a in e['route']:
if math.hypot(a['x']-.0359,a['y']+4.5644)<.01:a['x'],a['y']=.45,-4.35
# Remove the redundant leftward A+ fanout jog that obstructed the adjacent B+ escape.
for e in j:
if e['type']=='pcb_trace' and key(e) in nets and nets[key(e)]['name']=='A_PLUS':
for a in e['route']:
if a.get('layer')=='top' and -.01<a['x']<.3 and -5<a['y']<-4:a['x']=.5
# Flip C_VCP so its supply pad faces away from the charge-pump routing pocket.
cc=next(c for c in comp.values() if src[c['source_component_id']]['name']=='C_VCP');cid=cc['pcb_component_id'];cc['rotation']=270
for e in j:
if e.get('pcb_component_id')==cid and e['type'] in ['pcb_port','pcb_smtpad','pcb_solder_paste']:e['y']=-23.2-e['y']
# Reroute the VCP jog that enclosed the capacitor supply pad.
j[:]=[e for e in j if e.get('pcb_trace_id') not in ['freerouted_trace_277','freerouted_trace_278']]
# Relocate the MCU bypass capacitor to open the voltage-sense escape.
cc=next(c for c in comp.values() if src[c['source_component_id']]['name']=='C_MCU');cid=cc['pcb_component_id']
for e in j:
if e.get('pcb_component_id')==cid:
if 'x' in e:e['x']+=.1;e['y']-=1.5
if 'center' in e:e['center']['x']+=.1;e['center']['y']-=1.5
if 'anchor_position' in e:e['anchor_position']['x']+=.1;e['anchor_position']['y']-=1.5
if 'display_offset_x' in e:e['display_offset_x']+=.1;e['display_offset_y']-=1.5
if 'position' in e:e['position']['x']+=.1;e['position']['y']-=1.5
for p0 in e.get('route',[]):
if 'x' in p0:p0['x']+=.1;p0['y']-=1.5
j[:]=[e for e in j if e.get('pcb_trace_id') not in ['freerouted_trace_42','freerouted_trace_201']]
# Move the 3V3 bottom diagonal out of the status-signal escape corridor.
j[:]=[e for e in j if e.get('pcb_trace_id') not in ['freerouted_trace_185','freerouted_trace_234']]
# Reserve three non-crossing top fanouts before routing their bottom continuations.
fanout_records=[]
fanouts={'PD_GOOD':(2.95,-.3),'DATA_PRESENT':(3.2,-1.1),'VM_SENSE':(4.3,-1.9)}
for name,(x,y) in fanouts.items():
p=findport('U_MCU',name);net=sp[p['source_port_id']]['subcircuit_connectivity_map_key'];tid='manual_fanout_'+name
bend=(p['x'],y) if name!='PD_GOOD' else (x,p['y'])
route=[{'route_type':'wire','x':p['x'],'y':p['y'],'layer':'top','width':.16,'start_pcb_port_id':p['pcb_port_id']},{'route_type':'wire','x':bend[0],'y':bend[1],'layer':'top','width':.16},{'route_type':'wire','x':x,'y':y,'layer':'top','width':.16},{'route_type':'via','x':x,'y':y,'from_layer':'top','to_layer':'bottom','via_diameter':.5,'via_hole_diameter':.25},{'route_type':'wire','x':x,'y':y,'layer':'bottom','width':.16}]
j.append({'type':'pcb_trace','pcb_trace_id':tid,'route':route,'source_net_id':nets[net]['source_net_id'],'source_trace_id':traces[net],'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','pcb_port_ids':[p['pcb_port_id']]})
fanout_records.append({'connection':'U_MCU.'+name,'net':name,'route':route,'vias':1,'method':'reserved top fanout'})
j.append({'type':'pcb_via','pcb_via_id':tid+'_via','pcb_trace_id':tid,'source_trace_id':traces[net],'source_net_id':nets[net]['source_net_id'],'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','x':x,'y':y,'hole_diameter':.25,'outer_diameter':.5,'layers':['top','bottom']})
repairs=fanout_records
for cname,pname in [('U_MCU','PD_GOOD'),('U_MCU','VM_SENSE'),('U_MCU','DATA_PRESENT'),('U_DRV','OUT2A'),('C_VCP','pin1'),('C_VCP','pin2'),('PATCH','ENABLE_N'),('PATCH','V3V3'),('C_MCU','pin1'),('C_MCU','pin2')]:
if cname=='PATCH':
x,y=((5.3187,-2.7939) if pname=='V3V3' else (.4759,-.764));p={'x':x,'y':y,'pcb_port_id':None};net=next(k for k,v in nets.items() if v['name']==pname)
else:p=findport(cname,pname);net=sp[p['source_port_id']]['subcircuit_connectivity_map_key']
width=.16
goals={}
for e in j:
if e['type']=='pcb_trace' and key(e)==net and not e['pcb_trace_id'].startswith('manual_fanout'):
for a in e['route']:
if a['route_type']=='wire':gx,gy=xy(a['x'],a['y']);goals[(gx,gy,0 if a['layer']=='top' else 1)]=(a['x'],a['y'])
if not goals:
for pp in ports.values():
if pp['pcb_port_id']!=p['pcb_port_id'] and sp[pp['source_port_id']].get('subcircuit_connectivity_map_key')==net:
gx,gy=xy(pp['x'],pp['y']);goals[(gx,gy,0)]=(pp['x'],pp['y'])
if any(e.get('pcb_port_id')==pp['pcb_port_id'] and e['type']=='pcb_plated_hole' for e in pads):goals[(gx,gy,1)]=(pp['x'],pp['y'])
if pname=='VM_SENSE':
goals={g:v for g,v in goals.items() if math.hypot(v[0]-p['x'],v[1]-p['y'])>2}
if cname=='PATCH':
x,y=((2.2473,2.1127) if pname=='V3V3' else (7.1997,-1.3061));gx,gy=xy(x,y);goals={(gx,gy,1):(x,y),(gx,gy,0):(x,y)}
if cname=='PATCH' and pname=='V3V3':
goals={}
for t in j:
if t.get('pcb_trace_id') in ['freerouted_trace_180','freerouted_trace_184']:
for a in t['route']:
gx,gy=xy(a['x'],a['y']);goals[(gx,gy,0 if a['layer']=='top' else 1)]=(a['x'],a['y'])
if cname=='C_MCU' and pname=='pin1':
gx,gy=xy(4.675,-2.6267);goals={(gx,gy,0):(4.675,-2.6267)}
if cname=='C_MCU' and pname=='pin2':
gx,gy=xy(6.9767,-1.9);goals={(gx,gy,0):(6.9767,-1.9)}
# Remove the source pad itself from candidate targets.
goals={g:v for g,v in goals.items() if math.hypot(v[0]-p['x'],v[1]-p['y'])>.25}
if not goals:raise RuntimeError('No target copper for '+cname+'.'+pname)
if cname=='U_MCU' and pname in fanouts:
x,y=fanouts[pname];p={**p,'x':x,'y':y,'pcb_port_id':None}
blocked=obstacle(net,width);vb=obstacle(net,width,True);start=(*xy(p['x'],p['y']),1 if cname=='PATCH' and pname!='V3V3' or cname=='U_MCU' and pname in fanouts else 0)
pts=np.asarray([[g[0],g[1],g[2]] for g in goals]);lo=pts[:,:2].min(axis=0);hi=pts[:,:2].max(axis=0)
def heuristic(s):return max(lo[0]-s[0],0,s[0]-hi[0])+max(lo[1]-s[1],0,s[1]-hi[1])
heap=[(heuristic(start),0,start)];best={start:0};prev={};end=None;expanded=0
while heap:
_,cost,s=heapq.heappop(heap)
if cost!=best.get(s):continue
if s in goals:end=s;break
expanded+=1
if expanded>600000:break
x,y,l=s
options=[(x+dx,y+dy,l,1.41421356 if dx and dy else 1) for dx,dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]]
if not vb[0][y,x] and not vb[1][y,x]:options.append((x,y,1-l,35))
for nx,ny,nl,dc in options:
if not(0<=nx<N and 0<=ny<N) or blocked[nl][ny,nx]:continue
if nl==l and nx!=x and ny!=y and (blocked[l][y,nx] or blocked[l][ny,x]):continue
t=(nx,ny,nl);nc=cost+dc
if nc+1e-8<best.get(t,float('inf')):best[t]=nc;prev[t]=s;heapq.heappush(heap,(nc+heuristic(t),nc,t))
if end is None:pathlib.Path('/tmp/repair-debug.json').write_text(json.dumps(j));raise RuntimeError(f'No collision-free repair for {cname}.{pname} after {expanded} nodes')
nodes=[end]
while nodes[-1]!=start:nodes.append(prev[nodes[-1]])
nodes.reverse()
# Retain turns and layer changes; simplify only exact collinear runs.
slim=[nodes[0]]
for i in range(1,len(nodes)-1):
a,b,c=nodes[i-1:i+2]
if a[2]!=b[2] or b[2]!=c[2] or (b[0]-a[0],b[1]-a[1])!=(c[0]-b[0],c[1]-b[1]):slim.append(b)
slim.append(nodes[-1]);route=[];newvias=[]
for i,s in enumerate(slim):
x,y=world(s);layer='top' if s[2]==0 else 'bottom'
if i==0:x,y=p['x'],p['y']
if i==len(slim)-1:x,y=goals[end]
if route and route[-1]['layer']!=layer:
old=route[-1]['layer'];route.append({'route_type':'via','x':x,'y':y,'from_layer':old,'to_layer':layer,'via_diameter':.5,'via_hole_diameter':.25})
newvias.append((x,y))
route.append({'route_type':'wire','x':x,'y':y,'width':width,'layer':layer})
if p['pcb_port_id']:route[0]['start_pcb_port_id']=p['pcb_port_id']
tid=f'manual_repair_{cname}_{pname}'
j.append({'type':'pcb_trace','pcb_trace_id':tid,'route':route,'source_net_id':nets[net]['source_net_id'],'source_trace_id':traces[net],'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','pcb_port_ids':[p['pcb_port_id']] if p['pcb_port_id'] else []})
for i,(x,y) in enumerate(newvias):j.append({'type':'pcb_via','pcb_via_id':tid+'_via'+str(i),'pcb_trace_id':tid,'source_trace_id':traces[net],'source_net_id':nets[net]['source_net_id'],'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','x':x,'y':y,'hole_diameter':.25,'outer_diameter':.5,'layers':['top','bottom']})
repairs.append({'connection':cname+'.'+pname,'net':nets[net]['name'],'points':len(route),'vias':len(newvias),'expandedNodes':expanded,'route':route})
print(repairs[-1]['connection'],nets[net]['name'],len(route),'points',len(newvias),'vias',expanded,'searched nodes',flush=True)
for e in j:
if e['type']=='pcb_via':e['tented_on_bottom']=True;e['tented_on_top']=True
path.write_text(json.dumps(j,indent=2)+'\n');(ROOT/'artifacts/manual-repairs.json').write_text(json.dumps(repairs,indent=2)+'\n')