2 Commits

3 changed files with 154 additions and 491 deletions
-35
View File
@@ -40,7 +40,6 @@ The routing method selection logic was updated so perpendicular port-angle cases
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-04
### Scope
@@ -208,37 +207,3 @@ Contract tests were added to verify electrical-family PCB routing and left-to-ri
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-15
### Scope
Backend automatic route spacing extension for U-bend and mixed S-bend/U-bend bundles in `mxpic_router`.
### Summary
Recent work focused on extending the existing S-bend `Lstart` spacing strategy to U-bend routes and improving collision avoidance when both route types appear in the same bundle.
### Completed Work
#### 1. U-Bend Spacing Support
Automatic `ubend_p2p` routes now receive width-aware `length` offsets within compatible route groups.
The U-bend ordering logic was refined so inner routes use shorter lengths and outer routes use longer lengths, including reversed port-order cases.
#### 2. Mixed Route-Type Coordination
Mixed S-bend/U-bend groups now share coordinated spacing.
U-bend routes are assigned the shorter length range first, while S-bend routes receive larger `Lstart` values after accounting for the U-bend group envelope, including route width, spacing, and bend radius.
#### 3. Test Coverage Update
Contract tests were added and updated for U-bend spacing, reversed port-order U-bend routing, and mixed S-bend/U-bend bundle spacing.
### Modified Files
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
+8 -118
View File
@@ -313,7 +313,7 @@ def _route_bundle_links(links: list, pin_map: dict, Route, warnings: list) -> No
plans.append(plan)
by_order[order] = plan
_assign_bend_route_spacing(plans)
_assign_sbend_lstart_spacing(plans)
accepted = []
for plan in plans:
@@ -365,78 +365,29 @@ def _route_bundle_links(links: list, pin_map: dict, Route, warnings: list) -> No
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
)
if plan.get("route_options", {}).get("length"):
warnings.append(
f"Applied ubend length {plan['route_options']['length']:g}um to "
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
)
_route_link(plan["link"], pin_map, Route, warnings, plan.get("route_options"))
def _assign_bend_route_spacing(plans: list) -> None:
def _assign_sbend_lstart_spacing(plans: list) -> None:
groups = {}
for plan in plans:
if not plan.get("automatic"):
continue
method_name = _route_method_name_for_pins(plan["pin1"], plan["pin2"])
if method_name not in {"sbend_p2p", "ubend_p2p"}:
if _route_method_name_for_pins(plan["pin1"], plan["pin2"]) != "sbend_p2p":
continue
key = _bend_spacing_group_key(plan, method_name)
key = _sbend_spacing_group_key(plan)
if key is None:
continue
bucket = groups.setdefault(key, {"method": method_name, "plans": []})
bucket["plans"].append(plan)
groups.setdefault(key, []).append(plan)
for bucket in groups.values():
group = bucket["plans"]
for group in groups.values():
if len(group) < 2:
continue
axis = _sbend_forward_axis(group[0])
coord_key = "y" if axis == "horizontal" else "x"
step = _sbend_lstart_step(group)
if bucket["method"] == "sbend_p2p":
ranked_plans = _sbend_lstart_ranks(group, coord_key)
else:
ranked_plans = _ubend_length_ranks(group, coord_key)
for plan, rank in ranked_plans:
if bucket["method"] == "sbend_p2p":
for plan, rank in _sbend_lstart_ranks(group, coord_key):
plan["route_options"]["Lstart"] = rank * step
else:
plan["route_options"]["length"] = rank * step
_assign_mixed_bend_route_spacing(plans)
def _assign_mixed_bend_route_spacing(plans: list) -> None:
groups = {}
for plan in plans:
if not plan.get("automatic"):
continue
method_name = _route_method_name_for_pins(plan["pin1"], plan["pin2"])
if method_name not in {"sbend_p2p", "ubend_p2p"}:
continue
key = _mixed_bend_spacing_group_key(plan)
if key is None:
continue
bucket = groups.setdefault(key, {"methods": set(), "plans": []})
bucket["methods"].add(method_name)
bucket["plans"].append(plan)
for bucket in groups.values():
if len(bucket["methods"]) < 2:
continue
group = bucket["plans"]
axis = _sbend_forward_axis(group[0])
coord_key = "y" if axis == "horizontal" else "x"
step = _sbend_lstart_step(group)
for method_name, plan, rank in _mixed_bend_route_ranks(group, coord_key, step):
if method_name == "sbend_p2p":
plan["route_options"]["Lstart"] = rank * step
plan["route_options"].pop("length", None)
else:
plan["route_options"]["length"] = rank * step
plan["route_options"].pop("Lstart", None)
def _sbend_lstart_step(group: list) -> float:
@@ -462,52 +413,13 @@ def _sbend_lstart_ranks(group: list, coord_key: str) -> list:
]
def _ubend_length_ranks(group: list, coord_key: str) -> list:
spans = [round(abs(_route_coord_delta(plan, coord_key)), 6) for plan in group]
nonzero_spans = [span for span in spans if span > 1e-9]
if nonzero_spans and len(set(nonzero_spans)) > 1:
ordered_spans = sorted(set(spans))
return [(plan, ordered_spans.index(round(abs(_route_coord_delta(plan, coord_key)), 6)) + 1) for plan in group]
center = sum(_route_start_coord(plan, coord_key) for plan in group) / len(group)
distances = sorted({round(abs(_route_start_coord(plan, coord_key) - center), 6) for plan in group})
return [
(plan, distances.index(round(abs(_route_start_coord(plan, coord_key) - center), 6)) + 1)
for plan in group
]
def _mixed_bend_route_ranks(group: list, coord_key: str, step: float) -> list:
ubend_plans = [plan for plan in group if _route_method_name_for_pins(plan["pin1"], plan["pin2"]) == "ubend_p2p"]
sbend_plans = [plan for plan in group if _route_method_name_for_pins(plan["pin1"], plan["pin2"]) == "sbend_p2p"]
ranked = []
ubend_ranked = _ubend_length_ranks(ubend_plans, coord_key)
for plan, rank in ubend_ranked:
ranked.append(("ubend_p2p", plan, rank))
first_sbend_rank = _mixed_first_sbend_rank_after_ubends(ubend_ranked, step)
for plan, rank in _sbend_lstart_ranks(sbend_plans, coord_key):
ranked.append(("sbend_p2p", plan, first_sbend_rank + rank - 1))
return ranked
def _mixed_first_sbend_rank_after_ubends(ubend_ranked: list, step: float) -> int:
if not ubend_ranked:
return 1
max_length = max(rank * step for plan, rank in ubend_ranked)
max_radius = max((_safe_float(plan["link"].radius, 10.0) or 10.0) for plan, rank in ubend_ranked)
total_width = sum((_safe_float(plan["link"].width, 0.5) or 0.5) for plan, rank in ubend_ranked)
spacing_slots = len(ubend_ranked) + 1
ubend_envelope = max_length + max_radius + total_width + spacing_slots * ROUTE_MIN_SPACING
return max(1, math.ceil(ubend_envelope / step))
def _route_coord_delta(plan: dict, coord_key: str) -> float:
start = plan["points"][0]
end = plan["points"][-1]
return float(end[coord_key]) - float(start[coord_key])
def _bend_spacing_group_key(plan: dict, method_name: str):
def _sbend_spacing_group_key(plan: dict):
link = plan["link"]
axis = _sbend_forward_axis(plan)
main_key = "x" if axis == "horizontal" else "y"
@@ -521,27 +433,6 @@ def _bend_spacing_group_key(plan: dict, method_name: str):
_sbend_span_bucket(plan["points"][0][main_key]),
_sbend_span_bucket(plan["points"][-1][main_key]),
)
return (
method_name,
axis,
_route_direction_sign(main_delta),
route_scope,
str(link.xsection or ""),
_safe_float(link.width, 0.0),
_safe_float(link.radius, 0.0),
)
def _mixed_bend_spacing_group_key(plan: dict):
link = plan["link"]
axis = _sbend_forward_axis(plan)
main_key = "x" if axis == "horizontal" else "y"
main_delta = _route_coord_delta(plan, main_key)
explicit_group = _link_explicit_route_group(link)
if explicit_group:
route_scope = ("explicit", explicit_group)
else:
route_scope = ("start-span", _sbend_span_bucket(plan["points"][0][main_key]))
return (
axis,
_route_direction_sign(main_delta),
@@ -1252,7 +1143,6 @@ class _NazcaInterconnectRoute:
width=self._route_width(width),
radius=self._route_radius(radius),
xs=self._route_xs(xs),
length=kwargs.get("length", 0),
arrow=arrow,
)
-192
View File
@@ -773,198 +773,6 @@ class EdaRouterPinsContractTest(unittest.TestCase):
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts, [200.0, 150.0, 100.0, 50.0])
def test_ubend_spacing_uses_inner_shorter_outer_longer_length_within_bundle(self):
from mxpic_router.builder import _route_bundle_links
from mxpic_router.eda_loader import LinkSpec
class FakePin:
def __init__(self, x, y, angle):
self.x = x
self.y = y
self.a = angle
class FakeRoute:
calls = []
def __init__(self, **kwargs):
pass
def ubend_p2p(self, **kwargs):
self.calls.append(("ubend_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="r1", src_pin="out", dst_inst="p1", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="r2", src_pin="out", dst_inst="p2", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="r3", src_pin="out", dst_inst="p3", dst_pin="in", width=40, radius=10),
]
pin_map = {
("r1", "out"): FakePin(0, 40, 0),
("p1", "in"): FakePin(100, 40, 0),
("r2", "out"): FakePin(0, 30, 0),
("p2", "in"): FakePin(100, 30, 0),
("r3", "out"): FakePin(0, 20, 0),
("p3", "in"): FakePin(100, 20, 0),
}
warnings = []
_route_bundle_links(links, pin_map, FakeRoute, warnings)
calls = [call[1] for call in FakeRoute.calls if call[0] == "ubend_p2p"]
self.assertEqual([call["length"] for call in calls], [100.0, 50.0, 100.0])
self.assertTrue(all("Lstart" not in call for call in calls))
self.assertTrue(any("Applied ubend length 50um" in warning for warning in warnings))
def test_ubend_spacing_uses_symmetric_nested_lengths_for_even_group(self):
from mxpic_router.builder import _route_bundle_links
from mxpic_router.eda_loader import LinkSpec
class FakePin:
def __init__(self, x, y, angle):
self.x = x
self.y = y
self.a = angle
class FakeRoute:
calls = []
def __init__(self, **kwargs):
pass
def ubend_p2p(self, **kwargs):
self.calls.append(("ubend_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="r1", src_pin="out", dst_inst="p1", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="r2", src_pin="out", dst_inst="p2", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="r3", src_pin="out", dst_inst="p3", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="r4", src_pin="out", dst_inst="p4", dst_pin="in", width=40, radius=10),
]
pin_map = {
("r1", "out"): FakePin(0, 40, 0),
("p1", "in"): FakePin(100, 40, 0),
("r2", "out"): FakePin(0, 30, 0),
("p2", "in"): FakePin(100, 30, 0),
("r3", "out"): FakePin(0, 20, 0),
("p3", "in"): FakePin(100, 20, 0),
("r4", "out"): FakePin(0, 10, 0),
("p4", "in"): FakePin(100, 10, 0),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
calls = [call[1] for call in FakeRoute.calls if call[0] == "ubend_p2p"]
self.assertEqual([call["length"] for call in calls], [100.0, 50.0, 50.0, 100.0])
def test_ubend_spacing_uses_route_span_for_reversed_port_order(self):
from mxpic_router.builder import _route_bundle_links
from mxpic_router.eda_loader import LinkSpec
class FakePin:
def __init__(self, x, y, angle):
self.x = x
self.y = y
self.a = angle
class FakeRoute:
calls = []
def __init__(self, **kwargs):
pass
def ubend_p2p(self, **kwargs):
self.calls.append(("ubend_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="port_3", src_pin="io1", dst_inst="port_4", dst_pin="io4", width=0.5, radius=10),
LinkSpec(src_inst="port_3", src_pin="io2", dst_inst="port_4", dst_pin="io3", width=0.5, radius=10),
LinkSpec(src_inst="port_3", src_pin="io3", dst_inst="port_4", dst_pin="io2", width=0.5, radius=10),
LinkSpec(src_inst="port_3", src_pin="io4", dst_inst="port_4", dst_pin="io1", width=0.5, radius=10),
]
pin_map = {
("port_3", "io1"): FakePin(1772.6, -2515.2, 0),
("port_3", "io2"): FakePin(1772.6, -2525.2, 0),
("port_3", "io3"): FakePin(1772.6, -2535.2, 0),
("port_3", "io4"): FakePin(1772.6, -2545.2, 0),
("port_4", "io1"): FakePin(1771.3, -2399.9, 0),
("port_4", "io2"): FakePin(1771.3, -2409.9, 0),
("port_4", "io3"): FakePin(1771.3, -2419.9, 0),
("port_4", "io4"): FakePin(1771.3, -2429.9, 0),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
calls = [call[1] for call in FakeRoute.calls if call[0] == "ubend_p2p"]
self.assertEqual([call["length"] for call in calls], [10.5, 21.0, 31.5, 42.0])
def test_mixed_sbend_and_ubend_spacing_share_route_ranks(self):
from mxpic_router.builder import _route_bundle_links
from mxpic_router.eda_loader import LinkSpec
class FakePin:
def __init__(self, x, y, angle):
self.x = x
self.y = y
self.a = angle
class FakeRoute:
calls = []
def __init__(self, **kwargs):
pass
def sbend_p2p(self, **kwargs):
self.calls.append(("sbend_p2p", kwargs))
return self
def ubend_p2p(self, **kwargs):
self.calls.append(("ubend_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="port_7", src_pin="io2", dst_inst="port_5", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="port_7", src_pin="io1", dst_inst="port_5", dst_pin="io2", width=0.5, radius=10),
LinkSpec(src_inst="port_6", src_pin="io2", dst_inst="port_5", dst_pin="io3", width=0.5, radius=10),
LinkSpec(src_inst="port_6", src_pin="io1", dst_inst="port_5", dst_pin="io4", width=0.5, radius=10),
]
pin_map = {
("port_7", "io2"): FakePin(2071.8, -2464.4, 0),
("port_7", "io1"): FakePin(2071.8, -2454.4, 0),
("port_6", "io2"): FakePin(2173.4, -2405.6, 180),
("port_6", "io1"): FakePin(2173.4, -2415.6, 180),
("port_5", "io1"): FakePin(2074.2, -2513.1, 0),
("port_5", "io2"): FakePin(2074.2, -2523.1, 0),
("port_5", "io3"): FakePin(2074.2, -2533.1, 0),
("port_5", "io4"): FakePin(2074.2, -2543.1, 0),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
calls = [(name, call) for name, call in FakeRoute.calls if name in {"sbend_p2p", "ubend_p2p"}]
ubend_lengths = [call["length"] for name, call in calls if name == "ubend_p2p"]
sbend_lstarts = [call["Lstart"] for name, call in calls if name == "sbend_p2p"]
self.assertEqual(ubend_lengths, [10.5, 21.0])
self.assertTrue(max(ubend_lengths) < min(sbend_lstarts))
self.assertEqual(sbend_lstarts, [63.0, 73.5])
def test_route_backend_falls_back_to_nazca_interconnect_when_forge_is_absent(self):
import mxpic_router.builder as builder