AnasSarkiz/usb-c-pd-brushed-motor
USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.
- Version
- 0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
- License
- unset
- Stars
- 0
scripts/plan-local-decoupling.py
"""Propose native component placements using unchanged supplier courtyards.
This planning screen does not replace native PCB/functional/visual validation.
Run from the board directory with the recorded Shapely Python environment.
"""
import gzip,json,math
from pathlib import Path
from shapely.geometry import Polygon,box,Point
from shapely.affinity import rotate,translate
j=json.load(gzip.open('evidence/validated-preroute-circuit-A22.json.gz'))
placement=json.load(open('evidence/local-decoupling-original-A22.json'))
src={e['source_component_id']:e['name']for e in j if e['type']=='source_component'}
pc={e['pcb_component_id']:src.get(e.get('source_component_id'))for e in j if e['type']=='pcb_component'}
shapes={}
for e in j:
if e['type'] not in ['pcb_courtyard_outline','pcb_courtyard_rect']:continue
ref=pc.get(e.get('pcb_component_id'))
if not ref:continue
if 'outline'in e:shape=Polygon([(p['x'],p['y'])for p in e['outline']])
else:
c=e['center'];shape=box(c['x']-e['width']/2,c['y']-e['height']/2,c['x']+e['width']/2,c['y']+e['height']/2)
old=placement[ref];shapes[ref]=rotate(translate(shape,xoff=-old['x'],yoff=-old['y']),-old['ccwRotationDegrees'],origin=(0,0))
requests=[
('R69',(-22,-29.5),None,[0],0),
('R70',(-12.5,-29.5),None,[0],0),
('R71',(-2,-29.5),None,[0],0),
('R72',(7.5,-29.5),None,[0],0),
('U1',(-24.4,4),None,[0],2),
('C18',(25.5,-14),None,[0],3),
('R2',(-14,6),(-22.4,2.7501264),[180],3),
('R3',(-13,8.25),(-22.4,3.2498736),[90,0,180,270],3),
('D3',(-17,6),None,[270],0),
('D2',(-22,8),None,[0,90,180,270],2),
('Q7',(-2,-10),None,[0,90,180,270],3),
('Q2',(35.5,-11),None,[0,90,180,270],2),
('Q3',(35.5,-15),None,[0,90,180,270],2),
('D9',(35,-20),None,[0,90,180,270],2),
('C6',(-29.2,6.5),(-26.4,4.2498736),[90,270],2),
('C7',(-29.2,1.6),(-26.4,3.2498936),[270,90],2),
('C8',(-25.2,-.2),(-26.4,2.7501264),[0,180],3),
('C4',(-28.6,11.5),(-29.86893,14.024895),[270,0,180,90],3),
('C5',(-22,11.5),(-22.86893,14.024895),[270,0,180,90],3),
('C2',(-36,14),(-34.13107,14.024895),[0,90,180,270],3),
('C3',(-26,11.5),(-27.13107,14.024895),[270,0,180,90],3),
('C16',(15.2,-16),(10.099965,-18.5),[0],3),
('C17',(15.2,-19.5),(10.099965,-18.5),[0],3),
('C15',(-6.7,-13.5),(-8.065,-16.230892),[180,270,0,90],2),
('C13',(-9.2,-10.6),(-8.065,-16.230892),[180,0],1),
('C9',(-25.8,-24),(-24.873248,-21.925078),[270,0,180,90],3),
('C10',(-27.5,-15.2),(-24.873248,-16.074922),[180,0,270,90],3),
('C11',(-18,-13.8),(-19.126752,-16.724922),[0,180,90,270],3),
('R4',(-21,0),(-23.1501264,2),[0,180,90,270],4),
('R56',(-17,-10.2),None,[0,180,90,270],3),
('R17',(-13.2,-14.3),(-10.605,-16.230892),[270,0,180,90],3),
('R7',(-28.3,-18.65),None,[180],1),
('R8',(-31,-23),None,[0,180,90,270],3),
('R9',(-27.5,-25.25),None,[0,180,90,270],3),
('R22',(-9.5,-24.95),(-9.335,-21.769108),[270],1),
('C20',(-13.3,-22.8),(-9.335,-21.769108),[180],.5),
('C19',(-12.35,-25.05),(-9.5,-25.65),[180],.5),
('R19',(-16.8,-24),(-10.605,-21.769108),[180],.5),
('R18',(-16.7,-16.75),None,[270,90],1),
('R55',(-16.7,-20.6),None,[270,90],1),
('R20',(-19.8,-25.4),(-10.605,-21.769108),[270,90,180,0],1),
('R21',(-16.1,-26.1),(-10.605,-21.769108),[180,0,270,90],1),
('C21',(5.9,13.9),(3.635,11.7051),[90,0,180,270],4),
('C22',(7.4,11.5),(4.905,11.7051),[0,180,90,270],4),
('C23',(-.9,13),(1.095,11.7051),[90,0,180,270],3),
('C26',(23.1,5.5),(24.024922,2.873248),[0],1),
('C27',(26,5.9),(25.325078,2.873248),[90,270],1),
('C28',(28.3,5.9),(25.975106,2.873248),[270],1),
('C25',(8.5,7.7),(11.095,7.7051),[180,0,90,270],4),
]
moving={r[0]for r in requests}
def position_shape(ref,p):return translate(rotate(shapes[ref],p['ccwRotationDegrees'],origin=(0,0)),xoff=p['x'],yoff=p['y'])
occupied={ref:position_shape(ref,p)for ref,p in placement.items()if ref not in moving}
keepouts=[Point(x,y).buffer(3.5)for x in [-35,35]for y in [-27.5,27.5]]
keepouts.extend([Point(-9,20.94).buffer(8),Point(10,21).buffer(6)])
board=box(-39.5,-32,39.5,32)
manifest={p['ref']:p for p in json.load(open('docs/design-manifest.json'))}
port_offsets={}
for ref in moving:
probe=json.load(open('dist/tests/supplier-audit/'+manifest[ref]['code']+'/circuit.json'))
port=next((e for e in probe if e['type']=='source_port'and e.get('pin_number')==1),None)
pp=next((e for e in probe if e['type']=='pcb_port'and e['source_port_id']==port['source_port_id']),None)if port else None
if pp:port_offsets[ref]=Point(pp['x'],pp['y'])
report=[]
for ref,preferred,target,rotations,radius in requests:
candidates=[]
preferred_shape=position_shape(ref,{'x':preferred[0],'y':preferred[1],'ccwRotationDegrees':rotations[0]})
print('Preferred occupied',ref,[(n,round(preferred_shape.distance(sh),3))for n,sh in occupied.items()if preferred_shape.distance(sh)<.25],flush=True)
for rot in rotations:
for xi in range(-int(radius*4),int(radius*4)+1):
for yi in range(-int(radius*4),int(radius*4)+1):
x=preferred[0]+xi*.25;y=preferred[1]+yi*.25;p={'x':x,'y':y,'ccwRotationDegrees':rot};shape=position_shape(ref,p)
if not board.contains(shape):continue
if any(shape.distance(s)<.25-1e-8 for s in occupied.values()):continue
if any(shape.distance(s)<.25-1e-8 for s in keepouts):continue
if target is not None and ref not in port_offsets:
raise RuntimeError('Required electrical pin missing from supplier probe: '+ref)
pin=translate(rotate(port_offsets[ref],rot,origin=(0,0)),xoff=x,yoff=y)if target is not None else None
pin_distance=pin.distance(Point(target))if target is not None else None
cost=math.hypot(x-preferred[0],y-preferred[1])+.35*(pin_distance if pin_distance is not None else 0)+.02*rotations.index(rot)
candidates.append((cost,p,shape,pin_distance))
print(ref, 'candidates', len(candidates), flush=True)
if not candidates:
p={'x':preferred[0],'y':preferred[1],'ccwRotationDegrees':rotations[0]};shape=position_shape(ref,p)
print('Preferred blockers',[(n,round(shape.distance(s),3))for n,s in occupied.items()if shape.distance(s)<.25], flush=True)
raise RuntimeError('No legal native placement candidate for '+ref)
cost,p,shape,pin_distance=min(candidates,key=lambda c:c[0]);placement[ref]=p;occupied[ref]=shape
print('chosen',ref,p,pin_distance,flush=True)
report.append({'ref':ref,'placement':p,'pinToTargetMm':pin_distance,'courtyardBounds':shape.bounds})
Path('.cache/placement-local-decoupling-proposed-A22.json').write_text(json.dumps(placement,indent=2)+'\n')
Path('evidence/local-decoupling-proposal-A22.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))