← Commit history

Delete design/gen/hs.py

John Lauer ·efaa030390 ·7d ago ·parent 5417369
1 file changed −559
design/gen/hs.pydeleted−559
@@ -1,559 +0,0 @@-#!/usr/bin/env python3-"""High-speed routing for the OCP NIC 3.0 SFF -> 4x OCuLink riser.--Board frame: x across the card (0..76), y from the faceplate edge (0) to the gold-finger edge (115), KiCad y-down.-Layers: L1=F.Cu, L2=In1 GND, L3=In2, L4=In3 GND, L5=In4, L6=B.Cu GND (and the A-side fingers).--Each differential pair is a centreline of nodes (x, y, layer). Consecutive nodes on one layer are a trace; a layer-change is a via pair placed across the direction of travel. The n trace is the left offset (west when heading north),-p the right one; that holds the whole way because every path is an octilinear S-shape.--Lane reversal (OCuLink ports face the faceplate, so each port sees its lanes in the opposite order to the fingers):-  stage A (just past the via row): lanes 0<->1 and 2<->3 cross; lane 0 on L1, lane 1 on L3, lane 3 on L1, lane 2 on L5-  lane 3 drops L1 -> L5 in band M-  stage B (before the connector): block {1,0} (L3, L1) crosses block {3,2} (both L5)-"""-import math, json, os, sys-sys.path.insert(0, os.path.dirname(__file__))-from footprints import ocp_pin_x--F = 38.0 - 0.258          # datum F: H (card centre line, x=38) minus 0.258 (SFF-TA-1002 Fig 5-39, OCP Detail A)-EDGE_Y = 115.0-PAD_TOP = EDGE_Y - 3.00   # inner end of every gold finger-CX = [16.1 + 14.6 * k for k in range(4)]   # OCuLink port centres (pitch 14.6, inside the 59.1 mm faceplate zone)--# 85 ohm differential, JLC06161H-3313 (design/zsolve.py, 2D field solve)-PAIR = {"L1": (0.163, 0.15), "L6": (0.163, 0.15), "L3": (0.131, 0.15), "L5": (0.121, 0.15)}-KLAY = {"L1": "F.Cu", "L3": "In2.Cu", "L5": "In4.Cu", "L6": "B.Cu"}-VIA_P = 0.70               # signal via pair pitch-VIA_D, VIA_H = 0.40, 0.20  # transition vias-VIP_D, VIP_H = 0.25, 0.15  # via-in-pad at the OCuLink pads (JLC 6L: filled and capped by default)---def pitch(layer):-    w, s = PAIR[layer]-    return w + s---class Pair:-    def __init__(self, name, nnet, pnet, kind):-        self.name, self.nnet, self.pnet, self.kind = name, nnet, pnet, kind-        self.nodes = []          # (x, y, layer)-        self.start = None        # ((xn, yn), (xp, yp), layer)  pad ends-        self.end = None          # ((xn, yn), (xp, yp), layer, via_in_pad)-        self.tracks = []         # (layer, net, x1, y1, x2, y2, w)-        self.vias = []           # (x, y, net, d, h)--    def add(self, x, y, layer):-        self.nodes.append((round(x, 4), round(y, 4), layer))-        return self---def unit(dx, dy):-    L = math.hypot(dx, dy)-    return (dx / L, dy / L)---def left(d):-    return (d[1], -d[0])---def offset_polyline(pts, off):-    """Mitred offset of an octilinear polyline (left side positive)."""-    out = []-    n = len(pts)-    for i in range(n):-        if i == 0:-            d = unit(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]); l = left(d)-            out.append((pts[0][0] + l[0] * off, pts[0][1] + l[1] * off))-        elif i == n - 1:-            d = unit(pts[-1][0] - pts[-2][0], pts[-1][1] - pts[-2][1]); l = left(d)-            out.append((pts[-1][0] + l[0] * off, pts[-1][1] + l[1] * off))-        else:-            d1 = unit(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1])-            d2 = unit(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1])-            l1, l2 = left(d1), left(d2)-            bis = (l1[0] + l2[0], l1[1] + l2[1])-            bl = math.hypot(*bis)-            if bl < 1e-9:-                raise ValueError("reversal in path")-            bis = (bis[0] / bl, bis[1] / bl)-            cosh = bis[0] * l1[0] + bis[1] * l1[1]-            k = off / cosh-            out.append((pts[i][0] + bis[0] * k, pts[i][1] + bis[1] * k))-    return out---def build(pair):-    """Turn a pair's centreline nodes into n/p tracks and vias."""-    nodes = []-    for nd in pair.nodes:-        if nodes and abs(nd[0] - nodes[-1][0]) < 1e-6 and abs(nd[1] - nodes[-1][1]) < 1e-6 and nd[2] == nodes[-1][2]:-            continue-        nodes.append(nd)-    # drop collinear middle points on the same layer-    clean = [nodes[0]]-    for i in range(1, len(nodes) - 1):-        a, b, c = clean[-1], nodes[i], nodes[i + 1]-        if a[2] == b[2] == c[2]:-            cr = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])-            dot = (b[0] - a[0]) * (c[0] - b[0]) + (b[1] - a[1]) * (c[1] - b[1])-            if abs(cr) < 1e-9 and dot > 0:-                continue-        clean.append(b)-    clean.append(nodes[-1])-    nodes = clean-    pair.nodes = nodes-    # split into runs per layer-    runs = []-    cur = [nodes[0]]-    for nd in nodes[1:]:-        if nd[2] == cur[-1][2]:-            cur.append(nd)-        else:-            runs.append(cur)-            cur = [nd]-    runs.append(cur)-    vias = []  # positions between runs: last node of run i == first node xy of run i+1-    for i in range(len(runs) - 1):-        a = runs[i][-1]; b = runs[i + 1][0]-        assert abs(a[0] - b[0]) < 1e-6 and abs(a[1] - b[1]) < 1e-6, (pair.name, a, b)-    for ri, run in enumerate(runs):-        layer = run[0][2]-        p = pitch(layer); w = PAIR[layer][0]-        pts = [(x, y) for (x, y, _) in run]-        # entry and exit fans to via pitch-        fan_in = ri > 0-        fan_out = ri < len(runs) - 1-        for sign, net in ((+1, pair.nnet), (-1, pair.pnet)):-            off = sign * p / 2-            line = offset_polyline(pts, off)-            if fan_in:-                d = unit(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]); l = left(d)-                vx, vy = pts[0][0] + l[0] * sign * VIA_P / 2, pts[0][1] + l[1] * sign * VIA_P / 2-                f = (VIA_P - p) / 2-                line[0] = (line[0][0] + d[0] * f, line[0][1] + d[1] * f)-                line.insert(0, (vx, vy))-            if fan_out:-                d = unit(pts[-1][0] - pts[-2][0], pts[-1][1] - pts[-2][1]); l = left(d)-                vx, vy = pts[-1][0] + l[0] * sign * VIA_P / 2, pts[-1][1] + l[1] * sign * VIA_P / 2-                f = (VIA_P - p) / 2-                line[-1] = (line[-1][0] - d[0] * f, line[-1][1] - d[1] * f)-                line.append((vx, vy))-            # pad ends-            if ri == 0 and pair.start is not None:-                padpt = pair.start[0] if sign > 0 else pair.start[1]-                line = pad_join(padpt, line, at_start=True, p=p)-            if ri == len(runs) - 1 and pair.end is not None:-                padpt = pair.end[0] if sign > 0 else pair.end[1]-                line = pad_join(padpt, line, at_start=False, p=p)-            for (x1, y1), (x2, y2) in zip(line, line[1:]):-                if math.hypot(x2 - x1, y2 - y1) > 1e-6:-                    pair.tracks.append((layer, net, x1, y1, x2, y2, w))-        if fan_out:-            d = unit(pts[-1][0] - pts[-2][0], pts[-1][1] - pts[-2][1]); l = left(d)-            for sign, net in ((+1, pair.nnet), (-1, pair.pnet)):-                pair.vias.append((pts[-1][0] + l[0] * sign * VIA_P / 2, pts[-1][1] + l[1] * sign * VIA_P / 2, net, VIA_D, VIA_H))-    if pair.end is not None and pair.end[3]:-        for (pt, net) in ((pair.end[0], pair.nnet), (pair.end[1], pair.pnet)):-            pair.vias.append((pt[0], pt[1], net, VIP_D, VIP_H))-    return pair---def pad_join(padpt, line, at_start, p):-    """Join a trace end to a pad point with a 45 degree jog (pads are on the pair's axis line)."""-    if at_start:-        q = line[0]-        dx, dy = q[0] - padpt[0], q[1] - padpt[1]-        # go straight along the major axis, then 45 degrees-        if abs(dx) < 1e-6:-            return [padpt] + line-        s = 1 if dy > 0 else -1-        mid = (padpt[0], q[1] - s * abs(dx))-        return [padpt, mid] + line-    else:-        q = line[-1]-        dx, dy = padpt[0] - q[0], padpt[1] - q[1]-        if abs(dx) < 1e-6:-            return line + [padpt]-        s = 1 if dy > 0 else -1-        mid = (padpt[0], q[1] + s * abs(dx)) if abs(dy) > abs(dx) else None-        # move diagonally first, then straight onto the pad-        mid = (q[0] + dx, q[1] + s * abs(dx))-        return line + [mid, padpt]---# ------------------------------------------------------------------------------------------ geometry plan-PX = ocp_pin_x()---def finger(pin):-    return (F + PX[pin], PAD_TOP)---Y_FAN0 = 107.6      # fan diagonals start (clear of the key slots, which end at y = 108.0)-Y_VIA = 97.6        # transition via row-Y_SA0 = 96.9        # stage A start-LANE_PITCH = 2.8-WITHIN = 0.7        # P/R offset from the lane cell centre after the via row-# lane cell centres at the via row-XC = []-x = 21.0-for k in range(4):-    row = []-    for l in range(4):-        row.append(x + l * LANE_PITCH)-    XC.append(row)-    x = row[-1] + LANE_PITCH + 1.5--# which side of its cell each pair sits on after the via row (P first, R second), per lane index in port-#   b0: P west / R east, b1: R west / P east, b2: P west / R east, b3: R west / P east-SIDE = {0: (-1, +1), 1: (+1, -1), 2: (-1, +1), 3: (+1, -1)}-# layer of each bundle in band S and at the connector-LAYER_S = {0: "L1", 1: "L3", 2: "L5", 3: "L1"}-LAYER_C = {0: "L1", 1: "L3", 2: "L5", 3: "L5"}--# connector-end pair centres relative to the port centre, and how each pair ends-#   P pairs land on B-row pads, R pairs pass the B row through a gap and land on A-row pads-CONN = {-    # lane: (P centre, R gap x, R pad centre)-    0: (4.0, 4.0, 3.5),-    1: (2.5, 1.0, 2.0),-    2: (-2.0, -0.5, -2.5),-    3: (-3.5, -5.15, -4.0),-}-Y_BROW, Y_AROW = 7.37, 5.18-Y_STRIP = 6.27          # between the A and B rows, where R pairs turn to their A pads-Y_NB_END = 13.0         # stage B finished: pairs at their connector x-Y_NB = 19.5             # stage B starts (block swap, 45 degree)---def lane_pins(lane):-    starts = [17, 20, 23, 26, 30, 33, 36, 39, 44, 47, 50, 53, 56, 59, 62, 65]-    s = starts[lane]-    return f"{s}", f"{s+1}"---# band M straight-section positions relative to the port centre (before the band-M2 shift back west)-#   order W->E: R1, P1, P0, R0 | crossing-via gap | R3, P3, P2, R2-XEND = {("R", 1): 1.0, ("P", 1): 2.5, ("P", 0): 4.0, ("R", 0): 5.2,-        ("R", 3): -5.15, ("P", 3): -3.5, ("P", 2): -2.0, ("R", 2): -0.5}-D_SWAP = 6.0-def xm_rel(kind, l):-    x = XEND[(kind, l)]-    return x - D_SWAP if l in (0, 1) else x + D_SWAP--S_EAST = 6.0            # band M corridors sit this far east of their connectors-Y_SW = 93.4             # lane 3 drops L1 -> L5 here-Y_M0 = 92.6             # band M diagonals start-Y_M2 = 36.0             # band M2: shift back west by S_EAST-Y_NB = Y_NB_END + D_SWAP---def plan_lane(k, l):-    """(P pair, R pair) for port k, port-lane l, finger to OCuLink pad."""-    lane = 4 * k + l-    n, p = lane_pins(lane)-    P = Pair(f"P{k}{l}", f"PETn{lane}", f"PETp{lane}", "PET")-    R = Pair(f"R{k}{l}", f"PERn{lane}", f"PERp{lane}", "PER")-    cx = CX[k]-    xc_f = F + PX["B" + n] + 0.3-    sP, sR = SIDE[l]-    partner = {0: 1, 1: 0, 2: 3, 3: 2}[l]-    for pr, kind, s_side, startlayer in ((P, "P", sP, "L1"), (R, "R", sR, "L6")):-        xv = XC[k][l] + s_side * WITHIN-        xa = XC[k][partner] + s_side * WITHIN-        xm = cx + S_EAST + xm_rel(kind, l)-        xn = cx + xm_rel(kind, l)-        xe = cx + XEND[(kind, l)]-        pin_n = ("B" if kind == "P" else "A") + n-        pin_p = ("B" if kind == "P" else "A") + p-        pr.start = (finger(pin_n), finger(pin_p), startlayer)-        pc = pitch(startlayer)-        y0 = PAD_TOP - 0.25 - (0.6 - pc) / 2-        pr.add(xc_f, y0, startlayer)-        pr.add(xc_f, Y_FAN0, startlayer)-        pr.add(xv, Y_FAN0 - abs(xv - xc_f), startlayer)-        lay = LAYER_S[l]-        pr.add(xv, Y_VIA, startlayer)-        if lay != startlayer:-            pr.add(xv, Y_VIA, lay)-        pr.add(xv, Y_SA0, lay)-        pr.add(xa, Y_SA0 - abs(xa - xv), lay)-        if l == 3:                      # lane 3 drops to L5 for stage B-            pr.add(xa, Y_SW, lay)-            pr.add(xa, Y_SW, "L5")-            lay = "L5"-        pr.add(xa, Y_M0, lay)-        pr.add(xm, Y_M0 - abs(xm - xa), lay)-        for (tx, ty) in TUNE.get(pr.name, []):-            pr.add(tx, ty, lay)-        pr.add(xm, Y_M2, lay)-        pr.add(xn, Y_M2 - abs(xn - xm), lay)-        pr.add(xn, Y_NB, lay)-        pr.add(xe, Y_NB_END, lay)-        connector_end(pr, kind, l, cx, lay)-    return P, R---def connector_end(pr, kind, l, cx, lay):-    xe = cx + XEND[(kind, l)]-    if kind == "P":-        pads = (cx + 5.25 - 0.5 * (b_pin(l, "n") - 1), cx + 5.25 - 0.5 * (b_pin(l, "p") - 1))-        xc = (pads[0] + pads[1]) / 2-        assert abs(xc - xe) < 1e-6, (pr.name, xc, xe)-        pr.add(xe, Y_BROW + 0.45, lay)-        pr.end = ((pads[0], Y_BROW), (pads[1], Y_BROW), lay, lay != "L1")-        return-    # R pairs: L1 (lane 0) drops to L3 first, then the B-row gap, the strip, the A pads-    if lay == "L1":-        yv = 11.6-        pr.add(xe, yv, "L1")-        pr.add(xe, yv, "L3")-        lay = "L3"-        xg = cx + CONN[l][1]-        pr.add(xe, yv - 0.6, lay)-        pr.add(xg, yv - 0.6 - abs(xg - xe), lay)-    xg = cx + CONN[l][1]-    xa = cx + CONN[l][2]-    pads = (cx + 4.75 - 0.5 * (a_pin(l, "n") - 1), cx + 4.75 - 0.5 * (a_pin(l, "p") - 1))-    assert abs((pads[0] + pads[1]) / 2 - xa) < 1e-6, (pr.name, pads, xa)-    c = min(0.3, abs(xa - xg) / 2)-    sgn = 1 if xa > xg else -1-    pr.add(xg, Y_STRIP + c, lay)-    pr.add(xg + sgn * c, Y_STRIP, lay)-    pr.add(xa - sgn * c, Y_STRIP, lay)-    pr.add(xa, Y_STRIP - c, lay)-    pr.add(xa, Y_AROW + 0.42, lay)-    pr.end = ((pads[0], Y_AROW), (pads[1], Y_AROW), lay, True)---def b_pin(l, pol):-    return {0: {"p": 3, "n": 4}, 1: {"p": 6, "n": 7}, 2: {"p": 15, "n": 16}, 3: {"p": 18, "n": 19}}[l][pol]---def a_pin(l, pol):-    return {0: {"p": 3, "n": 4}, 1: {"p": 6, "n": 7}, 2: {"p": 15, "n": 16}, 3: {"p": 18, "n": 19}}[l][pol]---TUNE = {}   # pair name -> centreline points inserted in band M (filled by tune())--# band-M tuning windows and direction (away from same-layer neighbours)-TUNE_DIR = {("P", 0): -1, ("R", 0): +1, ("R", 1): -1, ("P", 1): +1,-            ("R", 3): -1, ("P", 3): +1, ("P", 2): -1, ("R", 2): +1}-# y windows (top, bottom) so that P3 (east bumps) and P2 (west bumps) never bulge at the same height-TUNE_WIN = {("P", 0): (56.0, 38.0), ("R", 0): (56.0, 38.0), ("R", 1): (77.0, 58.0), ("P", 1): (77.0, 58.0),-            ("R", 3): (77.0, 58.0), ("P", 3): (77.0, 67.6), ("P", 2): (67.0, 58.0), ("R", 2): (77.0, 58.0)}---def accordion(x0, ytop, side, amp, n, g=1.1, c=0.4):-    """Centreline points of n accordion periods heading north from (x0, ytop) (y decreasing)."""-    pts = []-    y = ytop-    x1 = x0 + side * amp-    for i in range(n):-        y1 = y; y2 = y - g; y3 = y - 2 * g-        pts += [(x0, y1 + c), (x0 + side * c, y1), (x1 - side * c, y1), (x1, y1 - c),-                (x1, y2 + c), (x1 - side * c, y2), (x0 + side * c, y2), (x0, y2 - c)]-        y = y3-    return pts---def bumps(x0, ytop, side, amp, n, flat=0.4, gap=0.5):-    pts = []-    y = ytop-    for i in range(n):-        pts += [(x0, y), (x0 + side * amp, y - amp), (x0 + side * amp, y - amp - flat), (x0, y - 2 * amp - flat)]-        y = y - 2 * amp - flat - gap-    return pts---def tune(iterations=4, tol_ps=0.3):-    """Delay-match each port's TX lanes and RX lanes (by ps, including via barrels)."""-    import delay-    TUNE.clear()-    for it in range(iterations):-        pairs = [build(pp) for k in range(4) for l in range(4) for pp in plan_lane(k, l)]-        byname = {pp.name: pp for pp in pairs}-        changed = False-        for k in range(4):-            for kind in "PR":-                grp = [byname[f"{kind}{k}{l}"] for l in range(4)]-                d = {pp.name: delay.pair_delay(pp) for pp in grp}-                tgt = max(d.values())-                for pp in grp:-                    l = int(pp.name[2])-                    need_ps = tgt - d[pp.name]-                    if need_ps < tol_ps:-                        continue-                    lay = "L5" if l == 3 else LAYER_S[l]-                    need_mm = need_ps / delay.per_mm(lay)-                    cur = TUNE.get(pp.name)-                    xm = CX[k] + S_EAST + xm_rel(kind, l)-                    ytop, ybot = TUNE_WIN[(kind, l)]-                    side = TUNE_DIR[(kind, l)]-                    # current extra length of the existing tuning, then add the shortfall-                    have = getattr(tune, "extra", {}).get(pp.name, 0.0)-                    want = have + need_mm-                    if l == 0:-                        n = max(1, math.ceil(want / 4.6))-                        amp = want / (2 * n) + 0.234   # chamfer correction: 4 corners x 0.586c / (2 per period)-                        pts = accordion(xm, ytop, side, amp, n)-                    else:-                        n = max(1, math.ceil(want / 0.45))-                        amp = want / (0.828 * n)-                        pts = bumps(xm, ytop, side, amp, n)-                    if pts[-1][1] < ybot:-                        raise SystemExit(f"tuning for {pp.name} overruns its window: {pts[-1][1]:.1f} < {ybot}")-                    TUNE[pp.name] = pts-                    tune.extra = getattr(tune, "extra", {})-                    tune.extra[pp.name] = want-                    changed = True-        if not changed:-            break-    return TUNE---def all_lanes():-    out = []-    for k in range(4):-        for l in range(4):-            P, R = plan_lane(k, l)-            out += [build(P), build(R)]-    return out---# ------------------------------------------------------------------------------------------ refclks and the band-M bus-Y_BUS = {"PERST1": 80.0, "REFCLK2": 81.5, "PERST2": 83.0, "REFCLK3": 84.5, "PERST3": 86.0, "REFCLK1": 87.5}-X_CORR_VIA = lambda k: CX[k] + S_EAST + 0.025          # in the R0-R3 gap of corridor k (band M)-G_CONT, G_PERST, G_REF = 12.35, 13.15, 14.4            # positions in gap (k-1, k), relative to cx[k-1] (band M)-Y_REF_TURN, Y_PERST_TURN = 12.3, 10.9-BUS_START_X = 16.0                                      # PERST bus lines start here (L1 -> L5 via)---def corner_path(pts_xy, layer, c=0.3):-    """Expand a rectilinear list of points into chamfered octilinear nodes (45 degree chamfer c)."""-    out = [pts_xy[0]]-    for i in range(1, len(pts_xy) - 1):-        a, b, cc = pts_xy[i - 1], pts_xy[i], pts_xy[i + 1]-        d1 = unit(b[0] - a[0], b[1] - a[1]); d2 = unit(cc[0] - b[0], cc[1] - b[1])-        if abs(d1[0] * d2[1] - d1[1] * d2[0]) < 1e-9:-            out.append(b); continue-        out.append((b[0] - d1[0] * c, b[1] - d1[1] * c))-        out.append((b[0] + d2[0] * c, b[1] + d2[1] * c))-    out.append(pts_xy[-1])-    return out---def bus_route(k_from, k_to, y, first_layer="L5"):-    """Waypoint list (x, y, layer) for a bus line that starts west of corridor k_from on L5 and exits into-    gap (k_to - 1, k_to). Each corridor: L5 under its west half, a via in the R0-R3 gap, L3 over its east half;-    each gap passed through: back to L5."""-    nodes = []-    for k in range(k_from, k_to):-        xv = X_CORR_VIA(k)-        nodes.append((xv, y, "L5"))-        nodes.append((xv, y, "L3"))-        if k < k_to - 1:-            xg = CX[k] + G_CONT-            nodes.append((xg, y, "L3"))-            nodes.append((xg, y, "L5"))-    return nodes---def refclk(k):-    """REFCLK k from its gold fingers to OCuLink port k (A12 p / A13 n)."""-    P = Pair(f"CLK{k}", f"REFCLKn{k}", f"REFCLKp{k}", "REFCLK")-    if k in (0, 1):-        pins = ("B14", "B15") if k == 0 else ("A14", "A15")-    else:-        pins = ("OCP_B11", "OCP_B12") if k == 2 else ("OCP_A11", "OCP_A12")-    lay0 = "L1" if pins[0].startswith(("B", "OCP_B")) else "L6"-    P.start = (finger(pins[0]), finger(pins[1]), lay0)-    xc_f = F + PX[pins[0]] + 0.3-    y0 = PAD_TOP - 0.25 - (0.6 - pitch(lay0)) / 2-    P.add(xc_f, y0, lay0)-    cx = CX[k]-    if k == 0:-        P.add(xc_f, 107.9, "L1"); P.add(19.1, 107.9 - (xc_f - 19.1), "L1"); P.add(19.1, 97.0, "L1")-        P.add(19.1, 97.0, "L3"); P.add(19.1, 96.3, "L3"); P.add(3.6, 96.3 - 15.5, "L3")-        for (x, y) in corner_path([(3.6, 80.8), (3.6, Y_REF_TURN), (cx - 0.5, Y_REF_TURN), (cx - 0.5, Y_REF_TURN - 0.8)], "L3")[1:]:-            P.add(x, y, "L3")-        lay = "L3"-    else:-        yb = Y_BUS[f"REFCLK{k}"]-        if k == 1:-            P.add(xc_f, 108.4, "L6"); P.add(17.6, 108.4 - (xc_f - 17.6), "L6"); P.add(17.6, 98.0, "L6")-            P.add(17.6, 98.0, "L5"); xs = 17.6-        elif k == 2:-            P.add(xc_f, 107.9, "L1"); P.add(12.6, 107.9 - (xc_f - 12.6), "L1"); P.add(12.6, 106.5, "L1")-            P.add(12.6, 106.5, "L5"); xs = 12.6-        else:-            P.add(xc_f, 107.9, "L6"); P.add(14.6, 107.9 - (14.6 - xc_f), "L6"); P.add(14.6, 106.5, "L6")-            P.add(14.6, 106.5, "L5"); xs = 14.6-        for (x, y) in corner_path([(xs, 106.0 if k > 1 else 97.6), (xs, yb), (X_CORR_VIA(0), yb)], "L5")[1:]:-            P.add(x, y, "L5")-        for nd in bus_route(0, k, yb)[1:]:-            P.add(*nd)-        g = CX[k - 1] + G_REF-        lay = "L3"-        pts = corner_path([(P.nodes[-1][0], yb), (g, yb), (g, Y_M2)], "L3")-        for (x, y) in pts[1:]:-            P.add(x, y, lay)-        P.add(g - S_EAST, Y_M2 - S_EAST, lay)-        for (x, y) in corner_path([(g - S_EAST, Y_M2 - S_EAST), (g - S_EAST, Y_REF_TURN), (cx - 0.5, Y_REF_TURN), (cx - 0.5, Y_REF_TURN - 0.8)], lay)[1:]:-            P.add(x, y, lay)-    # B-row gap B11/B14, strip, A12/A13-    xg, xa = cx - 0.5, cx - 1.0-    c = 0.25-    P.add(xg, Y_STRIP + c, lay); P.add(xg - c, Y_STRIP, lay); P.add(xa + c, Y_STRIP, lay); P.add(xa, Y_STRIP - c, lay)-    P.add(xa, Y_AROW + 0.42, lay)-    P.end = ((cx + 4.75 - 0.5 * 12, Y_AROW), (cx + 4.75 - 0.5 * 11, Y_AROW), lay, True)-    return build(P)---class Line:-    """A single-ended route: nodes (x, y, layer); a layer change places one via."""-    def __init__(self, name, net, w=0.15):-        self.name, self.net, self.w = name, net, w-        self.nodes, self.tracks, self.vias = [], [], []--    def add(self, x, y, layer):-        self.nodes.append((round(x, 4), round(y, 4), layer)); return self--    def build(self):-        for a, b in zip(self.nodes, self.nodes[1:]):-            if a[2] != b[2]:-                assert abs(a[0] - b[0]) < 1e-6 and abs(a[1] - b[1]) < 1e-6-                self.vias.append((a[0], a[1], self.net, VIA_D, VIA_H))-            elif math.hypot(b[0] - a[0], b[1] - a[1]) > 1e-6:-                self.tracks.append((a[2], self.net, a[0], a[1], b[0], b[1], self.w))-        return self---def perst_bus(k):-    """PORTk_PERST# from its bus start via (fed from U4 through R1x by the maze router) to the B12 pad of port k."""-    L = Line(f"PERST{k}", f"PORT{k}_PERST#")-    y = Y_BUS[f"PERST{k}"]-    cx = CX[k]-    L.add(BUS_START_X, y, "L1"); L.add(BUS_START_X, y, "L5")-    for nd in bus_route(0, k, y):-        L.add(*nd)-    g = CX[k - 1] + G_PERST-    L.add(g, y, "L3"); L.add(g, y, "L1")-    for (x, yy) in corner_path([(g, y), (g, Y_M2), (g - S_EAST, Y_M2 - S_EAST), (g - S_EAST, Y_PERST_TURN),-                                (cx - 0.25, Y_PERST_TURN), (cx - 0.25, Y_BROW)], "L1", c=0.3)[1:]:-        L.add(x, yy, "L1")-    return L.build()---def all_routes():-    tune()-    pairs = all_lanes() + [refclk(k) for k in range(4)]-    lines = [perst_bus(k) for k in (1, 2, 3)]-    return pairs, lines---if __name__ == "__main__":-    pairs = all_lanes()-    print(len(pairs), "pairs", sum(len(p.tracks) for p in pairs), "tracks", sum(len(p.vias) for p in pairs), "vias")