Add --reduce-vias and --via-cost to the exact engine
--reduce-vias is a last stage: each net that has vias is routed again on its own with vias made eight times dearer, everything else held as fixed copper (the window stage's board), and its new copper is kept only if the net fails nothing new and has fewer vias; the nets with the most vias go first, within a share of the routing's own time. --via-cost exposes the search's layer-change price (default 10 grid steps). bench.py records each run's via count and prints via totals per variant.
This commit is contained in:
+16
-1
@@ -100,13 +100,18 @@ def run_one(board, variant, seed: int, timeout: float) -> dict:
|
|||||||
)
|
)
|
||||||
rss_mb = round(usage.ru_maxrss / 1024)
|
rss_mb = round(usage.ru_maxrss / 1024)
|
||||||
secs = round(time.monotonic() - start, 1)
|
secs = round(time.monotonic() - start, 1)
|
||||||
|
try:
|
||||||
|
with open(f"{tmp}/out.ses", encoding="utf-8") as f:
|
||||||
|
vias = f.read().count("(via ")
|
||||||
|
except OSError:
|
||||||
|
vias = None
|
||||||
missing = NOT_ROUTED.search(err)
|
missing = NOT_ROUTED.search(err)
|
||||||
zone = TO_ZONE.search(err)
|
zone = TO_ZONE.search(err)
|
||||||
return {
|
return {
|
||||||
"board": name, "variant": vname, "seed": seed, "status": status,
|
"board": name, "variant": vname, "seed": seed, "status": status,
|
||||||
"unrouted": int(missing.group(1)) if missing else 0,
|
"unrouted": int(missing.group(1)) if missing else 0,
|
||||||
"to_zone": int(zone.group(1)) if zone else 0,
|
"to_zone": int(zone.group(1)) if zone else 0,
|
||||||
"secs": secs, "rss_mb": rss_mb,
|
"secs": secs, "rss_mb": rss_mb, "vias": vias,
|
||||||
"tail": err.strip().splitlines()[-1] if status != "ok" and err.strip() else "",
|
"tail": err.strip().splitlines()[-1] if status != "ok" and err.strip() else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +152,16 @@ def report(rows: list[dict], boards, variants) -> None:
|
|||||||
line += (f" {dm:+.1f} ± {se:.1f} ({better} better, {worse} worse,"
|
line += (f" {dm:+.1f} ± {se:.1f} ({better} better, {worse} worse,"
|
||||||
f" {len(diffs) - better - worse} same) {verdict}")
|
f" {len(diffs) - better - worse} same) {verdict}")
|
||||||
print(line)
|
print(line)
|
||||||
|
# vias, where the runs recorded them (older result files did not)
|
||||||
|
print("\nvias per variant, summed over boards (mean over seeds per board)\n")
|
||||||
|
for vname, _, _ in variants:
|
||||||
|
total, boards_counted = 0.0, 0
|
||||||
|
for bname, _, _ in boards:
|
||||||
|
vs = [r["vias"] for k, r in ok.items() if k[:2] == (bname, vname) and r.get("vias") is not None]
|
||||||
|
if vs:
|
||||||
|
total += sum(vs) / len(vs)
|
||||||
|
boards_counted += 1
|
||||||
|
print(f"{vname:14} {total:8.1f} vias over {boards_counted} boards")
|
||||||
failed = [r for r in rows if r["status"] != "ok"]
|
failed = [r for r in rows if r["status"] != "ok"]
|
||||||
for r in failed:
|
for r in failed:
|
||||||
print(f"FAILED {r['board']} {r['variant']} seed {r['seed']}: {r['status']} {r['tail']}")
|
print(f"FAILED {r['board']} {r['variant']} seed {r['seed']}: {r['status']} {r['tail']}")
|
||||||
|
|||||||
+47
-2
@@ -30,10 +30,13 @@ from rpmroute.ses import write_ses
|
|||||||
#: which optional flags each engine actually implements. Anything not listed for
|
#: which optional flags each engine actually implements. Anything not listed for
|
||||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access", "negotiate", "rollback", "tree"}),
|
"grid": frozenset({
|
||||||
|
"rip_up", "max_passes", "seed", "orders", "promote", "access", "negotiate", "rollback",
|
||||||
|
"tree", "via_cost",
|
||||||
|
}),
|
||||||
"exact": frozenset({
|
"exact": frozenset({
|
||||||
"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access",
|
"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access",
|
||||||
"negotiate", "rollback", "tree", "window",
|
"negotiate", "rollback", "tree", "window", "vias", "via_cost",
|
||||||
}),
|
}),
|
||||||
"room": frozenset({"shove", "pack"}),
|
"room": frozenset({"shove", "pack"}),
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,8 @@ _FLAG_NAMES = {
|
|||||||
"rollback": "--no-rollback",
|
"rollback": "--no-rollback",
|
||||||
"tree": "--no-tree",
|
"tree": "--no-tree",
|
||||||
"window": "--window-reroute",
|
"window": "--window-reroute",
|
||||||
|
"vias": "--reduce-vias",
|
||||||
|
"via_cost": "--via-cost",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -237,6 +242,37 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
metavar="MM",
|
metavar="MM",
|
||||||
help="the first round's window margin around the stuck connection's pins (default 1.5)",
|
help="the first round's window margin around the stuck connection's pins (default 1.5)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--via-cost",
|
||||||
|
type=float,
|
||||||
|
default=None,
|
||||||
|
metavar="C",
|
||||||
|
help="grid and exact engines: what a layer change costs the search, in grid "
|
||||||
|
"steps of track (default 10)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--reduce-vias",
|
||||||
|
action="store_true",
|
||||||
|
default=None,
|
||||||
|
help="exact engine: after routing, route each net that has vias again on its own "
|
||||||
|
"with vias made dearer, everything else held fixed; its new copper is kept only "
|
||||||
|
"if it fails nothing new and has fewer vias",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--reduce-vias-budget",
|
||||||
|
type=float,
|
||||||
|
default=0.5,
|
||||||
|
metavar="F",
|
||||||
|
help="via reduction stops starting nets once it has spent this share of the "
|
||||||
|
"routing's own time (default 0.5)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--reduce-vias-margin",
|
||||||
|
type=float,
|
||||||
|
default=2.0,
|
||||||
|
metavar="MM",
|
||||||
|
help="how far past its pins and copper a net may go when routed again (default 2.0)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--hole-to-hole",
|
"--hole-to-hole",
|
||||||
type=float,
|
type=float,
|
||||||
@@ -278,6 +314,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
("rollback", not args.rollback),
|
("rollback", not args.rollback),
|
||||||
("tree", not args.tree_nets),
|
("tree", not args.tree_nets),
|
||||||
("window", args.window_reroute is not None),
|
("window", args.window_reroute is not None),
|
||||||
|
("vias", args.reduce_vias is not None),
|
||||||
|
("via_cost", args.via_cost is not None),
|
||||||
)
|
)
|
||||||
if asked
|
if asked
|
||||||
]
|
]
|
||||||
@@ -321,7 +359,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"window_margin_mm": args.window_margin,
|
"window_margin_mm": args.window_margin,
|
||||||
"window_budget": args.window_budget,
|
"window_budget": args.window_budget,
|
||||||
"window_cap": args.window_cap,
|
"window_cap": args.window_cap,
|
||||||
|
"reduce_vias": bool(args.reduce_vias),
|
||||||
|
"reduce_vias_budget": args.reduce_vias_budget,
|
||||||
|
"reduce_vias_margin_mm": args.reduce_vias_margin,
|
||||||
}
|
}
|
||||||
|
if args.via_cost is not None:
|
||||||
|
options["via_cost"] = args.via_cost
|
||||||
elif args.engine == "grid":
|
elif args.engine == "grid":
|
||||||
options = {
|
options = {
|
||||||
"layers": layers,
|
"layers": layers,
|
||||||
@@ -335,6 +378,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"rollback": args.rollback,
|
"rollback": args.rollback,
|
||||||
"tree_nets": args.tree_nets,
|
"tree_nets": args.tree_nets,
|
||||||
}
|
}
|
||||||
|
if args.via_cost is not None:
|
||||||
|
options["via_cost"] = args.via_cost
|
||||||
if args.engine == "room":
|
if args.engine == "room":
|
||||||
result = build_rooms_routing_result(
|
result = build_rooms_routing_result(
|
||||||
dsn, layers=layers, shove=args.shove, pack=args.pack,
|
dsn, layers=layers, shove=args.shove, pack=args.pack,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from rpmroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
|||||||
from .exact_router import ExactRouteResult, route_board_exact
|
from .exact_router import ExactRouteResult, route_board_exact
|
||||||
from .grid_router import RouteResult, route_board
|
from .grid_router import RouteResult, route_board
|
||||||
from .room_router import route_board_rooms
|
from .room_router import route_board_rooms
|
||||||
|
from .vias import reduce_vias as reduce_vias_stage
|
||||||
from .window import window_reroute as window_reroute_stage
|
from .window import window_reroute as window_reroute_stage
|
||||||
|
|
||||||
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
||||||
@@ -124,6 +125,7 @@ def route_dsn_board(
|
|||||||
hole_to_hole_mm: float = 0.25,
|
hole_to_hole_mm: float = 0.25,
|
||||||
rollback: bool = True,
|
rollback: bool = True,
|
||||||
tree_nets: bool = True,
|
tree_nets: bool = True,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> tuple[RouteResult, int, list[str]]:
|
) -> tuple[RouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||||
@@ -153,6 +155,7 @@ def route_dsn_board(
|
|||||||
net_widths=_net_widths_board(dsn, scale),
|
net_widths=_net_widths_board(dsn, scale),
|
||||||
via_drill=_via_drill_board(dsn, scale),
|
via_drill=_via_drill_board(dsn, scale),
|
||||||
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
||||||
|
via_cost=via_cost,
|
||||||
)
|
)
|
||||||
return result, scale, [layer.name for layer in dsn.layers]
|
return result, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
@@ -206,6 +209,7 @@ def build_routing_result(
|
|||||||
hole_to_hole_mm: float = 0.25,
|
hole_to_hole_mm: float = 0.25,
|
||||||
rollback: bool = True,
|
rollback: bool = True,
|
||||||
tree_nets: bool = True,
|
tree_nets: bool = True,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||||
route, scale, layer_names = route_dsn_board(
|
route, scale, layer_names = route_dsn_board(
|
||||||
@@ -217,6 +221,7 @@ def build_routing_result(
|
|||||||
hole_to_hole_mm=hole_to_hole_mm,
|
hole_to_hole_mm=hole_to_hole_mm,
|
||||||
rollback=rollback,
|
rollback=rollback,
|
||||||
tree_nets=tree_nets,
|
tree_nets=tree_nets,
|
||||||
|
via_cost=via_cost,
|
||||||
)
|
)
|
||||||
return _to_routing_result(route, dsn, scale, layer_names)
|
return _to_routing_result(route, dsn, scale, layer_names)
|
||||||
|
|
||||||
@@ -244,6 +249,10 @@ def route_dsn_board_exact(
|
|||||||
window_margin_mm: float = 1.5,
|
window_margin_mm: float = 1.5,
|
||||||
window_budget: float = 0.25,
|
window_budget: float = 0.25,
|
||||||
window_cap: int = 40,
|
window_cap: int = 40,
|
||||||
|
reduce_vias: bool = False,
|
||||||
|
reduce_vias_budget: float = 0.5,
|
||||||
|
reduce_vias_margin_mm: float = 2.0,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||||
@@ -272,6 +281,7 @@ def route_dsn_board_exact(
|
|||||||
"net_widths": _net_widths_board(dsn, scale),
|
"net_widths": _net_widths_board(dsn, scale),
|
||||||
"via_drill": _via_drill_board(dsn, scale),
|
"via_drill": _via_drill_board(dsn, scale),
|
||||||
"hole_to_hole": _mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
"hole_to_hole": _mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
||||||
|
"via_cost": via_cost,
|
||||||
}
|
}
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
exact = route_board_exact(
|
exact = route_board_exact(
|
||||||
@@ -297,6 +307,18 @@ def route_dsn_board_exact(
|
|||||||
budget_s=max(float(window_budget), 0.0) * (time.monotonic() - started),
|
budget_s=max(float(window_budget), 0.0) * (time.monotonic() - started),
|
||||||
max_windows=window_cap,
|
max_windows=window_cap,
|
||||||
)
|
)
|
||||||
|
if reduce_vias:
|
||||||
|
via_half = common["via_half"] or max(width_board // 2, 1)
|
||||||
|
exact.result = reduce_vias_stage(
|
||||||
|
dsn, exact.result, common,
|
||||||
|
resolution=resolution,
|
||||||
|
margin=_mm_to_board(dsn, scale, reduce_vias_margin_mm) or 0,
|
||||||
|
via_half=via_half,
|
||||||
|
via_drill=common["via_drill"] or 2 * via_half,
|
||||||
|
via_name=_via_padstack(dsn),
|
||||||
|
# a share of what the routing itself took
|
||||||
|
budget_s=max(float(reduce_vias_budget), 0.0) * (time.monotonic() - started),
|
||||||
|
)
|
||||||
return exact, scale, [layer.name for layer in dsn.layers]
|
return exact, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
@@ -323,6 +345,10 @@ def build_exact_routing_result(
|
|||||||
window_margin_mm: float = 1.5,
|
window_margin_mm: float = 1.5,
|
||||||
window_budget: float = 0.25,
|
window_budget: float = 0.25,
|
||||||
window_cap: int = 40,
|
window_cap: int = 40,
|
||||||
|
reduce_vias: bool = False,
|
||||||
|
reduce_vias_budget: float = 0.5,
|
||||||
|
reduce_vias_margin_mm: float = 2.0,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||||
exact, scale, layer_names = route_dsn_board_exact(
|
exact, scale, layer_names = route_dsn_board_exact(
|
||||||
@@ -347,6 +373,10 @@ def build_exact_routing_result(
|
|||||||
window_margin_mm=window_margin_mm,
|
window_margin_mm=window_margin_mm,
|
||||||
window_budget=window_budget,
|
window_budget=window_budget,
|
||||||
window_cap=window_cap,
|
window_cap=window_cap,
|
||||||
|
reduce_vias=reduce_vias,
|
||||||
|
reduce_vias_budget=reduce_vias_budget,
|
||||||
|
reduce_vias_margin_mm=reduce_vias_margin_mm,
|
||||||
|
via_cost=via_cost,
|
||||||
)
|
)
|
||||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"""Via reduction: a last stage that routes each net again with vias made dear.
|
||||||
|
|
||||||
|
The main search prices a layer change at a few millimetres of track, which
|
||||||
|
is right while the board is crowded and wrong once it is routed: a net that
|
||||||
|
hopped layers to slip past a route can often stay on one layer around it.
|
||||||
|
So each net that has vias is ripped and routed again on its own, everything
|
||||||
|
else held as fixed copper, with the via cost raised; the new copper is kept
|
||||||
|
only if the net loses no connection and ends with fewer vias.
|
||||||
|
|
||||||
|
It reuses the window stage's board: the net's own items and copper, and of
|
||||||
|
everything else only what lies near the box the net is routed in. The nets
|
||||||
|
with the most vias go first, and no new net starts once ``budget_s`` is
|
||||||
|
spent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rpmroute.board import build_board
|
||||||
|
from rpmroute.dsn import DsnBoard
|
||||||
|
from rpmroute.geometry import IntBox
|
||||||
|
|
||||||
|
from .exact_router import route_board_exact
|
||||||
|
from .grid_router import RouteResult
|
||||||
|
from .window import Copper, _inside, _item_box, _open, _window_board
|
||||||
|
|
||||||
|
|
||||||
|
def _net_box(copper: Copper, pins: list, grow: int) -> IntBox | None:
|
||||||
|
wires, vias = copper
|
||||||
|
points = [p for _layer, path in wires for p in path] + list(vias) + pins
|
||||||
|
if not points:
|
||||||
|
return None
|
||||||
|
xs, ys = [p.x for p in points], [p.y for p in points]
|
||||||
|
return IntBox(min(xs) - grow, min(ys) - grow, max(xs) + grow, max(ys) + grow)
|
||||||
|
|
||||||
|
|
||||||
|
def reduce_vias(
|
||||||
|
dsn: DsnBoard,
|
||||||
|
result: RouteResult,
|
||||||
|
route_args: dict[str, Any],
|
||||||
|
*,
|
||||||
|
resolution: int,
|
||||||
|
margin: int,
|
||||||
|
via_half: int,
|
||||||
|
via_drill: int,
|
||||||
|
via_name: str,
|
||||||
|
via_cost_factor: float = 8.0,
|
||||||
|
budget_s: float | None = None,
|
||||||
|
) -> RouteResult:
|
||||||
|
"""Route each net with vias again with vias ``via_cost_factor`` times dearer;
|
||||||
|
keep a net's new copper only when it fails nothing new and has fewer vias.
|
||||||
|
|
||||||
|
``route_args`` are the exact engine's settings for the whole board.
|
||||||
|
``margin`` grows the box around the net's pins and copper that it is
|
||||||
|
routed in, so it may go round what it used to hop over."""
|
||||||
|
started = time.monotonic()
|
||||||
|
copper: dict[int, Copper] = {
|
||||||
|
net: (list(result.wires.get(net, [])), list(result.vias.get(net, [])))
|
||||||
|
for net in set(result.wires) | set(result.vias)
|
||||||
|
}
|
||||||
|
todo = sorted((n for n, c in copper.items() if c[1]), key=lambda n: -len(set(copper[n][1])))
|
||||||
|
if not todo:
|
||||||
|
return result
|
||||||
|
default_half = result.half_width
|
||||||
|
half = dict(result.net_half)
|
||||||
|
failed = list(result.failed)
|
||||||
|
base = build_board(dsn)
|
||||||
|
base_items = [(item, _item_box(item)) for item in base.get_items()]
|
||||||
|
layers = route_args.get("layers") or [
|
||||||
|
i for i, layer in enumerate(base.layer_structure.arr) if layer.is_signal
|
||||||
|
]
|
||||||
|
pins_of: dict[int, list] = {}
|
||||||
|
for pin in base.get_pins():
|
||||||
|
for net in pin.net_nos:
|
||||||
|
pins_of.setdefault(net, []).append(pin.location)
|
||||||
|
context = 2 * (route_args["trace_width"] + route_args["clearance"]) + 2 * via_half + (
|
||||||
|
route_args.get("edge_clearance") or 0
|
||||||
|
)
|
||||||
|
args = dict(route_args)
|
||||||
|
args["via_cost"] = float(route_args.get("via_cost", 10.0)) * via_cost_factor
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
for net in todo:
|
||||||
|
if budget_s is not None and time.monotonic() - started >= budget_s:
|
||||||
|
break
|
||||||
|
old_vias = len(set(copper[net][1]))
|
||||||
|
window = _net_box(copper[net], pins_of.get(net, []), margin)
|
||||||
|
if window is None:
|
||||||
|
continue
|
||||||
|
before = _open(f for f in failed if f[0] == net)
|
||||||
|
view = IntBox(window.ll.x - context, window.ll.y - context,
|
||||||
|
window.ur.x + context, window.ur.y + context)
|
||||||
|
keep = set(copper) - {net}
|
||||||
|
board = _window_board(base, base_items, view, {net}, copper, keep, half,
|
||||||
|
default_half, via_half, via_drill, layers, via_name)
|
||||||
|
sub = route_board_exact(
|
||||||
|
board, **args, resolution=resolution, only_nets=frozenset({net}), bounds=window,
|
||||||
|
).result
|
||||||
|
after = _open(f for f in sub.failed if f[0] == net)
|
||||||
|
new = (sub.wires.get(net, []), sub.vias.get(net, []))
|
||||||
|
if len(after) > len(before) or len(set(new[1])) >= old_vias:
|
||||||
|
continue
|
||||||
|
# the board left out what lies away from the box, so copper reaching
|
||||||
|
# out of it was never checked against that
|
||||||
|
if (new[0] or new[1]) and not _inside(new, window):
|
||||||
|
continue
|
||||||
|
copper[net] = (list(new[0]), list(new[1]))
|
||||||
|
half.pop(net, None)
|
||||||
|
if net in sub.net_half:
|
||||||
|
half[net] = sub.net_half[net]
|
||||||
|
failed = [f for f in failed if f[0] != net] + [f for f in sub.failed if f[0] == net]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return result
|
||||||
|
out = RouteResult(default_half)
|
||||||
|
for net, (wires, vias) in copper.items():
|
||||||
|
for layer, path in wires:
|
||||||
|
out.add_wire(net, layer, path)
|
||||||
|
for location in dict.fromkeys(vias):
|
||||||
|
out.add_via(net, location)
|
||||||
|
out.failed = failed
|
||||||
|
out.unrouted = len(_open(failed))
|
||||||
|
out.net_half = half
|
||||||
|
out.narrowed = list(result.narrowed)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""Via reduction: each net with vias is routed again with vias made dear,
|
||||||
|
kept only when it fails nothing new and has fewer vias."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rpmroute.board import build_board
|
||||||
|
from rpmroute.dsn import parse_dsn
|
||||||
|
from rpmroute.geometry import IntPoint
|
||||||
|
from rpmroute.route.grid_router import RouteResult
|
||||||
|
from rpmroute.route.pipeline import (
|
||||||
|
_rule_clearance_dsn,
|
||||||
|
_rule_width_dsn,
|
||||||
|
_via_half_board,
|
||||||
|
route_dsn_board_exact,
|
||||||
|
)
|
||||||
|
from rpmroute.route.vias import reduce_vias
|
||||||
|
|
||||||
|
VIA = "Via[0-1]_600:300_um"
|
||||||
|
|
||||||
|
|
||||||
|
def _board(second_on_bottom: bool = False) -> str:
|
||||||
|
"""Two pads 14 mm apart on an empty two-layer board; the second on the
|
||||||
|
bottom layer when ``second_on_bottom``."""
|
||||||
|
p2_stack = "Bot_Pad" if second_on_bottom else "Top_Pad"
|
||||||
|
return f"""(pcb "vias.dsn"
|
||||||
|
(parser (string_quote ") (space_in_quoted_tokens on) (host_cad "t") (host_version "1"))
|
||||||
|
(resolution um 10)
|
||||||
|
(unit um)
|
||||||
|
(structure
|
||||||
|
(layer F.Cu (type signal) (property (index 0)))
|
||||||
|
(layer B.Cu (type signal) (property (index 1)))
|
||||||
|
(boundary (path pcb 0 0 0 20000 0 20000 -10000 0 -10000 0 0))
|
||||||
|
(via "{VIA}")
|
||||||
|
(rule (width 200) (clearance 200))
|
||||||
|
)
|
||||||
|
(library
|
||||||
|
(padstack Top_Pad (shape (rect F.Cu -300 -300 300 300)) (attach off))
|
||||||
|
(padstack Bot_Pad (shape (rect B.Cu -300 -300 300 300)) (attach off))
|
||||||
|
(padstack "{VIA}" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off))
|
||||||
|
(image TOP (pin Top_Pad 1 0 0))
|
||||||
|
(image SECOND (pin {p2_stack} 1 0 0))
|
||||||
|
)
|
||||||
|
(placement
|
||||||
|
(component TOP (place P1 3000 -5000 front 0))
|
||||||
|
(component SECOND (place P2 17000 -5000 front 0))
|
||||||
|
)
|
||||||
|
(network
|
||||||
|
(net N (pins P1-1 P2-1))
|
||||||
|
(class default (rule (width 200) (clearance 200)))
|
||||||
|
)
|
||||||
|
(wiring)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _route_args(dsn) -> dict:
|
||||||
|
scale = max(dsn.resolution.value, 1)
|
||||||
|
return {
|
||||||
|
"heuristic_weight": 1.0,
|
||||||
|
"via_half": _via_half_board(dsn, scale),
|
||||||
|
"edge_clearance": None,
|
||||||
|
"trace_width": round(_rule_width_dsn(dsn) * scale),
|
||||||
|
"clearance": round(_rule_clearance_dsn(dsn) * scale),
|
||||||
|
"layers": None,
|
||||||
|
"max_passes": 10,
|
||||||
|
"rip_up": True,
|
||||||
|
"shove": False,
|
||||||
|
"diagonal": False,
|
||||||
|
"rollback": True,
|
||||||
|
"tree_nets": True,
|
||||||
|
"net_widths": {},
|
||||||
|
"via_drill": None,
|
||||||
|
"hole_to_hole": 0,
|
||||||
|
"via_cost": 10.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _reduce(text: str, result: RouteResult) -> RouteResult:
|
||||||
|
dsn = parse_dsn(text)
|
||||||
|
args = _route_args(dsn)
|
||||||
|
return reduce_vias(
|
||||||
|
dsn, result, args, resolution=1, margin=20000,
|
||||||
|
via_half=args["via_half"], via_drill=2 * args["via_half"], via_name=VIA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pins(text: str) -> tuple[IntPoint, IntPoint]:
|
||||||
|
pins = {p.component_ref: p.location for p in build_board(parse_dsn(text)).get_pins()}
|
||||||
|
return pins["P1"], pins["P2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_needless_via_pair_is_routed_out():
|
||||||
|
text = _board()
|
||||||
|
a, b = _pins(text)
|
||||||
|
# a detour: down to the bottom layer and back up, for no reason
|
||||||
|
v1, v2 = IntPoint(a.x + 40000, a.y), IntPoint(b.x - 40000, b.y)
|
||||||
|
detour = RouteResult(1000)
|
||||||
|
detour.add_wire(1, 0, [a, v1])
|
||||||
|
detour.add_via(1, v1)
|
||||||
|
detour.add_wire(1, 1, [v1, v2])
|
||||||
|
detour.add_via(1, v2)
|
||||||
|
detour.add_wire(1, 0, [v2, b])
|
||||||
|
out = _reduce(text, detour)
|
||||||
|
assert out.failed == []
|
||||||
|
assert sum(len(v) for v in out.vias.values()) == 0
|
||||||
|
assert out.wires.get(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_needed_via_stays():
|
||||||
|
text = _board(second_on_bottom=True)
|
||||||
|
exact, _scale, _layers = route_dsn_board_exact(parse_dsn(text), resolution=1)
|
||||||
|
routed = exact.result
|
||||||
|
assert routed.failed == []
|
||||||
|
vias_before = sum(len(set(v)) for v in routed.vias.values())
|
||||||
|
assert vias_before >= 1
|
||||||
|
out = _reduce(text, routed)
|
||||||
|
assert out.failed == []
|
||||||
|
assert sum(len(set(v)) for v in out.vias.values()) == vias_before
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_flag_runs_the_stage_end_to_end():
|
||||||
|
exact, _scale, _layers = route_dsn_board_exact(parse_dsn(_board()), resolution=1, reduce_vias=True)
|
||||||
|
assert exact.result.failed == []
|
||||||
@@ -174,6 +174,8 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
|||||||
"rollback": ["--no-rollback"],
|
"rollback": ["--no-rollback"],
|
||||||
"tree": ["--no-tree"],
|
"tree": ["--no-tree"],
|
||||||
"window": ["--window-reroute", "1"],
|
"window": ["--window-reroute", "1"],
|
||||||
|
"vias": ["--reduce-vias"],
|
||||||
|
"via_cost": ["--via-cost", "40"],
|
||||||
}
|
}
|
||||||
for engine, options in ENGINE_OPTIONS.items():
|
for engine, options in ENGINE_OPTIONS.items():
|
||||||
for option in options:
|
for option in options:
|
||||||
|
|||||||
Reference in New Issue
Block a user