molecule
AM/FM Receiver Molecule
Public Made by Adomby adom
USB-powered AM/FM receiver on an Adom Molecule: discrete BJT and op-amp superhet (10.7 MHz and 455 kHz IFs), STM32G0B1 tuning, self-calibration and USB audio over one USB-C cable. 96 x 96 mm pin grid, 4 layers, KiCad 10.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
"""Grid A* router that completes connections the autorouter left open.
For every ratsnest edge still open after Freerouting (+ zone fill), route from the smaller
copper island to the other one on F.Cu/B.Cu (vias allowed), honouring netclass width and
clearance. For plane nets (GND/+5VA/+3V3) a via dropped inside the net's plane zone counts
as reaching the target.
"""
import math, heapq, sys, os
import numpy as np
import pcbnew
RES = 0.05 # grid resolution (mm)
# Items removed from the board stay referenced here: letting SWIG garbage-collect a removed
# PCB_TRACK corrupts pcbnew's type table for the rest of the process.
GRAVEYARD = []
VIA_D, VIA_DRILL = 0.6, 0.3
PLANE_LAYER = {'GND': pcbnew.In1_Cu, '+5VA': pcbnew.In2_Cu, '+3V3': pcbnew.In2_Cu}
def tomm(v):
return pcbnew.ToMM(v)
def mm(v):
return pcbnew.FromMM(v)
class Router:
def __init__(self, board, ox, oy, w, h):
self.b = board
self.ox, self.oy = ox, oy
self.nx, self.ny = int(round(w / RES)) + 1, int(round(h / RES)) + 1
xs = ox + np.arange(self.nx) * RES
ys = oy + np.arange(self.ny) * RES
self.X, self.Y = np.meshgrid(xs, ys, indexing='ij')
self.w, self.h = w, h
self.collect()
# ------------------------------------------------------------------ copper inventory
def collect(self):
self.items = [] # dict(kind, net, layers, geometry)
for fp in self.b.GetFootprints():
for p in fp.Pads():
bb = p.GetBoundingBox()
lays = set()
for L, key in ((pcbnew.F_Cu, 'F'), (pcbnew.B_Cu, 'B')):
if p.IsOnLayer(L):
lays.add(key)
if p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
lays = {'F', 'B'}
self.items.append(dict(kind='pad', net=p.GetNetCode(), layers=lays, obj=p,
rect=(tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()),
tomm(bb.GetBottom())), hole=p.GetAttribute() != pcbnew.PAD_ATTRIB_SMD))
for t in self.b.GetTracks():
if t.GetClass() == 'PCB_VIA':
p = t.GetPosition()
self.items.append(dict(kind='via', net=t.GetNetCode(), layers={'F', 'B'}, obj=t,
c=(tomm(p.x), tomm(p.y)), r=tomm(t.GetWidth(pcbnew.F_Cu)) / 2))
else:
L = t.GetLayer()
if L not in (pcbnew.F_Cu, pcbnew.B_Cu):
continue
a, e = t.GetStart(), t.GetEnd()
self.items.append(dict(kind='track', net=t.GetNetCode(), layers={'F' if L == pcbnew.F_Cu else 'B'},
obj=t, a=(tomm(a.x), tomm(a.y)), e=(tomm(e.x), tomm(e.y)),
r=tomm(t.GetWidth()) / 2))
# ------------------------------------------------------------------ rasterisation
def _window(self, x0, y0, x1, y1):
i0 = max(0, int(math.floor((x0 - self.ox) / RES)))
i1 = min(self.nx, int(math.ceil((x1 - self.ox) / RES)) + 1)
j0 = max(0, int(math.floor((y0 - self.oy) / RES)))
j1 = min(self.ny, int(math.ceil((y1 - self.oy) / RES)) + 1)
return i0, i1, j0, j1
def paint(self, grid, it, margin):
if it['kind'] == 'pad':
x0, y0, x1, y1 = it['rect']
i0, i1, j0, j1 = self._window(x0 - margin, y0 - margin, x1 + margin, y1 + margin)
if i0 >= i1 or j0 >= j1:
return
X, Y = self.X[i0:i1, j0:j1], self.Y[i0:i1, j0:j1]
dx = np.maximum(np.maximum(x0 - X, 0), X - x1)
dy = np.maximum(np.maximum(y0 - Y, 0), Y - y1)
grid[i0:i1, j0:j1] |= (dx * dx + dy * dy) <= margin * margin
elif it['kind'] == 'via':
cx, cy = it['c']
R = it['r'] + margin
i0, i1, j0, j1 = self._window(cx - R, cy - R, cx + R, cy + R)
X, Y = self.X[i0:i1, j0:j1], self.Y[i0:i1, j0:j1]
grid[i0:i1, j0:j1] |= (X - cx) ** 2 + (Y - cy) ** 2 <= R * R
else:
(ax, ay), (ex, ey), r = it['a'], it['e'], it['r']
R = r + margin
i0, i1, j0, j1 = self._window(min(ax, ex) - R, min(ay, ey) - R, max(ax, ex) + R, max(ay, ey) + R)
X, Y = self.X[i0:i1, j0:j1], self.Y[i0:i1, j0:j1]
vx, vy = ex - ax, ey - ay
L2 = vx * vx + vy * vy
if L2 == 0:
t = np.zeros_like(X)
else:
t = np.clip(((X - ax) * vx + (Y - ay) * vy) / L2, 0, 1)
px, py = ax + t * vx, ay + t * vy
grid[i0:i1, j0:j1] |= (X - px) ** 2 + (Y - py) ** 2 <= R * R
def obstacle_maps(self, net, hw, clr):
blk = {k: np.zeros((self.nx, self.ny), bool) for k in 'FB'}
via_blk = np.zeros((self.nx, self.ny), bool)
rv = VIA_D / 2
for it in self.items:
same = it['net'] == net and net != 0
for L in it['layers']:
if not same:
self.paint(blk[L], it, clr + hw + RES * 0.75)
if not same:
self.paint(via_blk, it, clr + rv + RES * 0.75)
else:
if it['kind'] == 'pad':
self.paint(via_blk, it, rv + 0.05) # no via-in-pad
if it['kind'] == 'via':
self.paint(via_blk, it, VIA_DRILL / 2 + 0.25 + RES) # hole-to-hole
if it['kind'] == 'pad' and it.get('hole'):
self.paint(via_blk, it, rv + 0.25)
# board edge
e = 0.3
for g in (blk['F'], blk['B']):
g[(self.X < self.ox + e + hw) | (self.X > self.ox + self.w - e - hw) |
(self.Y < self.oy + e + hw) | (self.Y > self.oy + self.h - e - hw)] = True
via_blk[(self.X < self.ox + e + rv) | (self.X > self.ox + self.w - e - rv) |
(self.Y < self.oy + e + rv) | (self.Y > self.oy + self.h - e - rv)] = True
return blk, via_blk
def target_maps(self, items):
tg = {k: np.zeros((self.nx, self.ny), bool) for k in 'FB'}
for it in items:
for L in it['layers']:
self.paint(tg[L], it, 0.0)
return tg
def plane_mask(self, netname):
"""Cells where a via would land inside the net's plane zone (outline, 0.8 mm margin)."""
L = PLANE_LAYER.get(netname)
m = np.zeros((self.nx, self.ny), bool)
if L is None:
return m
for z in self.b.Zones():
if z.GetNetname() != netname or not z.IsOnLayer(L):
continue
bb = z.Outline().BBox()
x0, y0, x1, y1 = tomm(bb.GetLeft()) + 0.8, tomm(bb.GetTop()) + 0.8, tomm(bb.GetRight()) - 0.8, tomm(bb.GetBottom()) - 0.8
m |= (self.X >= x0) & (self.X <= x1) & (self.Y >= y0) & (self.Y <= y1)
return m
# ------------------------------------------------------------------ search
def astar(self, starts, tg, blk, via_blk, plane_goal, via_cost=40, max_nodes=3000000):
"""starts: list of (i, j, layer). Returns list of (i, j, layer, 'via'|'') or None."""
nx, ny = self.nx, self.ny
LAY = {'F': 0, 'B': 1}
goal_idx = [np.argwhere(tg['F']), np.argwhere(tg['B'])]
pg = np.argwhere(plane_goal & ~via_blk) if plane_goal is not None else np.zeros((0, 2), int)
allg = [g for g in (goal_idx[0], goal_idx[1], pg) if len(g)]
if not allg:
return None
allg = np.vstack(allg)
# coarse heuristic: distance to nearest goal among a subsample
sub = allg[:: max(1, len(allg) // 400)]
def h(i, j):
d = np.min(np.abs(sub[:, 0] - i) + np.abs(sub[:, 1] - j))
return 0.9 * d
open_ = []
g = {}
parent = {}
for (i, j, L) in starts:
s = (i, j, LAY[L])
g[s] = 0.0
heapq.heappush(open_, (h(i, j), 0.0, s))
parent[s] = None
dirs = [(1, 0, 1.0), (-1, 0, 1.0), (0, 1, 1.0), (0, -1, 1.0),
(1, 1, 1.414), (1, -1, 1.414), (-1, 1, 1.414), (-1, -1, 1.414)]
blkL = [blk['F'], blk['B']]
tgL = [tg['F'], tg['B']]
hcache = {}
n = 0
while open_:
f, gc, s = heapq.heappop(open_)
if g.get(s, 1e18) < gc - 1e-9:
continue
i, j, L = s
n += 1
if n > max_nodes:
return None
if tgL[L][i, j] and parent[s] is not None:
return self._path(parent, s)
if plane_goal is not None and plane_goal[i, j] and not via_blk[i, j] and parent[s] is not None:
return self._path(parent, s) + [(i, j, L, 'plane')]
for di, dj, c in dirs:
a, b_ = i + di, j + dj
if a < 0 or b_ < 0 or a >= nx or b_ >= ny:
continue
if blkL[L][a, b_] and not tgL[L][a, b_]:
continue
# changing direction costs a little (keeps tracks straight)
ns = (a, b_, L)
ng = gc + c
if ng < g.get(ns, 1e18):
g[ns] = ng
parent[ns] = (s, '')
key = (a // 8, b_ // 8)
if key not in hcache:
hcache[key] = h(a, b_)
heapq.heappush(open_, (ng + hcache[key], ng, ns))
if not via_blk[i, j]:
ns = (i, j, 1 - L)
ng = gc + via_cost
if not blkL[1 - L][i, j] and ng < g.get(ns, 1e18):
g[ns] = ng
parent[ns] = (s, 'via')
key = (i // 8, j // 8)
if key not in hcache:
hcache[key] = h(i, j)
heapq.heappush(open_, (ng + hcache[key], ng, ns))
return None
def _path(self, parent, s):
out = []
cur = s
while cur is not None:
p = parent[cur]
out.append((cur[0], cur[1], cur[2], p[1] if p else ''))
cur = p[0] if p else None
out.reverse()
return out
def commit(self, path, net, width, locked=False, start_anchor=None, end_anchor=None):
"""Turn a cell path into straight track runs and vias on the board. Anchors (x, y) pull the
ends onto the centre of the pad/via/track they touch so KiCad sees a solid connection."""
pts = [(self.ox + i * RES, self.oy + j * RES, L, flag) for i, j, L, flag in path]
if start_anchor is not None:
pts.insert(0, (start_anchor[0], start_anchor[1], pts[0][2], ''))
if end_anchor is not None and pts[-1][3] != 'plane':
pts.append((end_anchor[0], end_anchor[1], pts[-1][2], ''))
layer_id = [pcbnew.F_Cu, pcbnew.B_Cu]
segs, vias = [], []
run = [pts[0]]
for k in range(1, len(pts)):
x, y, L, flag = pts[k]
if flag == 'via':
segs.append(run)
vias.append((x, y))
run = [pts[k]]
continue
run.append(pts[k])
segs.append(run)
if pts[-1][3] == 'plane':
vias.append((pts[-1][0], pts[-1][1]))
added = []
for run in segs:
if len(run) < 2:
continue
# compress collinear points
simp = [run[0]]
for k in range(1, len(run) - 1):
x0, y0 = simp[-1][0], simp[-1][1]
x1, y1 = run[k][0], run[k][1]
x2, y2 = run[k + 1][0], run[k + 1][1]
if abs((x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1)) > 1e-9:
simp.append(run[k])
simp.append(run[-1])
for a, b in zip(simp, simp[1:]):
t = pcbnew.PCB_TRACK(self.b)
t.SetStart(pcbnew.VECTOR2I(mm(a[0]), mm(a[1])))
t.SetEnd(pcbnew.VECTOR2I(mm(b[0]), mm(b[1])))
t.SetWidth(mm(width))
t.SetLayer(layer_id[a[2]])
t.SetNetCode(net)
self.b.Add(t)
added.append(t)
self.items.append(dict(kind='track', net=net, layers={'FB'[a[2]]}, obj=t, a=(a[0], a[1]),
e=(b[0], b[1]), r=width / 2))
for x, y in vias:
v = pcbnew.PCB_VIA(self.b)
v.SetPosition(pcbnew.VECTOR2I(mm(x), mm(y)))
v.SetWidth(mm(VIA_D))
v.SetDrill(mm(VIA_DRILL))
v.SetViaType(pcbnew.VIATYPE_THROUGH)
v.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
v.SetNetCode(net)
self.b.Add(v)
added.append(v)
self.items.append(dict(kind='via', net=net, layers={'F', 'B'}, obj=v, c=(x, y), r=VIA_D / 2))
return added
def item_dicts(router, objs):
ids = {id(o) for o in objs}
out = []
keyset = set()
for o in objs:
keyset.add(o.m_Uuid.AsString())
for it in router.items:
if it['obj'].m_Uuid.AsString() in keyset:
out.append(it)
return out
def anchor_for(r, items, x, y, layer):
"""Centre of the copper item (pad/via) or nearest centre-line point (track) under (x, y)."""
best, bd = None, 1e9
for it in items:
if layer not in it['layers']:
continue
if it['kind'] == 'pad':
x0, y0, x1, y1 = it['rect']
d = math.hypot(max(x0 - x, 0, x - x1), max(y0 - y, 0, y - y1))
c = ((x0 + x1) / 2, (y0 + y1) / 2)
pos = it['obj'].GetPosition()
c = (tomm(pos.x), tomm(pos.y))
elif it['kind'] == 'via':
d = max(0.0, math.hypot(x - it['c'][0], y - it['c'][1]) - it['r'])
c = it['c']
else:
(ax, ay), (ex, ey) = it['a'], it['e']
vx, vy = ex - ax, ey - ay
L2 = vx * vx + vy * vy
t = 0.0 if L2 == 0 else max(0.0, min(1.0, ((x - ax) * vx + (y - ay) * vy) / L2))
c = (ax + t * vx, ay + t * vy)
d = max(0.0, math.hypot(x - c[0], y - c[1]) - it['r'])
if d < bd:
best, bd = c, d
return best if bd <= RES * 2 else None
TYPES = [pcbnew.PCB_TRACE_T, pcbnew.PCB_PAD_T, pcbnew.PCB_VIA_T, pcbnew.PCB_ARC_T]
def net_islands(board):
"""{netcode: (netname, [island, ...])} for nets whose pads sit on more than one copper island.
An island is a list of board items (pads, tracks, vias) that connect, zones included."""
board.BuildConnectivity()
conn = board.GetConnectivity()
pads_by_net = {}
for fp in board.GetFootprints():
for p in fp.Pads():
if p.GetNetCode() > 0:
pads_by_net.setdefault(p.GetNetCode(), []).append(p)
out = {}
for code, pads in pads_by_net.items():
if len(pads) < 2:
continue
seen = {}
islands = []
for p in pads:
u = p.m_Uuid.AsString()
if u in seen:
continue
items = list(conn.GetConnectedItems(p)) + [p]
idx = len(islands)
islands.append(items)
for it in items:
seen[it.m_Uuid.AsString()] = idx
if len(islands) > 1:
out[code] = (pads[0].GetNetname(), islands)
return out
def fix(board, ox, oy, w, h, width_of, clearance_of, log=print, max_rounds=3, first=()):
r = Router(board, ox, oy, w, h)
routed = failed = 0
for rnd in range(max_rounds):
todo = net_islands(board)
log(' round %d: %d nets with open connections %s' % (rnd, len(todo),
[v[0] for v in todo.values()]))
if not todo:
break
progress = False
order = sorted(todo.items(), key=lambda kv: (kv[0] not in first, kv[0]))
for code, (netname, islands) in order:
islands.sort(key=len)
src_items = item_dicts(r, islands[0])
tgt_items = item_dicts(r, [it for isl in islands[1:] for it in isl])
tg = r.target_maps(tgt_items)
src = r.target_maps(src_items)
plane_goal = r.plane_mask(netname) if netname in PLANE_LAYER else None
path = None
# full netclass width first; neck down (board minimum 0.15 mm) only to escape fine-pitch pins
for w in sorted({width_of(netname), 0.2, 0.15}, reverse=True):
if w > width_of(netname):
continue
hw = w / 2
blk, via_blk = r.obstacle_maps(code, hw, clearance_of(netname))
starts = []
for L in 'FB':
cells = np.argwhere(src[L] & ~blk[L])
if len(cells) == 0:
cells = np.argwhere(src[L])
for c in cells[:: max(1, len(cells) // 80)]:
starts.append((int(c[0]), int(c[1]), L))
path = r.astar(starts, tg, blk, via_blk, plane_goal)
if path is not None:
break
if path is None:
log(' FAILED %s' % netname)
failed += 1
continue
i0, j0, L0, _ = path[0]
i1, j1, L1, f1 = path[-1]
sa = anchor_for(r, src_items, ox + i0 * RES, oy + j0 * RES, 'FB'[L0])
ea = None if f1 == 'plane' else anchor_for(r, tgt_items, ox + i1 * RES, oy + j1 * RES, 'FB'[L1])
r.commit(path, code, 2 * hw, start_anchor=sa, end_anchor=ea)
board.BuildConnectivity()
routed += 1
progress = True
log(' routed %s (%d cells)' % (netname, len(path)))
if not progress:
break
return routed, failed
def remove_item(board, r, obj):
u = obj.m_Uuid.AsString()
r.items = [it for it in r.items if it['obj'].m_Uuid.AsString() != u]
board.Remove(obj)
GRAVEYARD.append(obj)
def cleanup_orphans(board, r, log=print):
"""Delete track/via clusters that touch no pad (dangling leftovers of a rip-up)."""
board.BuildConnectivity()
conn = board.GetConnectivity()
removed = 0
for t in list(board.GetTracks()):
if t.IsLocked():
continue
items = list(conn.GetConnectedItems(t))
if not any(i.GetClass() == 'PAD' or i.GetClass() == 'ZONE' for i in items):
remove_item(board, r, t)
removed += 1
if removed:
log(' removed %d orphan track/via pieces' % removed)
def rip_near(board, r, code, radius=1.6, log=print):
"""Remove other-net unlocked track segments passing within `radius` of `code`'s pads that
are stranded (the failed island). Returns the set of affected net codes."""
board.BuildConnectivity()
todo = net_islands(board)
if code not in todo:
return set()
netname, islands = todo[code]
islands.sort(key=len)
pads = [it for it in islands[0] if it.GetClass() == 'PAD']
hit = set()
for p in pads:
pos = p.GetPosition()
px, py = tomm(pos.x), tomm(pos.y)
for t in list(board.GetTracks()):
if t.GetNetCode() == code or t.IsLocked():
continue
if t.GetClass() == 'PCB_VIA':
q = t.GetPosition()
d = math.hypot(tomm(q.x) - px, tomm(q.y) - py)
else:
a, e = t.GetStart(), t.GetEnd()
from pcbgeom import seg_point_dist
d = seg_point_dist(tomm(a.x), tomm(a.y), tomm(e.x), tomm(e.y), px, py)
if d < radius:
hit.add(t.GetNetCode())
remove_item(board, r, t)
log(' ripped up segments of %d nets around %s' % (len(hit), netname))
return hit
def rip_corridor(board, r, code, width_of, clearance_of, log=print):
"""For a net blocked somewhere along a long route: find its path with only pads as obstacles,
then remove the other nets' tracks and vias that sit on that corridor. Returns their net codes."""
board.BuildConnectivity()
todo = net_islands(board)
if code not in todo:
return set()
netname, islands = todo[code]
islands.sort(key=len)
src_items = item_dicts(r, islands[0])
tgt_items = item_dicts(r, [it for isl in islands[1:] for it in isl])
hw, clr = min(width_of(netname), 0.2) / 2, clearance_of(netname)
saved = r.items
r.items = [it for it in saved if it['kind'] == 'pad' or it['net'] == code]
blk, via_blk = r.obstacle_maps(code, hw, clr)
r.items = saved
tg, src = r.target_maps(tgt_items), r.target_maps(src_items)
starts = []
for L in 'FB':
cells = np.argwhere(src[L] & ~blk[L])
for c in cells[:: max(1, len(cells) // 80)]:
starts.append((int(c[0]), int(c[1]), L))
path = r.astar(starts, tg, blk, via_blk, None, via_cost=60)
if path is None:
log(' corridor: no pad-only path for %s' % netname)
return set()
pts = {0: [], 1: []}
for i, j, L, flag in path:
x, y = r.ox + i * RES, r.oy + j * RES
pts[L].append((x, y))
if flag == 'via':
pts[1 - L].append((x, y))
P = {L: np.array(v) if v else np.zeros((0, 2)) for L, v in pts.items()}
hit = set()
for t in list(board.GetTracks()):
if t.GetNetCode() == code or t.IsLocked():
continue
if t.GetClass() == 'PCB_VIA':
q = t.GetPosition()
qx, qy = tomm(q.x), tomm(q.y)
reach = tomm(t.GetWidth(pcbnew.F_Cu)) / 2 + hw + clr
d = min((np.min(np.hypot(P[L][:, 0] - qx, P[L][:, 1] - qy)) for L in (0, 1) if len(P[L])), default=1e9)
else:
L = 0 if t.GetLayer() == pcbnew.F_Cu else 1 if t.GetLayer() == pcbnew.B_Cu else None
if L is None or not len(P[L]):
continue
a, e = t.GetStart(), t.GetEnd()
ax, ay, ex, ey = tomm(a.x), tomm(a.y), tomm(e.x), tomm(e.y)
vx, vy = ex - ax, ey - ay
L2 = vx * vx + vy * vy or 1e-12
tt = np.clip(((P[L][:, 0] - ax) * vx + (P[L][:, 1] - ay) * vy) / L2, 0, 1)
d = np.min(np.hypot(P[L][:, 0] - (ax + tt * vx), P[L][:, 1] - (ay + tt * vy)))
reach = tomm(t.GetWidth()) / 2 + hw + clr
if d < reach:
hit.add(t.GetNetCode())
remove_item(board, r, t)
log(' corridor: ripped up segments of %d nets along the path of %s' % (len(hit), netname))
return hit
def fix_with_ripup(board, ox, oy, w, h, width_of, clearance_of, log=print, attempts=4):
routed, failed = fix(board, ox, oy, w, h, width_of, clearance_of, log=log)
for k in range(attempts):
todo = net_islands(board)
if not todo:
break
r = Router(board, ox, oy, w, h)
for code in list(todo):
if k == 2: # third try: clear the whole blocked corridor instead of just around the pads
rip_corridor(board, r, code, width_of, clearance_of, log=log)
else:
rip_near(board, r, code, radius=1.2 + 0.6 * k, log=log)
cleanup_orphans(board, r, log=log)
# route the originally failed nets first, then everything that got cut
routed2, failed = fix(board, ox, oy, w, h, width_of, clearance_of, log=log, max_rounds=4,
first=set(todo))
routed += routed2
return routed, failed
def remove_dangling(board, log=print):
"""Iteratively delete unlocked track segments with an unconnected end."""
total = 0
for _ in range(20):
board.BuildConnectivity()
conn = board.GetConnectivity()
dead = [t for t in board.GetTracks()
if t.GetClass() != 'PCB_VIA' and not t.IsLocked() and conn.TestTrackEndpointDangling(t, False)]
if not dead:
break
for t in dead:
board.Remove(t)
GRAVEYARD.append(t)
total += len(dead)
if total:
log(' removed %d dangling track stubs' % total)
return total