Merge pull request 'Removing mxpic_forge from dependency' (#2) from main into pengkun_main #3

Merged
PotatoMaxwell merged 16 commits from pengkun_main into qinyue_main 2026-06-10 11:34:17 +00:00
Showing only changes of commit 759a32cfc5 - Show all commits
+448 -6
View File
@@ -9,6 +9,11 @@ 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
def build_project_gds(
project_dir: str,
output_path: str,
@@ -105,8 +110,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
@@ -268,7 +272,398 @@ def _metal_route_pcb_enabled(xsection: str) -> bool:
return normalized in {"metal_1", "metal1", "metal_2", "metal2"}
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
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
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=_metal_route_pcb_enabled(plan["link"].xsection),
)
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 = _dominant_spacing_axis(group[0]["points"][0], group[0]["points"][-1])
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_mid_coord(plan, coord_key))
return [(plan, len(ordered) - index) 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_mid_coord(plan, coord_key), reverse=True)
return [(plan, len(ordered) - index) for index, plan in enumerate(ordered)]
center = sum(_route_mid_coord(plan, coord_key) for plan in group) / len(group)
distances = sorted({round(abs(_route_mid_coord(plan, coord_key) - center), 6) for plan in group})
return [
(plan, distances.index(round(abs(_route_mid_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 = _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
return (
axis,
str(link.xsection or ""),
_safe_float(link.width, 0.0),
_safe_float(link.radius, 0.0),
)
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_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=_metal_route_pcb_enabled(link.xsection))
p1 = pin_map.get((link.src_inst, link.src_pin))
p2 = pin_map.get((link.dst_inst, link.dst_pin))
@@ -291,20 +686,66 @@ def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> 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 = [
@@ -615,6 +1056,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,
)