board
OCuLink Riser for the Supermicro AIOM Slot
Public Unreviewedby John Lauer
Passive OCP NIC 3.0 SFF card that breaks a Supermicro AIOM slot's PCIe x16 into four OCuLink 4i ports (4 x4, Gen4): 6-layer, 85 ohm, length-matched, DRC-clean KiCad project with Gerbers, BOM and CPL.
main
John Lauer
README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/
39a59ef
7d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
#!/usr/bin/env python3
"""Multi-layer A* maze router for the riser's low-speed nets, on a 0.1 mm grid.
Copper of every net is rasterised per layer into a label grid; a net is routed through cells whose distance to
other nets' copper is at least (clearance + width/2) (scipy distance transform per net). Vias need a free disk
on every copper layer. Routed copper is rasterised back before the next net."""
import heapq, json, math, os, sys
import numpy as np
from scipy import ndimage
from shapely.geometry import Polygon, LineString, Point, box
from shapely import affinity
G = 0.1
NX, NY = 760, 1150
LAYERS = ["L1", "L3", "L5", "L6"] # routing/obstacle layers (L2/L4 are solid GND planes)
LI = {l: i for i, l in enumerate(LAYERS)}
VIA_D, VIA_H = 0.40, 0.20
class Board:
def __init__(self):
self.lab = np.full((4, NY, NX), -1, np.int32) # net id of copper in the cell, -1 none
self.block = np.zeros((4, NY, NX), bool) # static keepouts per layer
self.nets = {}
self.netname = []
self.hs = np.zeros((4, NY, NX), bool) # high-speed copper (gets extra clearance)
yy, xx = np.mgrid[0:NY, 0:NX]
self.X = xx * G
self.Y = yy * G
def nid(self, name):
if name not in self.nets:
self.nets[name] = len(self.netname)
self.netname.append(name)
return self.nets[name]
def paint_geom(self, layer_idx, geom, net, hs=False):
"""Rasterise a shapely geometry (cells whose centre lies inside), dilated by 0.06 mm so the painted cells
cover the true copper edge (a cell centre can sit up to ~0.07 mm inside it)."""
geom = geom.buffer(0.06)
x0, y0, x1, y1 = geom.bounds
i0, i1 = max(0, int(x0 / G) - 1), min(NX - 1, int(x1 / G) + 2)
j0, j1 = max(0, int(y0 / G) - 1), min(NY - 1, int(y1 / G) + 2)
if i1 < i0 or j1 < j0:
return
from shapely import contains_xy
sub = contains_xy(geom, self.X[j0:j1 + 1, i0:i1 + 1], self.Y[j0:j1 + 1, i0:i1 + 1])
# thin geometry may contain no cell centre: paint the nearest cells of its centreline
if not sub.any():
c = geom.centroid
sub = np.zeros_like(sub)
jj, ii = int(round(c.y / G)) - j0, int(round(c.x / G)) - i0
if 0 <= jj < sub.shape[0] and 0 <= ii < sub.shape[1]:
sub[jj, ii] = True
for li in ([layer_idx] if isinstance(layer_idx, int) else layer_idx):
region = self.lab[li, j0:j1 + 1, i0:i1 + 1]
region[sub] = net
if hs:
self.hs[li, j0:j1 + 1, i0:i1 + 1][sub] = True
def track(self, layer, net, x1, y1, x2, y2, w, hs=False):
g = LineString([(x1, y1), (x2, y2)]).buffer(w / 2, cap_style=1) if (x1, y1) != (x2, y2) else Point(x1, y1).buffer(w / 2)
self.paint_geom(LI[layer], g, self.nid(net), hs)
def via(self, x, y, net, d, hs=False):
self.paint_geom([0, 1, 2, 3], Point(x, y).buffer(d / 2), self.nid(net), hs)
def free_mask(B, net, w, clr, clr_hs, li):
"""cells where a track of width w for `net` may have its centreline on layer li."""
lab = B.lab[li]
other = (lab >= 0) & (lab != net)
d = ndimage.distance_transform_edt(~other) * G
ok = d >= clr + w / 2 - 1e-9
if clr_hs > clr:
oh = other & B.hs[li]
dh = ndimage.distance_transform_edt(~oh) * G
ok &= dh >= clr_hs + w / 2 - 1e-9
ok &= ~B.block[li]
return ok
def route_net(B, net, terminals, w=0.15, clr=0.15, clr_hs=0.3, allow=("L1", "L3", "L5"), via_cost=12.0,
layer_cost=None, l6_region=None, max_expand=6_000_000):
"""terminals: list of sets of (li, j, i) cells (each a pad). Returns list of paths (lists of (li,j,i))."""
nid = B.nid(net)
layer_cost = layer_cost or {"L1": 1.0, "L3": 1.15, "L5": 1.15, "L6": 3.0}
ok = np.zeros((4, NY, NX), bool)
for l in allow:
ok[LI[l]] = free_mask(B, nid, w, clr, clr_hs, LI[l])
if "L6" in allow and l6_region is not None:
ok[LI["L6"]] &= l6_region
# via allowed where a via disk is clear on every layer (incl. L6, never in keepouts)
vok = np.ones((NY, NX), bool)
for li in range(4):
lab = B.lab[li]
other = (lab >= 0) & (lab != nid)
d = ndimage.distance_transform_edt(~other) * G
vok &= d >= clr + VIA_D / 2 - 1e-9
oh = other & B.hs[li]
dh = ndimage.distance_transform_edt(~oh) * G
vok &= dh >= clr_hs + VIA_D / 2 - 1e-9
vok &= ~B.block_via[li] if hasattr(B, "block_via") else True
# terminals' own cells are always enterable on their layers
for t in terminals:
for (li, j, i) in t:
ok[li, j, i] = True
tree = set(terminals[0])
paths = []
remaining = [set(t) for t in terminals[1:]]
allowed_li = [LI[l] for l in allow]
lc = np.array([layer_cost.get(l, 1.0) for l in LAYERS])
while remaining:
goal = set().union(*remaining)
gj = np.array([c[1] for c in goal]); gi = np.array([c[2] for c in goal])
gcj, gci = gj.mean(), gi.mean()
rad = max(np.hypot(gj - gcj, gi - gci).max(), 0)
def h(j, i):
dj, di = abs(j - gcj), abs(i - gci)
return max(0.0, (max(dj, di) + 0.414 * min(dj, di)) - rad)
start = list(tree)
dist = {}
prev = {}
heap = []
for s in start:
dist[s] = 0.0
heapq.heappush(heap, (h(s[1], s[2]), 0.0, s))
found = None
n = 0
while heap:
f, g, c = heapq.heappop(heap)
if g > dist.get(c, 1e18) + 1e-9:
continue
if c in goal:
found = c; break
n += 1
if n > max_expand:
break
li, j, i = c
for dj, di, cost in ((0, 1, 1), (0, -1, 1), (1, 0, 1), (-1, 0, 1), (1, 1, 1.4142), (1, -1, 1.4142), (-1, 1, 1.4142), (-1, -1, 1.4142)):
jj, ii = j + dj, i + di
if 0 <= jj < NY and 0 <= ii < NX and ok[li, jj, ii]:
if dj and di and not (ok[li, j, ii] and ok[li, jj, i]):
continue
nc = (li, jj, ii)
ng = g + cost * lc[li]
if ng < dist.get(nc, 1e18):
dist[nc] = ng; prev[nc] = c
heapq.heappush(heap, (ng + h(jj, ii), ng, nc))
if vok[j, i]:
for l2 in allowed_li:
if l2 != li and ok[l2, j, i]:
nc = (l2, j, i)
ng = g + via_cost
if ng < dist.get(nc, 1e18):
dist[nc] = ng; prev[nc] = c
heapq.heappush(heap, (ng + h(j, i), ng, nc))
if found is None:
return None, f"no path ({n} expanded)"
path = [found]
while path[-1] in prev:
path.append(prev[path[-1]])
path.reverse()
paths.append(path)
tree |= set(path)
for r in remaining:
if found in r:
tree |= r
remaining.remove(r)
break
return paths, "ok"
def path_to_copper(path, net, w):
"""Cells -> merged straight tracks and vias."""
tracks, vias = [], []
seg = [path[0]]
def flush(seg):
if len(seg) < 2:
return
pts = [(c[2] * G, c[1] * G) for c in seg]
# merge collinear
out = [pts[0]]
for k in range(1, len(pts) - 1):
a, b, c = out[-1], pts[k], pts[k + 1]
if abs((b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])) < 1e-9:
continue
out.append(b)
out.append(pts[-1])
for a, b in zip(out, out[1:]):
tracks.append((LAYERS[seg[0][0]], net, round(a[0], 4), round(a[1], 4), round(b[0], 4), round(b[1], 4), w))
for c in path[1:]:
if c[0] != seg[-1][0]:
flush(seg)
vias.append((round(c[2] * G, 4), round(c[1] * G, 4), net, VIA_D, VIA_H))
seg = [c]
else:
seg.append(c)
flush(seg)
return tracks, vias