← Commit history

Delete kicad/generator-scripts/route_fixup.py

TylerK ·626507d20a ·1mo ago ·parent f0574d0
1 file changed −328
kicad/generator-scripts/route_fixup.pydeleted−328
@@ -1,329 +0,0 @@-#!/usr/bin/env python3-"""Second pass: route the handful of links the main route.py pass left missing-(all proven independently routable in isolation -- they only lost the race for-scarce corridor space against everything else already routed). This parses the-ALREADY-ROUTED board so existing segments/vias count as real obstacles, adds-B.Cu as a third emergency routing layer (it's otherwise just the GND zone pour-placed on it, so a short escape trace there is safe), and uses the same-obstacle-aware A* as the main pass rather than an unchecked direct bodge.-"""-import re, math, heapq, json, os, uuid as U--def uid(): return str(U.uuid4())--HERE = os.path.dirname(__file__)-PCB_PATH = "/mnt/c/Users/kirkt/Documents/mppt-power-system-mcu/mppt-power-system-mcu.kicad_pcb"-GRID = 0.3-LAYERS = ["F.Cu", "In2.Cu", "B.Cu"]-VIA_PENALTY = 30-SIGNAL_WIDTH = 0.25-VIA_SIZE, VIA_DRILL = 0.4, 0.2--MISSING_LINKS = [tuple(x) for x in json.load(open(os.path.join(HERE, "missing_links.json")))]-print(f"loaded {len(MISSING_LINKS)} missing links from route.py's output")---def split_blocks(text, tag):-    out = []-    i = 0-    n = len(text)-    needle = '(' + tag-    while True:-        i = text.find(needle, i)-        if i == -1:-            break-        j = i + len(needle)-        if j < n and (text[j].isalnum() or text[j] == '_'):-            i = j-            continue-        depth = 0-        k = i-        ins = False-        esc = False-        while k < n:-            ch = text[k]-            if esc:-                esc = False-            elif ch == '\\':-                esc = True-            elif ch == '"':-                ins = not ins-            elif not ins:-                if ch == '(':-                    depth += 1-                elif ch == ')':-                    depth -= 1-                    if depth == 0:-                        k += 1-                        break-            k += 1-        out.append(text[i:k])-        i = k-    return out---text = open(PCB_PATH, errors='replace').read()--# ---- pads -----pads = {}-for blk in split_blocks(text, 'footprint'):-    m_ref = re.search(r'\(property "Reference" "([^"]*)"', blk)-    ref = m_ref.group(1) if m_ref else "?"-    m_at = re.search(-        r'\(layer "[^"]*"\)\s*\n\s*\(uuid "[^"]*"\)\s*\n\s*\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', blk)-    if not m_at:-        continue-    fx, fy, frot = float(m_at.group(1)), float(m_at.group(2)), float(m_at.group(3) or 0)-    th = math.radians(frot)-    cos_t, sin_t = math.cos(th), math.sin(th)-    for pblk in split_blocks(blk, 'pad'):-        mp = re.match(r'\(pad "(\w*)"\s+(\w+)\s+(\w+)', pblk)-        if not mp:-            continue-        pnum = mp.group(1)-        if not pnum:-            continue-        m_lat = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', pblk)-        if not m_lat:-            continue-        lx, ly = float(m_lat.group(1)), float(m_lat.group(2))-        m_layers = re.search(r'\(layers ([^)]*)\)', pblk)-        layers = m_layers.group(1) if m_layers else ''-        any_layer = '*.Cu' in layers-        m_net = re.search(r'\(net (\d+) "([^"]*)"\)', pblk)-        netnum, netname = (int(m_net.group(1)), m_net.group(2)) if m_net else (0, "")-        m_size = re.search(r'\(size ([-\d.]+) ([-\d.]+)\)', pblk)-        pw, ph = (float(m_size.group(1)), float(m_size.group(2))) if m_size else (0.5, 0.5)-        wx = fx + lx * cos_t + ly * sin_t-        wy = fy + ly * cos_t - lx * sin_t-        pads[(ref, pnum)] = {"x": wx, "y": wy, "net": netname, "netnum": netnum,-                              "any_layer": any_layer, "radius": max(pw, ph) / 2}---def to_grid(x, y):-    return (round(x / GRID), round(y / GRID))--half = max(1, round((SIGNAL_WIDTH / 2 + 0.2) / GRID))-obstacle = {}   # (layer_idx, gx, gy) -> net name (as string) that owns this cell--for (ref, num), p in pads.items():-    gx, gy = to_grid(p["x"], p["y"])-    layer_idxs = range(len(LAYERS)) if p["any_layer"] else [0]-    pad_inflate = max(1, round((p["radius"] + 0.05) / GRID))-    for li in layer_idxs:-        for dx in range(-pad_inflate, pad_inflate + 1):-            for dy in range(-pad_inflate, pad_inflate + 1):-                obstacle[(li, gx + dx, gy + dy)] = p["net"] or "?"--for sblk in split_blocks(text, 'segment'):-    m_start = re.search(r'\(start ([-\d.]+) ([-\d.]+)\)', sblk)-    m_end = re.search(r'\(end ([-\d.]+) ([-\d.]+)\)', sblk)-    m_layer = re.search(r'\(layer "([^"]*)"\)', sblk)-    m_net = re.search(r'\(net (\d+)\)', sblk)-    if not (m_start and m_end and m_layer and m_net):-        continue-    layer = m_layer.group(1)-    if layer not in LAYERS:-        continue-    li = LAYERS.index(layer)-    netnum = str(int(m_net.group(1)))-    x1, y1 = float(m_start.group(1)), float(m_start.group(2))-    x2, y2 = float(m_end.group(1)), float(m_end.group(2))-    gx1, gy1 = to_grid(x1, y1)-    gx2, gy2 = to_grid(x2, y2)-    steps = max(abs(gx2 - gx1), abs(gy2 - gy1), 1)-    for s in range(steps + 1):-        gx = round(gx1 + (gx2 - gx1) * s / steps)-        gy = round(gy1 + (gy2 - gy1) * s / steps)-        for dx in range(-half, half + 1):-            for dy in range(-half, half + 1):-                key = (li, gx + dx, gy + dy)-                if key not in obstacle:-                    obstacle[key] = netnum--existing_vias = []   # (x, y, netnum) for dedup/reuse-for vblk in split_blocks(text, 'via'):-    m_at = re.search(r'\(at ([-\d.]+) ([-\d.]+)\)', vblk)-    m_net = re.search(r'\(net (\d+)\)', vblk)-    m_layers = re.search(r'\(layers ([^)]*)\)', vblk)-    if not (m_at and m_net):-        continue-    x, y = float(m_at.group(1)), float(m_at.group(2))-    netnum = str(int(m_net.group(1)))-    existing_vias.append((x, y, netnum))-    gx, gy = to_grid(x, y)-    # every via in this design is a through-via -- it physically spans and-    # blocks ALL copper layers, not just the two named in its (layers ...)-    for li in range(len(LAYERS)):-        for dx in range(-half, half + 1):-            for dy in range(-half, half + 1):-                key = (li, gx + dx, gy + dy)-                if key not in obstacle:-                    obstacle[key] = netnum--# re-assert exact pad cells so no halo ever blocks a pad's own point-for (ref, num), p in pads.items():-    gx, gy = to_grid(p["x"], p["y"])-    layer_idxs = range(len(LAYERS)) if p["any_layer"] else [0]-    for li in layer_idxs:-        obstacle[(li, gx, gy)] = p["net"] or "?"--MOVES = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)]--def cell_free(li, gx, gy, net):-    o = obstacle.get((li, gx, gy))-    return o is None or o == net or o == str(net)--def mark_occ(li, gx, gy, net):-    for dx in range(-half, half + 1):-        for dy in range(-half, half + 1):-            obstacle[(li, gx + dx, gy + dy)] = net--def astar(src, dst, net, margin_cells, src_layers, dst_layers):-    sx, sy = to_grid(*src)-    tx, ty = to_grid(*dst)-    x0, x1 = min(sx, tx) - margin_cells, max(sx, tx) + margin_cells-    y0, y1 = min(sy, ty) - margin_cells, max(sy, ty) + margin_cells-    nlayers = len(LAYERS)--    def h(node):-        li, gx, gy = node-        return math.hypot(gx - tx, gy - ty)--    # only start/end on a layer the pad actually exists on -- an SMD pad only-    # lives on F.Cu; treating every layer as a free start/end here produced-    # tracks that dead-end at the right (x,y) on the WRONG layer with no via-    starts = [(li, sx, sy) for li in src_layers]-    goals = {(li, tx, ty) for li in dst_layers}-    openq = []-    for s in starts:-        heapq.heappush(openq, (h(s), 0, s, None))-    came, gscore, visited = {}, {s: 0 for s in starts}, set()-    while openq:-        f, g, node, parent = heapq.heappop(openq)-        if node in visited:-            continue-        visited.add(node)-        came[node] = parent-        if node in goals:-            path = []-            cur = node-            while cur is not None:-                path.append(cur)-                cur = came[cur]-            path.reverse()-            return path-        li, gx, gy = node-        if not (x0 <= gx <= x1 and y0 <= gy <= y1):-            continue-        for dxn, dyn in MOVES:-            ngx, ngy = gx + dxn, gy + dyn-            if not (x0 <= ngx <= x1 and y0 <= ngy <= y1) or not cell_free(li, ngx, ngy, net):-                continue-            nnode = (li, ngx, ngy)-            ng = g + math.hypot(dxn, dyn)-            if nnode not in gscore or ng < gscore[nnode]:-                gscore[nnode] = ng-                heapq.heappush(openq, (ng + h(nnode), ng, nnode, node))-        for oli in range(nlayers):-            if oli == li or not cell_free(oli, gx, gy, net):-                continue-            nnode = (oli, gx, gy)-            ng = g + VIA_PENALTY-            if nnode not in gscore or ng < gscore[nnode]:-                gscore[nnode] = ng-                heapq.heappush(openq, (ng + h(nnode), ng, nnode, node))-    return None---def merge_collinear(pts):-    if len(pts) < 3:-        return pts-    out = [pts[0]]-    prev_dir = None-    for i in range(1, len(pts)):-        dx = pts[i][0] - out[-1][0]-        dy = pts[i][1] - out[-1][1]-        norm = math.hypot(dx, dy) or 1-        d = (dx / norm, dy / norm)-        if prev_dir is not None and abs(d[0] - prev_dir[0]) < 1e-6 and abs(d[1] - prev_dir[1]) < 1e-6:-            out[-1] = pts[i]-        else:-            out.append(pts[i])-            prev_dir = d-    return out---def cell_xy(gx, gy):-    return gx * GRID, gy * GRID---all_new = []-still_missing = []-for net_name, a_ref, a_num, b_ref, b_num in MISSING_LINKS:-    a = pads.get((a_ref, a_num))-    b = pads.get((b_ref, b_num))-    if not a or not b:-        still_missing.append((net_name, a_ref, a_num, b_ref, b_num, "pad not found"))-        continue-    net_num = a["netnum"]-    a_layers = tuple(range(len(LAYERS))) if a["any_layer"] else (0,)-    b_layers = tuple(range(len(LAYERS))) if b["any_layer"] else (0,)-    path = None-    for margin in (60, 150, 400):-        path = astar((a["x"], a["y"]), (b["x"], b["y"]), net_name, margin, a_layers, b_layers)-        if path:-            break-    if not path:-        still_missing.append((net_name, a_ref, a_num, b_ref, b_num, "no path"))-        continue-    for (li, gx, gy) in path:-        mark_occ(li, gx, gy, net_name)-    i = 0-    n = len(path)-    while i < n - 1:-        li0 = path[i][0]-        j = i-        while j + 1 < n and path[j + 1][0] == li0:-            j += 1-        pts = [cell_xy(path[k][1], path[k][2]) for k in range(i, j + 1)]-        if i == 0:-            pts[0] = (a["x"], a["y"])-        if j == n - 1:-            pts[-1] = (b["x"], b["y"])-        pts = merge_collinear(pts)-        for k in range(len(pts) - 1):-            x1, y1 = pts[k]-            x2, y2 = pts[k + 1]-            if (x1, y1) == (x2, y2):-                continue-            all_new.append(-                f'\t(segment\n\t\t(start {x1:.6f} {y1:.6f})\n\t\t(end {x2:.6f} {y2:.6f})\n'-                f'\t\t(width {SIGNAL_WIDTH})\n\t\t(layer "{LAYERS[li0]}")\n\t\t(net {net_num})\n'-                f'\t\t(uuid "{uid()}")\n\t)')-        if j + 1 < n:-            _, vx, vy = path[j]-            x, y = cell_xy(vx, vy)-            next_li = path[j + 1][0]-            via_layers = sorted({LAYERS[li0], LAYERS[next_li]})-            # KiCad through-vias always span the whole stack physically;-            # declare the outer two copper layers so the object is a normal-            # through via regardless of which two routing layers it links-            all_new.append(-                f'\t(via\n\t\t(at {x:.6f} {y:.6f})\n\t\t(size {VIA_SIZE})\n\t\t(drill {VIA_DRILL})\n'-                f'\t\t(layers "F.Cu" "B.Cu")\n\t\t(net {net_num})\n\t\t(uuid "{uid()}")\n\t)')-        i = j + 1--print(f"fixup: routed {len(MISSING_LINKS) - len(still_missing)}/{len(MISSING_LINKS)} links, "-      f"{len(all_new)} new pcb objects")-if still_missing:-    print("STILL MISSING:", still_missing)--if all_new:-    insertion = '\n'.join(all_new) + '\n'-    stripped = text.rstrip()-    idx = stripped.rfind('\n')-    new_text = stripped[:idx + 1] + insertion + stripped[idx + 1:] + '\n'-    open(PCB_PATH, "w").write(new_text)-    print("wrote fixup routes back to", PCB_PATH)