shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/import-whole-board.py
"""Convert routed physical copper into complete source-associated paths.
Connectivity failures stop the import; the candidate still requires DRC.
"""
from route_geometry import *
from collections import defaultdict
from shapely.ops import unary_union
from shapely.strtree import STRtree
import heapq, sys, os, hashlib
# Use electrical identities, not render-order IDs: deleting an unrelated trace
# must not turn an Ethernet signal into an outer-layer-only power trunk.
preferred_widths={'USB_VBUS_A':.35,'USB_VBUS_B':.35,'BUCK_IN_CAP':.5,'BUCK_SW':.3,
'VM_INPUT':.5,'VM_FUSED':.5,'VM_TVS_GND':.8,'VM_BULK_GND':.8,
'DRV_VMA':.3,'DRV_VMB':.3,'VBUS_SOURCE_1':.6,'VBUS_SOURCE_2':.6,
'VBUS_DRAIN_5':.6,'VBUS_DRAIN_6':.6,'VBUS_DRAIN_7':.6,'BUCK_INPUT_BYPASS':.5}
outer_source_names={'USB_VBUS_A','USB_VBUS_B','BUCK_SW','VM_INPUT','VM_FUSED','BUCK_INPUT_BYPASS'}
def contact_width(bridge,existing_copper):
# Describe existing pad/annulus metal at a contact without manufacturing
# extra copper outside that union or inventing a 0.12 mm neck inside it.
return next((w for w in [.8,.6,.5,.45,.35,.3,.25,.2,.15,.12]
if existing_copper.buffer(2e-4).covers(bridge.buffer(w/2))),.12)
circuit = load_circuit()
root = connectivity(circuit)
mapping = json.loads(Path("routing/net-map.json").read_text())
net_for_root = mapping["roots"]
ports = {e["pcb_port_id"]: e for e in circuit if e["type"] == "pcb_port"}
for e in circuit:
if e["type"] in ["pcb_smtpad", "pcb_plated_hole"] and e.get("pcb_port_id") in ports:
ports[e["pcb_port_id"]]["layers"] = e.get("layers", [e.get("layer")])
by_source = {e["source_port_id"]: e for e in ports.values()}
source_traces = [e for e in circuit if e["type"] == "source_trace"]
tree = parse_sexpr(Path(sys.argv[1] if len(sys.argv)>1 else "routing/board.ses").read_text())
routes = one(tree, "routes")
res = one(routes, "resolution")
scale = {"um": .001, "mm": 1, "mil": .0254}[res[1]] / float(res[2])
assert scale <= .0001+1e-12, "Routing session needs at least 0.1 micrometre coordinate precision"
extra_routes=[] if os.environ.get('IGNORE_PROTECTED') else json.loads(Path("routing/protected-routes.json").read_text())
if os.environ.get("INCLUDE_CLOSURE"):
extra_routes+=json.loads(Path("routing/closure-patches.json").read_text())
# SES writes integer coordinates at its declared resolution. Restore exact
# pad/protected-escape coordinates only within that format's rounding error
# (at most 0.071 micrometres here), avoiding false graph gaps and duplicate
# drills after the round trip. This is not an electrical clearance tolerance.
snap_radius=math.sqrt(2)*scale/2+1e-8
anchors=defaultdict(list)
def snap_xy(x,y,register=False):
cell=(math.floor(x/scale),math.floor(y/scale))
nearby=[p for dx in [-1,0,1] for dy in [-1,0,1]
for p in anchors[(cell[0]+dx,cell[1]+dy)]
if math.dist((x,y),p)<=snap_radius]
if nearby:
return min(nearby,key=lambda p:math.dist((x,y),p))
if register:
anchors[cell].append((x,y))
return x,y
for p in ports.values():
snap_xy(p["x"],p["y"],True)
for t in extra_routes:
for p in t["route"]:
snap_xy(p["x"],p["y"],True)
for t in extra_routes:
for p in t["route"]:
p["x"],p["y"]=snap_xy(p["x"],p["y"])
wires, vias = defaultdict(list), defaultdict(list)
for n in entries(one(routes, "network_out"), "net"):
net = n[1]
for w in entries(n, "wire"):
p = one(w, "path")
nums = [float(x)*scale for x in p[3:]]
pts = [snap_xy(x,y) for x,y in zip(nums[::2], nums[1::2])]
for a,b in zip(pts, pts[1:]):
if math.dist(a,b)>1e-7:
wires[net].append((LineString([a,b]), p[1], float(p[2])*scale))
for v in entries(n, "via"):
vias[net].append(snap_xy(float(v[2])*scale, float(v[3])*scale))
if Path("routing/native-features.json").exists():
features=json.loads(Path("routing/native-features.json").read_text())
for v in features["thermal_vias"]+features.get("auxiliary_vias",[]):
vias[v["net"]].append((v["x"],v["y"]))
for trace in extra_routes:
net = trace["connection_name"]
for p in trace["route"]:
if p["route_type"] == "via":
vias[net].append((p["x"], p["y"]))
for a,b in zip(trace["route"], trace["route"][1:]):
if a["route_type"] == b["route_type"] == "wire" and a["layer"] == b["layer"]:
wires[net].append((LineString([(a["x"],a["y"]),(b["x"],b["y"])]), a["layer"], max(a["width"],b["width"])))
for net in vias:
vias[net]=list(dict.fromkeys((round(x,6),round(y,6)) for x,y in vias[net]))
for e in circuit:
if e["type"]=="pcb_trace" and e.get("pcb_component_id"):
p=ports.get(e["route"][0].get("start_pcb_port_id"))
if not p:
continue
net=net_for_root[root(p["source_port_id"])]
for a,b in zip(e["route"],e["route"][1:]):
if a["route_type"]==b["route_type"]=="wire" and a["layer"]==b["layer"]:
wires[net].append((LineString([(a["x"],a["y"]),(b["x"],b["y"])]),a["layer"],a["width"]))
pads = defaultdict(list)
for e in circuit:
if e["type"] not in ["pcb_smtpad", "pcb_plated_hole"] or e.get("pcb_port_id") not in ports:
continue
net = net_for_root[root(ports[e["pcb_port_id"]]["source_port_id"])]
pads[net].append((e, geometry(e, "outer_" if e["type"]=="pcb_plated_hole" else "")))
traces_by_net = defaultdict(list)
for t in source_traces:
ids = t.get("connected_source_port_ids", []) + t.get("connected_source_net_ids", [])
if ids and root(ids[0]) in net_for_root:
traces_by_net[net_for_root[root(ids[0])]].append(t)
output, failed, stats, remaining_links = [], [], [], []
for net, traces in traces_by_net.items():
graph = defaultdict(dict)
special = defaultdict(set)
pad_contacts = defaultdict(set)
def node(x,y,l):
return (round(x,6), round(y,6), l)
def add(a,b,width,kind="wire",extra=None):
if a==b:
return
cost = math.dist(a[:2],b[:2]) if kind=="wire" else 1.6 if kind=="via" else .001
value = (cost,width,kind,extra)
if b not in graph[a] or width>graph[a][b][1]:
graph[a][b]=value
graph[b][a]=value
for x,y in vias[net]:
ns = [node(x,y,l) for l in LAYERS]
for a,b in zip(ns,ns[1:]):
add(a,b,0,"via")
for n in ns:
special[n[2]].add(n)
for e,g in pads[net]:
p=ports[e["pcb_port_id"]]
for layer in e.get("layers",[e.get("layer")]):
pc=node(p["x"],p["y"],layer)
for via_node in special[layer]:
center=Point(via_node[:2])
if g.distance(center)<=.225+1e-6:
bridge=LineString([pc[:2],via_node[:2]])
if g.buffer(1e-4).union(center.buffer(.2251)).covers(bridge):
add(pc,via_node,contact_width(bridge,g.union(center.buffer(.2251))))
pad_contacts[pc].add(via_node)
if e["type"]=="pcb_plated_hole":
ns=[node(p["x"],p["y"],l) for l in e["layers"]]
for a,b in zip(ns,ns[1:]):
add(a,b,.12,"pad",e["pcb_plated_hole_id"])
for n in ns:
pad_contacts[n].update(ns)
for n in ns:
special[n[2]].add(n)
for layer in LAYERS:
segments=[(g,w) for g,l,w in wires[net] if l==layer]
if not segments:
continue
geos=[g for g,w in segments]
stree=STRtree(geos)
union=unary_union(geos)
lines=[union] if union.geom_type=="LineString" else list(union.geoms)
layerpads=[(e,g) for e,g in pads[net] if layer in e.get("layers",[e.get("layer")])]
bridges=[]
for line in lines:
coords=list(line.coords)
for a,b in zip(coords,coords[1:]):
seg=LineString([a,b])
midpoint=seg.interpolate(.5,normalized=True)
widths=[segments[int(i)][1] for i in stree.query(seg.buffer(1e-5))
if geos[int(i)].distance(midpoint)<1e-5]
width=max(widths or [.12])
cuts=[(0,node(*a,layer)),(seg.length,node(*b,layer))]
for n in special[layer]:
q=Point(n[:2])
if seg.distance(q)<1e-5:
cuts.append((seg.project(q),n))
for e,g in layerpads:
if seg.distance(g)>width/2+1e-5:
continue
p=ports[e["pcb_port_id"]]
center=Point(p["x"],p["y"])
distance=seg.project(center)
projection=seg.interpolate(distance)
pc=node(p["x"],p["y"],layer)
pn=node(projection.x,projection.y,layer)
bridge=LineString([pc[:2],pn[:2]])
if not g.buffer(2e-4).union(seg.buffer(width/2+2e-4)).covers(bridge):
continue
cuts.append((distance,pn))
bridges.append((pc,pn,contact_width(bridge,g.union(seg.buffer(width/2)))))
cuts.sort(key=lambda p:p[0])
for (_,aa),(_,bb) in zip(cuts,cuts[1:]):
add(aa,bb,width)
for a,b,w in bridges:
add(a,b,w)
pad_contacts[a].add(b)
minimum_width=0
def shortest(starts,targets,outer_only=False):
queue=[(0,start) for start in starts]
heapq.heapify(queue)
cost={start:0 for start in starts}
previous={}
while queue:
distance,a=heapq.heappop(queue)
if distance>cost[a]+1e-9:
continue
if a in targets:
path=[a]
while path[-1] in previous:
path.append(previous[path[-1]])
return list(reversed(path))
for b,(length,w,kind,extra) in graph[a].items():
if outer_only and kind=="wire" and a[2] not in ["top","bottom"]:
continue
new=distance+length
if kind=='wire' and w+1e-8<minimum_width:
new+=length*1_000_000
if new<cost.get(b,float("inf")):
cost[b]=new
previous[b]=a
heapq.heappush(queue,(new,b))
return None
def portnode(p):
return node(p["x"],p["y"],p["layers"][0])
def emit_physical_path(path,source):
chunks=[];chunk=[path[0]]
for a,b in zip(path,path[1:]):
if graph[a][b][2]=='pad':
if len(chunk)>1:chunks.append(chunk)
chunk=[b]
else:chunk.append(b)
if len(chunk)>1:chunks.append(chunk)
for segment in chunks:
result=[]
for a,b in zip(segment,segment[1:]):
_,w,kind,extra=graph[a][b]
if kind=='wire':
result.extend({'route_type':'wire','x':p[0],'y':p[1],'layer':p[2],'width':w} for p in [a,b])
elif kind=='via':
if result and result[-1].get('route_type')=='via' and result[-1]['x']==a[0] and result[-1]['y']==a[1]:
result[-1]['to_layer']=b[2]
else:result.append({'route_type':'via','x':a[0],'y':a[1],'from_layer':a[2],'to_layer':b[2],
'via_diameter':.45,'via_hole_diameter':.3})
else:raise AssertionError('Unsplit connector barrel')
first,last=segment[0],segment[-1]
first_port=next((p for p in netports if node(p['x'],p['y'],first[2])==first and first[2] in p['layers']),None)
last_port=next((p for p in netports if node(p['x'],p['y'],last[2])==last and last[2] in p['layers']),None)
assert first_port and last_port,('unanchored physical ground network',first,last)
fw=next((p['width'] for p in result if p['route_type']=='wire'),.12)
lw=next((p['width'] for p in reversed(result) if p['route_type']=='wire'),fw)
result.insert(0,{'route_type':'wire','x':first[0],'y':first[1],'layer':first[2],'width':fw,
'start_pcb_port_id':first_port['pcb_port_id']})
result.append({'route_type':'wire','x':last[0],'y':last[1],'layer':last[2],'width':lw,
'end_pcb_port_id':last_port['pcb_port_id']})
output.append({'type':'pcb_trace','pcb_trace_id':f'whole_board_{len(output)}',
'source_trace_id':source['source_trace_id'],'subcircuit_id':source['subcircuit_id'],
'subcircuit_connectivity_map_key':source.get('subcircuit_connectivity_map_key'),'route':result})
netports=[p for p in ports.values() if root(p["source_port_id"]) in net_for_root
and net_for_root[root(p["source_port_id"])]==net]
# Export real disconnected copper islands for short, targeted finishing
# routes. No connectivity record or unrouted error is removed here.
port_nodes={node(p["x"],p["y"],l) for p in netports for l in p["layers"]}
unseen=set(graph)|port_nodes
islands=[]
while unseen:
todo=[next(iter(unseen))]
island=set()
while todo:
a=todo.pop()
if a in island:
continue
island.add(a)
todo.extend(b for b in graph[a] if b not in island)
unseen.difference_update(island)
if island&port_nodes:
islands.append(island)
if len(islands)>1:
islands.sort(key=lambda nodes:len(nodes&port_nodes),reverse=True)
connected=islands.pop(0)
while islands:
options=[]
for i,island in enumerate(islands):
for layer in LAYERS:
candidates=sorted(n for n in connected if n[2]==layer)
if not candidates:
continue
tree=STRtree([Point(n[:2]) for n in candidates])
for a in sorted(island):
b=candidates[int(tree.nearest(Point(a[:2])))]
options.append((math.dist(a[:2],b[:2])+(0 if a[2]==b[2] else 2),i,a,b))
_,i,a,b=min(options)
port=next((p for p in netports if node(p["x"],p["y"],a[2])==a),None)
constraints=[t.get("min_trace_thickness",.12) for t in traces
if port and port["source_port_id"] in t.get("connected_source_port_ids",[])]
remaining_links.append({"net":net,"a":a,"b":b,
"width":max(constraints or [.12]),
"port":port and port["pcb_port_id"],
"a_vias":[[x,y,"inner1"] for x,y in sorted(vias[net],key=lambda q:math.dist(q,a[:2]))
if node(x,y,"inner1") in islands[i]][:100],
"b_vias":[[x,y,"inner1"] for x,y in sorted(vias[net],key=lambda q:math.dist(q,b[:2]))
if node(x,y,"inner1") in connected][:100]})
connected=connected|islands.pop(i)
for t in traces:
minimum_width=preferred_widths.get(t.get('name'),t.get('min_trace_thickness',0))
ends=[by_source[i] for i in t.get("connected_source_port_ids",[]) if i in by_source]
if not ends:
continue
start=ends[0]
targets=ends[1:]
if not targets:
targets=[p for p in netports if p["pcb_port_id"]!=start["pcb_port_id"]]
if not targets:
continue
targetsets=[{node(p["x"],p["y"],l) for p in targets for l in p["layers"]}]
else:
targetsets=[{node(p["x"],p["y"],l) for l in p["layers"]} for p in targets]
covered = {portnode(start)}
anchors = {portnode(start)}
for targetset in targetsets:
# Build one physical tree rather than duplicate the root-to-leaf
# trunk for every terminal of a multi-port source net. Keep every
# declared terminal explicitly port-anchored, including when a
# previous wire merely grazed its pad rather than ending there.
if anchors&targetset:
continue
path=shortest(anchors,targetset,t.get('name') in outer_source_names)
if path:
planar=sum(graph[a][b][0] for a,b in zip(path,path[1:]) if graph[a][b][2]=='wire')
transitions={(a[0],a[1]) for a,b in zip(path,path[1:]) if graph[a][b][2]=='via'}
if planar+1.6*len(transitions)>t.get('max_length',float('inf')):
preferred_width=minimum_width;minimum_width=0
path=shortest(anchors,targetset,t.get('name') in outer_source_names|{'BUCK_IN_CAP'})
minimum_width=preferred_width
if not path:
failed.append({"source_trace_id":t["source_trace_id"],"name":t.get("name"),
"net":net,"start":portnode(start),"targets":list(targetset),
"degree":len(graph[portnode(start)])})
continue
covered.update(path)
anchors.add(path[-1])
# A plated barrel is existing connector copper, not another via.
# End one port-anchored wire at it and start another on its other
# layer. This preserves the real connection and avoids unsupported
# through_pad rendering or manufacturing a second drill there.
chunks, chunk = [], [path[0]]
for a,b in zip(path,path[1:]):
if graph[a][b][2] == "pad":
if len(chunk)>1:
chunks.append(chunk)
chunk=[b]
else:
chunk.append(b)
if len(chunk)>1:
chunks.append(chunk)
for path in chunks:
result=[]
for a,b in zip(path,path[1:]):
_,w,kind,extra=graph[a][b]
if kind=="wire":
for p in [a,b]:
result.append({"route_type":"wire","x":p[0],"y":p[1],"layer":p[2],"width":w})
elif kind=="via":
if result and result[-1].get("route_type")=="via" and result[-1]["x"]==a[0] and result[-1]["y"]==a[1]:
result[-1]["to_layer"]=b[2]
else:
result.append({"route_type":"via","x":a[0],"y":a[1],
"from_layer":a[2],"to_layer":b[2],"via_diameter":.45,"via_hole_diameter":.3})
else:
raise AssertionError("Unsplit connector barrel")
first,last=path[0],path[-1]
first_width=next((p["width"] for p in result if p["route_type"]=="wire"),.12)
last_width=next((p["width"] for p in reversed(result) if p["route_type"]=="wire"),first_width)
first_port=next((p for p in netports if node(p["x"],p["y"],first[2])==first and first[2] in p["layers"]),None)
end=next((p for p in netports if node(p["x"],p["y"],last[2])==last and last[2] in p["layers"]),None)
assert first_port and end, (t["name"],"unanchored physical route")
result.insert(0,{"route_type":"wire","x":first[0],"y":first[1],"layer":first[2],
"width":first_width,"start_pcb_port_id":first_port["pcb_port_id"]})
result.append({"route_type":"wire","x":last[0],"y":last[1],"layer":last[2],"width":last_width,
"end_pcb_port_id":end["pcb_port_id"]})
output.append({"type":"pcb_trace","pcb_trace_id":f"whole_board_{len(output)}",
"source_trace_id":t["source_trace_id"],"subcircuit_id":t["subcircuit_id"],
"subcircuit_connectivity_map_key":t.get("subcircuit_connectivity_map_key"),"route":result})
# A virtual net endpoint can be satisfied by a nearby pin. Those
# local shortest paths alone do NOT retain the bridges between ground
# clusters. Export a real terminal-spanning tree as well, using only the
# measured physical graph. Disconnected islands remain reported above.
network_sources=[t for t in traces if t.get('connected_source_net_ids')
and t.get('max_length',float('inf'))>=1000 and t.get('max_via_count') is None]
network_source=max(network_sources,key=lambda t:len(t.get('connected_source_port_ids',[])),default=None)
if network_source:
minimum_width=0
anchors=set()
for p in netports:
targets={node(p['x'],p['y'],layer) for layer in p['layers']}
if anchors and not anchors&targets:
path=shortest(anchors,targets)
if path and len(path)>1:emit_physical_path(path,network_source)
anchors.add(portnode(p))
stats.append({"net":net,"wire_segments":len(wires[net]),"vias":len(vias[net]),"graph_nodes":len(graph)})
# Preserve the deliberately authored supply and return trunks, even where a
# shorter, narrow branch of the same net also connects the declared terminals.
# These are real checked copper, not substitute connectivity records.
load_names={'USB_VBUS_A','USB_VBUS_B','BUCK_IN_CAP','BUCK_SW','VM_INPUT','VM_FUSED',
'VM_PROTECTED','VM_TVS_GND','VM_BULK_GND','DRV_VMA','DRV_VMB'}
load_ids={t['source_trace_id'] for t in source_traces if t.get('name') in load_names}
assert len(load_ids)==len(load_names),'all required power-source identities must exist'
load_routes={}
for trace in extra_routes:
if trace.get('source_trace_id') in load_ids:
key=(trace['source_trace_id'],tuple((p['route_type'],p['x'],p['y'],p.get('layer'),p.get('from_layer'),p.get('to_layer')) for p in trace['route']))
if key in load_routes:
for a,b in zip(load_routes[key]['route'],trace['route']):
if a['route_type']=='wire':a['width']=max(a['width'],b['width'])
else:load_routes[key]={**trace,'route':[dict(p) for p in trace['route']]}
for trace in load_routes.values():
source=next(t for t in source_traces if t['source_trace_id']==trace['source_trace_id'])
assert source.get('max_length') is None
output.append({**trace,'subcircuit_id':source['subcircuit_id'],
'subcircuit_connectivity_map_key':source.get('subcircuit_connectivity_map_key')})
# An explicitly paired source can retain its checked authored route instead
# of taking an incidental narrow shortcut through the same electrical net.
# Virtual-net spanning trees above are deliberately not replaced here.
paired={}
for trace in extra_routes:
source=next((t for t in source_traces if t['source_trace_id']==trace.get('source_trace_id')),None)
if not source or source.get('connected_source_net_ids') or len(source.get('connected_source_port_ids',[]))!=2:continue
terminals=[by_source.get(id) for id in source['connected_source_port_ids']]
if not all(terminals):continue
wire_points=[p for p in trace['route'] if p['route_type']=='wire']
if len(wire_points)<2:continue
first,last=wire_points[0],wire_points[-1]
match=lambda p,q:math.hypot(p['x']-q['x'],p['y']-q['y'])<1e-5 and p['layer'] in q['layers']
if match(first,terminals[1]) and match(last,terminals[0]):terminals.reverse()
if not (match(first,terminals[0]) and match(last,terminals[1])):continue
candidate={**trace,'route':[dict(p) for p in trace['route']],
'subcircuit_id':source['subcircuit_id'],
'subcircuit_connectivity_map_key':source.get('subcircuit_connectivity_map_key')}
next(p for p in candidate['route'] if p['route_type']=='wire')['start_pcb_port_id']=terminals[0]['pcb_port_id']
next(p for p in reversed(candidate['route']) if p['route_type']=='wire')['end_pcb_port_id']=terminals[1]['pcb_port_id']
score=min(p['width'] for p in wire_points)
if score>=paired.get(source['source_trace_id'],(-1,None))[0]:paired[source['source_trace_id']]=(score,candidate)
output=[t for t in output if t['source_trace_id'] not in paired]+[t for score,t in paired.values()]
# The two authored zero-via clock trees use crystal-centred branches. Emitting
# the generic consecutive port pairs would duplicate the capacitor branch and
# incorrectly add its length twice. Emit the actual connected tree once, with
# exact terminal IDs, rather than relaxing either clock's 10 mm limit.
clock_path=Path('routing/review-clock.json')
if clock_path.exists() and not os.environ.get('AUDIT_ONLY'):
clock_trees=json.loads(clock_path.read_text())
for sid in ['source_trace_18','source_trace_19']:
source=next(s for s in source_traces if s['source_trace_id']==sid)
assert source['max_length']==10 and source['max_via_count']==0
authored=[];covered=set();length=0
for t in [t for t in clock_trees if t['source_trace_id']==sid]:
route=[dict(p) for p in t['route']]
assert all(p['route_type']=='wire' and p['layer']=='top' for p in route)
ends=[]
for p in [route[0],route[-1]]:
peers=[by_source[id] for id in source['connected_source_port_ids']
if math.hypot(by_source[id]['x']-p['x'],by_source[id]['y']-p['y'])<1e-6]
assert len(peers)==1
ends.append(peers[0]);covered.add(peers[0]['source_port_id'])
route[0]['start_pcb_port_id']=ends[0]['pcb_port_id']
route[-1]['end_pcb_port_id']=ends[1]['pcb_port_id']
length+=sum(math.dist((a['x'],a['y']),(b['x'],b['y'])) for a,b in zip(route,route[1:]))
authored.append({**t,'route':route,'subcircuit_id':source['subcircuit_id'],
'subcircuit_connectivity_map_key':source.get('subcircuit_connectivity_map_key')})
assert covered==set(source['connected_source_port_ids']) and length<=10
output=[t for t in output if t['source_trace_id']!=sid]+authored
preserved_path=Path('routing/review-preserved-pairs.json')
if preserved_path.exists() and not os.environ.get('AUDIT_ONLY'):
preserved=json.loads(preserved_path.read_text())
for sid in sorted({t['source_trace_id'] for t in preserved}):
source=next(s for s in source_traces if s['source_trace_id']==sid)
assert not source.get('connected_source_net_ids') and len(source['connected_source_port_ids'])==2
paths=[t for t in preserved if t['source_trace_id']==sid]
for t in paths:
first,last=t['route'][0],t['route'][-1]
terminals=[by_source[id] for id in source['connected_source_port_ids']]
assert all(any(math.hypot(p['x']-q['x'],p['y']-q['y'])<1e-6 for p in terminals) for q in [first,last]), 'Preserved route terminals moved'
output=[t for t in output if t['source_trace_id']!=sid]+[{**t,'subcircuit_id':source['subcircuit_id'],'subcircuit_connectivity_map_key':source.get('subcircuit_connectivity_map_key')} for t in paths]
constant_width=[]
for trace in output:
run=[];width=None
for p in trace['route']:
if p['route_type']=='wire' and width is not None and abs(p['width']-width)>1e-8:
# Core coalesces coincident route points. Keep a width step as
# two physically joined traces so that coalescing cannot widen
# the preceding fine-pitch escape along its entire segment.
assert run and math.hypot(run[-1]['x']-p['x'],run[-1]['y']-p['y'])<1e-5
constant_width.append({**trace,'route':run})
run=[];width=None
run.append(p)
if p['route_type']=='wire':width=p['width']
if run:constant_width.append({**trace,'route':run})
output=[{**t,'pcb_trace_id':f'whole_board_{i}'} for i,t in enumerate(constant_width)]
output_dir=Path(os.environ.get('IMPORT_OUTPUT_DIR','routing'));output_dir.mkdir(parents=True,exist_ok=True)
(output_dir/"import-report.json").write_text(json.dumps({"paths":len(output),"failures":failed,
"circuit_sha256":hashlib.sha256(Path(os.environ.get('CIRCUIT_INPUT','routing/placement.json')).read_bytes()).hexdigest(),
"session_sha256":hashlib.sha256(Path(sys.argv[1] if len(sys.argv)>1 else 'routing/board.ses').read_bytes()).hexdigest(),
"audit_only":bool(os.environ.get('AUDIT_ONLY')),"ignored_protected":bool(os.environ.get('IGNORE_PROTECTED')),
"physical_island_links":len(remaining_links),"stats":stats},indent=2))
(output_dir/"remaining-links.json").write_text(json.dumps(remaining_links,indent=2))
(output_dir/"physical-copper.json").write_text(json.dumps({
"wires":[{"net":n,"layer":l,"width":w,"points":list(g.coords)} for n,items in wires.items() for g,l,w in items],
"vias":[{"net":n,"x":x,"y":y} for n,items in vias.items() for x,y in items],
},indent=2))
if not os.environ.get('AUDIT_ONLY') and not failed and not remaining_links:
(output_dir/"complete-routes.json").write_text(json.dumps(output,indent=2))
print(json.dumps({"paths":len(output),"failures":len(failed)}))
for failure in failed[:15]:
print(json.dumps({**failure, "targets": failure["targets"][:3]}))
if failed or remaining_links:
sys.exit(1)