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/route-round7-usb.py

"""Bounded local copper closure for the explicitly placed USB gate.

This does not modify the replay used by a normal build. Old non-USB copper
is fixed. Uses the exact new placement, 0.15 mm USB / 0.12 mm logic tracks,
0.30/0.45 mm through-vias, solder-land drill exclusions and original screw
envelopes. A separate import and complete emitted-copper validation follow.
"""
from route_geometry import *
from shapely.ops import unary_union,nearest_points
from shapely import contains_xy, prepare
from shapely.strtree import STRtree
from scipy.ndimage import distance_transform_edt
from collections import defaultdict
import numpy as np
import heapq,time
import os

rework=os.environ.get("ROUND8_REWORK") in ["1","motor","extended"]
motor_rework=os.environ.get("ROUND8_REWORK")=="motor"
extended_rework=os.environ.get("ROUND8_REWORK")=="extended"
round9_plan=json.loads(Path(os.environ['ROUND9_PLAN']).read_text()) if os.environ.get('ROUND9_PLAN') else None

c=load_circuit(round9_plan['placement'] if round9_plan else "routing/placement.json");root=connectivity(c)
mapping=json.loads(Path("routing/net-map.json").read_text())["roots"]
sources={e["source_trace_id"]:e for e in c if e["type"]=="source_trace"}
ports={e["pcb_port_id"]:e for e in c if e["type"]=="pcb_port"}
by_source={p["source_port_id"]:p for p in ports.values()}
def net(t):
    s=sources[t["source_trace_id"]]
    return mapping[root((s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[]))[0])]
retained=json.loads(Path(round9_plan['retained'] if round9_plan else "routing/round8-extended-retained.json" if extended_rework else "routing/round8-motor-retained.json" if motor_rework else "routing/round8-retained.json" if rework else "routing/round7-usb-retained.json").read_text())
objects=[];drills=[];pads=[];wires=[];seen=set();connected_ports=set()
def add_wire(n,l,a,b,w):
    if math.dist(a,b)<1e-8:return
    key=n,l,tuple(sorted([tuple(a),tuple(b)])),w
    if key in seen:return
    seen.add(key);line=LineString([a,b]);g=line.buffer(w/2,quad_segs=16)
    objects.append(dict(net=n,layers=[l],shape=g,centerline=line,kind="wire"));wires.append(dict(net=n,layer=l,width=w,points=[a,b]))
def add_via(n,x,y):
    if any(math.dist((x,y),(v["x"],v["y"]))<1e-6 for v in drills):return
    drills.append(dict(net=n,x=x,y=y));objects.append(dict(net=n,layers=LAYERS,shape=Point(x,y).buffer(.225,quad_segs=16),kind="via"))
for t in retained:
    n=net(t)
    for p in t["route"]:
        for k in ["start_pcb_port_id","end_pcb_port_id"]:
            q=ports.get(p.get(k))
            if q and mapping[root(q["source_port_id"])]==n and math.hypot(q["x"]-p["x"],q["y"]-p["y"])<1e-5:
                connected_ports.add(q["pcb_port_id"])
        if p["route_type"]=="via":add_via(n,p["x"],p["y"])
    for a,b in zip(t["route"],t["route"][1:]):
        if a["route_type"]==b["route_type"]=="wire" and a["layer"]==b["layer"]:
            add_wire(n,a["layer"],(a["x"],a["y"]),(b["x"],b["y"]),max(a["width"],b["width"]))
native=json.loads(Path("routing/native-features.json").read_text())
for v in native["thermal_vias"]+native.get("auxiliary_vias",[]):add_via(v["net"],v["x"],v["y"])
holes=[];pth=[]
for e in c:
    if e["type"] in ["pcb_smtpad","pcb_plated_hole"]:
        p=ports.get(e.get("pcb_port_id"));n=mapping[root(p["source_port_id"])] if p else "NC:"+e[e["type"]+"_id"]
        g=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")
        item=dict(net=n,layers=e.get("layers",[e.get("layer")]),shape=g,kind="pad",port=e.get("pcb_port_id"))
        objects.append(item);pads.append(item)
        if e["type"]=="pcb_plated_hole":pth.append((n,geometry(e,"hole_")))
    elif e["type"]=="pcb_hole":holes.append(geometry(e,"hole_"))
pitch=.05
extent=round9_plan['extent'] if round9_plan else (-20.9,-20.9,20.9,20.9) if extended_rework else (-11,-20.9,8,-8.5) if motor_rework else (-8,-2,12,11) if rework else (-20.9,7.5,20.9,20.9)
axisx=np.round(np.arange(extent[0],extent[2]+.0001,pitch),8);axisy=np.round(np.arange(extent[1],extent[3]+.0001,pitch),8)
xx,yy=np.meshgrid(axisx,axisy);ny,nx=xx.shape
region=box(axisx[0]-.5,axisy[0]-.5,axisx[-1]+.5,axisy[-1]+.5)
def raster(items):
    g=unary_union(items)
    prepare(g)
    return contains_xy(g,xx,yy)
def coord(i,j):return float(axisx[i]),float(axisy[j])
def point_grid(x,y):return int(round((x-axisx[0])/pitch)),int(round((y-axisy[0])/pitch))
def route(s,start_override=None,target_override=None,force_inner=False):
    n=net(s);ids=s.get("connected_source_port_ids",[])
    pp=[by_source[p] for p in ids if p in by_source]
    if len(pp)==2:
        area=lambda p:sum(o["shape"].area for o in pads if o.get("port")==p["pcb_port_id"])
        if pp[1]["pcb_component_id"]=="pcb_component_72" or pp[0]["pcb_component_id"]!="pcb_component_72" and area(pp[1])<area(pp[0]):pp.reverse()
        if pp[0]["pcb_port_id"] in connected_ports and pp[1]["pcb_port_id"] not in connected_ports:pp.reverse()
        cap_starts={"USB_GATE_SUPPLY":"U_USB_GATE","C_U_1V2_OUT_SUPPLY":"C_U_1V2_OUT","C_U_3V3_OUT_SUPPLY":"C_U_3V3_OUT"}
        if rework:
            moved=set(json.loads(Path("round8-placement.json").read_text()))
            moved_ids={e["pcb_component_id"] for e in c if e["type"]=="pcb_component" and any(z["type"]=="source_component" and z["source_component_id"]==e["source_component_id"] and z["name"] in moved for z in c)}
            pp.sort(key=lambda p:p["pcb_component_id"] not in moved_ids)
        if s.get("name") in cap_starts:
            cap_source=next(e["source_component_id"] for e in c if e["type"]=="source_component" and e["name"]==cap_starts[s["name"]])
            cap_pcb=next(e["pcb_component_id"] for e in c if e["type"]=="pcb_component" and e["source_component_id"]==cap_source)
            pp.sort(key=lambda p:p["pcb_component_id"]!=cap_pcb)
    if not pp and not start_override:return dict(name=s.get("name"),skip="no physical terminals")
    start=start_override or pp[0];start_xy=start["x"],start["y"];start_layers=start["layers"]
    name=s.get("name","")
    width=.3 if name=="U_USB_ESD_PIN2_CONNECTION" else .15 if name.startswith(("USB_DM","USB_DP","USB_GATE_HOST","USB_GATE_SOC")) or re.match(r"U_USB_ESD_PIN[1346]_",name) else .12
    width=max(width,s.get("min_trace_thickness",0))
    if motor_rework:width=max(width,.3)
    nearby=[o for o in objects if o["shape"].intersects(region)]
    blockers={l:[] for l in LAYERS};via_blocks=[];goals={l:[] for l in LAYERS};landings={l:[] for l in LAYERS}
    target=target_override or (pp[1] if len(pp)>1 else None)
    target_component=None
    if target and (not target_override or target_override.get('connected_tree')):
        # A real net tree may already reach this terminal through another
        # source-associated path. Target that exact copper component, not
        # an abstract net or an unrelated island. No synthesized link is used.
        own=[o for o in objects if o["net"]==n]
        tree=STRtree([o["shape"] for o in own])
        todo=([i for i,o in enumerate(own) if set(o['layers'])&set(target['layers']) and o['shape'].buffer(1e-7).covers(Point(target['x'],target['y']))]
              if target_override else [i for i,o in enumerate(own) if o.get("port")==target["pcb_port_id"]])
        assert todo,('target must be actual same-net copper',target)
        seen_own=set()
        while todo:
            i=todo.pop()
            if i in seen_own:continue
            seen_own.add(i);a=own[i]
            for j in tree.query(a["shape"]):
                j=int(j);b=own[j]
                if j not in seen_own and set(a["layers"])&set(b["layers"]) and a["shape"].intersects(b["shape"]):todo.append(j)
        target_component=[own[i] for i in seen_own]
        if any(o.get("port")==start["pcb_port_id"] for o in target_component) or (start_override and any(set(o['layers'])&set(start_layers) and o['shape'].buffer(1e-7).covers(Point(start_xy)) for o in target_component)):
            return dict(name=s.get("name"),source=s["source_trace_id"],alreadyConnectedByExplicitCopper=True)
        for o in target_component:
            for l in o["layers"]:
                goals[l].append(o["shape"].buffer(width/2-.005));landings[l].append(o)
    if target_override and not target_override.get('connected_tree'):
        for l in target_override["layers"]:
            g=Point(target_override["x"],target_override["y"])
            goals[l].append(g.buffer(.03));landings[l].append(dict(kind="point",shape=g))
    for o in nearby:
        if o["net"]!=n:
            for l in o["layers"]:blockers[l].append(o["shape"].buffer(.1003+width/2))
            via_blocks.append(o["shape"].buffer(.354))
        elif not target and (o["kind"]!="pad" or o.get("port") in connected_ports) and not o["shape"].covers(Point(start_xy)):
            for l in o["layers"]:
                goals[l].append(o["shape"].buffer(width/2-.005));landings[l].append(o)
        if o["kind"]=="pad":via_blocks.append(o["shape"].buffer(.331))
        if target and target_override is None and target_component is None and o.get("port")==target["pcb_port_id"]:
            for l in o["layers"]:
                goals[l].append(o["shape"].buffer(-.005));landings[l].append(o)
    for v in drills:
        p=Point(v["x"],v["y"]);via_blocks.append(p.buffer(.554))
        if v["net"]!=n:
            for l in LAYERS:blockers[l].append(p.buffer(.354+width/2))
    for n2,h in pth:
        via_blocks.append(h.buffer(.429))
        if n2!=n:
            for l in LAYERS:blockers[l].append(h.buffer((.3003 if l.startswith("inner") else .2803)+width/2))
    for h in holes:
        via_blocks.append(h.buffer(.429))
        for l in LAYERS:blockers[l].append(h.buffer(.2003+width/2))
    for x in [-15.5,15.5]:
        for y in [-15.5,15.5]:
            # Full-stack via annuli must clear the larger top screw head.
            via_blocks.append(Point(x,y).buffer(3.1))
            blockers["top"].append(Point(x,y).buffer(2.8503+width/2))
            blockers["bottom"].append(Point(x,y).buffer(2.6003+width/2))
    # Copper must leave the actual board-edge margin after accounting for
    # half its width. The grid boundary alone is not a copper clearance.
    edge=21.15-.2003-width/2
    frame=region.difference(box(-edge,-edge,edge,edge))
    for layer in LAYERS:blockers[layer].append(frame)
    if motor_rework:
        # These paths carry winding current, including the low-side current
        # regulation shunts. Keep all replacements on outer copper.
        blockers["inner1"].append(region);blockers["inner2"].append(region)
        if name=="DRV_ISEN_B":
            # Both independent shunt escapes selected the same legal stitch.
            # Reserve that measured stitch for A, then give B another exit.
            for layer in LAYERS:blockers[layer].append(Point(-7.3,-18.3).buffer(.354+width/2))
    if s.get('allowed_layers'):
        for layer in set(LAYERS)-set(s['allowed_layers']):blockers[layer].append(region)
    if force_inner:
        # Engineering constraint for the low-current 3.3 V crossing: leave
        # the top-side USB escape channel open, using two legal through-vias.
        blockers["top"].append(box(-10.9,7,-9.9,22))
        blockers["bottom"].append(region)
    if name=="USB_DATA_VBUS_JOIN":
        # Do not bridge VBUS laterally across the SMT pin bank: that would
        # fence off the CC and data-pin escapes. Join the VBUS ends using
        # legal through-vias and an inner-layer crossing instead.
        blockers["top"].append(box(-8.97,7,-5.03,22))
    if start.get("pcb_port_id")=="ground_cap_junction":
        # Keep the rebuilt ground bridge north of the moved capacitor,
        # leaving the switch's short VCC/bypass escape corridor open.
        blockers["bottom"].append(box(-22,7,22,12.75))
    if name=="USB_ATTACH_OE":
        blockers["bottom"].append(box(-22,7,22,11.45))
    if name in ["USB_GATE_VCC","USB_GATE_SUPPLY"]:
        # Supply and bypass leave north of the package. Keep the west
        # middle-pin escape free for USB P instead of crossing it in copper.
        blockers["bottom"].append(box(-22,7,22,11.55))
    if name=="USB_GATE_GND":
        # Leave south before turning west: the two west-side USB channels
        # need the rows above this ground-return track for their escapes.
        blockers["bottom"].append(box(-22,10.4,-10.1,22))
        blockers["bottom"].append(box(-9.64,7,22,22))
    if name=="USB_GATE_SOC_P":
        # Reserve the lower-west through-via cell for the host-side P pin.
        for layer in LAYERS:
            blockers[layer].append(box(-11.45,10.3,-10.8,10.95))
            blockers[layer].append(Point(-11.1,10.8).buffer(.5))
    if name=="C_U_1V2_OUT_SUPPLY":
        # Do not reclaim the host-VBUS escape pocket for the core rail.
        for layer in LAYERS:blockers[layer].append(Point(-4.93,14.1272).buffer(.55))
    if name in ["C_U_1V2_OUT_SUPPLY","C_U_3V3_OUT_SUPPLY"]:
        # Regulator-capacitor connections use the east/north side. Keep
        # the four USB data fanout cells to the west unoccupied by power.
        for layer in LAYERS:blockers[layer].append(box(-12.5,9.5,-8.75,12.5))
    blocked=[raster(blockers[l]) for l in LAYERS]
    goal=[raster(goals[l]) for l in LAYERS]
    via_ok=~raster(via_blocks)
    via_ok&=(np.abs(xx)<20.72)&(np.abs(yy)<20.72)
    if motor_rework and name=="DRV_ISEN_B":via_ok&=np.hypot(xx+7.3,yy+18.3)>.6
    if name=="USB_GATE_GND":
        # Put the ground transition south of the USB-P escape corridor.
        # The closer west-side via otherwise fences that signal in.
        via_ok&=(yy<=10.45)&(xx<=-11.5)
    if name=="USB_GATE_SOC_P":via_ok&=(yy>=11.1)&(xx<=-11.55)
    any_goal=np.logical_or.reduce(goal)
    assert any_goal.any(),("no connected goal in USB region",s["source_trace_id"],s.get("name"))
    assert any((goal[i]&~blocked[i]).any() for i in range(4)),("every goal blocked",s["source_trace_id"],s.get("name"))
    distance=distance_transform_edt(~any_goal)
    heap=[];costs={};previous={};prefix={};blocked_geometries=[unary_union(blockers[l]) for l in LAYERS]
    ix,jy=point_grid(*start_xy)
    for l in start_layers:
        li=LAYERS.index(l)
        for i in range(max(0,ix-2),min(nx,ix+3)):
            for j in range(max(0,jy-2),min(ny,jy+3)):
                p=coord(i,j)
                if blocked[li][j,i] or LineString([start_xy,p]).intersects(blocked_geometries[li]):continue
                node=li,i,j;cost=math.dist(start_xy,p)/pitch
                if cost<costs.get(node,math.inf):costs[node]=cost;prefix[node]=[start_xy,p];heapq.heappush(heap,(cost+distance[j,i],cost,node))
    if not heap:
        print(json.dumps(dict(blockedTerminal=start_xy,foreign=[dict(net=o["net"],kind=o["kind"],bounds=o["shape"].bounds,distance=o["shape"].distance(Point(start_xy))) for o in nearby if o["net"]!=n and start_layers[0] in o["layers"] and o["shape"].distance(Point(start_xy))<.22])),flush=True)
    assert heap,("no legal terminal escape",s["source_trace_id"],s.get("name"),start_xy)
    def exact_landing(li,xy):
        p=Point(xy);choices=[]
        for o in landings[LAYERS[li]]:
            if p.distance(o["shape"])>width/2+.015:continue
            if o["kind"]=="wire":g=o["centerline"]
            elif o["kind"]=="pad":
                g=o["shape"].buffer(-width/2-.01)
                if g.is_empty:continue
            else:g=o["shape"].centroid
            q=nearest_points(p,g)[1];end=(q.x,q.y);dist=p.distance(q)
            # Snap endpoint landings to the actual existing vertex. At an
            # interior landing cross the centerline, keeping the exact
            # projection as a real intermediate copper vertex below.
            anchor=end
            if o["kind"]=="wire":
                endpoints=list(g.coords)
                vertex=min(endpoints,key=lambda v:math.dist(v,end))
                if math.dist(vertex,end)<1e-7:
                    anchor=end=tuple(vertex)
                elif dist>1e-8:
                    end=(q.x+(q.x-p.x)*.01/dist,q.y+(q.y-p.y)*.01/dist)
            stub=LineString([xy,end])
            if dist<1e-8 or not stub.intersects(blocked_geometries[li]):choices.append((dist,anchor,end))
        return min(choices,key=lambda a:a[0])[1:] if choices else None
    began=time.monotonic();iterations=0;found=None;landing=None
    while heap:
        _,cost,node=heapq.heappop(heap);li,i,j=node
        if cost>costs[node]+1e-8:continue
        iterations+=1
        if goal[li][j,i]:
            landing=exact_landing(li,coord(i,j))
            if landing is not None:found=node;break
        if iterations>600000 or time.monotonic()-began>22:break
        for di,dj in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]:
            ii,jj=i+di,j+dj
            if not(0<=ii<nx and 0<=jj<ny) or blocked[li][jj,ii]:continue
            if di and dj and (blocked[li][j,ii] or blocked[li][jj,i]):continue
            nxt=li,ii,jj;new=cost+math.hypot(di,dj)
            if new<costs.get(nxt,math.inf):costs[nxt]=new;previous[nxt]=node;heapq.heappush(heap,(new+distance[jj,ii],new,nxt))
        if via_ok[j,i]:
            for ll in range(4):
                if ll==li or blocked[ll][j,i]:continue
                nxt=ll,i,j;new=cost+(100 if width==.15 else 35)
                if new<costs.get(nxt,math.inf):costs[nxt]=new;previous[nxt]=node;heapq.heappush(heap,(new+distance[j,i],new,nxt))
    if found is None:
        checks=[]
        samples=[(.2,-15.6),(.2,-16.2),(.2,-17),(2.2,-15.6),(2.2,-16.2)] if motor_rework else [(-4.1,14.2),(-4.1,14.4),(-4,14.5)]
        for x,y in samples:
            i,j=point_grid(x,y)
            if not(0<=i<nx and 0<=j<ny):continue
            checks.append(dict(x=x,y=y,via=bool(via_ok[j,i]),blocked=[bool(m[j,i]) for m in blocked],visited=[(l,i,j) in costs for l in range(4)]))
        print(json.dumps(dict(start=start_xy,net=n,pocketNodes=len(costs),layers=list(set(l for l,i,j in costs)),checks=checks)),flush=True)
    assert found is not None,("no local closure",s["source_trace_id"],s.get("name"),iterations)
    path=[found]
    while path[-1] in previous:path.append(previous[path[-1]])
    path.reverse();points=prefix[path[0]]
    points=[(path[0][0],*p) for p in points]+[(l,*coord(i,j)) for l,i,j in path[1:]]
    for vertex in landing:
        if math.dist(points[-1][1:],vertex)>1e-8:points.append((found[0],*vertex))
    simple=[points[0]]
    for a,b,d in zip(points,points[1:],points[2:]):
        if a[0]!=b[0] or b[0]!=d[0] or abs((b[1]-a[1])*(d[2]-b[2])-(b[2]-a[2])*(d[1]-b[1]))>1e-8:simple.append(b)
    simple.append(points[-1])
    for a,b in zip(simple,simple[1:]):
        if a[0]!=b[0]:add_via(n,a[1],a[2])
        else:add_wire(n,LAYERS[a[0]],a[1:],b[1:],width)
    connected_ports.add(start["pcb_port_id"])
    if target:connected_ports.add(target["pcb_port_id"])
    return dict(name=s.get("name"),source=s["source_trace_id"],net=n,width=width,iterations=iterations,
                seconds=round(time.monotonic()-began,2),points=simple)

def write_candidate(prefix,completed):
    Path(f"routing/{prefix}-physical-candidate.json").write_text(json.dumps(dict(wires=wires,vias=drills),indent=2)+"\n")
    net_ids=sorted({w["net"] for w in wires}|{v["net"] for v in drills})
    num=lambda x:str(round(float(x)*10000,4))
    rows=["(session local_rework (routes (resolution um 10) (network_out"]
    for n in net_ids:
        rows.append(f"(net {n}")
        for w in wires:
            if w["net"]==n:rows.append(f"(wire (path {w['layer']} {num(w['width'])} "+" ".join(num(v) for p in w["points"] for v in p)+"))")
        for v in drills:
            if v["net"]==n:rows.append(f"(via VIA {num(v['x'])} {num(v['y'])})")
        rows.append(")")
    rows.append(")))")
    Path(f"routing/{prefix}-candidate.ses").write_text("\n".join(rows)+"\n")
    Path(f"reports/{prefix}-closure.json").write_text(json.dumps(dict(complete=True,notInstalled=True,routes=completed),indent=2)+"\n")
    print(json.dumps(dict(complete=True,wires=len(wires),vias=len(drills),notInstalled=True)),flush=True)

if round9_plan:
    prefix=round9_plan.get('prefix','round9-coordinated')
    completed=[]
    try:
        for job in round9_plan['jobs']:
            s={**sources[job['source']],**job.get('constraints',{})}
            result=route(s,start_override=job.get('start'),target_override=job.get('target'))
            completed.append(result)
            print(json.dumps({k:v for k,v in result.items() if k!='points'}),flush=True)
    except Exception as exc:
        write_candidate(prefix+'-partial',completed)
        Path(f'reports/{prefix}-attempt.json').write_text(json.dumps(dict(complete=False,notInstalled=True,error=str(exc),routes=completed),indent=2)+'\n')
        raise
    write_candidate(prefix,completed)
    raise SystemExit(0)

if rework:
    affected=["source_trace_336","source_trace_334","source_trace_353","source_trace_354","source_trace_355","source_trace_356"] if motor_rework else json.loads(Path("reports/round8-extended-routing-plan.json" if extended_rework else "reports/round8-routing-plan.json").read_text())["affectedSourceTraceIds"]
    if motor_rework and os.environ.get("MOTOR_FIRST")=="A":affected[:2]=list(reversed(affected[:2]))
    completed=[]
    try:
        for sid in affected:
            result=route(sources[sid]);completed.append(result)
            print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
    except Exception as exc:
        prefix="round8-extended" if extended_rework else "round8-motor" if motor_rework else "round8"
        Path(f"reports/{prefix}-attempt.json").write_text(json.dumps(dict(complete=False,notInstalled=True,error=str(exc),routes=completed),indent=2)+"\n")
        raise
    write_candidate("round8-extended" if extended_rework else "round8-motor" if motor_rework else "round8",completed)
    raise SystemExit(0)

affected=json.loads(Path("reports/round7-usb-routing-plan.json").read_text())["affectedSourceTraceIds"]
work=[sources[s] for s in affected]
priority={"C_U_1V2_OUT_SUPPLY":-2,"C_U_3V3_OUT_SUPPLY":-1,"USB_GATE_GND":0,"USB_GATE_VCC":1,"USB_GATE_SUPPLY":2,"USB_ATTACH_OE":3,"USB_GATE_SOC_N":4,"USB_GATE_SOC_P":5,"USB_GATE_HOST_P":6,"USB_GATE_HOST_N":7,"USB_DATA_VBUS_JOIN":8,"USB_DM_JOIN":9,"USB_DP_JOIN":9,"U_USB_ESD_PIN1_CONNECTION":10,"U_USB_ESD_PIN3_CONNECTION":10,"USB_DATA_CC2_RD":11}
work.sort(key=lambda s:(priority.get(s.get("name"),12 if len(s.get("connected_source_port_ids",[]))==2 else 13),s["source_trace_id"]))
completed=[]
try:
    # Another source-associated ground tree duplicated the old ESD pad's
    # top-side leaf. That pad moved to the bottom: retire its obsolete dead
    # spur too, rather than treating it as a fixed obstacle or a terminal.
    old_esd_ground=[(-12.3582,10.2639),(-11.6221,11),(-10.39,11)]
    gnet=net(next(s for s in sources.values() if s.get("name")=="U_USB_ESD_PIN2_CONNECTION"))
    for w in list(wires):
        if w["net"]==gnet and w["layer"]=="top" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(old_esd_ground,old_esd_ground[1:])):
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==gnet and o["layers"]==["top"] and o["shape"].equals(g))]
    # Retire the moved CC2 resistor's old ground spur. Moving this small
    # resistor opens the north-west control escape without changing the
    # buzzer/MCU rail or its existing length budget.
    old_cc_ground=[((-12.6,12.79),(-12.305,12.495)),
                   ((-12.305,12.495),(-12.9,11.9)),
                   ((-12.9,11.9),(-13.8,11.9)),
                   ((-13.8,11.9),(-14.4,11.3)),
                   ((-12.6,12.79),(-12.2,12.6))]
    cc_ground=net(next(s for s in sources.values() if s.get("name")=="USB_DATA_CC2_GND"))
    for w in list(wires):
        if w["net"]==cc_ground and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in old_cc_ground):
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==cc_ground and o["layers"]==["bottom"] and o["shape"].equals(g))]
    # Remove the former capacitor/ESD ground spur. Its solder-land contact
    # endpoints moved; the retained branch at (-12.2,12.6) is reconnected by
    # the new explicit component-to-ground routes, not an imaginary pad.
    old_ground=[(-8.9575,11.4),(-9.5175,11.4),(-9.6575,11.4),(-9.75,11.3075),(-9.75,12.2),(-10.15,12.6),(-12.2,12.6)]
    gn=net(next(s for s in sources.values() if s.get("name")=="C_U_3V3_OUT_GROUND"))
    for w in list(wires):
        if w["net"]==gn and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(old_ground,old_ground[1:])):
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==gn and o["layers"]==["bottom"] and o["shape"].equals(g))]
    # The former regulator-capacitor supply leaf has no terminal after the
    # capacitor move. Retire both segments, including its wide contact stub.
    old_cap_supply=[((-8.4923,9.3615),(-10.790192,11.391293)),
                    ((-10.790192,11.391293),(-10.7825,11.4))]
    n2=net(next(s for s in sources.values() if s.get("name")=="C_U_3V3_OUT_SUPPLY"))
    for w in list(wires):
        if w["net"]==n2 and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in old_cap_supply):
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==n2 and o["layers"]==["bottom"] and o["shape"].equals(g))]
    # The former P-side ESD transition sits in the south-channel escape.
    # Retire it and its obsolete inner stub; reconnect P to its retained
    # inner2 trunk at a new legal full-through transition north of the gate.
    pnet=net(next(s for s in sources.values() if s.get("name")=="USB_GATE_SOC_P"))
    old_p=(-10.6536,8.8317)
    drills[:]=[v for v in drills if not(v["net"]==pnet and math.dist((v["x"],v["y"]),old_p)<1e-6)]
    old_shape=Point(old_p).buffer(.225,quad_segs=16)
    objects[:]=[o for o in objects if not(o["kind"]=="via" and o["net"]==pnet and o["shape"].equals(old_shape))]
    for w in list(wires):
        if w["net"]==pnet and w["layer"]=="inner2" and all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [old_p,(-8.6541,10.8312)]):
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==pnet and o["layers"]==["inner2"] and o["shape"].equals(g))]
    # Retire the old ESD-end data stubs instead of forcing the new switch
    # to fit around them. Keep only the checked SoC-side fanout and vias.
    old_data_chains={"USB_GATE_SOC_N":[(-8.41,11.5),(-8.41,11.8217),(-8.2495,11.9822),(-4.446,11.9822)],
                     "USB_GATE_SOC_P":[(-8.41,10.5),(-9.6075,10.5),(-10.6536,9.4539),(-10.6536,8.8317)]}
    for name,chain in old_data_chains.items():
        n=net(next(s for s in sources.values() if s.get("name")==name))
        displaced=[w for w in wires if w["net"]==n and w["layer"]=="top" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(chain,chain[1:]))]
        assert len(displaced)==3,("expected three old ESD-end data segments",name,displaced)
        for w in displaced:
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["top"] and o["shape"].equals(g))]
    # Route the existing rail around the relocated output-capacitor land.
    # Retain both real three-way junctions and the original 0.12 mm width.
    rail_pass=next(s for s in sources.values() if s.get("name")=="USB_GATE_SUPPLY")
    pass_chain=[(-7.5094,16.3906),(-7.714,16.5952),(-9.2623,16.5952),(-10.5335,15.324)]
    displaced=[w for w in wires if w["net"]==net(rail_pass) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(pass_chain,pass_chain[1:]))]
    assert len(displaced)==3,("expected three cap-area rail segments",displaced)
    for w in displaced:
        wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
        objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
    result=route(rail_pass,dict(x=pass_chain[0][0],y=pass_chain[0][1],layers=["bottom"],pcb_port_id="cap_area_rail_a"),dict(x=pass_chain[-1][0],y=pass_chain[-1][1],layers=["bottom"],pcb_port_id="cap_area_rail_b"))
    result["name"]="moved_3V3_cap_foreign_rail_detour";completed.append(result)
    print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
    # The moved output capacitor shared a real bottom-side ground tree.
    # Remove the three old branches which cross its new positive pad and
    # rebuild all three junctions, rather than leaving stale copper behind.
    ground=next(s for s in sources.values() if s.get("name")=="C_U_1V2_OUT_GROUND")
    cap_supply=next(s for s in sources.values() if s.get("name")=="C_U_1V2_OUT_SUPPLY")
    cap_chain=[(-8.82,14.755),(-8.82,14),(-5.488374,14.838374),(-4.25,15.15)]
    cap_old=[w for w in wires if w["net"]==net(cap_supply) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(cap_chain,cap_chain[1:]))]
    for w in cap_old:
        wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
        objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
    ground_chain=[((-5.7004,13.5),(-7.7783,13.5)),
                  ((-7.7783,13.5),(-8.1733,13.105)),
                  ((-8.1733,13.105),(-8.82,13.105)),
                  ((-8.82,13.105),(-9.5175,13.105)),
                  ((-9.5175,13.105),(-10.5858,13.105)),
                  ((-9.5175,13.105),(-9.5175,11.4))]
    displaced=[w for w in wires if w["net"]==net(ground) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in ground_chain)]
    assert len(displaced)==6,("expected six moved-cap ground segments",displaced)
    for w in displaced:
        wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
        objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
    # The old third endpoint was a contact on the now-moved 3.3 V capacitor's
    # ground land, not another terminal. Join the two retained real branches
    # directly; each moved capacitor receives its own ground connection below.
    for a in [(-5.7004,13.5)]:
        result=route(ground,dict(x=a[0],y=a[1],layers=["bottom"],pcb_port_id="ground_cap_junction"),dict(x=-10.5858,y=13.105,layers=["bottom"],pcb_port_id="ground_existing_trunk"))
        result["name"]="moved_cap_ground_tree";completed.append(result)
        print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
    # Reclaim the old CORE-rail drill pocket for the previously unused host
    # VBUS pad. Move that CORE transition near its regulator; keep the actual
    # regulator-to-output-capacitor trunk and the SoC-side inner2 trunk.
    old_core=(-4.93,14.1272);new_core=(-5.5,15.9)
    rail=next(s for s in sources.values() if s.get("name")=="U_1V2_OUTPUT")
    rail_net=net(rail)
    removed_vias=[v for v in drills if math.dist((v["x"],v["y"]),old_core)<1e-6]
    assert len(removed_vias)==1 and removed_vias[0]["net"]==rail_net
    drills.remove(removed_vias[0])
    old_shape=Point(old_core).buffer(.225,quad_segs=16)
    objects[:]=[o for o in objects if not(o["kind"]=="via" and o["net"]==rail_net and o["shape"].equals(old_shape))]
    chains={"bottom":[old_core,(-4.95,14.3),(-5.488374,14.838374)],
            "inner2":[old_core,(-4.95,14.5174),(-4.0113,15.4561)]}
    for layer,chain in chains.items():
        displaced=[w for w in wires if w["net"]==rail_net and w["layer"]==layer and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(chain,chain[1:]))]
        assert len(displaced)==2,("expected two CORE transition segments",layer,displaced)
        for w in displaced:
            wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
            objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==rail_net and o["layers"]==[layer] and o["shape"].equals(g))]
    q=Point(new_core)
    assert all(o["net"]==rail_net or q.distance(o["shape"])>=.354 for o in objects),"new CORE drill clears all foreign copper"
    assert all(q.distance(o["shape"])>=.331 for o in pads),"new CORE drill is outside every solder land"
    assert all(math.dist(new_core,(v["x"],v["y"]))>=.554 for v in drills),"new CORE drill spacing"
    assert all(q.distance(h)>=.429 for h in holes),"new CORE drill-to-NPTH clearance"
    add_via(rail_net,*new_core)
    for layer,end in [("bottom",(-4.25,15.15)),("inner2",(-4.0113,15.4561))]:
        result=route(rail,dict(x=end[0],y=end[1],layers=[layer],pcb_port_id="core_trunk_junction"),dict(x=new_core[0],y=new_core[1],layers=[layer],pcb_port_id="new_core_transition"))
        result["name"]="CORE_transition_"+layer;completed.append(result)
        print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
    for s in work:
        start=None
        if s.get("name")=="USB_GATE_SUPPLY":
            # Feed the already-connected bypass land, leaving OE's north
            # escape free. The independent physical import still must prove
            # the specified gate VCC terminal reaches this real copper tree.
            source=next(e for e in c if e["type"]=="source_component" and e["name"]=="C_USB_GATE")
            pin=next(e for e in c if e["type"]=="source_port" and e["source_component_id"]==source["source_component_id"] and e.get("pin_number")==1)
            start=by_source[pin["source_port_id"]]
        result=route(s,start_override=start);completed.append(result);print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
    # Restore the real ground-cluster bridge exposed by moving the old
    # capacitor/CC ground lands. Both endpoints are existing ground copper.
    ground_source=next(s for s in sources.values() if s.get("name")=="N_GND")
    bridge={**ground_source,"name":"GND_CLUSTER_BRIDGE","min_trace_thickness":.3}
    result=route(bridge,dict(x=.9,y=13.45,layers=["top"],pcb_port_id="avcc_existing_ground_via"),dict(x=.69,y=12.3,layers=["top"],pcb_port_id="soc_existing_ground_junction"))
    completed.append(result);print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
except Exception as exc:
    Path("routing/round7-usb-physical-candidate.json").write_text(json.dumps(dict(wires=wires,vias=drills),indent=2)+"\n")
    Path("reports/round7-usb-local-closure.json").write_text(json.dumps(dict(notInstalled=True,complete=False,error=str(exc),routes=completed),indent=2)+"\n")
    raise
physical=dict(wires=wires,vias=drills)
Path("routing/round7-usb-physical-candidate.json").write_text(json.dumps(physical,indent=2)+"\n")
net_ids=sorted({w["net"] for w in wires}|{v["net"] for v in drills})
num=lambda x:str(round(float(x)*10000,4))
rows=["(session usb_local (routes (resolution um 10) (network_out"]
for n in net_ids:
    rows.append(f"(net {n}")
    for w in wires:
        if w["net"]==n:rows.append(f"(wire (path {w['layer']} {num(w['width'])} "+" ".join(num(v) for p in w["points"] for v in p)+"))")
    for v in drills:
        if v["net"]==n:rows.append(f"(via VIA {num(v['x'])} {num(v['y'])})")
    rows.append(")")
rows.append(")))")
Path("routing/round7-usb-candidate.ses").write_text("\n".join(rows)+"\n")
Path("reports/round7-usb-local-closure.json").write_text(json.dumps(dict(notInstalled=True,complete=True,routes=completed),indent=2)+"\n")
print(json.dumps(dict(complete=True,wires=len(wires),vias=len(drills),notInstalled=True)),flush=True)