muse/book-reading-clip-lamp

A tscircuit package created by muse.

Version
0.4.6
License
unset
Stars
0

verify-explicit-routing.py

"""Reject stale explicit copper when pads, nets or generated trace bindings change."""
import json
from pathlib import Path
root=Path(__file__).parent
reference=json.loads((root/'explicit-route-reference.json').read_text())
actual=json.loads((root/'dist/index/circuit.json').read_text())
routes=json.loads((root/'explicit-pcb-routes.json').read_text())
def index(c):
    components={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'}
    ports={e['source_port_id']:(components.get(e['source_component_id'],e['source_component_id']),e.get('pin_number'),tuple(e.get('port_hints',[]))) for e in c if e['type']=='source_port'}
    nets={e['source_net_id']:e['name'] for e in c if e['type']=='source_net'}
    traces={e['source_trace_id']:(sorted(ports[p] for p in e['connected_source_port_ids']),sorted(nets[n] for n in e.get('connected_source_net_ids',[]))) for e in c if e['type']=='source_trace'}
    pads={ports[e['source_port_id']]:(e['x'],e['y'],e['layers']) for e in c if e['type']=='pcb_port' and e.get('source_port_id') in ports and ports[e['source_port_id']][0] in components.values()}
    return traces,pads
comps={e['source_component_id']:e['name'] for e in actual if e['type']=='source_component'}
ports={e['source_port_id']:[comps.get(e['source_component_id'],e['source_component_id']),e.get('pin_number'),e.get('port_hints',[])] for e in actual if e['type']=='source_port'}
pcb_ports={e['pcb_port_id']:e for e in actual if e['type']=='pcb_port'}
for pid,binding in routes['portBindings'].items():
    assert ports[pcb_ports[pid]['source_port_id']]==binding, f'Explicit endpoint binding changed: {pid}'
old_traces,old_pads=index(reference)
new_traces,new_pads=index(actual)
for trace in routes['traces']:
    tid=trace['source_trace_id']
    assert old_traces[tid]==new_traces[tid],f'Explicit copper electrical binding changed: {tid}'
for name,pad in old_pads.items():
    current=new_pads[name]
    assert abs(pad[0]-current[0])<1e-6 and abs(pad[1]-current[1])<1e-6 and pad[2]==current[2],f'Pad moved: {name}'
# Reference omits pcb_component records, so compare geometry without owner IDs.
def shapes(c):
    return sorted(json.dumps({k:v for k,v in e.items() if not k.endswith("_id") and not k.endswith("_ids") and k!="subcircuit_connectivity_map_key"},sort_keys=True) for e in c if e["type"] in {"pcb_smtpad","pcb_plated_hole"})
assert shapes(reference)==shapes(actual), "Footprint copper geometry changed; review explicit routes"
assert sum(e["type"]=="pcb_trace" for e in actual)==len(routes["traces"]), "Build does not match explicit route source"
errors=[e for e in actual if e['type'].endswith('_error') or str(e.get('error_type','')).endswith('_error')]
assert not errors,f'{len(errors)} circuit error records'
vias=[e for e in actual if e['type']=='pcb_via']
expected_via_count=len(routes['vias'])+4  # four named, grounded exposed-pad vias VG1..VG4
assert len(vias)==expected_via_count, f'Unexpected via count: {len(vias)} vs {expected_via_count}'
assert all(e['hole_diameter']==0.3 and e['outer_diameter']==0.45 and set(e['layers'])=={'top','inner1','inner2','bottom'} for e in vias)
print(f'PASS: {len(routes["traces"])} explicit layer paths retain their electrical bindings; {len(old_pads)} component pads unchanged; {len(vias)} full-stack 0.30/0.45 mm vias; zero circuit errors')