skill
EDA Skillpack
Public Made by Adomby adom
All skills for all EDA capabilities across the Adom ecosystem.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
#!/usr/bin/env python3
"""Routing gates: layout rules as numbers that fail the build. Reads a .kicad_pcb with the standard library.
python3 routing_gates.py <board.kicad_pcb> <spec.json> [--json out.json] [--only grid,necks,...]
Run it on a board whose zones have been REFILLED by KiCad (kicad-cli pcb drc --refill-zones --save-board):
stored fills can be stale, and two checks measure the filled copper.
The spec names the parts the rules are about (nothing is guessed from reference designators):
{
"powerStage": {"ic": "U1", "vinPin": "3", "gndPin": "1", "inputCaps": ["C2", "C3"],
"outputCaps": ["C6", "C7", "C8"], "inductor": "L1", "inductorOutPin": "2",
"planeLayer": "B.Cu", "planeNet": "GND"},
"powerPaths": [{"net": "SW", "from": "U1.2", "to": "L1.1", "amps": 1.0}],
"decoupling": [{"pin": "U1.3", "cap": "C2"}],
"thresholds": {...optional overrides of THRESH below...}
}
Exit 0 when every gate passes, 1 when any fails, 2 on a spec or board error.
"""
import json, math, sys
from collections import defaultdict, deque
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import kicad_board as K # noqa: E402
THRESH = {
'off_grid_segments': 0, # 0/45/90 only; arcs are reported separately and allowed only if the spec says so
'extra_bends': 0, # collinear (redundant) vertices plus short jogs that return to their heading
'jog_max_mm': 0.6, # a bend pair closer than this that restores the heading is a jog
'inline_vias': 0, # a via where two same-layer track ends meet (the trace continues through it)
'input_pad_to_pin_mm': 2.5, # best input cap: VIN pad to the IC VIN pin, GND pad to the IC GND pin
'input_loop_area_mm2': 6.0, # IC VIN pin -> cap VIN pad -> cap GND pad -> IC GND pin
'output_cluster_spread_mm': 5.0,
'output_to_inductor_mm': 5.0,
'plane_intrusions': 0, # non-plane-net copper on the plane layer under the hot loop
'hot_loop_margin_mm': 1.0,
'neck_track_vs_pad': 1.0, # power path: narrowest track / the narrow side of the pad it leaves
'neck_copper_vs_pad': 1.0, # power path: narrowest filled-copper width along the path / the same pad side
'meander_ratio': 1.15, # power path: routed track length / straight pad-to-pad distance
'path_bends': 4, # power path: direction changes along its track route
'raster_mm': 0.02, # filled-copper width raster
}
def seg_angle_ok(a, b):
ang = math.degrees(math.atan2(b[1] - a[1], b[0] - a[0])) % 45
return min(ang, 45 - ang) < 0.05
def unit(a, b):
dx, dy = b[0] - a[0], b[1] - a[1]; n = math.hypot(dx, dy) or 1
return dx / n, dy / n
def key(p):
return (round(p[0], 3), round(p[1], 3))
class Gates:
def __init__(self, board, spec):
self.B, self.S = board, spec
self.T = {**THRESH, **spec.get('thresholds', {})}
self.res, self.fails = {}, []
self.straight = [t for t in board.tracks if not t['arc']]
def fail(self, msg):
self.fails.append(msg)
# ---- appearance ---------------------------------------------------------------------------------
def grid(self):
off = [(t['net'], self.B.design(t['a']), self.B.design(t['b'])) for t in self.straight if not seg_angle_ok(t['a'], t['b'])]
arcs = sum(t['arc'] for t in self.B.tracks)
self.res['grid'] = {'segments': len(self.straight), 'off_grid': len(off), 'examples': off[:5], 'arcs': arcs}
if len(off) > self.T['off_grid_segments']: self.fail(f'{len(off)} of {len(self.straight)} segments off the 0/45/90 grid')
if arcs and not self.S.get('allowArcs'): self.fail(f'{arcs} arc track(s); set "allowArcs" if they are intended')
def _chains(self):
"""Per net and layer: vertices where exactly two segments of equal width meet and nothing else touches."""
touch = defaultdict(list)
for i, t in enumerate(self.straight):
for e in ('a', 'b'):
touch[(t['net'], t['layer'], key(t[e]))].append(i)
pads = [(p, K.pad_polygon(p)) for p in self.B.pads]
vias = {key((v['x'], v['y'])) for v in self.B.vias}
out = []
for (net, layer, k), ids in touch.items():
if len(ids) != 2 or k in vias: continue
if any(p['net'] == net and K.inside(k, poly) for p, poly in pads): continue
out.append((net, layer, k, [self.straight[i] for i in ids]))
return out
def bends(self):
redundant, jogs = [], []
chains = self._chains()
for net, layer, k, (s1, s2) in chains:
o1 = s1['a'] if key(s1['b']) == k else s1['b']; o2 = s2['a'] if key(s2['b']) == k else s2['b']
u1, u2 = unit(o1, k), unit(k, o2)
if abs(u1[0] * u2[1] - u1[1] * u2[0]) < 1e-6 and u1[0] * u2[0] + u1[1] * u2[1] > 0 and s1['w'] == s2['w']:
redundant.append((net, self.B.design(k)))
# jogs: a short segment between two bends whose outer headings agree (a sidestep that adds two bends)
mids = {}
for net, layer, k, segs in chains:
mids[(net, layer, k)] = segs
for t in self.straight:
ln = math.dist(t['a'], t['b'])
if ln >= self.T['jog_max_mm']: continue
ends = []
for e in ('a', 'b'):
segs = mids.get((t['net'], t['layer'], key(t[e])))
if not segs: break
other = segs[0] if segs[1] is t else segs[1]
far = other['a'] if key(other['b']) == key(t[e]) else other['b']
ends.append((e, far))
if len(ends) != 2: continue
ua = unit(ends[0][1], t['a']); ub = unit(t['b'], ends[1][1])
if abs(ua[0] * ub[1] - ua[1] * ub[0]) < 1e-3 and ua[0] * ub[0] + ua[1] * ub[1] > 0 and not (abs(ua[0] * unit(t['a'], t['b'])[1] - ua[1] * unit(t['a'], t['b'])[0]) < 1e-3):
jogs.append((t['net'], self.B.design(t['a']), round(ln, 3)))
self.res['bends'] = {'redundant_vertices': len(redundant), 'jogs': len(jogs), 'examples': (redundant + jogs)[:6]}
n = len(redundant) + len(jogs)
if n > self.T['extra_bends']: self.fail(f'{n} extra bend(s): {len(redundant)} redundant vertex(es), {len(jogs)} short jog(s)')
def inline_vias(self):
ends = defaultdict(int)
for t in self.B.tracks:
for e in ('a', 'b'):
ends[(key(t[e]), t['layer'])] += 1
bad = [(v['net'], self.B.design((v['x'], v['y']))) for v in self.B.vias
if any(ends[(key((v['x'], v['y'])), l)] >= 2 for l in ('F.Cu', 'B.Cu'))]
self.res['inline_vias'] = {'count': len(bad), 'examples': bad[:5]}
if len(bad) > self.T['inline_vias']: self.fail(f'{len(bad)} via(s) inside a trace (two same-layer track ends meet at the via)')
# ---- connectivity graph (tracks, vias, pads, filled zones) -------------------------------------------------
def graph(self, net):
"""Nodes: track ends, pads and vias of `net`. Edges carry (length, kind): 'track' (its length), 'join' (a track
end inside a pad or via), and 'fill': members of one filled polygon are joined pairwise at their straight-line
distance, so crossing a pour costs the distance across it, never zero."""
g = defaultdict(list)
def edge(a, b, mm, kind):
g[a].append((b, mm, kind)); g[b].append((a, mm, kind))
trs = [t for t in self.B.tracks if t['net'] == net]
pads = [p for p in self.B.pads if p['net'] == net]
vias = [v for v in self.B.vias if v['net'] == net]
for t in trs:
edge(('pt', key(t['a']), t['layer']), ('pt', key(t['b']), t['layer']), math.dist(t['a'], t['b']), 'track')
on_layer = lambda p, layer: layer in p['layers'] or '*.Cu' in p['layers']
for t in trs:
for e in ('a', 'b'):
n = ('pt', key(t[e]), t['layer'])
for p in pads:
if on_layer(p, t['layer']) and K.inside(t[e], K.pad_polygon(p)):
edge(n, ('pad', p['ref'], p['num']), math.dist(t[e], (p['x'], p['y'])), 'join')
for v in vias:
if math.dist(t[e], (v['x'], v['y'])) <= v['d'] / 2:
edge(n, ('via', key((v['x'], v['y']))), math.dist(t[e], (v['x'], v['y'])), 'join')
for z in self.B.zones:
if z['net'] != net: continue
for f in z['fills']:
members = [(('pad', p['ref'], p['num']), (p['x'], p['y'])) for p in pads if on_layer(p, f['layer']) and K.inside((p['x'], p['y']), f['pts'])]
members += [(('via', key((v['x'], v['y']))), (v['x'], v['y'])) for v in vias if K.inside((v['x'], v['y']), f['pts'])]
members += [(('pt', key(t[e]), t['layer']), t[e]) for t in trs if t['layer'] == f['layer'] for e in ('a', 'b') if K.inside(t[e], f['pts'])]
for i in range(len(members)):
for j in range(i + 1, len(members)):
edge(members[i][0], members[j][0], math.dist(members[i][1], members[j][1]), 'fill')
return g
@staticmethod
def shortest(g, src, dst_pred):
dist, prev, todo, seen = {src: 0.0}, {}, [src], set()
while todo:
u = min(todo, key=dist.get); todo.remove(u)
if u in seen: continue
seen.add(u)
if dst_pred(u):
path, kinds = [u], []
while path[-1] in prev:
q, kind = prev[path[-1]]; kinds.append(kind); path.append(q)
return dist[u], (path[::-1], kinds[::-1])
for v, w, kind in g[u]:
if dist[u] + w < dist.get(v, 1e18):
dist[v] = dist[u] + w; prev[v] = (u, kind); todo.append(v)
return None, None
# ---- decoupling: cap first, loops measured, never a silent None ---------------------------------------------
def decoupling(self):
rows = []
for d in self.S.get('decoupling', []):
ref, num = d['pin'].split('.')
pin = self.B.pad(ref, num); net = pin['net']
cap_pads = self.B.pads_of(d['cap']); cap = next((p for p in cap_pads if p['net'] == net), None)
gnd = next((p for p in cap_pads if p['net'] != net), None)
row = {'pin': d['pin'], 'cap': d['cap'], 'net': net}
if cap is None:
row['error'] = f"{d['cap']} has no pad on {net}"; rows.append(row); self.fail(row['error']); continue
g = self.graph(net)
length, pk = self.shortest(g, ('pad', ref, num), lambda u: u == ('pad', d['cap'], cap['num']))
row['supplyPathMm'] = None if length is None else round(length, 3)
# cap first: the pin reaches its cap over its own copper. A pour on the way, or a node where a third
# route joins (a track end, pad or via with other routes), is a junction before the cap.
junction = None
if pk:
path, kinds = pk
if 'fill' in kinds:
junction = 'a pour'
for u in path[1:-1]:
if junction: break
if len([1 for _, _, kd in g[u] if kd != 'fill']) >= 3:
junction = u[0] if u[0] != 'pt' else self.B.design(u[1])
row['capFirst'] = pk is not None and junction is None
row['junction'] = junction
gg = self.graph(gnd['net'])
sink, spk = self.shortest(gg, ('pad', d['cap'], gnd['num']), lambda u: u[0] == 'via' or (u[0] == 'pad' and u[1] in self.S.get('groundPins', [])))
row['gndSinkMm'] = None if sink is None else round(sink, 3)
row['gndSink'] = None if not spk else (spk[0][-1][0] if spk[0][-1][0] == 'via' else '.'.join(spk[0][-1][1:]))
rows.append(row)
if row['supplyPathMm'] is None: self.fail(f"decoupling {d['pin']} -> {d['cap']}: supply path not traced")
elif not row['capFirst']: self.fail(f"decoupling {d['pin']} -> {d['cap']}: a junction ({row['junction']}) before the cap")
if row['gndSinkMm'] is None: self.fail(f"decoupling {d['cap']}: ground sink not traced")
self.res['decoupling'] = rows
# ---- power stage ------------------------------------------------------------------------------------------
def power_stage(self):
ps = self.S.get('powerStage')
if not ps: return
vin, gndp = self.B.pad(ps['ic'], ps['vinPin']), self.B.pad(ps['ic'], ps['gndPin'])
caps = {}
for r in ps['inputCaps']:
v = next(p for p in self.B.pads_of(r) if p['net'] == vin['net']); g = next(p for p in self.B.pads_of(r) if p['net'] == gndp['net'])
P = lambda p: (p['x'], p['y'])
caps[r] = {'vin_mm': round(math.dist(P(v), P(vin)), 3), 'gnd_mm': round(math.dist(P(g), P(gndp)), 3),
'loop_mm2': round(K.area([P(vin), P(v), P(g), P(gndp)]), 2)}
best = min(caps, key=lambda r: max(caps[r]['vin_mm'], caps[r]['gnd_mm']))
b = caps[best]
lv = self.B.pad(ps['inductor'], ps['inductorOutPin'])
outs = {r: next(p for p in self.B.pads_of(r) if p['net'] == lv['net']) for r in ps['outputCaps']}
pts = {r: (p['x'], p['y']) for r, p in outs.items()}
spread = max(math.dist(pts[a], pts[c]) for a in pts for c in pts)
far = max(math.dist(p, (lv['x'], lv['y'])) for p in pts.values())
self.res['power_stage'] = {'input': caps, 'best_input': best, 'output_spread_mm': round(spread, 3), 'output_to_inductor_mm': round(far, 3)}
if max(b['vin_mm'], b['gnd_mm']) > self.T['input_pad_to_pin_mm']:
self.fail(f"input loop: {best} pads {b['vin_mm']} / {b['gnd_mm']} mm from the VIN / GND pins (> {self.T['input_pad_to_pin_mm']})")
if b['loop_mm2'] > self.T['input_loop_area_mm2']: self.fail(f"input loop area {b['loop_mm2']} mm2 (> {self.T['input_loop_area_mm2']})")
if spread > self.T['output_cluster_spread_mm']: self.fail(f'output caps spread {spread:.2f} mm (> {self.T["output_cluster_spread_mm"]})')
if far > self.T['output_to_inductor_mm']: self.fail(f'an output cap is {far:.2f} mm from the inductor (> {self.T["output_to_inductor_mm"]})')
# plane under the hot loop
hot = [vin, gndp] + [p for r in ps['inputCaps'] for p in self.B.pads_of(r)] + [self.B.pad(ps['ic'], pp['from'].split('.')[1])
for pp in self.S.get('powerPaths', []) if pp['from'].startswith(ps['ic'] + '.')]
m = self.T['hot_loop_margin_mm']
xs = [p['x'] for p in hot]; ys = [p['y'] for p in hot]
box = (min(xs) - m, min(ys) - m, max(xs) + m, max(ys) + m)
ins = lambda q: box[0] <= q[0] <= box[2] and box[1] <= q[1] <= box[3]
layer, pnet = ps['planeLayer'], ps['planeNet']
intr = [(t['net'], 'track') for t in self.B.tracks if t['layer'] == layer and t['net'] != pnet and (ins(t['a']) or ins(t['b']))]
intr += [(v['net'], 'via') for v in self.B.vias if v['net'] != pnet and ins((v['x'], v['y']))]
intr += [(z['net'], 'fill') for z in self.B.zones if z['net'] != pnet for f in z['fills'] if f['layer'] == layer and any(ins(q) for q in f['pts'])]
self.res['plane_under_hot_loop'] = {'layer': layer, 'intrusions': intr}
if len(intr) > self.T['plane_intrusions']: self.fail(f'{len(intr)} non-{pnet} item(s) on {layer} under the hot loop')
# ---- power paths: necks and meanders -----------------------------------------------------------------------
def power_paths(self):
rows = []
for pp in self.S.get('powerPaths', []):
fr, to = pp['from'].split('.'), pp['to'].split('.')
a, b = self.B.pad(*fr), self.B.pad(*to)
pad_side = min(a['w'], a['h'])
net = pp['net']
# the track route: shortest track-only path between the two pads (fills excluded, so a trace is judged as drawn)
full = self.graph(net)
g = defaultdict(list, {u: [(v, w, k) for v, w, k in es if k != 'fill'] for u, es in full.items()})
length, pk = self.shortest(g, ('pad', *fr), lambda u: u == ('pad', *to))
path = pk[0] if pk else None
row = {'net': net, 'from': pp['from'], 'to': pp['to'], 'padNarrowSideMm': round(pad_side, 3),
'straightMm': round(math.dist((a['x'], a['y']), (b['x'], b['y'])), 3)}
if path:
pts = [u[1] for u in path if u[0] == 'pt']
widths = []
for t in self.B.tracks:
if t['net'] == net and key(t['a']) in pts and key(t['b']) in pts: widths.append(t['w'])
heads = [unit(p, q) for p, q in zip(pts, pts[1:]) if math.dist(p, q) > 1e-6]
turns = sum(1 for u, v in zip(heads, heads[1:]) if abs(u[0] * v[1] - u[1] * v[0]) > 1e-3 or u[0] * v[0] + u[1] * v[1] < 0)
row.update(trackRouteMm=round(length, 3), minTrackMm=min(widths) if widths else None, bends=turns,
meanderRatio=round(length / row['straightMm'], 3) if row['straightMm'] else None)
else:
row.update(trackRouteMm=None, minTrackMm=None, bends=None, meanderRatio=None)
row['copperNeckMm'] = self.copper_neck(net, a, b)
rows.append(row)
if row['minTrackMm'] is not None and row['minTrackMm'] < self.T['neck_track_vs_pad'] * pad_side:
self.fail(f"{net}: the trace necks to {row['minTrackMm']} mm, narrower than the {pad_side:.2f} mm pin it leaves ({pp['from']})")
if row['meanderRatio'] is not None and row['meanderRatio'] > self.T['meander_ratio']:
self.fail(f"{net}: the trace wanders, {row['trackRouteMm']} mm routed for {row['straightMm']} mm straight (x{row['meanderRatio']})")
if row['bends'] is not None and row['bends'] > self.T['path_bends']:
self.fail(f"{net}: {row['bends']} bends between {pp['from']} and {pp['to']} (> {self.T['path_bends']})")
if row['copperNeckMm'] is None:
self.fail(f"{net}: no filled-copper path between {pp['from']} and {pp['to']} (refill the zones first)")
elif row['copperNeckMm'] < self.T['neck_copper_vs_pad'] * pad_side:
self.fail(f"{net}: the filled copper necks to {row['copperNeckMm']} mm, narrower than the {pad_side:.2f} mm pin ({pp['from']})")
self.res['power_paths'] = rows
def copper_neck(self, net, a, b):
"""Widest-path bottleneck of the net's copper on the pads' layer, outside the two terminal pads: the widest
conductor you can push from pad a to pad b. Copper = tracks (capsules), pads and filled polygons of the net."""
layer = 'F.Cu' if 'F.Cu' in a['layers'] or '*.Cu' in a['layers'] else 'B.Cu'
r = self.T['raster_mm']
trs = [t for t in self.straight if t['net'] == net and t['layer'] == layer]
pads = [p for p in self.B.pads if p['net'] == net and (layer in p['layers'] or '*.Cu' in p['layers'])]
fills = [f['pts'] for z in self.B.zones if z['net'] == net for f in z['fills'] if f['layer'] == layer]
xs = [a['x'], b['x']] + [q[0] for t in trs for q in (t['a'], t['b'])] + [q[0] for f in fills for q in f]
ys = [a['y'], b['y']] + [q[1] for t in trs for q in (t['a'], t['b'])] + [q[1] for f in fills for q in f]
x0, y0 = min(xs) - 2, min(ys) - 2; nx, ny = int((max(xs) + 2 - x0) / r) + 1, int((max(ys) + 2 - y0) / r) + 1
if nx * ny > 4_000_000: return None
cu = bytearray(nx * ny); term = bytearray(nx * ny)
def fill_poly(poly, mark):
py0 = max(0, int((min(q[1] for q in poly) - y0) / r)); py1 = min(ny - 1, int((max(q[1] for q in poly) - y0) / r) + 1)
for j in range(py0, py1 + 1):
y = y0 + (j + 0.5) * r; xs_ = []
for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1]):
if (y1 > y) != (y2 > y): xs_.append(x1 + (y - y1) * (x2 - x1) / (y2 - y1))
xs_.sort()
for k in range(0, len(xs_) - 1, 2):
i0 = max(0, int((xs_[k] - x0) / r)); i1 = min(nx - 1, int((xs_[k + 1] - x0) / r))
for i in range(i0, i1 + 1): mark[j * nx + i] = 1
for f in fills: fill_poly(f, cu)
for p in pads: fill_poly(K.pad_polygon(p), cu)
for t in trs:
hw = t['w'] / 2; (ax, ay), (bx, by) = t['a'], t['b']
i0 = max(0, int((min(ax, bx) - hw - x0) / r)); i1 = min(nx - 1, int((max(ax, bx) + hw - x0) / r) + 1)
j0 = max(0, int((min(ay, by) - hw - y0) / r)); j1 = min(ny - 1, int((max(ay, by) + hw - y0) / r) + 1)
L = (bx - ax) ** 2 + (by - ay) ** 2
for j in range(j0, j1 + 1):
y = y0 + (j + 0.5) * r
for i in range(i0, i1 + 1):
x = x0 + (i + 0.5) * r
s = 0 if L == 0 else max(0, min(1, ((x - ax) * (bx - ax) + (y - ay) * (by - ay)) / L))
if (x - ax - s * (bx - ax)) ** 2 + (y - ay - s * (by - ay)) ** 2 <= hw * hw: cu[j * nx + i] = 1
fill_poly(K.pad_polygon(a), term); fill_poly(K.pad_polygon(b), term)
# distance to the nearest non-copper cell (chamfer 5-7-11, about 2 % error), in mm
INF = 1 << 30; d = [0 if not c else INF for c in cu]
W = ((-1, -1, 7), (0, -1, 5), (1, -1, 7), (-1, 0, 5), (-2, -1, 11), (-1, -2, 11), (1, -2, 11), (2, -1, 11))
for j in range(ny):
for i in range(nx):
k = j * nx + i
if d[k]:
best = d[k]
for di, dj, w in W:
ii, jj = i + di, j + dj
v = d[jj * nx + ii] + w if 0 <= ii < nx and 0 <= jj < ny else w
if v < best: best = v
d[k] = best
for j in range(ny - 1, -1, -1):
for i in range(nx - 1, -1, -1):
k = j * nx + i
if d[k]:
best = d[k]
for di, dj, w in W:
ii, jj = i - di, j - dj
v = d[jj * nx + ii] + w if 0 <= ii < nx and 0 <= jj < ny else w
if v < best: best = v
d[k] = best
width = [2 * v * r / 5 if v < INF else 0 for v in d] # local conductor width ~ twice the distance
# widest path: maximise the minimum width over cells outside the terminal pads
import heapq
starts = [k for k in range(nx * ny) if term[k] and cu[k] and K.inside((x0 + (k % nx + 0.5) * r, y0 + (k // nx + 0.5) * r), K.pad_polygon(a))]
goal = set(k for k in range(nx * ny) if term[k] and K.inside((x0 + (k % nx + 0.5) * r, y0 + (k // nx + 0.5) * r), K.pad_polygon(b)))
best = {}; h = []
for k in starts: best[k] = 1e9; heapq.heappush(h, (-1e9, k))
while h:
negw, k = heapq.heappop(h); w = -negw
if k in goal: return round(w if w < 1e8 else min(a['w'], a['h']), 3)
if best.get(k, -1) > w: continue
i, j = k % nx, k // nx
for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
ii, jj = i + di, j + dj
if not (0 <= ii < nx and 0 <= jj < ny): continue
kk = jj * nx + ii
if not cu[kk]: continue
nw = w if term[kk] else min(w, width[kk])
if nw > best.get(kk, -1):
best[kk] = nw; heapq.heappush(h, (-nw, kk))
return None
def run(self, only=None):
for name in ('grid', 'bends', 'inline_vias', 'decoupling', 'power_stage', 'power_paths'):
if only and name not in only: continue
getattr(self, name)()
self.res['thresholds'] = self.T; self.res['failures'] = self.fails; self.res['passed'] = not self.fails
return self.res
def main(argv):
if len(argv) < 2:
print(__doc__); return 2
try:
board, spec = K.Board(argv[0]), json.load(open(argv[1]))
except Exception as e:
print(f'ERROR: {e}'); return 2
only = set(argv[argv.index('--only') + 1].split(',')) if '--only' in argv else None
res = Gates(board, spec).run(only)
if '--json' in argv:
json.dump(res, open(argv[argv.index('--json') + 1], 'w'), indent=1, default=str)
for k in ('grid', 'bends', 'inline_vias', 'power_stage', 'power_paths', 'decoupling', 'plane_under_hot_loop'):
if k in res: print(k, json.dumps(res[k], default=str)[:400])
print('PASS' if res['passed'] else 'FAIL:\n ' + '\n '.join(res['failures']))
return 0 if res['passed'] else 1
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))