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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#!/usr/bin/env python3
"""Split the finished routing into (a) the start board's copper and (b) what AI Flow lands live:
placement moves for every part that is not a fixed interface part, and a routing plan (one entry per
polyline, layer markers for vias) for every maze-routed net and GND fan-out."""
import json, os, math
from collections import defaultdict
ROOT = os.path.join(os.path.dirname(__file__), "..")
R = json.load(open(os.path.join(ROOT, "design", "routes.json")))
LS = json.load(open(os.path.join(ROOT, "design", "routes_ls.json")))
lay = json.load(open(os.path.join(ROOT, "design", "layout.json")))
KL = {"L1": "F.Cu", "L3": "In2.Cu", "L5": "In4.Cu", "L6": "B.Cu"}
FIXED = {"J1", "J2", "J3", "J4", "J5", "H1", "H2"}
# live copper: everything the maze produced (routes_ls minus the fixed copper it inherited) plus GND fan-outs
if "fixed_tracks" in LS:
fixed_T = set(map(tuple, LS["fixed_tracks"])); fixed_V = set(map(tuple, LS["fixed_vias"]))
else: # older routes_ls.json: rebuild the fixed copper exactly as route_all does before routing
import sys; sys.path.insert(0, os.path.dirname(__file__))
import route_all as RA
fixed_T = set(map(tuple, RA.T)); fixed_V = set(map(tuple, RA.V))
# taken from the FINAL routes (routes.json): the finishing pass prunes stubs and may rip and re-route a few nets, so
# the maze's own output is stale for those; GND copper is live only where the maze or the fan-out made it
ls_T = {tuple(t) for t in LS["tracks"]} - fixed_T; ls_V = {tuple(v) for v in LS["vias"]} - fixed_V
fan_T = set(map(tuple, R["fanout_tracks"])); fan_V = set(map(tuple, R["fanout_vias"]))
live_T = [t for t in map(tuple, R["tracks"]) if t not in fixed_T and (t[1] != "GND" or t in ls_T or t in fan_T)]
live_V = [v for v in map(tuple, R["vias"]) if v not in fixed_V and (v[2] != "GND" or v in ls_V or v in fan_V)]
live_Ts, live_Vs = set(live_T), set(live_V)
start_T = [t for t in map(tuple, R["tracks"]) if t not in live_Ts]
start_V = [v for v in map(tuple, R["vias"]) if v not in live_Vs]
def key(x, y):
return (round(x, 3), round(y, 3))
def chains(net, tracks, vias):
"""Decompose one net's live copper into polylines; a via joins layers at one point."""
adj = defaultdict(list) # (layer, pt) -> [(edge id, other node)]
edges = []
for (L, n, x1, y1, x2, y2, w) in tracks:
a, b = (L, key(x1, y1)), (L, key(x2, y2))
eid = len(edges); edges.append((a, b, w))
adj[a].append((eid, b)); adj[b].append((eid, a))
viapts = {key(v[0], v[1]) for v in vias}
lone = []
for vp in viapts: # via: connect every layer node at that point
nodes = [nd for nd in list(adj) if nd[1] == vp]
if len(nodes) < 2: # copper on one layer only (the other side is a pad or a pour): a via of its own
lone.append((vp, nodes[0][0] if nodes else "L1"))
for i in range(len(nodes) - 1):
a, b = nodes[i], nodes[i + 1]
eid = len(edges); edges.append((a, b, None))
adj[a].append((eid, b)); adj[b].append((eid, a))
used = set()
out = []
order = sorted(adj, key=lambda nd: len(adj[nd]) % 2 == 0) # odd-degree nodes first
for start in order:
while any(e not in used for e, _ in adj[start]):
path = [start]; cur = start; w = None
while True:
nxt = [(e, o) for e, o in adj[cur] if e not in used]
if not nxt:
break
e, o = nxt[0]
used.add(e)
if edges[e][2] is not None:
w = edges[e][2]
path.append(o); cur = o
out.append((path, w))
entries = []
for path, w in out:
pts = []
layer = path[0][0]
for i, (L, (x, y)) in enumerate(path):
if i == 0:
pts.append([x, y])
elif L != layer:
pts.append({"x": x, "y": y, "layer": KL[L]})
layer = L
elif pts and isinstance(pts[-1], dict) and abs(pts[-1]["x"] - x) < 1e-6 and abs(pts[-1]["y"] - y) < 1e-6:
continue
else:
pts.append([x, y])
# a via at the start of a path: express as a layer marker on the first point
entries.append({"net": net, "width": w or 0.15, "viaSize": 0.4, "viaDrill": 0.2, "layer": KL[path[0][0]], "points": pts})
vsize = {key(v[0], v[1]): (v[3], v[4]) for v in vias}
for (x, y), L in lone:
other = "B.Cu" if KL[L] == "F.Cu" else "F.Cu"
d, h = vsize[(x, y)]
entries.append({"net": net, "width": 0.15, "viaSize": d, "viaDrill": h, "layer": KL[L],
"points": [[x, y], {"x": x, "y": y, "layer": other}]})
return entries
bynet_T, bynet_V = defaultdict(list), defaultdict(list)
for t in live_T:
bynet_T[t[1]].append(t)
for v in live_V:
bynet_V[v[2]].append(v)
plan = []
for net in sorted(set(bynet_T) | set(bynet_V)):
plan += chains(net, bynet_T[net], bynet_V[net])
moves = {}
stage = {}
k = 0
for ref, (x, y, rot, side) in lay["placement"].items():
if ref in FIXED:
continue
moves[ref] = {"x": x, "y": y, "rotation": rot}
stage[ref] = (84.0 + 9.0 * (k % 4), 8.0 + 6.0 * (k // 4), rot, side)
k += 1
json.dump({"nets": plan}, open(os.path.join(ROOT, "aiflow", "route-plan.json"), "w"), indent=0)
json.dump(moves, open(os.path.join(ROOT, "aiflow", "moves.json"), "w"), indent=1)
json.dump({"tracks": start_T, "vias": start_V, "zones": R["zones"], "texts": R.get("texts", []), "stage": stage},
open(os.path.join(ROOT, "design", "routes_start.json"), "w"))
print(f"live: {len(plan)} plan entries ({len(live_T)} tracks, {len(live_V)} vias); moves {len(moves)} parts;"
f" start board copper {len(start_T)} tracks, {len(start_V)} vias")