Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d1994125 | |||
| 2a35095418 |
-35
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user