Compare commits

17 Commits

Author SHA1 Message Date
PotatoMaxwell 84d1994125 Merge pull request 'Merge pull request 'Removing mxpic_forge from dependency' (#2) from main into pengkun_main' (#3) from pengkun_main into qinyue_main
Reviewed-on: #3
2026-06-10 11:34:16 +00:00
pengkun0129 eaea124b07 上传文件至「mxpic_router」 2026-06-10 08:04:29 +00:00
pengkun0129 bf483c0ea6 上传文件至「tests」 2026-06-10 08:04:11 +00:00
pengkun0129 5f85b390ef 上传文件至「/」 2026-06-10 08:03:53 +00:00
pengkun0129 a9187e8a29 上传文件至「/」 2026-06-09 17:48:53 +00:00
pengkun0129 f736cff672 上传文件至「mxpic_router」 2026-06-09 17:48:28 +00:00
pengkun0129 7a76cb44cf 上传文件至「tests」 2026-06-09 17:47:43 +00:00
pengkun0129 e7f707292a 上传文件至「tests」 2026-06-04 17:57:31 +00:00
pengkun0129 d7698d1ee5 上传文件至「mxpic_router」 2026-06-04 17:57:02 +00:00
pengkun0129 531c1f1f45 上传文件至「/」 2026-06-04 17:56:28 +00:00
pengkun0129 f367fd4fc6 上传文件至「tests」 2026-06-04 10:13:35 +00:00
pengkun0129 759a32cfc5 上传文件至「mxpic_router」 2026-06-04 10:13:12 +00:00
pengkun0129 4d17ec7e9e 上传文件至「/」 2026-06-04 10:12:43 +00:00
pengkun0129 92e818163c 上传文件至「tests」 2026-06-03 02:04:57 +00:00
pengkun0129 5ba7b66e9e 上传文件至「mxpic_router」 2026-06-03 02:04:10 +00:00
pengkun0129 eb45d2f040 上传文件至「/」 2026-06-03 02:03:34 +00:00
PotatoMaxwell 6272f6ac4f Merge pull request 'Removing mxpic_forge from dependency' (#2) from main into pengkun_main
Reviewed-on: #2
2026-06-01 05:22:22 +00:00
4 changed files with 2443 additions and 1125 deletions
+209
View File
@@ -0,0 +1,209 @@
# Backend Routing Work Log - PENG KUN
## 2026-06-02
### Scope
Backend routing generation and GDS build consistency in `mxpic_router`.
### Summary
Recent work focused on improving routing correctness in `mxpic_router/mxpic_router/builder.py`, including route width investigation, rotation convention alignment, and perpendicular routing behavior correction.
### Completed Work
#### 1. Route Width Issue Investigation
A mismatch was identified between the route width configured in the EDA frontend and the width exported into the layout YAML/GDS build flow.
Backend tracing showed that the router consumed the width value from the YAML as expected, while the exported YAML still used the default width in several cases. The issue was isolated to the frontend serialization/export logic and reported to the frontend owner for correction.
#### 2. Frontend/Backend Rotation Convention Alignment
A rotation inconsistency was identified between the EDA canvas representation and the generated GDS layout.
For rotated devices such as resistors, the frontend display and backend placement used different rotation conventions, which caused incorrect component orientation or port direction in the final GDS.
The backend build logic was updated to convert EDA instance rotation into layout rotation during component placement and metadata pin registration, while preserving the original behavior for element-based ports and anchors.
#### 3. Perpendicular Routing Behavior Correction
A routing issue was identified for port pairs with a 90-degree or 270-degree angle difference.
The previous automatic route selection fell back to the generic `bend_p2p` method, which could generate a `bend - diagonal straight - bend` path.
The routing method selection logic was updated so perpendicular port-angle cases use `strt_bend_strt_p2p`, producing the expected `straight - bend - straight` routing style.
### Modified Files
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-04
### Scope
Backend routing spacing behavior and automatic S-bend refinement in `mxpic_router`.
### Summary
Recent work focused on reducing overlap and crossing risks for grouped automatic S-bend connections while keeping the generated GDS impact limited.
### Completed Work
#### 1. S-Bend Spacing Issue Investigation
A route overlap issue was investigated for layouts containing multiple parallel or near-parallel automatic S-bend routes.
The issue was most visible when several component output ports were connected to several port elements. In these cases, the initial straight section of multiple S-bends could overlap or become too close when source and target ports were arranged in parallel.
#### 2. Conservative `Lstart`-Based S-Bend Adjustment
Spacing control was implemented through the native Nazca `sbend_p2p` `Lstart` parameter.
The backend now groups compatible automatic S-bend routes and assigns different `Lstart` values before route generation. This preserves the native S-bend route style and avoids manually constructed detours.
The adjustment applies only to automatic `sbend_p2p` routes. Manual-point routes, U-bends, perpendicular `strt_bend_strt_p2p` routes, and generic bend routes are left unchanged.
#### 3. Bundle-Level Route Grouping and Ordering
Compatible S-bend routes are grouped by routing direction, xsection, width, and radius. The `Lstart` assignment logic was refined for both same-direction fan-out/fan-in routes and symmetric convergence/divergence routes, so outer routes can receive larger offsets when needed.
#### 4. Width-Aware Spacing Step
The base step is now calculated as:
```text
route width + 10um
```
For example, a 0.5um-wide route uses a 10.5um `Lstart` step, while a 40um-wide route uses a 50um `Lstart` step.
#### 5. Test Coverage Update
Contract tests were added and updated to cover automatic S-bend `Lstart` assignment, bundle-level grouping behavior, route ordering, width-aware spacing, and preservation of earlier rotation and perpendicular-routing fixes.
### Modified Files
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-05
### Scope
Backend S-bend grouping contract and `Lstart` ordering refinement in `mxpic_router`.
### Summary
Recent work focused on improving automatic S-bend spacing behavior for multi-port routing cases, especially MMI-to-port and resistor-array routing patterns.
### Completed Work
#### 1. Explicit Route Group Metadata Support
Backend support was added for explicit link grouping metadata from the layout YAML.
`LinkSpec` now records the parent bundle name and accepts optional route grouping fields such as `route_group`, `routeGroup`, `group`, `bundle_group`, and `bundleGroup`.
This prepares the backend for future frontend-provided bundle/group selection while preserving the existing geometry-based fallback behavior.
#### 2. S-Bend Stage Grouping Refinement
The S-bend grouping key was refined to avoid mixing routes from different routing stages within the same YAML bundle.
Automatic S-bend routes are now grouped by routing axis, direction, route stage/span, xsection, width, and radius unless an explicit route group is provided.
This prevents separate stages such as component-to-component routes and component-to-port routes from interfering with each other's `Lstart` ordering.
#### 3. Lstart Ordering Correction
The `Lstart` ordering logic was reviewed for resistor-array and MMI-to-port routing cases.
The sorting basis was corrected to use the source-side route coordinate for same-direction fan-out/fan-in routes, and S-bend axis detection was updated to prefer port direction over raw geometric span.
This fixes cases where routes with large vertical offset were incorrectly classified and assigned reversed `Lstart` values.
#### 4. Test Coverage Update
Contract tests were added and updated to cover explicit route group metadata parsing, route-stage separation, MMI-to-port two-output fan-out ordering, width-aware `Lstart` spacing, and preservation of earlier S-bend spacing behavior.
### Modified Files
mxpic_router/mxpic_router/eda_loader.py
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-09
### Scope
Frontend/backend routing metadata integration and electrical routing behavior review.
### Summary
Recent work focused on aligning the backend route grouping logic with the updated EDA frontend bundle-group export format, and reviewing why resistor routes were not using PCB-style routing.
### Completed Work
#### 1. Frontend Bundle Group Integration
The updated frontend exports user-selected route bundle groups as YAML bundle keys instead of duplicating `bundle_group` inside each link.
The backend route grouping logic was updated to treat custom YAML bundle keys as explicit route groups for S-bend `Lstart` spacing.
Default groups such as `output_bus`, `free_wires`, and `free_wires_*` are still treated as fallback groups to avoid unintentionally merging unrelated free-wire routes.
#### 2. Contract Test Update
A contract test was added to verify that custom frontend bundle keys are recognized as backend route groups, while default bundle names remain excluded from explicit grouping.
#### 3. PCB Routing Condition Review
The PCB routing condition was reviewed for resistor-based test layouts.
The backend currently enables `PCB=True` based on route `xsection` only. Routes using `metal_1`, `metal1`, `metal_2`, or `metal2` enable PCB routing; routes exported as `strip` remain optical-style routes.
The current resistor test YAML was found to export `xsection: strip` and `family: optical`, so the issue was traced to frontend route metadata export rather than the backend PCB flag logic.
### Modified Files
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
## 2026-06-10
### Scope
Backend routing condition alignment and automatic route direction normalization in `mxpic_router`.
### Summary
Recent work focused on aligning backend routing behavior with updated frontend metadata and reducing direction-related crossing issues in bundled automatic routes.
### Completed Work
#### 1. PCB Routing Condition Update
The PCB routing condition was updated to use the YAML link `family` field instead of route `xsection`.
Routes with `family: electrical` now enable `PCB=True`, regardless of the specific metal layer or xsection name. Optical routes remain unchanged.
#### 2. Automatic Route Direction Normalization
Automatic bundled routes are now normalized to route from the lower-x endpoint to the higher-x endpoint.
This keeps route direction consistent inside the same bundle and reduces crossing risks caused by mixed left-to-right and right-to-left link definitions.
#### 3. Test Coverage Update
Contract tests were added to verify electrical-family PCB routing and left-to-right automatic route ordering.
### Modified Files
mxpic_router/mxpic_router/builder.py
mxpic_router/tests/test_eda_router_contract.py
+550 -14
View File
@@ -9,6 +9,12 @@ from .eda_loader import CellSpec, InstanceSpec, LinkSpec, load_cell_spec
from .technology import apply_technology_manifest, load_technology_manifest
ROUTE_MIN_SPACING = 10.0
ROUTE_ENDPOINT_SPACING_IGNORE = ROUTE_MIN_SPACING * 2.0
ROUTE_SPACING_GEOMETRY_ADJUSTMENT_ENABLED = False
ROUTE_SBEND_GROUP_SPAN_GRID = 100.0
def build_project_gds(
project_dir: str,
output_path: str,
@@ -71,16 +77,17 @@ def _build_cell(spec: CellSpec, built_cells: dict, pdk_root: str, prefer_full_gd
pin_map = {}
with nd.Cell(name=spec.name) as top:
for instance_name, instance in spec.instances.items():
layout_rotation = _layout_rotation(instance.rotation)
if _is_basic_component(instance.component):
basic_cell = _build_basic_component(instance, nd)
placed = basic_cell.put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
placed = basic_cell.put(instance.x, instance.y, layout_rotation, flip=instance.flip, flop=instance.flop)
for pin_name in getattr(placed, "pin", {}):
if pin_name != "org":
pin_map[(instance_name, pin_name)] = placed.pin[pin_name]
continue
if instance.component in built_cells:
placed = built_cells[instance.component].put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
placed = built_cells[instance.component].put(instance.x, instance.y, layout_rotation, flip=instance.flip, flop=instance.flop)
for pin_name in getattr(placed, "pin", {}):
if pin_name != "org":
pin_map[(instance_name, pin_name)] = placed.pin[pin_name]
@@ -91,7 +98,7 @@ def _build_cell(spec: CellSpec, built_cells: dict, pdk_root: str, prefer_full_gd
warnings.append(f"Missing GDS for {instance_name}: {instance.component}")
continue
loaded = nd.load_gds(asset["gds_path"])
loaded.put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
loaded.put(instance.x, instance.y, layout_rotation, flip=instance.flip, flop=instance.flop)
_register_metadata_pins(pin_map, instance_name, instance, asset.get("metadata") or {}, nd, pdk_root)
for pin_name, pin in spec.pins.items():
@@ -104,8 +111,7 @@ def _build_cell(spec: CellSpec, built_cells: dict, pdk_root: str, prefer_full_gd
_register_element_pins(pin_map, element_name, element, nd)
for bundle in spec.bundles.values():
for link in bundle.links:
_route_link(link, pin_map, Route, warnings)
_route_bundle_links(bundle.links, pin_map, Route, warnings)
return top
@@ -227,6 +233,7 @@ def _safe_cell_name(value: str) -> str:
def _register_metadata_pins(pin_map, instance_name, instance, metadata: dict, nd, pdk_root: str) -> None:
layout_rotation = _layout_rotation(instance.rotation)
for pin_name, pin in _metadata_pins(metadata, pdk_root).items():
x, y, angle = _transform_port(
_safe_float(pin.get("x"), 0.0),
@@ -234,7 +241,7 @@ def _register_metadata_pins(pin_map, instance_name, instance, metadata: dict, nd
_safe_float(pin.get("a", pin.get("angle")), 0.0),
instance.x,
instance.y,
instance.rotation,
layout_rotation,
instance.flip,
instance.flop,
)
@@ -261,13 +268,478 @@ def _allows_pdk_ports_as_pins(pdk_root: str) -> bool:
return "/opt_pdk_public/" in f"{normalized}/" or "/opt_pdk_atlas/" in f"{normalized}/"
def _metal_route_pcb_enabled(xsection: str) -> bool:
normalized = str(xsection or "").strip().lower().replace("-", "_")
return normalized in {"metal_1", "metal1", "metal_2", "metal2"}
def _route_pcb_enabled(link: LinkSpec) -> bool:
return str(getattr(link, "family", "") or "").strip().lower() == "electrical"
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
route = Route(radius=link.radius or 10, width=link.width, xs=link.xsection, PCB=_metal_route_pcb_enabled(link.xsection))
def _route_ordered_pins_for_automatic_link(link: LinkSpec, pin1, pin2):
if len(link.points or []) >= 2:
return pin1, pin2
point1 = _pin_point(pin1)
point2 = _pin_point(pin2)
if point1 is None or point2 is None:
return pin1, pin2
if point1["x"] > point2["x"]:
return pin2, pin1
return pin1, pin2
def _route_bundle_links(links: list, pin_map: dict, Route, warnings: list) -> None:
plans = []
by_order = {}
for order, link in enumerate(links):
p1 = pin_map.get((link.src_inst, link.src_pin))
p2 = pin_map.get((link.dst_inst, link.dst_pin))
if p1 is None or p2 is None:
by_order[order] = {"link": link, "adjusted_points": None}
continue
p1, p2 = _route_ordered_pins_for_automatic_link(link, p1, p2)
points = _route_spacing_reference_points(link, p1, p2)
if len(points) < 2:
by_order[order] = {"link": link, "adjusted_points": None, "adjusted_offset": None}
continue
plan = {
"order": order,
"link": link,
"pin1": p1,
"pin2": p2,
"points": points,
"automatic": len(link.points or []) < 2,
"adjusted_points": None,
"adjusted_offset": None,
"check_points": None,
"route_options": {},
}
plans.append(plan)
by_order[order] = plan
_assign_sbend_lstart_spacing(plans)
accepted = []
for plan in plans:
check_points = _route_spacing_check_points(plan["points"])
if _route_points_have_spacing(check_points, accepted):
plan["check_points"] = check_points
accepted.append(plan)
continue
adjustment = _first_spacing_adjusted_route(plan, accepted)
if adjustment is not None:
warnings.append(
f"Detected route spacing below {ROUTE_MIN_SPACING:g}um for "
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
)
if ROUTE_SPACING_GEOMETRY_ADJUSTMENT_ENABLED:
plan["adjusted_points"] = adjustment["points"]
plan["check_points"] = _route_spacing_check_points(adjustment["points"])
else:
plan["check_points"] = check_points
else:
plan["check_points"] = check_points
accepted.append(plan)
for order in range(len(links)):
plan = by_order.get(order)
if not plan:
continue
if plan.get("adjusted_points"):
route = Route(
radius=plan["link"].radius or 10,
width=plan["link"].width,
xs=plan["link"].xsection,
PCB=_route_pcb_enabled(plan["link"]),
)
if plan.get("automatic"):
if _route_guided_straight_link(plan["link"], route, warnings, plan["adjusted_points"]):
warnings.append(
f"Applied {ROUTE_MIN_SPACING:g}um spacing lane to "
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
)
continue
if _route_guided_link(plan["link"], route, warnings, plan["adjusted_points"]):
continue
if plan.get("route_options", {}).get("Lstart"):
warnings.append(
f"Applied sbend Lstart {plan['route_options']['Lstart']: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_sbend_lstart_spacing(plans: list) -> None:
groups = {}
for plan in plans:
if not plan.get("automatic"):
continue
if _route_method_name_for_pins(plan["pin1"], plan["pin2"]) != "sbend_p2p":
continue
key = _sbend_spacing_group_key(plan)
if key is None:
continue
groups.setdefault(key, []).append(plan)
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)
for plan, rank in _sbend_lstart_ranks(group, coord_key):
plan["route_options"]["Lstart"] = rank * step
def _sbend_lstart_step(group: list) -> float:
width = max((_safe_float(plan["link"].width, 0.5) or 0.5) for plan in group)
return ROUTE_MIN_SPACING + width
def _sbend_lstart_ranks(group: list, coord_key: str) -> list:
deltas = [_route_coord_delta(plan, coord_key) for plan in group]
nonzero_deltas = [delta for delta in deltas if abs(delta) > 1e-9]
if nonzero_deltas and all(delta > 0 for delta in nonzero_deltas):
ordered = sorted(group, key=lambda plan: _route_start_coord(plan, coord_key), reverse=True)
return [(plan, index + 1) for index, plan in enumerate(ordered)]
if nonzero_deltas and all(delta < 0 for delta in nonzero_deltas):
ordered = sorted(group, key=lambda plan: _route_start_coord(plan, coord_key), reverse=True)
return [(plan, len(ordered) - index) for index, plan in enumerate(ordered)]
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 _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 _sbend_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 = (
"span",
_sbend_span_bucket(plan["points"][0][main_key]),
_sbend_span_bucket(plan["points"][-1][main_key]),
)
return (
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 _sbend_forward_axis(plan: dict) -> str:
angle = _pin_angle(plan["pin1"])
if angle is None:
angle = _pin_angle(plan["pin2"])
if angle is None:
return _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
normalized = angle % 180
if abs(normalized) <= 45 or abs(normalized - 180) <= 45:
return "horizontal"
if abs(normalized - 90) <= 45:
return "vertical"
return _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
def _link_explicit_route_group(link: LinkSpec) -> str:
route_group = str(getattr(link, "route_group", "") or "").strip()
if route_group:
return route_group
bundle = str(getattr(link, "bundle", "") or "").strip()
if _is_default_route_bundle(bundle):
return ""
return bundle
def _is_default_route_bundle(bundle: str) -> bool:
name = str(bundle or "").strip().lower()
if not name:
return True
if name == "output_bus":
return True
return name == "free_wires" or name.startswith("free_wires_")
def _route_direction_sign(delta: float) -> int:
if delta > 1e-9:
return 1
if delta < -1e-9:
return -1
return 0
def _sbend_span_bucket(value: float) -> int:
return math.floor(float(value) / ROUTE_SBEND_GROUP_SPAN_GRID)
def _route_group_needs_spacing(group: list) -> bool:
check_points = [_route_spacing_check_points(plan["points"]) for plan in group]
threshold = _sbend_lstart_step(group)
for index in range(len(check_points)):
for other_index in range(index + 1, len(check_points)):
if _polyline_spacing(check_points[index], check_points[other_index]) < threshold:
return True
return False
def _route_mid_coord(plan: dict, coord_key: str) -> float:
start = plan["points"][0]
end = plan["points"][-1]
return (float(start[coord_key]) + float(end[coord_key])) / 2.0
def _route_start_coord(plan: dict, coord_key: str) -> float:
return float(plan["points"][0][coord_key])
def _route_spacing_reference_points(link: LinkSpec, pin1, pin2) -> list:
if len(link.points or []) >= 2:
return _route_points_with_pin_endpoints(link.points, pin1, pin2)
p1 = _pin_point(pin1)
p2 = _pin_point(pin2)
if p1 is None or p2 is None:
return []
return _automatic_route_reference_points(pin1, pin2, p1, p2)
def _automatic_route_reference_points(pin1, pin2, point1: dict, point2: dict) -> list:
angle1 = _pin_angle(pin1)
angle2 = _pin_angle(pin2)
if angle1 is None or angle2 is None:
return [point1, point2]
delta = (angle2 - angle1) % 360
if abs(delta - 90) <= 1e-6 or abs(delta - 270) <= 1e-6:
elbow = _orthogonal_elbow(point1, angle1, point2, angle2)
if elbow is not None and _distance(point1, elbow) > 1e-9 and _distance(elbow, point2) > 1e-9:
return [point1, elbow, point2]
return [point1, point2]
def _orthogonal_elbow(point1: dict, angle1: float, point2: dict, angle2: float):
dx1, dy1 = _direction_vector(angle1)
dx2, dy2 = _direction_vector(angle2)
determinant = dx2 * dy1 - dx1 * dy2
if abs(determinant) <= 1e-9:
return None
delta_x = point2["x"] - point1["x"]
delta_y = point2["y"] - point1["y"]
t1 = (dx2 * delta_y - dy2 * delta_x) / determinant
t2 = (dx1 * delta_y - dy1 * delta_x) / determinant
if t1 < -1e-9 or t2 < -1e-9:
return None
return {"x": point1["x"] + dx1 * t1, "y": point1["y"] + dy1 * t1}
def _route_spacing_check_points(points_or_plan) -> list:
source_points = points_or_plan.get("points", []) if isinstance(points_or_plan, dict) else points_or_plan
points = [
{"x": float(point["x"]), "y": float(point["y"])}
for point in source_points
if isinstance(point, dict) and "x" in point and "y" in point
]
if len(points) < 2:
return points
return _trim_polyline_ends(points, ROUTE_ENDPOINT_SPACING_IGNORE)
def _trim_polyline_ends(points: list, distance: float) -> list:
trimmed = _trim_polyline_start(points, distance)
trimmed = list(reversed(_trim_polyline_start(list(reversed(trimmed)), distance)))
return trimmed if len(trimmed) >= 2 else points
def _trim_polyline_start(points: list, distance: float) -> list:
if len(points) < 2 or distance <= 0:
return points
remaining = float(distance)
for index in range(len(points) - 1):
segment_length = _distance(points[index], points[index + 1])
if segment_length <= 1e-9:
continue
if remaining < segment_length:
start = _point_toward(points[index], points[index + 1], remaining)
return [start] + points[index + 1:]
remaining -= segment_length
return points[-2:]
def _route_points_have_spacing(points: list, accepted_plans: list) -> bool:
return all(_polyline_spacing(points, plan.get("check_points") or []) >= ROUTE_MIN_SPACING for plan in accepted_plans)
def _first_spacing_adjusted_route(plan: dict, accepted_plans: list):
for offset in _spacing_offsets(plan, accepted_plans):
points = _spacing_adjusted_route_points(plan["link"], plan["pin1"], plan["pin2"], offset)
if _route_points_have_spacing(_route_spacing_check_points(points), accepted_plans):
return {"offset": offset, "points": points}
return None
def _spacing_offsets(plan: dict = None, accepted_plans: list = None, limit: int = 16) -> list:
preferred_sign = _preferred_spacing_sign(plan, accepted_plans)
offsets = []
for step in range(1, limit + 1):
distance = step * (ROUTE_MIN_SPACING / 2.0)
offsets.extend([preferred_sign * distance, -preferred_sign * distance])
return offsets
def _preferred_spacing_sign(plan: dict = None, accepted_plans: list = None) -> float:
if not plan or not accepted_plans:
return 1.0
axis = _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
coord_key = "y" if axis == "horizontal" else "x"
current_coord = _average_polyline_coord(_route_spacing_check_points(plan["points"]), coord_key)
nearest_coord = None
nearest_spacing = float("inf")
for accepted in accepted_plans:
spacing = _polyline_spacing(_route_spacing_check_points(plan["points"]), accepted.get("check_points") or [])
if spacing >= nearest_spacing:
continue
nearest_spacing = spacing
nearest_coord = _average_polyline_coord(accepted.get("check_points") or [], coord_key)
if nearest_coord is None:
return 1.0
if abs(current_coord - nearest_coord) <= 1e-9:
return 1.0
return 1.0 if current_coord > nearest_coord else -1.0
def _average_polyline_coord(points: list, coord_key: str) -> float:
values = [float(point[coord_key]) for point in points or [] if isinstance(point, dict) and coord_key in point]
if not values:
return 0.0
return sum(values) / len(values)
def _spacing_adjusted_route_points(link: LinkSpec, pin1, pin2, offset: float) -> list:
base_points = _route_spacing_reference_points(link, pin1, pin2)
if len(base_points) < 2:
return base_points
p1 = base_points[0]
p2 = base_points[-1]
axis = _dominant_spacing_axis(p1, p2)
escape = max((_safe_float(link.radius, 10.0) or 10.0), ROUTE_MIN_SPACING)
angle1 = _pin_angle(pin1) or _segment_angle(p1, p2) or 0.0
angle2 = _pin_angle(pin2) or ((angle1 + 180) % 360)
source_dx, source_dy = _direction_vector(angle1)
target_dx, target_dy = _direction_vector(angle2)
if axis == "horizontal":
source_escape = {"x": p1["x"] + source_dx * escape, "y": p1["y"]}
source_lane = {"x": source_escape["x"], "y": source_escape["y"] + offset}
target_escape = {"x": p2["x"] - target_dx * escape, "y": p2["y"]}
target_lane = {"x": target_escape["x"], "y": target_escape["y"] + offset}
else:
source_escape = {"x": p1["x"], "y": p1["y"] + source_dy * escape}
source_lane = {"x": source_escape["x"] + offset, "y": source_escape["y"]}
target_escape = {"x": p2["x"], "y": p2["y"] - target_dy * escape}
target_lane = {"x": target_escape["x"] + offset, "y": target_escape["y"]}
return [p1, source_escape, source_lane, target_lane, target_escape, p2]
def _dominant_spacing_axis(point1: dict, point2: dict) -> str:
return "horizontal" if abs(point2["x"] - point1["x"]) >= abs(point2["y"] - point1["y"]) else "vertical"
def _direction_vector(angle: float) -> tuple:
radians = math.radians(angle)
dx = math.cos(radians)
dy = math.sin(radians)
if abs(dx) < 1e-9:
dx = 0.0
if abs(dy) < 1e-9:
dy = 0.0
return dx, dy
def _polyline_spacing(points1: list, points2: list) -> float:
segments1 = _polyline_segments(points1)
segments2 = _polyline_segments(points2)
if not segments1 or not segments2:
return float("inf")
return min(_segment_spacing(seg1, seg2) for seg1 in segments1 for seg2 in segments2)
def _polyline_segments(points: list) -> list:
clean_points = [
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
for point in points or []
if isinstance(point, dict)
]
clean_points = [point for point in clean_points if point["x"] is not None and point["y"] is not None]
return list(zip(clean_points, clean_points[1:]))
def _segment_spacing(segment1: tuple, segment2: tuple) -> float:
a, b = segment1
c, d = segment2
if _segments_intersect(a, b, c, d):
return 0.0
return min(
_point_segment_distance(a, c, d),
_point_segment_distance(b, c, d),
_point_segment_distance(c, a, b),
_point_segment_distance(d, a, b),
)
def _segments_intersect(a: dict, b: dict, c: dict, d: dict) -> bool:
def orientation(p, q, r):
value = (q["y"] - p["y"]) * (r["x"] - q["x"]) - (q["x"] - p["x"]) * (r["y"] - q["y"])
if abs(value) <= 1e-9:
return 0
return 1 if value > 0 else 2
def on_segment(p, q, r):
return (
min(p["x"], r["x"]) - 1e-9 <= q["x"] <= max(p["x"], r["x"]) + 1e-9
and min(p["y"], r["y"]) - 1e-9 <= q["y"] <= max(p["y"], r["y"]) + 1e-9
)
o1 = orientation(a, b, c)
o2 = orientation(a, b, d)
o3 = orientation(c, d, a)
o4 = orientation(c, d, b)
if o1 != o2 and o3 != o4:
return True
return (
(o1 == 0 and on_segment(a, c, b))
or (o2 == 0 and on_segment(a, d, b))
or (o3 == 0 and on_segment(c, a, d))
or (o4 == 0 and on_segment(c, b, d))
)
def _point_segment_distance(point: dict, start: dict, end: dict) -> float:
dx = end["x"] - start["x"]
dy = end["y"] - start["y"]
length_squared = dx * dx + dy * dy
if length_squared <= 1e-18:
return _distance(point, start)
t = ((point["x"] - start["x"]) * dx + (point["y"] - start["y"]) * dy) / length_squared
t = max(0.0, min(1.0, t))
projection = {"x": start["x"] + t * dx, "y": start["y"] + t * dy}
return _distance(point, projection)
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list, route_options: dict = None) -> None:
route = Route(radius=link.radius or 10, width=link.width, xs=link.xsection, PCB=_route_pcb_enabled(link))
p1 = pin_map.get((link.src_inst, link.src_pin))
p2 = pin_map.get((link.dst_inst, link.dst_pin))
has_pin_endpoints = bool(link.src_inst or link.src_pin or link.dst_inst or link.dst_pin)
@@ -284,25 +756,72 @@ def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
if p1 is None or p2 is None:
warnings.append(f"Missing route pin for {link.src_inst}:{link.src_pin} -> {link.dst_inst}:{link.dst_pin}")
return
p1, p2 = _route_ordered_pins_for_automatic_link(link, p1, p2)
method_name = _route_method_name_for_pins(p1, p2)
route_method = getattr(route, method_name, None)
if route_method is None:
warnings.append(f"Route method {method_name} unavailable; falling back to sbend_p2p")
route_method = route.sbend_p2p
kwargs = {
"pin1": p1,
"pin2": p2,
"width": link.width,
"radius": link.radius or 10,
"xs": link.xsection,
"arrow": False,
}
if route_options:
kwargs.update(route_options)
try:
route_method(**kwargs).put()
except TypeError:
if not route_options:
raise
warnings.append(
f"Route method {method_name} rejected spacing options; retrying without options for "
f"{link.src_inst}:{link.src_pin} -> {link.dst_inst}:{link.dst_pin}"
)
for key in route_options:
kwargs.pop(key, None)
route_method(**kwargs).put()
def _route_guided_straight_link(link: LinkSpec, route, warnings: list, points_override=None) -> bool:
route_method = getattr(route, "strt_p2p", None)
if route_method is None:
warnings.append("Spacing route requires Route.strt_p2p; falling back to automatic p2p route")
return False
points = [
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
for point in points_override or []
if isinstance(point, dict)
]
points = [point for point in points if point["x"] is not None and point["y"] is not None]
if len(points) < 2:
return False
for index in range(len(points) - 1):
start = points[index]
end = points[index + 1]
angle = _segment_angle(start, end)
if angle is None or _distance(start, end) <= 1e-9:
continue
route_method(
pin1=p1,
pin2=p2,
pin1=(start["x"], start["y"], angle),
pin2=(end["x"], end["y"], angle),
width=link.width,
radius=link.radius or 10,
xs=link.xsection,
arrow=False,
).put()
return True
def _route_guided_link(link: LinkSpec, route, warnings: list, points_override=None) -> bool:
route_method = getattr(route, "strt_p2p", None)
if route_method is None:
warnings.append("Manual route points require Route.strt_p2p; falling back to automatic p2p route")
return False
bend_method = getattr(route, "strt_bend_strt_p2p", None)
if bend_method is None:
bend_method = getattr(route, "bend_p2p", None)
source_points = points_override if points_override is not None else (link.points or [])
points = [
@@ -474,6 +993,8 @@ def _route_method_name_for_pins(pin1, pin2) -> str:
delta = (angle2 - angle1) % 360
if delta <= 1e-6 or abs(delta - 360) <= 1e-6:
return "ubend_p2p"
if abs(delta - 90) <= 1e-6 or abs(delta - 270) <= 1e-6:
return "strt_bend_strt_p2p"
if 120 < delta < 240:
return "sbend_p2p"
return "bend_p2p"
@@ -562,6 +1083,10 @@ def _transform_port(px: float, py: float, pa: float, ix: float, iy: float, rotat
return ix + px * c - py * s, iy + px * s + py * c, (pa + rotation) % 360
def _layout_rotation(eda_rotation: float) -> float:
return -(_safe_float(eda_rotation, 0.0) or 0.0)
def _safe_float(value, default=0.0):
if value is None:
return default
@@ -607,6 +1132,7 @@ class _NazcaInterconnectRoute:
width=self._route_width(width),
radius=self._route_radius(radius),
xs=self._route_xs(xs),
Lstart=kwargs.get("Lstart", 0),
arrow=arrow,
)
@@ -620,6 +1146,16 @@ class _NazcaInterconnectRoute:
arrow=arrow,
)
def strt_bend_strt_p2p(self, pin1=None, pin2=None, width=None, radius=None, xs=None, arrow=True, **kwargs):
return self._interconnect.strt_bend_strt_p2p(
pin1=pin1,
pin2=pin2,
width=self._route_width(width),
radius=self._route_radius(radius),
xs=self._route_xs(xs),
arrow=arrow,
)
def bend_p2p(self, pin1=None, pin2=None, width=None, radius=None, xs=None, arrow=True, **kwargs):
route_method = getattr(self._interconnect, "bend_strt_bend_p2p", None)
if route_method is None:
+11
View File
@@ -53,6 +53,8 @@ class LinkSpec:
width: Optional[float] = None
radius: Optional[float] = None
routing_type: str = "euler_bend"
bundle: str = ""
route_group: str = ""
points: List[Dict[str, float]] = field(default_factory=list)
@@ -150,6 +152,15 @@ def parse_cell_dict(data: dict) -> CellSpec:
width=_optional_float(link_data.get("width"), bundle.width),
radius=_optional_float(link_data.get("radius"), bundle.radius),
routing_type=str(link_data.get("routing_type") or bundle.routing_type),
bundle=bundle.name,
route_group=str(
link_data.get("route_group")
or link_data.get("routeGroup")
or link_data.get("group")
or link_data.get("bundle_group")
or link_data.get("bundleGroup")
or ""
),
points=_points(link_data.get("points")),
))
spec.bundles[bundle.name] = bundle
+564 -2
View File
@@ -56,8 +56,63 @@ class EdaRouterPinsContractTest(unittest.TestCase):
self.assertEqual(link.dst_pin, "in")
self.assertEqual(link.xsection, "metal_1")
self.assertEqual(link.width, 5)
self.assertEqual(link.bundle, "output_bus")
self.assertEqual(link.points[1], {"x": 50.0, "y": 0.0})
def test_loader_accepts_explicit_link_route_group_metadata(self):
from mxpic_router.eda_loader import parse_cell_dict
spec = parse_cell_dict({
"name": "cell_a",
"bundles": {
"output_bus": {
"links": [{
"from": "inst_a:out",
"to": "inst_b:in",
"route_group": "stage_1",
}]
}
}
})
link = spec.bundles["output_bus"].links[0]
self.assertEqual(link.bundle, "output_bus")
self.assertEqual(link.route_group, "stage_1")
def test_builder_uses_frontend_bundle_key_as_explicit_route_group(self):
from mxpic_router.builder import _link_explicit_route_group
from mxpic_router.eda_loader import LinkSpec
self.assertEqual(
_link_explicit_route_group(LinkSpec(bundle="optical_bus")),
"optical_bus",
)
self.assertEqual(
_link_explicit_route_group(LinkSpec(bundle="free_wires")),
"",
)
self.assertEqual(
_link_explicit_route_group(LinkSpec(bundle="free_wires_metal_1")),
"",
)
self.assertEqual(
_link_explicit_route_group(LinkSpec(bundle="output_bus")),
"",
)
self.assertEqual(
_link_explicit_route_group(LinkSpec(bundle="output_bus", route_group="stage_1")),
"stage_1",
)
def test_route_pcb_flag_uses_electrical_family(self):
from mxpic_router.builder import _route_pcb_enabled
from mxpic_router.eda_loader import LinkSpec
self.assertTrue(_route_pcb_enabled(LinkSpec(xsection="custom_metal_top", family="electrical")))
self.assertTrue(_route_pcb_enabled(LinkSpec(xsection="strip", family="Electrical")))
self.assertFalse(_route_pcb_enabled(LinkSpec(xsection="metal_1", family="optical")))
self.assertFalse(_route_pcb_enabled(LinkSpec(xsection="metal_2", family="")))
def test_port_element_creates_named_io_pins_and_inside_route_pins(self):
from mxpic_router.builder import _register_element_pins
from mxpic_router.eda_loader import parse_cell_dict
@@ -221,6 +276,503 @@ class EdaRouterPinsContractTest(unittest.TestCase):
)
self.assertEqual(_metadata_pins(metadata, r"D:\mxpic\some_project_layout"), {})
def test_eda_rotation_is_converted_to_layout_rotation_for_metadata_pins(self):
from mxpic_router.builder import _layout_rotation, _transform_port
self.assertEqual(_layout_rotation(90), -90.0)
x, y, angle = _transform_port(
39.25,
0.0,
360.0,
1719.3,
-2080.5,
_layout_rotation(90),
)
self.assertAlmostEqual(x, 1719.3)
self.assertAlmostEqual(y, -2119.75)
self.assertEqual(angle, 270.0)
def test_perpendicular_pin_rotations_use_straight_bend_straight_route(self):
from mxpic_router.builder import _route_method_name_for_pins
class FakePin:
def __init__(self, angle):
self.a = angle
self.assertEqual(
_route_method_name_for_pins(FakePin(0), FakePin(90)),
"strt_bend_strt_p2p",
)
self.assertEqual(
_route_method_name_for_pins(FakePin(0), FakePin(270)),
"strt_bend_strt_p2p",
)
def test_route_spacing_detection_handles_crossing_and_endpoint_exceptions(self):
from mxpic_router.builder import (
ROUTE_ENDPOINT_SPACING_IGNORE,
ROUTE_MIN_SPACING,
_first_spacing_adjusted_route,
_orthogonal_elbow,
_polyline_spacing,
_route_spacing_reference_points,
_route_spacing_check_points,
_spacing_offsets,
_spacing_adjusted_route_points,
)
from mxpic_router.eda_loader import LinkSpec
class FakePin:
def __init__(self, x, y, angle):
self.x = x
self.y = y
self.a = angle
self.assertGreaterEqual(
_polyline_spacing(
[{"x": 0, "y": 0}, {"x": 100, "y": 0}],
[{"x": 0, "y": 12}, {"x": 100, "y": 12}],
),
ROUTE_MIN_SPACING,
)
self.assertLess(
_polyline_spacing(
[{"x": 0, "y": 0}, {"x": 100, "y": 0}],
[{"x": 50, "y": -20}, {"x": 50, "y": 20}],
),
ROUTE_MIN_SPACING,
)
device_plan = {
"link": LinkSpec(src_inst="mmi1", src_pin="o1", dst_inst="mmi2", dst_pin="i1"),
"points": [{"x": 0, "y": 0}, {"x": 100, "y": 0}],
}
nearby_port_plan = {
"link": LinkSpec(src_inst="port_1", src_pin="io1", dst_inst="mmi2", dst_pin="i1"),
"points": [{"x": 0, "y": 5}, {"x": 100, "y": 5}],
}
device_check_points = _route_spacing_check_points(device_plan)
nearby_port_check_points = _route_spacing_check_points(nearby_port_plan)
self.assertEqual(device_check_points[0], {"x": ROUTE_ENDPOINT_SPACING_IGNORE, "y": 0.0})
self.assertEqual(nearby_port_check_points[0], {"x": ROUTE_ENDPOINT_SPACING_IGNORE, "y": 5.0})
self.assertLess(_polyline_spacing(device_check_points, nearby_port_check_points), ROUTE_MIN_SPACING)
adjusted = _spacing_adjusted_route_points(
LinkSpec(width=0.5, radius=10),
FakePin(0, 0, 0),
FakePin(100, 0, 180),
offset=ROUTE_MIN_SPACING,
)
self.assertEqual(adjusted[0], {"x": 0.0, "y": 0.0})
self.assertEqual(adjusted[-1], {"x": 100.0, "y": 0.0})
self.assertEqual(adjusted[2]["y"], ROUTE_MIN_SPACING)
self.assertEqual(adjusted[3]["y"], ROUTE_MIN_SPACING)
current_plan = {
"link": LinkSpec(src_inst="a", src_pin="o1", dst_inst="b", dst_pin="i1"),
"pin1": FakePin(0, 5, 0),
"pin2": FakePin(100, 5, 180),
"points": [{"x": 0, "y": 5}, {"x": 100, "y": 5}],
}
accepted_below = [{
"check_points": [{"x": 10, "y": 0}, {"x": 90, "y": 0}],
}]
accepted_above = [{
"check_points": [{"x": 10, "y": 10}, {"x": 90, "y": 10}],
}]
self.assertEqual(_spacing_offsets(current_plan, accepted_below)[0], ROUTE_MIN_SPACING / 2.0)
self.assertEqual(_spacing_offsets(current_plan, accepted_above)[0], -ROUTE_MIN_SPACING / 2.0)
adjusted_from_close_route = _first_spacing_adjusted_route(current_plan, accepted_below)
self.assertIsNotNone(adjusted_from_close_route)
self.assertEqual(adjusted_from_close_route["points"][2]["y"], 10.0)
elbow = _orthogonal_elbow({"x": 0, "y": 0}, 0, {"x": 20, "y": 30}, 270)
self.assertEqual(elbow, {"x": 20.0, "y": 0.0})
reference = _route_spacing_reference_points(
LinkSpec(),
FakePin(0, 0, 0),
FakePin(20, 30, 270),
)
self.assertEqual(reference, [{"x": 0.0, "y": 0.0}, {"x": 20.0, "y": 0.0}, {"x": 20.0, "y": 30.0}])
def test_automatic_p2p_spacing_detection_does_not_change_geometry_by_default(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 strt_p2p(self, **kwargs):
self.calls.append(("strt_p2p", kwargs))
return self
def strt_bend_strt_p2p(self, **kwargs):
self.calls.append(("strt_bend_strt_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="mmi1", src_pin="b1", dst_inst="port_1", dst_pin="io1"),
LinkSpec(src_inst="mmi2", src_pin="b1", dst_inst="port_1", dst_pin="io2"),
]
pin_map = {
("mmi1", "b1"): FakePin(0, 0, 0),
("port_1", "io1"): FakePin(100, 0, 180),
("mmi2", "b1"): FakePin(0, 5, 0),
("port_1", "io2"): FakePin(100, 5, 180),
}
warnings = []
_route_bundle_links(links, pin_map, FakeRoute, warnings)
self.assertEqual([call[0] for call in FakeRoute.calls], ["sbend_p2p", "put", "sbend_p2p", "put"])
self.assertTrue(any("Detected route spacing below 10um" in warning for warning in warnings))
self.assertEqual(FakeRoute.calls[0][1]["Lstart"], 10.5)
self.assertEqual(FakeRoute.calls[2][1]["Lstart"], 10.5)
call_names = [call[0] for call in FakeRoute.calls]
self.assertNotIn("strt_bend_strt_p2p", call_names)
self.assertNotIn("strt_p2p", call_names)
def test_sbend_lstart_spacing_uses_whole_bundle_inner_outer_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 sbend_p2p(self, **kwargs):
self.calls.append(("sbend_p2p", kwargs))
return self
def put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="mmi_top", src_pin="b1", dst_inst="port_top", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi_top", src_pin="b2", dst_inst="port_mid_top", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi_bot", src_pin="b1", dst_inst="port_mid_bot", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi_bot", src_pin="b2", dst_inst="port_bot", dst_pin="io1", width=0.5, radius=10),
]
pin_map = {
("mmi_top", "b1"): FakePin(0, 12, 0),
("port_top", "io1"): FakePin(100, 12, 180),
("mmi_top", "b2"): FakePin(0, 4, 0),
("port_mid_top", "io1"): FakePin(100, 4, 180),
("mmi_bot", "b1"): FakePin(0, -4, 0),
("port_mid_bot", "io1"): FakePin(100, -4, 180),
("mmi_bot", "b2"): FakePin(0, -12, 0),
("port_bot", "io1"): FakePin(100, -12, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts, [21.0, 10.5, 10.5, 21.0])
def test_sbend_lstart_spacing_uses_monotonic_order_for_same_direction_fanout(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 put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="mmi1", src_pin="b1", dst_inst="port1", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi1", src_pin="b2", dst_inst="port2", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi2", src_pin="b1", dst_inst="port3", dst_pin="io1", width=0.5, radius=10),
LinkSpec(src_inst="mmi2", src_pin="b2", dst_inst="port4", dst_pin="io1", width=0.5, radius=10),
]
pin_map = {
("mmi1", "b1"): FakePin(0, 40, 0),
("port1", "io1"): FakePin(100, 20, 180),
("mmi1", "b2"): FakePin(0, 32, 0),
("port2", "io1"): FakePin(100, 12, 180),
("mmi2", "b1"): FakePin(0, 24, 0),
("port3", "io1"): FakePin(100, 4, 180),
("mmi2", "b2"): FakePin(0, 16, 0),
("port4", "io1"): FakePin(100, -4, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts, [42.0, 31.5, 21.0, 10.5])
def test_sbend_lstart_step_includes_route_width(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 put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="r1", src_pin="b1", dst_inst="p1", dst_pin="io1", width=40, radius=10),
LinkSpec(src_inst="r2", src_pin="b1", dst_inst="p2", dst_pin="io1", width=40, radius=10),
]
pin_map = {
("r1", "b1"): FakePin(0, 0, 0),
("p1", "io1"): FakePin(100, 0, 180),
("r2", "b1"): FakePin(0, 100, 0),
("p2", "io1"): FakePin(100, 100, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts, [50.0, 50.0])
def test_sbend_lstart_spacing_handles_same_source_mmi_fanout(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 put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="MMI_1", src_pin="b1", dst_inst="port_1", dst_pin="port_1_io2", width=0.7, radius=10),
LinkSpec(src_inst="MMI_1", src_pin="b2", dst_inst="port_1", dst_pin="port_1_io1", width=0.7, radius=10),
]
pin_map = {
("MMI_1", "b1"): FakePin(1963.1, -1931.15, 0),
("port_1", "port_1_io2"): FakePin(2047.2, -2056.3, 180),
("MMI_1", "b2"): FakePin(1963.1, -1939.85, 0),
("port_1", "port_1_io1"): FakePin(2047.2, -2066.3, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts, [21.4, 10.7])
def test_sbend_lstart_spacing_keeps_separate_route_stages_ordered(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 put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="r1", src_pin="en1", dst_inst="r5", dst_pin="ep1", width=40, radius=10),
LinkSpec(src_inst="r2", src_pin="en1", dst_inst="r6", dst_pin="ep1", width=40, radius=10),
LinkSpec(src_inst="r3", src_pin="en1", dst_inst="r7", dst_pin="ep1", width=40, radius=10),
LinkSpec(src_inst="r4", src_pin="en1", dst_inst="r8", dst_pin="ep1", width=40, radius=10),
LinkSpec(src_inst="r6", src_pin="en1", dst_inst="port", dst_pin="io3", width=40, radius=10),
LinkSpec(src_inst="r5", src_pin="en1", dst_inst="port", dst_pin="io4", width=40, radius=10),
LinkSpec(src_inst="r7", src_pin="en1", dst_inst="port", dst_pin="io2", width=40, radius=10),
LinkSpec(src_inst="r8", src_pin="en1", dst_inst="port", dst_pin="io1", width=40, radius=10),
]
pin_map = {
("r1", "en1"): FakePin(0, 40, 0),
("r5", "ep1"): FakePin(100, 70, 180),
("r2", "en1"): FakePin(0, 30, 0),
("r6", "ep1"): FakePin(100, 60, 180),
("r3", "en1"): FakePin(0, 20, 0),
("r7", "ep1"): FakePin(100, 50, 180),
("r4", "en1"): FakePin(0, 10, 0),
("r8", "ep1"): FakePin(100, 40, 180),
("r6", "en1"): FakePin(100, 60, 0),
("port", "io3"): FakePin(200, 30, 180),
("r5", "en1"): FakePin(100, 70, 0),
("port", "io4"): FakePin(200, 40, 180),
("r7", "en1"): FakePin(100, 50, 0),
("port", "io2"): FakePin(200, 20, 180),
("r8", "en1"): FakePin(100, 40, 0),
("port", "io1"): FakePin(200, 10, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertEqual(lstarts[:4], [50.0, 100.0, 150.0, 200.0])
self.assertEqual(lstarts[4:], [150.0, 200.0, 100.0, 50.0])
def test_automatic_routes_are_ordered_from_lower_x_to_higher_x(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 put(self):
self.calls.append(("put", {}))
FakeRoute.calls = []
links = [
LinkSpec(src_inst="left_top", src_pin="out", dst_inst="right_top", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="right_mid", src_pin="out", dst_inst="left_mid", dst_pin="in", width=40, radius=10),
LinkSpec(src_inst="left_bot", src_pin="out", dst_inst="right_bot", dst_pin="in", width=40, radius=10),
]
pin_map = {
("left_top", "out"): FakePin(0, 40, 0),
("right_top", "in"): FakePin(100, 10, 180),
("right_mid", "out"): FakePin(100, 0, 180),
("left_mid", "in"): FakePin(0, 30, 0),
("left_bot", "out"): FakePin(0, 20, 0),
("right_bot", "in"): FakePin(100, -10, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
calls = [call[1] for call in FakeRoute.calls if call[0] == "sbend_p2p"]
self.assertTrue(all(call["pin1"].x <= call["pin2"].x for call in calls))
self.assertEqual([call["Lstart"] for call in calls], [150.0, 100.0, 50.0])
def test_sbend_lstart_spacing_keeps_nearby_span_bucket_boundary_routes_together(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 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(2090.95, -1968.2, 0),
("p1", "in"): FakePin(2651.75, -2090.5, 180),
("r2", "out"): FakePin(2087.15, -2073.9, 0),
("p2", "in"): FakePin(2653.25, -2201.4, 180),
("r3", "out"): FakePin(2086.55, -2190.0, 0),
("p3", "in"): FakePin(2647.45, -2312.3, 180),
("r4", "out"): FakePin(2085.85, -2285.2, 0),
("p4", "in"): FakePin(2649.45, -2411.1, 180),
}
_route_bundle_links(links, pin_map, FakeRoute, [])
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_route_backend_falls_back_to_nazca_interconnect_when_forge_is_absent(self):
import mxpic_router.builder as builder
@@ -249,6 +801,10 @@ class EdaRouterPinsContractTest(unittest.TestCase):
self.calls.append(("bend_strt_bend_p2p", kwargs))
return "bend"
def strt_bend_strt_p2p(self, **kwargs):
self.calls.append(("strt_bend_strt_p2p", kwargs))
return "straight_bend_straight"
class FakeInterconnects:
Interconnect = FakeInterconnect
@@ -268,11 +824,17 @@ class EdaRouterPinsContractTest(unittest.TestCase):
self.assertEqual(route.backend_name, "nazca Interconnect")
self.assertEqual(route.strt_p2p(pin1="a", pin2="b", arrow=False), "straight")
self.assertEqual(
route.strt_bend_strt_p2p(pin1="a", pin2="b", radius=12, arrow=False),
"straight_bend_straight",
)
self.assertEqual(route.bend_p2p(pin1="a", pin2="b", radius=10, arrow=False), "bend")
self.assertEqual(FakeInterconnect.calls[0][0], "strt_p2p")
self.assertEqual(FakeInterconnect.calls[0][1]["width"], 0.7)
self.assertEqual(FakeInterconnect.calls[1][0], "bend_strt_bend_p2p")
self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 10)
self.assertEqual(FakeInterconnect.calls[1][0], "strt_bend_strt_p2p")
self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 12)
self.assertEqual(FakeInterconnect.calls[2][0], "bend_strt_bend_p2p")
self.assertEqual(FakeInterconnect.calls[2][1]["radius"], 10)
finally:
builder._import_mxpic_forge_route = original_import
builder._NazcaInterconnectRoute._create_interconnect = original_create