more bend work; bounds constrain edges
This commit is contained in:
parent
4714bed9a8
commit
58873692d6
15 changed files with 251 additions and 124 deletions
|
|
@ -3,8 +3,7 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
import rtree
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.prepared import prep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -129,19 +128,16 @@ class CollisionEngine:
|
|||
|
||||
# Precise check: is every point in the intersection close to either port?
|
||||
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
|
||||
|
||||
|
||||
is_near_start = False
|
||||
if start_port:
|
||||
if (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius):
|
||||
is_near_start = True
|
||||
|
||||
if start_port and (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius):
|
||||
is_near_start = True
|
||||
|
||||
is_near_end = False
|
||||
if end_port:
|
||||
if (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius):
|
||||
is_near_end = True
|
||||
|
||||
if end_port and (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and
|
||||
abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius):
|
||||
is_near_end = True
|
||||
if is_near_start or is_near_end:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Literal, Union
|
||||
from typing import NamedTuple, Literal, Any
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon, box
|
||||
|
|
@ -35,7 +35,7 @@ class Straight:
|
|||
|
||||
ex = start_port.x + dx
|
||||
ey = start_port.y + dy
|
||||
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex)
|
||||
ey = snap_search_grid(ey)
|
||||
|
|
@ -84,39 +84,83 @@ def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start
|
|||
|
||||
|
||||
def _apply_collision_model(
|
||||
arc_poly: Polygon,
|
||||
arc_poly: Polygon,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
|
||||
radius: float,
|
||||
width: float,
|
||||
cx: float = 0.0,
|
||||
cy: float = 0.0,
|
||||
clip_margin: float = 10.0
|
||||
) -> list[Polygon]:
|
||||
"""Applies the specified collision model to an arc geometry."""
|
||||
if isinstance(collision_type, Polygon):
|
||||
return [collision_type]
|
||||
|
||||
|
||||
if collision_type == "arc":
|
||||
return [arc_poly]
|
||||
|
||||
|
||||
# Get bounding box
|
||||
minx, miny, maxx, maxy = arc_poly.bounds
|
||||
bbox = box(minx, miny, maxx, maxy)
|
||||
|
||||
|
||||
if collision_type == "bbox":
|
||||
return [bbox]
|
||||
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
safe_zone = arc_poly.buffer(clip_margin)
|
||||
return [bbox.intersection(safe_zone)]
|
||||
|
||||
res_poly = bbox
|
||||
|
||||
# Determine quadrant signs from arc centroid relative to center
|
||||
# This ensures we always cut 'into' the box correctly
|
||||
ac = arc_poly.centroid
|
||||
sx = 1.0 if ac.x >= cx else -1.0
|
||||
sy = 1.0 if ac.y >= cy else -1.0
|
||||
|
||||
r_out_cut = radius + width / 2.0 + clip_margin
|
||||
r_in_cut = radius - width / 2.0 - clip_margin
|
||||
|
||||
corners = [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)]
|
||||
for px, py in corners:
|
||||
dx, dy = px - cx, py - cy
|
||||
dist = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
if dist > r_out_cut:
|
||||
# Outer corner: remove part furthest from center
|
||||
# We want minimum distance to line to be r_out_cut
|
||||
d_cut = r_out_cut * np.sqrt(2)
|
||||
elif r_in_cut > 0 and dist < r_in_cut:
|
||||
# Inner corner: remove part closest to center
|
||||
# We want maximum distance to line to be r_in_cut
|
||||
d_cut = r_in_cut
|
||||
else:
|
||||
continue
|
||||
|
||||
# The cut line is sx*(x-cx) + sy*(y-cy) = d_cut
|
||||
# sx*x + sy*y = sx*cx + sy*cy + d_cut
|
||||
val = cx * sx + cy * sy + d_cut
|
||||
|
||||
try:
|
||||
p1 = (px, py)
|
||||
p2 = (px, (val - sx * px) / sy)
|
||||
p3 = ((val - sy * py) / sx, py)
|
||||
|
||||
triangle = Polygon([p1, p2, p3])
|
||||
if triangle.is_valid and triangle.area > 1e-9:
|
||||
res_poly = res_poly.difference(triangle)
|
||||
except ZeroDivisionError:
|
||||
continue
|
||||
|
||||
return [res_poly]
|
||||
|
||||
return [arc_poly]
|
||||
|
||||
|
||||
class Bend90:
|
||||
@staticmethod
|
||||
def generate(
|
||||
start_port: Port,
|
||||
radius: float,
|
||||
width: float,
|
||||
direction: str = "CW",
|
||||
start_port: Port,
|
||||
radius: float,
|
||||
width: float,
|
||||
direction: str = "CW",
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
|
|
@ -133,9 +177,11 @@ class Bend90:
|
|||
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
||||
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
||||
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
|
||||
|
||||
|
||||
arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta)
|
||||
collision_polys = _apply_collision_model(arc_polys[0], collision_type, radius, clip_margin)
|
||||
collision_polys = _apply_collision_model(
|
||||
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
|
||||
)
|
||||
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
|
||||
|
||||
|
|
@ -143,10 +189,10 @@ class Bend90:
|
|||
class SBend:
|
||||
@staticmethod
|
||||
def generate(
|
||||
start_port: Port,
|
||||
offset: float,
|
||||
radius: float,
|
||||
width: float,
|
||||
start_port: Port,
|
||||
offset: float,
|
||||
radius: float,
|
||||
width: float,
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
|
|
@ -162,7 +208,7 @@ class SBend:
|
|||
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
||||
ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start))
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
|
||||
direction = 1 if offset > 0 else -1
|
||||
c1_angle = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(c1_angle)
|
||||
|
|
@ -180,6 +226,14 @@ class SBend:
|
|||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
||||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
||||
combined_arc = unary_union([arc1, arc2])
|
||||
|
||||
collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)
|
||||
collision_polys = [unary_union(col1 + col2)]
|
||||
else:
|
||||
collision_polys = _apply_collision_model(
|
||||
combined_arc, collision_type, radius, width, 0, 0, clip_margin
|
||||
)
|
||||
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class AStarRouter:
|
|||
if state in closed_set:
|
||||
continue
|
||||
closed_set.add(state)
|
||||
|
||||
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ class AStarRouter:
|
|||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapStraight")
|
||||
|
||||
|
||||
# B. Try SBend exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = np.radians(current.port.orientation)
|
||||
|
|
@ -174,9 +174,9 @@ class AStarRouter:
|
|||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
|
|
@ -189,8 +189,8 @@ class AStarRouter:
|
|||
lengths = self.config.straight_lengths
|
||||
if dist < 5.0:
|
||||
fine_steps = [0.1, 0.5]
|
||||
lengths = sorted(list(set(lengths + fine_steps)))
|
||||
|
||||
lengths = sorted(set(lengths + fine_steps))
|
||||
|
||||
for length in lengths:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"S{length}")
|
||||
|
|
@ -199,9 +199,9 @@ class AStarRouter:
|
|||
for radius in self.config.bend_radii:
|
||||
for direction in ["CW", "CCW"]:
|
||||
res = Bend90.generate(
|
||||
current.port,
|
||||
radius,
|
||||
net_width,
|
||||
current.port,
|
||||
radius,
|
||||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
|
|
@ -213,9 +213,9 @@ class AStarRouter:
|
|||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
offset,
|
||||
radius,
|
||||
current.port,
|
||||
offset,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
|
|
@ -276,7 +276,7 @@ class AStarRouter:
|
|||
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
|
||||
dilated_move.bounds[3] < prev_poly.bounds[1] - dilation:
|
||||
continue
|
||||
|
||||
|
||||
dilated_prev = prev_poly.buffer(dilation)
|
||||
if dilated_move.intersects(dilated_prev):
|
||||
overlap = dilated_move.intersection(dilated_prev)
|
||||
|
|
@ -286,10 +286,10 @@ class AStarRouter:
|
|||
seg_idx += 1
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
start_port=parent.port,
|
||||
length=result.length
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, TYPE_CHECKING, Any
|
||||
from typing import Literal, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class CostEvaluator:
|
|||
greedy_h_weight=greedy_h_weight,
|
||||
congestion_penalty=congestion_penalty,
|
||||
)
|
||||
|
||||
|
||||
# Use config values
|
||||
self.unit_length_cost = self.config.unit_length_cost
|
||||
self.greedy_h_weight = self.config.greedy_h_weight
|
||||
|
|
@ -73,12 +73,19 @@ class CostEvaluator:
|
|||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||
_ = net_width # Unused
|
||||
total_cost = length * self.unit_length_cost
|
||||
|
||||
# 1. Hard Collision check (Static obstacles)
|
||||
|
||||
# 1. Hard Collision & Boundary Check
|
||||
# We buffer by the full clearance to ensure distance >= clearance
|
||||
hard_dilation = self.collision_engine.clearance
|
||||
for poly in geometry:
|
||||
dilated_poly = poly.buffer(hard_dilation)
|
||||
|
||||
# Boundary Check: Physical edges must stay within design bounds
|
||||
minx, miny, maxx, maxy = dilated_poly.bounds
|
||||
if not (self.danger_map.is_within_bounds(minx, miny) and
|
||||
self.danger_map.is_within_bounds(maxx, maxy)):
|
||||
return 1e15 # Out of bounds is impossible
|
||||
|
||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
||||
return 1e15 # Impossible cost for hard collisions
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import numpy as np
|
||||
import pytest
|
||||
from shapely.geometry import Point
|
||||
|
||||
from inire.geometry.components import Bend90, SBend, Straight
|
||||
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||
|
|
@ -66,7 +64,7 @@ def test_bend_collision_models() -> None:
|
|||
|
||||
# 1. BBox model
|
||||
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
||||
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
||||
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
||||
assert minx <= 0.0 + 1e-6
|
||||
|
|
@ -89,7 +87,7 @@ def test_sbend_collision_models() -> None:
|
|||
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
||||
# Geometry should be a single bounding box polygon
|
||||
assert len(res_bbox.geometry) == 1
|
||||
|
||||
|
||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
||||
|
||||
|
|
@ -100,15 +98,15 @@ def test_sbend_continuity() -> None:
|
|||
offset = 4.0
|
||||
radius = 20.0
|
||||
width = 1.0
|
||||
|
||||
|
||||
res = SBend.generate(start, offset, radius, width)
|
||||
|
||||
|
||||
# Target orientation should be same as start
|
||||
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
||||
|
||||
|
||||
# For a port at 90 deg, +offset is a shift in -x direction
|
||||
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||
|
||||
|
||||
# Geometry should be connected (unary_union results in 1 polygon)
|
||||
assert len(res.geometry) == 1
|
||||
assert res.geometry[0].is_valid
|
||||
|
|
@ -119,17 +117,17 @@ def test_arc_sagitta_precision() -> None:
|
|||
start = Port(0, 0, 0)
|
||||
radius = 100.0 # Large radius to make sagitta significant
|
||||
width = 2.0
|
||||
|
||||
|
||||
# Coarse: 1um sagitta
|
||||
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
|
||||
# Fine: 0.01um (10nm) sagitta
|
||||
res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
|
||||
|
||||
|
||||
# Number of segments should be significantly higher for fine
|
||||
# Exterior points = (segments + 1) * 2
|
||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||
|
||||
|
||||
assert pts_fine > pts_coarse * 2
|
||||
|
||||
|
||||
|
|
@ -139,20 +137,20 @@ def test_component_transform_invariance() -> None:
|
|||
start0 = Port(0, 0, 0)
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
|
||||
res0 = Bend90.generate(start0, radius, width, direction="CCW")
|
||||
|
||||
|
||||
# Transform: Translate (10, 10) then Rotate 90
|
||||
dx, dy = 10.0, 5.0
|
||||
angle = 90.0
|
||||
|
||||
|
||||
# 1. Transform the generated geometry
|
||||
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
||||
|
||||
|
||||
# 2. Generate at transformed start
|
||||
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
||||
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
||||
|
||||
|
||||
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
||||
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
||||
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inire.geometry.primitives import Port
|
||||
|
|
@ -28,7 +27,7 @@ def validate_routing_result(
|
|||
obstacle_collision_geoms = []
|
||||
self_intersection_geoms = []
|
||||
connectivity_errors = []
|
||||
|
||||
|
||||
# 1. Connectivity Check
|
||||
total_length = 0.0
|
||||
for i, comp in enumerate(result.path):
|
||||
|
|
@ -38,7 +37,7 @@ def validate_routing_result(
|
|||
if expected_end:
|
||||
last_port = result.path[-1].end_port
|
||||
dist_to_end = np.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
||||
if dist_to_end > 0.005:
|
||||
if dist_to_end > 0.005:
|
||||
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
||||
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
||||
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
||||
|
|
@ -46,9 +45,9 @@ def validate_routing_result(
|
|||
# 2. Geometry Buffering
|
||||
dilation_half = clearance / 2.0
|
||||
dilation_full = clearance
|
||||
|
||||
|
||||
dilated_for_self = []
|
||||
|
||||
|
||||
for i, comp in enumerate(result.path):
|
||||
for poly in comp.geometry:
|
||||
# Check against obstacles
|
||||
|
|
@ -58,7 +57,7 @@ def validate_routing_result(
|
|||
intersection = d_full.intersection(obs)
|
||||
if intersection.area > 1e-9:
|
||||
obstacle_collision_geoms.append(intersection)
|
||||
|
||||
|
||||
# Save for self-intersection check
|
||||
dilated_for_self.append(poly.buffer(dilation_half))
|
||||
|
||||
|
|
@ -68,13 +67,13 @@ def validate_routing_result(
|
|||
if j > i + 1: # Non-adjacent
|
||||
if seg_i.intersects(seg_j):
|
||||
overlap = seg_i.intersection(seg_j)
|
||||
if overlap.area > 1e-6:
|
||||
if overlap.area > 1e-6:
|
||||
self_intersection_geoms.append((i, j, overlap))
|
||||
|
||||
is_valid = (len(obstacle_collision_geoms) == 0 and
|
||||
len(self_intersection_geoms) == 0 and
|
||||
is_valid = (len(obstacle_collision_geoms) == 0 and
|
||||
len(self_intersection_geoms) == 0 and
|
||||
len(connectivity_errors) == 0)
|
||||
|
||||
|
||||
reasons = []
|
||||
if obstacle_collision_geoms:
|
||||
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue