AnasSarkiz/NEMA14_RP2040

A NEMA motor assembly concept with a controller board, four proposed M3 screws and spacers, motor lead splices, and a JST 4-pin connector harness.

Version
1.13.0
License
ISC
Stars
0

scripts/repair-enlarged-vias.py

"""Relocate enlarged vias only into independently clear nearby positions.

Unrepairable old vias and conflicting trace legs are removed for physical
rerouting. This is construction, never a qualification result.
"""
import json,math
from shapely.geometry import Point,LineString,box
from shapely.strtree import STRtree
import verify_supplier_connectivity as g
j=g.j;stack=['top','inner1','inner2','bottom'];src=g.src;rows=[]
def objects():
 shapes=[];owners=[];ls=[]
 for e in j:
  if e['type'] in ['pcb_smtpad','pcb_plated_hole']:shapes.append(g.geometry(e));owners.append(e);ls.append(set(e.get('layers',[e.get('layer','top')])))
  elif e['type']=='pcb_via':shapes.append(Point(e['x'],e['y']).buffer(e['outer_diameter']/2));owners.append(e);ls.append(set(stack))
  elif e['type']=='pcb_trace':
   for a,b in zip(e['route'],e['route'][1:]):
    if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:
     shapes.append(LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2));owners.append(e);ls.append({a['layer']})
  elif e['type']=='pcb_hole':shapes.append(Point(e['x'],e['y']).buffer(e['hole_diameter']/2+.2));owners.append(e);ls.append(set(stack))
  elif e['type']=='pcb_keepout':c=e['center'];r=e['radius'];shapes.append(box(c['x']-r,c['y']-r,c['x']+r,c['y']+r));owners.append(e);ls.append(set(stack))
 return shapes,owners,ls,STRtree(shapes)
thermal=[]
for p in j:
 if p['type']=='pcb_smtpad' and p.get('width',0)>1 and p.get('height',0)>1:
  name=src[g.comp[p['pcb_component_id']]['source_component_id']]
  if name in ['U_MCU','U_DRV','U_PD']:thermal.append((p['x'],p['y']))
cached=None
for v in list(j):
 if v['type']!='pcb_via' or v.get('pcb_via_id','').startswith(('usb_','oscillator_')):continue
 if g.nets[g.key(v)]=='GND' and any(math.dist((v['x'],v['y']),p)<.001 for p in thermal):continue
 if cached is None:cached=objects()
 shapes,owners,ls,tree=cached;key=g.key(v);old=(v['x'],v['y'])
 def clear(x,y):
  shape=Point(x,y).buffer(v['outer_diameter']/2)
  if not box(-17.15,-17.15,17.15,17.15).covers(shape):return False
  for i in tree.query(shape.buffer(.153)):
   e=owners[i]
   if e is v:continue
   same=g.key(e)==key
   if same and e['type'] in ['pcb_trace','pcb_via']:continue
   margin=.015 if same and e['type'] in ['pcb_smtpad','pcb_plated_hole'] else .153
   if shape.distance(shapes[i])<margin:return False
  return True
 if clear(*old):continue
 candidate=None
 for ix,iy in sorted([(a,b) for a in range(-6,7) for b in range(-6,7) if a or b],key=lambda q:math.hypot(*q)):
  x,y=old[0]+ix*.025,old[1]+iy*.025
  if clear(x,y):candidate=(x,y);break
 cached=None
 if candidate:
  v['x'],v['y']=candidate;rows.append({'via':v['pcb_via_id'],'old_mm':old,'new_mm':candidate,'requires_physical_contact_verification':True})
 else:j.remove(v);rows.append({'removed_via_for_rerouting':v['pcb_via_id'],'net':g.nets[key]})
# Cut only foreign-clearance violations; preserve the remaining pad escapes and route legs.
shapes,owners,ls,tree=objects();out=[];cut=[]
for e in j:
 if e['type']!='pcb_trace' or e['pcb_trace_id'].startswith(('usb_','oscillator_')):out.append(e);continue
 run=[];runs=[]
 for a,b in zip(e['route'],e['route'][1:]):
  if a['route_type']!=b['route_type'] or a['route_type']!='wire' or a['layer']!=b['layer']:raise RuntimeError('Split multilayer records before this construction step')
  shape=LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2);bad=False
  for i in tree.query(shape.buffer(.150001)):
   q=owners[i]
   if q['type']!='pcb_via' or g.key(q)==g.key(e):continue
   if shape.distance(shapes[i])<.150001:bad=True;break
  if bad:
   if len(run)>=2:runs.append(run)
   run=[];cut.append(e['pcb_trace_id']);continue
  if not run:run=[a,b]
  elif run[-1]==a:run.append(b)
  else:runs.append(run);run=[a,b]
 if len(run)>=2:runs.append(run)
 for k,route in enumerate(runs):
  ports=sorted({p[tag] for p in route for tag in ['start_pcb_port_id','end_pcb_port_id'] if tag in p});out.append({**e,'pcb_trace_id':e['pcb_trace_id']+'_via_clearance_run_'+str(k),'route':route,'pcb_port_ids':ports})
g.path.write_text(json.dumps(out,indent=2)+'\n');(g.ROOT/'artifacts/upright-revision/via-reconstruction.json').write_text(json.dumps({'construction_not_qualification':True,'via_changes':rows,'trace_records_with_cut_legs':sorted(set(cut))},indent=2)+'\n');print('Relocated/removed',len(rows),'vias; cut legs in',len(set(cut)),'trace records')