← Commit history

Delete design/gen/finish_routes.py

John Lauer ·90487ac8ea ·7d ago ·parent 41599a2
1 file changed −329
design/gen/finish_routes.pydeleted−329
@@ -1,329 +0,0 @@-#!/usr/bin/env python3-"""After the maze: GND fan-out vias for SMD ground pads, GND stitching and return vias, pours and keepouts,-board text. Writes design/routes.json (everything build_board.py final needs)."""-import json, math, os, sys, time-import numpy as np-from scipy import ndimage-from shapely.geometry import Polygon, box, Point-sys.path.insert(0, os.path.dirname(__file__))-import route_all as R-import maze, hs-from maze import G, LI, NX, NY--ROOT = R.ROOT-ls = json.load(open(os.path.join(ROOT, "design", "routes_ls.json")))-B = R.B-T, V = R.T, R.V-_ts = set(map(tuple, T)); _vs = set(map(tuple, V))-for t in map(tuple, ls["tracks"]):-    if t not in _ts:-        T.append(t); B.track(*t); _ts.add(t)-for v in map(tuple, ls["vias"]):-    if v not in _vs:-        V.append(v); B.via(v[0], v[1], v[2], v[3]); _vs.add(v)-gnd = B.nid("GND")---def holes_mask(gap=0.62):-    """cells too close to any existing drilled hole (via or plated pad) for hole-to-hole spacing"""-    m = np.zeros((NY, NX), bool)-    pts = [(v[0], v[1], v[4] if len(v) > 4 else 0.2) for v in V]-    pts += [(p["x"], p["y"], max(p["drill"])) for p in R.pads if p["drill"][0] > 0]-    for (x, y, h) in pts:-        r = gap + h / 2-        j0, j1 = max(0, int((y - r) / G)), min(NY - 1, int((y + r) / G) + 1)-        i0, i1 = max(0, int((x - r) / G)), min(NX - 1, int((x + r) / G) + 1)-        yy_, xx_ = np.mgrid[j0:j1 + 1, i0:i1 + 1]-        m[j0:j1 + 1, i0:i1 + 1] |= (xx_ * G - x) ** 2 + (yy_ * G - y) ** 2 < r * r-    return m---def via_ok_mask(clr=0.25, clr_hs=0.35, d=maze.VIA_D):-    ok = ~holes_mask()-    for li in range(4):-        lab = B.lab[li]-        other = (lab >= 0) & (lab != gnd)-        ok &= ndimage.distance_transform_edt(~other) * G >= clr + d / 2-        oh = other & B.hs[li]-        ok &= ndimage.distance_transform_edt(~oh) * G >= clr_hs + d / 2-        ok &= ~B.block_via[li]-    # keep top/bottom copper out of the 2.75 mm long-edge strips (OCP: component and trace free)-    ok &= (B.X > 3.0) & (B.X < 73.0)-    return ok---# ---------------------------------------------------------------- probe-pad stubs (design/tp_stubs.json)-_st = os.path.join(ROOT, "design", "tp_stubs.json")-if os.path.exists(_st):-    for t in json.load(open(_st))["tracks"]:-        t = tuple(t); T.append(t); B.track(*t)--# ---------------------------------------------------------------- GND fan-out for SMD ground pads-fan = 0-FAN_T, FAN_V = [], []-FIXED_NETS = {t[1] for t in ls.get("fixed_tracks", [])} | {v[2] for v in ls.get("fixed_vias", [])}---def fan_pad(p):-    vok = via_ok_mask(0.25, 0.32)-    cx, cy = p["x"], p["y"]-    j0, i0 = int(round(cy / G)), int(round(cx / G))-    r = int(2.5 / G)-    jj, ii = np.nonzero(vok[max(0, j0 - r):j0 + r + 1, max(0, i0 - r):i0 + r + 1])-    goal = {(LI["L1"], int(j + max(0, j0 - r)), int(i + max(0, i0 - r))) for j, i in zip(jj, ii)}-    x0, y0, x1, y1 = p["geom"].bounds-    from shapely import contains_xy-    sj0, sj1, si0, si1 = int(y0 / G), int(y1 / G) + 1, int(x0 / G), int(x1 / G) + 1-    sub = contains_xy(p["geom"].buffer(-0.02), B.X[sj0:sj1 + 1, si0:si1 + 1], B.Y[sj0:sj1 + 1, si0:si1 + 1])-    sjs, sis = np.nonzero(sub)-    start = {(LI["L1"], int(j + sj0), int(i + si0)) for j, i in zip(sjs, sis)}-    if not goal or not start:-        return False-    paths, msg = maze.route_net(B, "GND", [start, goal], w=0.25, clr=0.2, clr_hs=0.32, allow=("L1",), max_expand=400000)-    if paths is None:-        # no via site nearby: a short top-layer trace to GND copper already there (another GND pad, a fan-out)-        own = p["geom"].buffer(0.25)-        gj, gi = np.nonzero(B.lab[LI["L1"], max(0, j0 - r):j0 + r + 1, max(0, i0 - r):i0 + r + 1] == gnd)-        goal2 = {(LI["L1"], int(j + max(0, j0 - r)), int(i + max(0, i0 - r))) for j, i in zip(gj, gi)}-        goal2 = {c for c in goal2 if not own.contains(Point(c[2] * G, c[1] * G))}-        if goal2:-            paths, msg = maze.route_net(B, "GND", [start, goal2], w=0.2, clr=0.15, clr_hs=0.3, allow=("L1",), max_expand=400000)-        if paths is not None:-            tr, _ = maze.path_to_copper(paths[0], "GND", 0.2)-            for t in tr:-                T.append(t); B.track(*t); FAN_T.append(t)-            print("GND trace to nearby GND copper for", p["ref"], p["num"])-            return True-    if paths is None:-        # no room beside the pad: a filled, capped via in the pad (JLC 6-layer POFV, as at the OCuLink pads)-        d_ip, h_ip = 0.3, 0.15-        vin = ~holes_mask(0.35)-        for li in range(4):-            other = (B.lab[li] >= 0) & (B.lab[li] != gnd)-            vin &= ndimage.distance_transform_edt(~other) * G >= 0.15 + d_ip / 2-            oh = other & B.hs[li]-            vin &= ndimage.distance_transform_edt(~oh) * G >= 0.28 + d_ip / 2-        cand = [(math.hypot(c[2] * G - cx, c[1] * G - cy), c) for c in start if vin[c[1], c[2]]-                and p["geom"].buffer(-0.03).contains(Point(c[2] * G, c[1] * G).buffer(d_ip / 2))]-        if not cand:-            cand = [(math.hypot(c[2] * G - cx, c[1] * G - cy), c) for c in start if vin[c[1], c[2]]]-        if not cand:-            return False-        c = min(cand)[1]-        v = (round(c[2] * G, 4), round(c[1] * G, 4), "GND", d_ip, h_ip)-        V.append(v); B.via(v[0], v[1], "GND", d_ip); FAN_V.append(v)-        print("GND via-in-pad for", p["ref"], p["num"], v[:2])-        return True-    tr, _ = maze.path_to_copper(paths[0], "GND", 0.25)-    end = paths[0][-1]-    for t in tr:-        T.append(t); B.track(*t); FAN_T.append(t)-    v = (round(end[2] * G, 4), round(end[1] * G, 4), "GND", maze.VIA_D, maze.VIA_H)-    V.append(v); B.via(v[0], v[1], "GND", v[3]); FAN_V.append(v)-    return True---def unpaint(net):-    nid = B.nid(net)-    B.lab[B.lab == nid] = -1-    for q in R.pads:-        if q["net"] == net:-            B.paint_geom(q["lis"], q["geom"], nid)---def rip_and_fan(p):-    """Last resort for a boxed-in GND pad: rip the low-speed nets crowding it on L1, fan the pad out, then-    route those nets again around the new copper (restoring them as they were if that fails)."""-    near = p["geom"].buffer(0.7)-    x0, y0, x1, y1 = near.bounds-    j0, j1, i0, i1 = int(y0 / G), int(y1 / G) + 1, int(x0 / G), int(x1 / G) + 1-    ids = set(np.unique(B.lab[:, j0:j1 + 1, i0:i1 + 1])) - {-1, gnd}-    rips = sorted(B.netname[i] for i in ids if B.netname[i] not in FIXED_NETS and not B.netname[i].startswith(("+", "PE", "REFCLK")))-    if not rips:-        return False-    saved = {n: ([t for t in T if t[1] == n], [v for v in V if v[2] == n]) for n in rips}-    for n in rips:-        T[:] = [t for t in T if t[1] != n]; V[:] = [v for v in V if v[2] != n]-        unpaint(n)-    nT, nV = len(FAN_T), len(FAN_V)-    ok = fan_pad(p)-    if ok:-        newc = {}-        for n in rips:-            w, allow = {r_[0]: (r_[1], r_[2]) for r_ in R.ROUTE}.get(n, (0.15, ("L1", "L3", "L5")))-            paths, msg = maze.route_net(B, n, R.terminals(n), w=w, clr=R.CLR, clr_hs=R.CLR_HS, allow=allow, l6_region=R.L6_REGION)-            if paths is None:-                ok = False; print("  reroute failed:", n, msg); break-            tr, vi = [], []-            for pth in paths:-                a_, b_ = maze.path_to_copper(pth, n, w); tr += a_; vi += b_-            for t in tr: B.track(*t)-            for v in vi: B.via(v[0], v[1], n, v[3])-            newc[n] = (tr, vi)-        if ok:-            for n, (tr, vi) in newc.items():-                T.extend(tr); V.extend(vi)-            print("  ripped and re-routed", rips, "for", p["ref"], p["num"])-            return True-        # undo: drop the fan-out and the partial reroutes, restore the old copper-        drop_t, drop_v = set(map(tuple, FAN_T[nT:])), set(map(tuple, FAN_V[nV:]))-        del FAN_T[nT:]; del FAN_V[nV:]-        T[:] = [t for t in T if tuple(t) not in drop_t]; V[:] = [v for v in V if tuple(v) not in drop_v]-        unpaint("GND")-        for t in T:-            if t[1] == "GND": B.track(*t)-        for v in V:-            if v[2] == "GND": B.via(v[0], v[1], "GND", v[3])-    for n in rips:-        unpaint(n)-        T[:] = [t for t in T if t[1] != n]; V[:] = [v for v in V if v[2] != n]-        tr, vi = saved[n]-        T.extend(tr); V.extend(vi)-        for t in tr: B.track(*t)-        for v in vi: B.via(v[0], v[1], n, v[3])-    return False---for p in R.pads:-    if p["net"] != "GND" or p["ref"] in ("J1", "J2", "J3", "J4", "J5", "H1", "H2") or p["drill"][0] > 0:-        continue-    if fan_pad(p) or rip_and_fan(p):-        fan += 1-    else:-        print("no GND fan-out for", p["ref"], p["num"])-print("GND fan-out vias:", fan)--# ---------------------------------------------------------------- return vias beside every high-speed via pair-vok = via_ok_mask(0.25, 0.3)-ret = 0-hsv = [v for v in V if v[2] != "GND" and (v[2].startswith(("PET", "PER", "REFCLK")))]-taken = []-for (x, y, net, d, h) in hsv:-    # look for a GND via spot within 1.1 mm, prefer the closest, at most one per signal via-    j0, i0 = int(round(y / G)), int(round(x / G))-    cands = []-    for dj in range(-11, 12):-        for di in range(-11, 12):-            j, i = j0 + dj, i0 + di-            if 0 <= j < NY and 0 <= i < NX and vok[j, i]:-                rr = math.hypot(dj, di) * G-                if 0.55 <= rr <= 1.1:-                    cands.append((rr, j, i))-    cands.sort()-    for rr, j, i in cands:-        if all(math.hypot(i * G - a, j * G - b) > 0.65 for (a, b) in taken):-            v = (i * G, j * G, "GND", maze.VIA_D, maze.VIA_H)-            V.append(v); B.via(i * G, j * G, "GND", maze.VIA_D)-            taken.append((i * G, j * G)); ret += 1-            vok = None-            break-    if vok is None:-        vok = via_ok_mask(0.25, 0.3)-print("return vias:", ret)--# ---------------------------------------------------------------- stitching grid-vok = via_ok_mask(0.25, 0.35)-st = 0-pitch = 2.0-for yy in np.arange(1.2, 112.0, pitch):-    for xx in np.arange(3.2, 73.0, pitch):-        j, i = int(round(yy / G)), int(round(xx / G))-        if vok[j, i]:-            V.append((round(xx, 3), round(yy, 3), "GND", maze.VIA_D, maze.VIA_H)); st += 1-            # keep the mask honest for the next candidates-            r = int(0.8 / G)-            vok[max(0, j - r):j + r + 1, max(0, i - r):i + r + 1] = False-print("stitching vias:", st)--# ---------------------------------------------------------------- pours-outline = Polygon(R.lay["outline"])-outer = outline.buffer(-0.3).intersection(box(2.75, 0.3, 76 - 2.75, 111.95))-inner = outline.buffer(-1.0).intersection(box(1.0, 0.3, 75.0, 113.6))---def poly_pts(g):-    if g.geom_type == "MultiPolygon":-        g = max(g.geoms, key=lambda q: q.area)-    return [(round(x, 4), round(y, 4)) for (x, y) in list(g.exterior.coords)[:-1]]----def prune_dangling():-    """Drop signal stubs that end nowhere (a fixed bus start the maze tapped further along): a via touching-    fewer than two tracks and no pad, and a track whose free end touches no pad, via or other track."""-    from shapely.geometry import LineString-    padg = {}-    for p in R.pads:-        padg.setdefault(p["net"], []).append(p["geom"])-    removed = 0-    while True:-        changed = False-        for net in sorted({t[1] for t in T} - {"GND"}):-            tr = [t for t in T if t[1] == net]-            vi = [v for v in V if v[2] == net]-            pg = padg.get(net, [])--            def on_track(x, y, layer, skip):-                pt = Point(x, y)-                return any(t is not skip and t[0] == layer and-                           LineString([(t[2], t[3]), (t[4], t[5])]).distance(pt) <= (t[6] + skip[6]) / 2 - 0.005 for t in tr)--            def at_via(x, y, w):-                return any(math.hypot(v[0] - x, v[1] - y) <= (v[3] + w) / 2 - 0.005 for v in vi)--            def in_pad(x, y, r=0.0):-                return any(g.distance(Point(x, y)) <= max(r - 0.005, 1e-3) for g in pg)-            for v in vi:-                n = sum(1 for t in tr if LineString([(t[2], t[3]), (t[4], t[5])]).distance(Point(v[0], v[1])) <= (v[3] + t[6]) / 2 - 0.005)-                if n < 2 and not in_pad(v[0], v[1], v[3] / 2):-                    V.remove(v); removed += 1; changed = True-            vi = [v for v in V if v[2] == net]-            for t in tr:-                for end in (0, 1):-                    x, y = (t[4], t[5]) if end else (t[2], t[3])-                    if in_pad(x, y, t[6] / 2) or at_via(x, y, t[6]) or on_track(x, y, t[0], t):-                        continue-                    # free end: cut the segment back to the last tap along it (another track's end, a via or-                    # a pad touching it), or drop it when nothing touches it but its other end-                    sx, sy = (t[2], t[3]) if end else (t[4], t[5])-                    L = math.hypot(x - sx, y - sy)-                    seg = LineString([(sx, sy), (x, y)])-                    us = []-                    for o in tr:-                        if o is t or o[0] != t[0]:-                            continue-                        for (ox, oy) in ((o[2], o[3]), (o[4], o[5])):-                            if seg.distance(Point(ox, oy)) <= (o[6] + t[6]) / 2 - 0.005:-                                us.append(seg.project(Point(ox, oy)))-                    for v in vi:-                        if seg.distance(Point(v[0], v[1])) <= (v[3] + t[6]) / 2 - 0.005:-                            us.append(seg.project(Point(v[0], v[1])))-                    for g in pg:-                        if g.distance(seg) <= t[6] / 2 - 0.005:-                            us.append(seg.project(g.centroid))-                    us = [u for u in us if 1e-3 < u < L - 1e-3]-                    T.remove(t); removed += 1; changed = True-                    if us:-                        u = max(us)-                        nx, ny = sx + (x - sx) * u / L, sy + (y - sy) * u / L-                        nt = (t[0], t[1], round(sx, 4), round(sy, 4), round(nx, 4), round(ny, 4), t[6])-                        T.append(nt)-                    break-        if not changed:-            return removed---print("dangling stubs pruned:", prune_dangling())--zones = []-for layer, g, clr in (("L1", outer, 0.35), ("L6", outer, 0.35), ("L3", inner, 0.3), ("L5", inner, 0.3),-                      ("L2", inner, 0.25), ("L4", inner, 0.25)):-    zones.append({"layer": layer, "net": "GND", "poly": poly_pts(g), "clearance": clr, "priority": 0,-                  "thermal": False})--texts = [-    {"text": "OCP NIC 3.0 SFF  ->  4x OCuLink 4i", "x": 38.0, "y": 104.6, "layer": "F.SilkS", "size": 1.0},-]-json.dump({"tracks": T, "vias": V, "zones": zones, "texts": [], "fanout_tracks": FAN_T, "fanout_vias": FAN_V, "stats": {"fanout": fan, "return": ret, "stitch": st}},-          open(os.path.join(ROOT, "design", "routes.json"), "w"))-print("routes.json:", len(T), "tracks,", len(V), "vias,", len(zones), "zones")