main
John Lauer README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/ 39a59ef 7d ago
#!/usr/bin/env python3
"""Assemble every route: high-speed (hs.py), fixed extras, maze-routed low-speed nets, GND fan-out, stitching,
zones and keepouts -> design/routes.json for build_board.py final."""
import json, math, os, sys, time
import numpy as np
from shapely.geometry import Polygon, Point, LineString, box
from shapely.ops import unary_union
from shapely import contains_xy
sys.path.insert(0, os.path.dirname(__file__))
import hs, maze
from maze import G, NX, NY, LI

ROOT = os.path.join(os.path.dirname(__file__), "..")
pads = json.load(open(os.path.join(ROOT, "design", "pads.json")))
lay = json.load(open(os.path.join(ROOT, "design", "layout.json")))
nl = json.load(open(os.path.join(ROOT, "design", "netlist.json")))
OUTLINE = Polygon(lay["outline"])
LAYMAP = {"F.Cu": "L1", "In2.Cu": "L3", "In4.Cu": "L5", "B.Cu": "L6"}

T, V = [], []           # all tracks and vias for routes.json
B = maze.Board()

# ------------------------------------------------------------------ fixed high-speed copper
pairs, lines = hs.all_routes()
for p in pairs:
    for t in p.tracks:
        T.append(t); B.track(*t, hs=True)
    for v in p.vias:
        V.append(v); B.via(v[0], v[1], v[2], v[3], hs=True)
for l in lines:
    for t in l.tracks:
        T.append(t); B.track(*t)
    for v in l.vias:
        V.append(v); B.via(v[0], v[1], v[2], v[3])

# ------------------------------------------------------------------ pads
PAD = {}
for p in pads:
    net = p["net"] or f"NC:{p['ref']}.{p['num']}"
    PAD[(p["ref"], p["num"], round(p["x"], 3), round(p["y"], 3))] = p
    geom = unary_union([Polygon(o) for o in p["poly"] if len(o) >= 3]) if p["poly"] else Point(p["x"], p["y"]).buffer(p["w"] / 2)
    p["geom"] = geom
    lis = [LI[LAYMAP[l]] for l in p["layers"] if l in LAYMAP]
    if len(p["layers"]) >= 6 or p["drill"][0] > 0:
        lis = [0, 1, 2, 3]
        # a plated hole occupies every layer with at least its drill
        dg = Point(p["x"], p["y"]).buffer(max(p["drill"]) / 2 + 0.05)
        p["geom"] = geom.union(dg)
    B.paint_geom(lis, p["geom"], B.nid(net))
    p["lis"] = lis

# ------------------------------------------------------------------ fixed extras
def add_track(t):
    T.append(t); B.track(*t)

def add_via(v, hsflag=False):
    V.append(v); B.via(v[0], v[1], v[2], v[3], hs=hsflag)

pin_net = {}
for n, ps in nl["nets"].items():
    for q in ps:
        pin_net[q] = n
# GND gold fingers: short stub off the inner end so the outer-layer GND pour picks them up
for p in pads:
    if p["ref"] == "J1" and p["net"] == "GND":
        layer = "L1" if "F.Cu" in p["layers"] else "L6"
        add_track((layer, "GND", p["x"], hs.PAD_TOP, p["x"], hs.PAD_TOP - 0.55, 0.3))
cols = {}
for p in pads:
    if p["ref"] == "J1":
        cols.setdefault(round(p["x"], 3), {})["F" if "F.Cu" in p["layers"] else "B"] = p["net"]
for x, sides in sorted(cols.items()):
    if sides.get("F") == "GND" and sides.get("B") == "GND":
        add_via((x, hs.PAD_TOP - 0.62, "GND", 0.4, 0.2))
# +12V fingers B1..B6: one bar
b1 = hs.finger("B1"); b6 = hs.finger("B6")
add_track(("L1", "+12V_EDGE", b1[0], b1[1], b1[0], 111.6, 0.3))
for n in range(2, 7):
    x, y = hs.finger(f"B{n}")
    add_track(("L1", "+12V_EDGE", x, y, x, 111.6, 0.3))
add_track(("L1", "+12V_EDGE", b1[0], 111.6, b6[0], 111.6, 0.3))
# OCuLink GND pads: via-in-pad (filled and capped)
for p in pads:
    if p["ref"] in ("J2", "J3", "J4", "J5") and p["net"] == "GND" and p["num"] != "SH":
        add_via((p["x"], p["y"], "GND", hs.VIP_D, hs.VIP_H), True)

# ------------------------------------------------------------------ finger escape reservations
# every used signal/power finger keeps a 2.4 mm column above it on its own layer, so no other net's via lands there
HS_NETS = set()
for _p in pairs:
    HS_NETS |= {_p.nnet, _p.pnet}
for p in pads:
    if p["ref"] != "J1" or not p["net"] or p["net"] in ("GND", "+12V_EDGE") or p["net"] in HS_NETS:
        continue
    li = LI["L1"] if "F.Cu" in p["layers"] else LI["L6"]
    w = 0.2
    B.paint_geom(li, box(p["x"] - w / 2, hs.PAD_TOP - 2.4, p["x"] + w / 2, hs.PAD_TOP), B.nid(p["net"]))

# ------------------------------------------------------------------ static keepouts
yy, xx = B.Y, B.X
inside = contains_xy(OUTLINE.buffer(-0.42), xx, yy)
for li in range(4):
    B.block[li] |= ~inside
# long-edge strips: no copper on outer layers within 2.75 mm, inner layers within 1.0 mm (OCP Fig. 8/10)
strip_outer = (xx < 2.75) | (xx > 76 - 2.75)
strip_inner = (xx < 1.0) | (xx > 75.0)
B.block[LI["L1"]] |= strip_outer; B.block[LI["L6"]] |= strip_outer
B.block[LI["L3"]] |= strip_inner; B.block[LI["L5"]] |= strip_inner
# gold-finger field: nothing else on the outer layers past the finger tops; inner copper clear of the bevel
B.block[LI["L1"]] |= yy > 112.05
B.block[LI["L6"]] |= yy > 112.05
B.block[LI["L3"]] |= yy > 113.6
B.block[LI["L5"]] |= yy > 113.6
B.block_via = np.zeros_like(B.block)
for li in range(4):
    B.block_via[li] = B.block[li] | (yy > 111.6)


def terminals(net):
    out = []
    for p in pads:
        if p["net"] != net:
            continue
        cells = set()
        x0, y0, x1, y1 = p["geom"].bounds
        i0, i1, j0, j1 = int(x0 / G), int(x1 / G) + 1, int(y0 / G), int(y1 / G) + 1
        sub = contains_xy(p["geom"].buffer(-0.02), xx[j0:j1 + 1, i0:i1 + 1], yy[j0:j1 + 1, i0:i1 + 1])
        js, iss = np.nonzero(sub)
        for li in p["lis"]:
            for j, i in zip(js + j0, iss + i0):
                cells.add((li, int(j), int(i)))
        if not cells:
            cells = {(li, int(round(p["y"] / G)), int(round(p["x"] / G))) for li in p["lis"]}
        out.append(cells)
    # existing copper of the net (fixed lines) is one more terminal
    nid = B.nid(net)
    ex = set()
    for (lay_, n, x1, y1, x2, y2, w) in T:
        if n == net:
            L = max(1, int(math.hypot(x2 - x1, y2 - y1) / G))
            for k in range(L + 1):
                x = x1 + (x2 - x1) * k / L; y = y1 + (y2 - y1) * k / L
                ex.add((LI[lay_], int(round(y / G)), int(round(x / G))))
    if ex:
        out.insert(0, ex)
    return out


L6_REGION = (yy > 101.0) & (xx < 30.0)
ROUTE = [
    # bottom-side (A) finger escapes first: each needs a via site above its own finger
    ("PERST1#", 0.15, ("L1", "L3", "L5", "L6")), ("PRSNTA0#", 0.15, ("L1", "L3", "L6")),
    ("PRSNTB2#", 0.15, ("L1", "L3", "L6")), ("SMCLK0", 0.15, ("L1", "L3", "L5", "L6")),
    ("SMDAT0", 0.15, ("L1", "L3", "L5", "L6")), ("PERST2#", 0.15, ("L1", "L3", "L5", "L6")),
    ("PERST3#", 0.15, ("L1", "L3", "L5", "L6")), ("WAKE0#", 0.15, ("L1", "L3", "L5", "L6")),
    ("SLOT_ID1", 0.15, ("L1", "L3", "L5", "L6")),
    # top side and power
    ("+3V3_EDGE", 0.2, ("L1", "L3", "L5")), ("+12V_EDGE", 0.2, ("L1", "L3", "L5")),
    ("PRSNTB3#", 0.15, ("L1", "L3")), ("PERST0#", 0.15, ("L1", "L3", "L5")),
    ("NIC_PWR_GOOD0", 0.15, ("L1", "L3", "L5")), ("SCAN0_LD#", 0.15, ("L1", "L3", "L5")),
    ("SCAN0_DATA_IN", 0.15, ("L1", "L3", "L5")), ("SCAN0_DATA_OUT", 0.15, ("L1", "L3", "L5")),
    ("SCAN0_CLK", 0.15, ("L1", "L3", "L5")), ("SLOT_ID0", 0.15, ("L1", "L3", "L5")),
    ("AUX_PWR_EN0", 0.15, ("L1", "L3", "L5")),
    ("PERST0_BUF", 0.15, ("L1", "L3", "L5")), ("PERST1_BUF", 0.15, ("L1", "L3", "L5")),
    ("PERST2_BUF", 0.15, ("L1", "L3", "L5")), ("PERST3_BUF", 0.15, ("L1", "L3", "L5")),
    ("PORT0_PERST#", 0.15, ("L1", "L3", "L5")), ("PORT1_PERST#", 0.15, ("L1", "L3", "L5")),
    ("PORT2_PERST#", 0.15, ("L1", "L3", "L5")), ("PORT3_PERST#", 0.15, ("L1", "L3", "L5")),
    ("PORT0_CPRSNT#", 0.15, ("L1", "L3", "L5")), ("PORT1_CPRSNT#", 0.15, ("L1", "L3", "L5")),
    ("PORT2_CPRSNT#", 0.15, ("L1", "L3", "L5")), ("PORT3_CPRSNT#", 0.15, ("L1", "L3", "L5")),
    ("FRU_WP", 0.15, ("L1", "L3", "L5")), ("SR0_SER", 0.15, ("L1", "L3", "L5")),
    ("LED3V3_A", 0.15, ("L1",)), ("LED12V_A", 0.15, ("L1",)),
]

CLR, CLR_HS = 0.14, 0.28


def paint_net(net, tracks, vias):
    for t in tracks:
        B.track(*t)
    for v in vias:
        B.via(v[0], v[1], v[2], v[3])


def route_list(order, routed, attempt_log):
    fails = []
    for (net, w, allow) in order:
        terms = terminals(net)
        if len(terms) < 2:
            continue
        paths, msg = maze.route_net(B, net, terms, w=w, clr=CLR, clr_hs=CLR_HS, allow=allow, l6_region=L6_REGION)
        if paths is None:
            fails.append(net); attempt_log.append(f"FAIL {net}: {msg}"); print(f"FAIL {net}: {msg}", flush=True); continue
        tr, vi = [], []
        for pth in paths:
            a_, b_ = maze.path_to_copper(pth, net, w)
            tr += a_; vi += b_
        routed[net] = (tr, vi)
        paint_net(net, tr, vi)
        print(f"{net}: {len(paths)} branch(es), {len(tr)} tracks, {len(vi)} vias", flush=True)
    return fails


def rebuild(routed):
    """Fresh raster with fixed copper + pads + reservations + everything routed so far."""
    global B
    lab0, hs0, blk, blkv = BASE
    B.lab = lab0.copy(); B.hs = hs0.copy()
    for net, (tr, vi) in routed.items():
        paint_net(net, tr, vi)


if __name__ == "__main__":
    t0 = time.time()
    FIXED_T, FIXED_V = list(T), list(V)
    BASE = (B.lab.copy(), B.hs.copy(), B.block.copy(), B.block_via.copy())
    W = {n: (w, a) for (n, w, a) in ROUTE}
    routed = {}
    log = []
    fails = route_list(ROUTE, routed, log)
    priority = []                      # every net that ever failed, routed first from then on
    for rnd in range(10):
        if not fails:
            break
        for n in fails:
            if n not in priority:
                priority.append(n)
        print(f"--- rip-up round {rnd+1}: {fails}  priority {priority}", flush=True)
        rip = set()
        for net in priority:
            for (li, j, i) in set().union(*terminals(net)):
                r = int(1.2 / G)
                for lj in range(4):
                    blk = B.lab[lj, max(0, j - r):j + r + 1, max(0, i - r):i + r + 1]
                    for nid in np.unique(blk):
                        if nid >= 0 and B.netname[nid] in routed:
                            rip.add(B.netname[nid])
        rip |= set(priority)
        for n in rip:
            routed.pop(n, None)
        rebuild(routed)
        # the nets that just failed get first pick, then the older failures, then the ripped-up rest
        first = list(fails) + [n for n in priority if n not in fails]
        order = [(n, *W[n]) for n in first] + [(n, *W[n]) for n in [x[0] for x in ROUTE] if n in rip and n not in priority]
        fails = route_list(order, routed, log)
    # ESD grounds: each ESD's GND pins to the GND pads of its connector
    for k in range(4):
        ref, conn = f"U{5+k}", f"J{2+k}"
        esd_cells, conn_cells = [], set()
        for p in pads:
            if p["net"] != "GND":
                continue
            if p["ref"] == ref or (p["ref"] == conn and p["num"] in ("B11", "B14", "B17", "B20")):
                x0, y0, x1, y1 = p["geom"].bounds
                i0, i1, j0, j1 = int(x0 / G), int(x1 / G) + 1, int(y0 / G), int(y1 / G) + 1
                sub = contains_xy(p["geom"].buffer(-0.02), xx[j0:j1 + 1, i0:i1 + 1], yy[j0:j1 + 1, i0:i1 + 1])
                js, iss = np.nonzero(sub)
                cells = {(LI["L1"], int(j + j0), int(i + i0)) for j, i in zip(js, iss)}
                if p["ref"] == ref:
                    esd_cells.append(cells)
                else:
                    conn_cells |= cells
        terms = [conn_cells] + esd_cells
        paths, msg = maze.route_net(B, "GND", terms, w=0.2, clr=CLR, clr_hs=CLR_HS, allow=("L1",))
        if paths is None:
            print(f"FAIL ESD GND {ref}: {msg}"); fails.append(f"GND:{ref}"); continue
        tr, vi = [], []
        for pth in paths:
            a_, b_ = maze.path_to_copper(pth, "GND", 0.2); tr += a_; vi += b_
        routed[f"GND:{ref}"] = (tr, vi); paint_net("GND", tr, vi)
        print(f"GND {ref}: {len(tr)} tracks")
    for net, (tr, vi) in routed.items():
        T.extend(tr); V.extend(vi)
    json.dump({"tracks": T, "vias": V, "fails": fails, "fixed_tracks": FIXED_T, "fixed_vias": FIXED_V},
              open(os.path.join(ROOT, "design", "routes_ls.json"), "w"))
    print("fails:", fails, f"[{time.time()-t0:.0f}s]")