Add coordinated multi-trace channel packing to the room track
Greedy per-net room routing sends each net through a shared obstacle gap on its own independently chosen path; the paths collide and some nets drop even when the gap is physically wide enough for all of them. shove cannot fix this -- it relocates blockers into existing space, it does not pack lanes. Add an opt-in pre-pass (pack=True, independent of shove) that groups 2-pin nets which must cross a common gap, assigns each a parallel lane across the gap's usable width (pad-ordered so lanes never cross, spaced >= width + clearance) with staggered fan-in trunks so the converge/diverge jogs never overlap, and realizes each as an orthogonal trace verified exactly DRC-clean. Packing is all-or-nothing per group, so it never leaves a channel worse than greedy. With pack=False the pre-pass is skipped and the output is unchanged. Scope: single-layer 2-pin nets through one axis-aligned gap, with enough fan depth to stack the trunks. Threaded through route_dsn_board_rooms / build_rooms_routing_result / route(engine="room").
This commit is contained in:
parent
99c3d20de5
commit
c28223ea9c
@ -141,31 +141,42 @@ def build_exact_routing_result(
|
||||
|
||||
|
||||
def route_dsn_board_rooms(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False, pack: bool = False
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
||||
return the result, the scale, and layer names. ``shove`` enables occupancy-
|
||||
aware gate placement and shove-in-rooms recovery of otherwise-dropped nets."""
|
||||
aware gate placement and shove-in-rooms recovery of otherwise-dropped nets.
|
||||
``pack`` enables the coordinated multi-trace channel packer."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
rooms = route_board_rooms(
|
||||
board, trace_width=width_board, clearance=clearance_board, layers=layers, shove=shove
|
||||
board,
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
shove=shove,
|
||||
pack=pack,
|
||||
)
|
||||
return rooms, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
|
||||
def build_rooms_routing_result(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False, pack: bool = False
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
||||
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers, shove=shove)
|
||||
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers, shove=shove, pack=pack)
|
||||
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
def route(
|
||||
dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid", shove: bool = False
|
||||
dsn_text: str,
|
||||
*,
|
||||
layers: list[int] | None = None,
|
||||
engine: str = "grid",
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
) -> str:
|
||||
"""Route a Specctra DSN string and return the routed SES string.
|
||||
|
||||
@ -181,7 +192,7 @@ def route(
|
||||
if engine == "exact":
|
||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||
elif engine == "room":
|
||||
result = build_rooms_routing_result(dsn, layers=layers, shove=shove)
|
||||
result = build_rooms_routing_result(dsn, layers=layers, shove=shove, pack=pack)
|
||||
else:
|
||||
result = build_routing_result(dsn, layers=layers)
|
||||
return write_ses(dsn, result)
|
||||
|
||||
@ -468,6 +468,7 @@ def route_board_rooms(
|
||||
layers: list[int] | None = None,
|
||||
via_cost: float = 50000.0,
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
) -> ExactRouteResult:
|
||||
@ -481,6 +482,13 @@ def route_board_rooms(
|
||||
``shove=False`` every new branch is skipped and the output is byte-for-byte
|
||||
the foundation router. ``max_shove_depth`` bounds the distinct blocker nets
|
||||
per recovery; ``shove_cap`` bounds how often one trace may be moved.
|
||||
|
||||
``pack`` (opt-in, independent of ``shove``) runs the coordinated channel
|
||||
packer first: groups of 2-pin nets that must cross a common obstacle gap are
|
||||
assigned consistent parallel lanes across the gap and routed together, so a
|
||||
channel that is physically wide enough for all of them does not drop nets to
|
||||
greedy per-net gate collisions. With ``pack=False`` the pre-pass is skipped
|
||||
and the output is unchanged.
|
||||
"""
|
||||
signal_layers = layers or [
|
||||
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||
@ -513,7 +521,26 @@ def route_board_rooms(
|
||||
via_layers = frozenset(signal_layers)
|
||||
accepted: dict[int, _Accepted] = {}
|
||||
shoves = 0
|
||||
packed: set[int] = set()
|
||||
if pack:
|
||||
packed = _pack_channels(
|
||||
board,
|
||||
tree,
|
||||
result,
|
||||
owner,
|
||||
outline,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
margin,
|
||||
trace_width,
|
||||
via_layers,
|
||||
accepted,
|
||||
shove,
|
||||
)
|
||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||
if net_no in packed:
|
||||
continue
|
||||
pins = _net_pins(board, net_no, signal_layers)
|
||||
for a, b in zip(pins, pins[1:], strict=False):
|
||||
shoves += _route_connection(
|
||||
@ -586,7 +613,9 @@ def _route_connection(
|
||||
for box, box_layers in boxes
|
||||
for layer in box_layers
|
||||
):
|
||||
_commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted)
|
||||
_commit(
|
||||
net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted
|
||||
)
|
||||
return 0
|
||||
|
||||
# the net would be dropped: try a transactional shove-in-rooms recovery
|
||||
@ -672,6 +701,337 @@ def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, sh
|
||||
acc.via_owners.append(oid)
|
||||
|
||||
|
||||
# --- coordinated channel packing ---------------------------------------------
|
||||
|
||||
|
||||
def _long(p: IntPoint, axis: str) -> int:
|
||||
"""Coordinate along a channel's length (the axis nets travel)."""
|
||||
return p.y if axis == "v" else p.x
|
||||
|
||||
|
||||
def _trans(p: IntPoint, axis: str) -> int:
|
||||
"""Coordinate across a channel (the axis lanes spread over)."""
|
||||
return p.x if axis == "v" else p.y
|
||||
|
||||
|
||||
def _box_long(b: IntBox, axis: str) -> tuple[int, int]:
|
||||
return (b.ll.y, b.ur.y) if axis == "v" else (b.ll.x, b.ur.x)
|
||||
|
||||
|
||||
def _box_trans(b: IntBox, axis: str) -> tuple[int, int]:
|
||||
return (b.ll.x, b.ur.x) if axis == "v" else (b.ll.y, b.ur.y)
|
||||
|
||||
|
||||
def _outline_trans(outline: IntBox, axis: str) -> tuple[int, int]:
|
||||
return (outline.min_x, outline.max_x) if axis == "v" else (outline.min_y, outline.max_y)
|
||||
|
||||
|
||||
def _axis_point(long_val: int, trans_val: int, axis: str) -> IntPoint:
|
||||
"""Build a point from (length-coord, transverse-coord) for the given axis."""
|
||||
return IntPoint(trans_val, long_val) if axis == "v" else IntPoint(long_val, trans_val)
|
||||
|
||||
|
||||
def _lane_polyline(
|
||||
top: IntPoint, bot: IntPoint, lane: int, trunk_top: int, trunk_bot: int, axis: str
|
||||
) -> list[IntPoint]:
|
||||
"""A pad -> trunk -> lane -> trunk -> pad orthogonal trace.
|
||||
|
||||
Each net converges to its lane on a *staggered* trunk line (``trunk_top`` /
|
||||
``trunk_bot``, a distinct length-coordinate per net just outside the gap), runs
|
||||
straight through the channel in its lane, then diverges to the far pad on the
|
||||
other trunk. Distinct trunk lines keep the fan-in horizontals from overlapping
|
||||
when pads spread wider than the gap. ``top`` is the pin with the larger
|
||||
length-coordinate. Collinear/duplicate points collapse."""
|
||||
tt = _trans(top, axis)
|
||||
tb = _trans(bot, axis)
|
||||
pts = [
|
||||
top,
|
||||
_axis_point(trunk_top, tt, axis),
|
||||
_axis_point(trunk_top, lane, axis),
|
||||
_axis_point(trunk_bot, lane, axis),
|
||||
_axis_point(trunk_bot, tb, axis),
|
||||
bot,
|
||||
]
|
||||
return _dedupe(pts)
|
||||
|
||||
|
||||
def _wall_rows(obstacles: list[IntBox], axis: str) -> list[list[IntBox]]:
|
||||
"""Cluster obstacle boxes into walls whose blocked length-ranges overlap; each
|
||||
cluster is one barrier that nets cross transversely through its gaps."""
|
||||
boxes = sorted(obstacles, key=lambda b: (_box_long(b, axis), _box_trans(b, axis)))
|
||||
used = [False] * len(boxes)
|
||||
rows: list[list[IntBox]] = []
|
||||
for i, b in enumerate(boxes):
|
||||
if used[i]:
|
||||
continue
|
||||
used[i] = True
|
||||
cluster = [b]
|
||||
lo, hi = _box_long(b, axis)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for j, c in enumerate(boxes):
|
||||
if used[j]:
|
||||
continue
|
||||
clo, chi = _box_long(c, axis)
|
||||
if clo < hi and chi > lo:
|
||||
used[j] = True
|
||||
cluster.append(c)
|
||||
lo, hi = min(lo, clo), max(hi, chi)
|
||||
changed = True
|
||||
rows.append(cluster)
|
||||
return rows
|
||||
|
||||
|
||||
def _channel_groups(obstacles, cands, axis, trace_width):
|
||||
"""Find (gap_lo, gap_hi, [net_no, ...]) channels on one axis: a gap between two
|
||||
obstacles in a wall row, and the >= 2 candidate nets that must cross that wall
|
||||
and are nearest this gap. Only gaps bounded by an obstacle on both sides count
|
||||
(an open board edge is not a channel)."""
|
||||
groups = []
|
||||
for cluster in _wall_rows(obstacles, axis):
|
||||
blo = max(_box_long(b, axis)[0] for b in cluster)
|
||||
bhi = min(_box_long(b, axis)[1] for b in cluster)
|
||||
if bhi <= blo:
|
||||
continue # no length-band the whole cluster blocks in common
|
||||
occ = sorted(_box_trans(b, axis) for b in cluster)
|
||||
merged: list[tuple[int, int]] = []
|
||||
for lo, hi in occ:
|
||||
if merged and lo <= merged[-1][1]:
|
||||
merged[-1] = (merged[-1][0], max(merged[-1][1], hi))
|
||||
else:
|
||||
merged.append((lo, hi))
|
||||
gaps = [
|
||||
(merged[k][1], merged[k + 1][0])
|
||||
for k in range(len(merged) - 1)
|
||||
if merged[k + 1][0] - merged[k][1] >= trace_width
|
||||
]
|
||||
if not gaps:
|
||||
continue
|
||||
crossing = []
|
||||
for net_no, (pa, pb) in cands.items():
|
||||
la, lb = _long(pa, axis), _long(pb, axis)
|
||||
if (la <= blo and lb >= bhi) or (lb <= blo and la >= bhi):
|
||||
crossing.append(net_no)
|
||||
for gap_lo, gap_hi in gaps:
|
||||
members = []
|
||||
for net_no in crossing:
|
||||
pa, pb = cands[net_no]
|
||||
center = (_trans(pa, axis) + _trans(pb, axis)) / 2
|
||||
nearest = min(gaps, key=lambda g: abs((g[0] + g[1]) / 2 - center))
|
||||
if nearest == (gap_lo, gap_hi):
|
||||
members.append(net_no)
|
||||
if len(members) >= 2:
|
||||
groups.append((gap_lo, gap_hi, blo, bhi, sorted(members)))
|
||||
return groups
|
||||
|
||||
|
||||
def _boxes_clear(boxes, net_no, placed, clearance) -> bool:
|
||||
"""True if none of ``boxes`` come within ``clearance`` of a different-net box in
|
||||
``placed`` (list of ``(net_no, layer, box)``) -- the in-group lane-vs-lane check
|
||||
that the search tree cannot do until copper is committed."""
|
||||
for box, box_layers in boxes:
|
||||
expanded = box.offset(clearance)
|
||||
for other_net, other_layer, other_box in placed:
|
||||
if other_net == net_no or other_layer not in box_layers:
|
||||
continue
|
||||
if other_box.overlaps(expanded):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _pack_channels(
|
||||
board,
|
||||
tree,
|
||||
result,
|
||||
owner,
|
||||
outline,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
margin,
|
||||
trace_width,
|
||||
via_layers,
|
||||
accepted,
|
||||
shove,
|
||||
) -> set[int]:
|
||||
"""Coordinated multi-trace channel packing (opt-in pre-pass).
|
||||
|
||||
Greedy per-net routing sends each net through a shared constriction on its own
|
||||
independently chosen path; the paths collide and some nets drop even when the
|
||||
gap is physically wide enough for all of them. This pass groups 2-pin nets that
|
||||
must cross a common obstacle gap, assigns each a parallel lane across the gap's
|
||||
usable width -- sorted by pad position, spacing >= trace_width + clearance so
|
||||
lanes never conflict and, being pad-ordered, never cross -- and realizes each as
|
||||
an orthogonal converge/straight/diverge trace verified exactly DRC-clean against
|
||||
the search tree. A net whose lane trace is not clean (e.g. it clips a pad) is
|
||||
left for the normal loop. Returns the fully-routed net numbers so the caller
|
||||
skips them. Scope: single-layer 2-pin nets through one axis-aligned gap."""
|
||||
routed: set[int] = set()
|
||||
spacing_min = trace_width + clearance
|
||||
cands: dict[int, tuple[int, IntPoint, IntPoint]] = {}
|
||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||
pins = _net_pins(board, net_no, signal_layers)
|
||||
if len(pins) != 2:
|
||||
continue
|
||||
shared = [
|
||||
layer for layer in pins[0].layers if layer in pins[1].layers and layer in signal_layers
|
||||
]
|
||||
if not shared:
|
||||
continue
|
||||
cands[net_no] = (min(shared), pins[0].location, pins[1].location)
|
||||
if len(cands) < 2:
|
||||
return routed
|
||||
|
||||
obstacles_by_layer: dict[int, list[IntBox]] = {layer: [] for layer in signal_layers}
|
||||
for obstacle in board.get_obstacle_areas():
|
||||
if obstacle.layer not in signal_layers:
|
||||
continue
|
||||
for tile in obstacle.tiles:
|
||||
box = tile.bounding_box()
|
||||
if not box.is_empty():
|
||||
obstacles_by_layer[obstacle.layer].append(box)
|
||||
|
||||
for layer in sorted(signal_layers):
|
||||
obstacles = obstacles_by_layer.get(layer, [])
|
||||
if not obstacles:
|
||||
continue
|
||||
layer_cands = {net_no: (pa, pb) for net_no, (lyr, pa, pb) in cands.items() if lyr == layer}
|
||||
if len(layer_cands) < 2:
|
||||
continue
|
||||
for axis in ("v", "h"):
|
||||
for gap_lo, gap_hi, blo, bhi, members in _channel_groups(
|
||||
obstacles, layer_cands, axis, trace_width
|
||||
):
|
||||
members = [n for n in members if n not in routed]
|
||||
if len(members) < 2:
|
||||
continue
|
||||
placed = _pack_group(
|
||||
members,
|
||||
layer_cands,
|
||||
axis,
|
||||
layer,
|
||||
gap_lo,
|
||||
gap_hi,
|
||||
blo,
|
||||
bhi,
|
||||
tree,
|
||||
half_width,
|
||||
clearance,
|
||||
margin,
|
||||
spacing_min,
|
||||
via_layers,
|
||||
outline,
|
||||
)
|
||||
if placed is None:
|
||||
continue # could not pack every member cleanly -> leave to greedy
|
||||
for net_no, wires in placed:
|
||||
_commit(
|
||||
net_no,
|
||||
wires,
|
||||
[],
|
||||
tree,
|
||||
result,
|
||||
owner,
|
||||
half_width,
|
||||
via_layers,
|
||||
shove,
|
||||
accepted,
|
||||
)
|
||||
routed.add(net_no)
|
||||
return routed
|
||||
|
||||
|
||||
def _pack_group(
|
||||
members,
|
||||
cands,
|
||||
axis,
|
||||
layer,
|
||||
gap_lo,
|
||||
gap_hi,
|
||||
blo,
|
||||
bhi,
|
||||
tree,
|
||||
half_width,
|
||||
clearance,
|
||||
margin,
|
||||
spacing_min,
|
||||
via_layers,
|
||||
outline,
|
||||
):
|
||||
"""Assign parallel lanes + staggered fan-in trunks to every net crossing one
|
||||
gap, and return ``[(net_no, wires), ...]`` if ALL of them realize exactly
|
||||
DRC-clean, else ``None``. All-or-nothing: a partial pack could leave the
|
||||
channel worse than greedy, so the group is committed only if it packs whole."""
|
||||
count = len(members)
|
||||
lo = gap_lo + margin
|
||||
hi = gap_hi - margin
|
||||
band = hi - lo
|
||||
if band < (count - 1) * spacing_min:
|
||||
return None # gap not physically wide enough for this many lanes
|
||||
|
||||
# per net: (top pin, bottom pin) by length-coordinate; top has the larger value
|
||||
ends = {}
|
||||
for net_no in members:
|
||||
pa, pb = cands[net_no]
|
||||
ends[net_no] = (pa, pb) if _long(pa, axis) >= _long(pb, axis) else (pb, pa)
|
||||
|
||||
# lanes in gap order, preserving pad order so lanes never cross
|
||||
lane_order = sorted(members, key=lambda n: (_trans(ends[n][0], axis), n))
|
||||
lane_of = {
|
||||
net_no: (lo if count == 1 else lo + round(band * i / (count - 1)))
|
||||
for i, net_no in enumerate(lane_order)
|
||||
}
|
||||
|
||||
# staggered trunks: the widest fan-in horizontal sits nearest the gap mouth so
|
||||
# it passes under the shorter nets' descents (a monotone, crossing-free fan)
|
||||
top_rank = {
|
||||
n: r
|
||||
for r, n in enumerate(
|
||||
sorted(members, key=lambda n: (-abs(_trans(ends[n][0], axis) - lane_of[n]), n))
|
||||
)
|
||||
}
|
||||
bot_rank = {
|
||||
n: r
|
||||
for r, n in enumerate(
|
||||
sorted(members, key=lambda n: (-abs(_trans(ends[n][1], axis) - lane_of[n]), n))
|
||||
)
|
||||
}
|
||||
top_pad = min(_long(ends[n][0], axis) for n in members)
|
||||
bot_pad = max(_long(ends[n][1], axis) for n in members)
|
||||
if bhi + margin + (count - 1) * spacing_min > top_pad - margin:
|
||||
return None # not enough room above the gap to stack the fan-in trunks
|
||||
if blo - margin - (count - 1) * spacing_min < bot_pad + margin:
|
||||
return None # not enough room below the gap
|
||||
|
||||
placed_boxes: list[tuple[int, int, IntBox]] = []
|
||||
to_commit = []
|
||||
for net_no in sorted(members):
|
||||
top_pt, bot_pt = ends[net_no]
|
||||
trunk_top = bhi + margin + top_rank[net_no] * spacing_min
|
||||
trunk_bot = blo - margin - bot_rank[net_no] * spacing_min
|
||||
corners = _lane_polyline(top_pt, bot_pt, lane_of[net_no], trunk_top, trunk_bot, axis)
|
||||
if len(corners) < 2:
|
||||
return None
|
||||
wires = [(layer, corners)]
|
||||
boxes = _collect_boxes(wires, [], half_width, via_layers, outline)
|
||||
if boxes is None:
|
||||
return None
|
||||
if any(
|
||||
tree.clearance_conflict(box, net_no, box_layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
for box_layer in box_layers
|
||||
):
|
||||
return None
|
||||
if not _boxes_clear(boxes, net_no, placed_boxes, clearance):
|
||||
return None
|
||||
for box, box_layers in boxes:
|
||||
for box_layer in box_layers:
|
||||
placed_boxes.append((net_no, box_layer, box))
|
||||
to_commit.append((net_no, wires))
|
||||
return to_commit
|
||||
|
||||
|
||||
# --- shove-in-rooms recovery -------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user