some perf improvements
1
ex1.ssc
1
ex2.ssc
1
ex3.ssc
1
ex4.ssc
1
ex5.ssc
1
ex6.ssc
1
ex7.ssc
1
ex8.ssc
1
ex9.ssc
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 78 KiB |
|
|
@ -103,7 +103,8 @@ def main() -> None:
|
||||||
})
|
})
|
||||||
|
|
||||||
# Save plots only for certain iterations to save time
|
# Save plots only for certain iterations to save time
|
||||||
if idx % 20 == 0 or idx == pf.max_iterations - 1:
|
# if idx % 20 == 0 or idx == pf.max_iterations - 1:
|
||||||
|
if False:
|
||||||
# Save a plot of this iteration's result
|
# Save a plot of this iteration's result
|
||||||
fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist)
|
fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist)
|
||||||
plot_danger_map(danger_map, ax=ax)
|
plot_danger_map(danger_map, ax=ax)
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 51 KiB |
|
|
@ -28,7 +28,7 @@ class CollisionEngine:
|
||||||
'dynamic_tree', 'dynamic_obj_ids', 'dynamic_grid', '_dynamic_id_counter',
|
'dynamic_tree', 'dynamic_obj_ids', 'dynamic_grid', '_dynamic_id_counter',
|
||||||
'metrics', '_dynamic_tree_dirty', '_dynamic_net_ids_array', '_inv_grid_cell_size',
|
'metrics', '_dynamic_tree_dirty', '_dynamic_net_ids_array', '_inv_grid_cell_size',
|
||||||
'_static_bounds_array', '_static_is_rect_array', '_locked_nets',
|
'_static_bounds_array', '_static_is_rect_array', '_locked_nets',
|
||||||
'_static_raw_tree', '_static_raw_obj_ids'
|
'_static_raw_tree', '_static_raw_obj_ids', '_dynamic_bounds_array'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -72,6 +72,7 @@ class CollisionEngine:
|
||||||
self._dynamic_id_counter = 0
|
self._dynamic_id_counter = 0
|
||||||
self._dynamic_tree_dirty = True
|
self._dynamic_tree_dirty = True
|
||||||
self._dynamic_net_ids_array = numpy.array([], dtype='<U32')
|
self._dynamic_net_ids_array = numpy.array([], dtype='<U32')
|
||||||
|
self._dynamic_bounds_array = numpy.array([], dtype=numpy.float64).reshape(0, 4)
|
||||||
self._locked_nets: set[str] = set()
|
self._locked_nets: set[str] = set()
|
||||||
|
|
||||||
self.metrics = {
|
self.metrics = {
|
||||||
|
|
@ -135,6 +136,7 @@ class CollisionEngine:
|
||||||
geoms = [self.dynamic_dilated[i] for i in ids]
|
geoms = [self.dynamic_dilated[i] for i in ids]
|
||||||
self.dynamic_tree = STRtree(geoms)
|
self.dynamic_tree = STRtree(geoms)
|
||||||
self.dynamic_obj_ids = numpy.array(ids, dtype=numpy.int32)
|
self.dynamic_obj_ids = numpy.array(ids, dtype=numpy.int32)
|
||||||
|
self._dynamic_bounds_array = numpy.array([g.bounds for g in geoms])
|
||||||
nids = [self.dynamic_geometries[obj_id][0] for obj_id in self.dynamic_obj_ids]
|
nids = [self.dynamic_geometries[obj_id][0] for obj_id in self.dynamic_obj_ids]
|
||||||
self._dynamic_net_ids_array = numpy.array(nids, dtype='<U32')
|
self._dynamic_net_ids_array = numpy.array(nids, dtype='<U32')
|
||||||
self._dynamic_tree_dirty = False
|
self._dynamic_tree_dirty = False
|
||||||
|
|
@ -191,20 +193,29 @@ class CollisionEngine:
|
||||||
self._ensure_static_tree()
|
self._ensure_static_tree()
|
||||||
if self.static_tree is None: return False
|
if self.static_tree is None: return False
|
||||||
|
|
||||||
# In sparse A*, result.dilated_geometry is buffered by C/2.
|
# 1. Fast total bounds check
|
||||||
# static_dilated is also buffered by C/2.
|
tb = result.total_bounds
|
||||||
# Total separation = C. Correct for waveguide-waveguide and waveguide-obstacle?
|
s_bounds = self._static_bounds_array
|
||||||
# Actually, if result.geometry is width Wi, then dilated is Wi + C.
|
possible_total = (tb[0] < s_bounds[:, 2]) & (tb[2] > s_bounds[:, 0]) & \
|
||||||
# Wait, result.dilated_geometry is buffered by self._self_dilation = C/2.
|
(tb[1] < s_bounds[:, 3]) & (tb[3] > s_bounds[:, 1])
|
||||||
# So dilated poly is Wi + C.
|
|
||||||
# Obstacle dilated by C/2 is Wo + C.
|
|
||||||
# Intersection means dist < (Wi+C)/2 + (Wo+C)/2? No.
|
|
||||||
# Let's keep it simple:
|
|
||||||
# result.geometry is the REAL waveguide polygon (width Wi).
|
|
||||||
# dilated_geometry is buffered by C/2.
|
|
||||||
# static_dilated is buffered by C/2.
|
|
||||||
# Intersecting them means dist < C. This is correct!
|
|
||||||
|
|
||||||
|
if not numpy.any(possible_total):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. Per-polygon AABB check
|
||||||
|
bounds_list = result.bounds
|
||||||
|
any_possible = False
|
||||||
|
for b in bounds_list:
|
||||||
|
possible = (b[0] < s_bounds[:, 2]) & (b[2] > s_bounds[:, 0]) & \
|
||||||
|
(b[1] < s_bounds[:, 3]) & (b[3] > s_bounds[:, 1])
|
||||||
|
if numpy.any(possible):
|
||||||
|
any_possible = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not any_possible:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. Real geometry check (Triggers Lazy Evaluation)
|
||||||
test_geoms = result.dilated_geometry if result.dilated_geometry else result.geometry
|
test_geoms = result.dilated_geometry if result.dilated_geometry else result.geometry
|
||||||
for i, poly in enumerate(result.geometry):
|
for i, poly in enumerate(result.geometry):
|
||||||
hits = self.static_tree.query(test_geoms[i], predicate='intersects')
|
hits = self.static_tree.query(test_geoms[i], predicate='intersects')
|
||||||
|
|
@ -215,15 +226,20 @@ class CollisionEngine:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
def check_move_congestion(self, result: ComponentResult, net_id: str) -> int:
|
||||||
if result.total_dilated_bounds is None: return 0
|
tb = result.total_dilated_bounds
|
||||||
|
if tb is None: return 0
|
||||||
self._ensure_dynamic_grid()
|
self._ensure_dynamic_grid()
|
||||||
if not self.dynamic_grid: return 0
|
if not self.dynamic_grid: return 0
|
||||||
b = result.total_dilated_bounds; cs = self.grid_cell_size
|
|
||||||
|
cs_inv = self._inv_grid_cell_size
|
||||||
|
gx_min, gy_min = int(tb[0] * cs_inv), int(tb[1] * cs_inv)
|
||||||
|
gx_max, gy_max = int(tb[2] * cs_inv), int(tb[3] * cs_inv)
|
||||||
|
|
||||||
any_possible = False
|
any_possible = False
|
||||||
dynamic_grid = self.dynamic_grid
|
dynamic_grid = self.dynamic_grid
|
||||||
dynamic_geometries = self.dynamic_geometries
|
dynamic_geometries = self.dynamic_geometries
|
||||||
for gx in range(int(b[0]/cs), int(b[2]/cs)+1):
|
for gx in range(gx_min, gx_max + 1):
|
||||||
for gy in range(int(b[1]/cs), int(b[3]/cs)+1):
|
for gy in range(gy_min, gy_max + 1):
|
||||||
cell = (gx, gy)
|
cell = (gx, gy)
|
||||||
if cell in dynamic_grid:
|
if cell in dynamic_grid:
|
||||||
for obj_id in dynamic_grid[cell]:
|
for obj_id in dynamic_grid[cell]:
|
||||||
|
|
@ -232,14 +248,40 @@ class CollisionEngine:
|
||||||
if any_possible: break
|
if any_possible: break
|
||||||
if any_possible: break
|
if any_possible: break
|
||||||
if not any_possible: return 0
|
if not any_possible: return 0
|
||||||
|
|
||||||
self.metrics['congestion_tree_queries'] += 1
|
self.metrics['congestion_tree_queries'] += 1
|
||||||
self._ensure_dynamic_tree()
|
self._ensure_dynamic_tree()
|
||||||
if self.dynamic_tree is None: return 0
|
if self.dynamic_tree is None: return 0
|
||||||
|
|
||||||
|
# 1. Fast total bounds check (LAZY SAFE)
|
||||||
|
tb = result.total_dilated_bounds
|
||||||
|
d_bounds = self._dynamic_bounds_array
|
||||||
|
possible_total = (tb[0] < d_bounds[:, 2]) & (tb[2] > d_bounds[:, 0]) & \
|
||||||
|
(tb[1] < d_bounds[:, 3]) & (tb[3] > d_bounds[:, 1])
|
||||||
|
|
||||||
|
# Filter by net_id (important for negotiated congestion)
|
||||||
|
valid_hits = (self._dynamic_net_ids_array != net_id)
|
||||||
|
if not numpy.any(possible_total & valid_hits):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 2. Per-polygon AABB check using query on geometries (LAZY triggering)
|
||||||
|
# We only trigger evaluation if total bounds intersect with other nets.
|
||||||
geoms_to_test = result.dilated_geometry if result.dilated_geometry else result.geometry
|
geoms_to_test = result.dilated_geometry if result.dilated_geometry else result.geometry
|
||||||
res_indices, tree_indices = self.dynamic_tree.query(geoms_to_test, predicate='intersects')
|
res_indices, tree_indices = self.dynamic_tree.query(geoms_to_test, predicate='intersects')
|
||||||
if tree_indices.size == 0: return 0
|
|
||||||
|
if tree_indices.size == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Filter out self-overlaps (from same net)
|
||||||
hit_net_ids = numpy.take(self._dynamic_net_ids_array, tree_indices)
|
hit_net_ids = numpy.take(self._dynamic_net_ids_array, tree_indices)
|
||||||
return int(numpy.sum(hit_net_ids != net_id))
|
valid_geoms_hits = (hit_net_ids != net_id)
|
||||||
|
if not numpy.any(valid_geoms_hits):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 3. Real geometry check (Only if AABBs intersect with other nets)
|
||||||
|
# We already have hits from STRtree which are accurate for polygons too.
|
||||||
|
# But wait, query(..., predicate='intersects') ALREADY does real check!
|
||||||
|
return int(numpy.sum(valid_geoms_hits))
|
||||||
|
|
||||||
def _is_in_safety_zone(self, geometry: Polygon, obj_id: int, start_port: Port | None, end_port: Port | None) -> bool:
|
def _is_in_safety_zone(self, geometry: Polygon, obj_id: int, start_port: Port | None, end_port: Port | None) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,20 @@ def snap_search_grid(value: float, snap_size: float = SEARCH_GRID_SNAP_UM) -> fl
|
||||||
class ComponentResult:
|
class ComponentResult:
|
||||||
"""
|
"""
|
||||||
Standard container for generated move geometry and state.
|
Standard container for generated move geometry and state.
|
||||||
|
Supports Lazy Evaluation for translation to improve performance.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'geometry', 'dilated_geometry', 'proxy_geometry', 'actual_geometry', 'dilated_actual_geometry',
|
'_geometry', '_dilated_geometry', '_proxy_geometry', '_actual_geometry', '_dilated_actual_geometry',
|
||||||
'end_port', 'length', 'move_type', 'bounds', 'dilated_bounds',
|
'end_port', 'length', 'move_type', '_bounds', '_dilated_bounds',
|
||||||
'total_bounds', 'total_dilated_bounds', '_t_cache', '_total_geom_list', '_offsets', '_coords_cache'
|
'_total_bounds', '_total_dilated_bounds', '_t_cache', '_total_geom_list', '_offsets', '_coords_cache',
|
||||||
|
'_base_result', '_offset', '_lazy_evaluated', 'rel_gx', 'rel_gy', 'rel_go'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
geometry: list[Polygon],
|
geometry: list[Polygon] | None = None,
|
||||||
end_port: Port,
|
end_port: Port | None = None,
|
||||||
length: float,
|
length: float = 0.0,
|
||||||
dilated_geometry: list[Polygon] | None = None,
|
dilated_geometry: list[Polygon] | None = None,
|
||||||
proxy_geometry: list[Polygon] | None = None,
|
proxy_geometry: list[Polygon] | None = None,
|
||||||
actual_geometry: list[Polygon] | None = None,
|
actual_geometry: list[Polygon] | None = None,
|
||||||
|
|
@ -45,54 +47,183 @@ class ComponentResult:
|
||||||
move_type: str = 'Unknown',
|
move_type: str = 'Unknown',
|
||||||
_total_geom_list: list[Polygon] | None = None,
|
_total_geom_list: list[Polygon] | None = None,
|
||||||
_offsets: list[int] | None = None,
|
_offsets: list[int] | None = None,
|
||||||
_coords_cache: numpy.ndarray | None = None
|
_coords_cache: numpy.ndarray | None = None,
|
||||||
|
_base_result: ComponentResult | None = None,
|
||||||
|
_offset: list[float] | None = None,
|
||||||
|
snap_size: float = SEARCH_GRID_SNAP_UM,
|
||||||
|
rel_gx: int | None = None,
|
||||||
|
rel_gy: int | None = None,
|
||||||
|
rel_go: int | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.geometry = geometry
|
|
||||||
self.dilated_geometry = dilated_geometry
|
|
||||||
self.proxy_geometry = proxy_geometry
|
|
||||||
self.actual_geometry = actual_geometry
|
|
||||||
self.dilated_actual_geometry = dilated_actual_geometry
|
|
||||||
self.end_port = end_port
|
self.end_port = end_port
|
||||||
self.length = length
|
self.length = length
|
||||||
self.move_type = move_type
|
self.move_type = move_type
|
||||||
self._t_cache = {}
|
self._t_cache = {}
|
||||||
|
|
||||||
if _total_geom_list is not None and _offsets is not None:
|
self._base_result = _base_result
|
||||||
self._total_geom_list = _total_geom_list
|
self._offset = _offset
|
||||||
self._offsets = _offsets
|
self._lazy_evaluated = False
|
||||||
self._coords_cache = _coords_cache
|
|
||||||
|
if rel_gx is not None:
|
||||||
|
self.rel_gx = rel_gx
|
||||||
|
self.rel_gy = rel_gy
|
||||||
|
self.rel_go = rel_go
|
||||||
|
elif end_port:
|
||||||
|
self.rel_gx = int(round(end_port.x / snap_size))
|
||||||
|
self.rel_gy = int(round(end_port.y / snap_size))
|
||||||
|
self.rel_go = int(round(end_port.orientation / 1.0))
|
||||||
else:
|
else:
|
||||||
# Flatten everything for fast vectorized translate
|
self.rel_gx = 0; self.rel_gy = 0; self.rel_go = 0
|
||||||
gl = list(geometry)
|
|
||||||
o = [len(geometry)]
|
|
||||||
if dilated_geometry: gl.extend(dilated_geometry)
|
|
||||||
o.append(len(gl))
|
|
||||||
if proxy_geometry: gl.extend(proxy_geometry)
|
|
||||||
o.append(len(gl))
|
|
||||||
if actual_geometry: gl.extend(actual_geometry)
|
|
||||||
o.append(len(gl))
|
|
||||||
if dilated_actual_geometry: gl.extend(dilated_actual_geometry)
|
|
||||||
self._total_geom_list = gl
|
|
||||||
self._offsets = o
|
|
||||||
self._coords_cache = shapely.get_coordinates(gl)
|
|
||||||
|
|
||||||
if not skip_bounds:
|
if _base_result is not None:
|
||||||
self.bounds = shapely.bounds(geometry)
|
# Lazy Mode
|
||||||
self.total_bounds = numpy.array([
|
self._geometry = None
|
||||||
numpy.min(self.bounds[:, 0]), numpy.min(self.bounds[:, 1]),
|
self._dilated_geometry = None
|
||||||
numpy.max(self.bounds[:, 2]), numpy.max(self.bounds[:, 3])
|
self._proxy_geometry = None
|
||||||
])
|
self._actual_geometry = None
|
||||||
if dilated_geometry is not None:
|
self._dilated_actual_geometry = None
|
||||||
self.dilated_bounds = shapely.bounds(dilated_geometry)
|
|
||||||
self.total_dilated_bounds = numpy.array([
|
# Bounds are computed on demand
|
||||||
numpy.min(self.dilated_bounds[:, 0]), numpy.min(self.dilated_bounds[:, 1]),
|
self._bounds = None
|
||||||
numpy.max(self.dilated_bounds[:, 2]), numpy.max(self.dilated_bounds[:, 3])
|
self._dilated_bounds = None
|
||||||
])
|
self._total_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
|
||||||
|
# No need to copy large arrays if we reference base
|
||||||
|
else:
|
||||||
|
# Eager Mode (Base Component)
|
||||||
|
self._geometry = geometry
|
||||||
|
self._dilated_geometry = dilated_geometry
|
||||||
|
self._proxy_geometry = proxy_geometry
|
||||||
|
self._actual_geometry = actual_geometry
|
||||||
|
self._dilated_actual_geometry = dilated_actual_geometry
|
||||||
|
|
||||||
|
if _total_geom_list is not None and _offsets is not None:
|
||||||
|
self._total_geom_list = _total_geom_list
|
||||||
|
self._offsets = _offsets
|
||||||
|
self._coords_cache = _coords_cache
|
||||||
else:
|
else:
|
||||||
self.dilated_bounds = None
|
# Flatten everything for fast vectorized translate
|
||||||
self.total_dilated_bounds = None
|
gl = []
|
||||||
|
if geometry: gl.extend(geometry)
|
||||||
|
o = [len(gl)]
|
||||||
|
if dilated_geometry: gl.extend(dilated_geometry)
|
||||||
|
o.append(len(gl))
|
||||||
|
if proxy_geometry: gl.extend(proxy_geometry)
|
||||||
|
o.append(len(gl))
|
||||||
|
if actual_geometry: gl.extend(actual_geometry)
|
||||||
|
o.append(len(gl))
|
||||||
|
if dilated_actual_geometry: gl.extend(dilated_actual_geometry)
|
||||||
|
self._total_geom_list = gl
|
||||||
|
self._offsets = o
|
||||||
|
self._coords_cache = shapely.get_coordinates(gl) if gl else None
|
||||||
|
|
||||||
def translate(self, dx: float, dy: float) -> ComponentResult:
|
if not skip_bounds and geometry:
|
||||||
|
self._bounds = shapely.bounds(geometry)
|
||||||
|
self._total_bounds = numpy.array([
|
||||||
|
numpy.min(self._bounds[:, 0]), numpy.min(self._bounds[:, 1]),
|
||||||
|
numpy.max(self._bounds[:, 2]), numpy.max(self._bounds[:, 3])
|
||||||
|
])
|
||||||
|
if dilated_geometry is not None:
|
||||||
|
self._dilated_bounds = shapely.bounds(dilated_geometry)
|
||||||
|
self._total_dilated_bounds = numpy.array([
|
||||||
|
numpy.min(self._dilated_bounds[:, 0]), numpy.min(self._dilated_bounds[:, 1]),
|
||||||
|
numpy.max(self._dilated_bounds[:, 2]), numpy.max(self._dilated_bounds[:, 3])
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
self._dilated_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
else:
|
||||||
|
self._bounds = None
|
||||||
|
self._total_bounds = None
|
||||||
|
self._dilated_bounds = None
|
||||||
|
self._total_dilated_bounds = None
|
||||||
|
|
||||||
|
def _ensure_evaluated(self) -> None:
|
||||||
|
if self._base_result is None or self._lazy_evaluated:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Perform Translation
|
||||||
|
dx, dy = self._offset
|
||||||
|
# Base uses its own coords cache
|
||||||
|
base_coords = self._base_result._coords_cache
|
||||||
|
if base_coords is None:
|
||||||
|
self._lazy_evaluated = True
|
||||||
|
return
|
||||||
|
|
||||||
|
new_coords = base_coords + [dx, dy]
|
||||||
|
|
||||||
|
# Translate ALL geometries at once
|
||||||
|
new_total_arr = shapely.set_coordinates(list(self._base_result._total_geom_list), new_coords)
|
||||||
|
new_total = new_total_arr.tolist()
|
||||||
|
|
||||||
|
o = self._base_result._offsets
|
||||||
|
self._geometry = new_total[:o[0]]
|
||||||
|
self._dilated_geometry = new_total[o[0]:o[1]] if self._base_result._dilated_geometry is not None else None
|
||||||
|
self._proxy_geometry = new_total[o[1]:o[2]] if self._base_result._proxy_geometry is not None else None
|
||||||
|
self._actual_geometry = new_total[o[2]:o[3]] if self._base_result._actual_geometry is not None else None
|
||||||
|
self._dilated_actual_geometry = new_total[o[3]:] if self._base_result._dilated_actual_geometry is not None else None
|
||||||
|
|
||||||
|
self._lazy_evaluated = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def geometry(self) -> list[Polygon]:
|
||||||
|
self._ensure_evaluated()
|
||||||
|
return self._geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated()
|
||||||
|
return self._dilated_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated()
|
||||||
|
return self._proxy_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actual_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated()
|
||||||
|
return self._actual_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_actual_geometry(self) -> list[Polygon] | None:
|
||||||
|
self._ensure_evaluated()
|
||||||
|
return self._dilated_actual_geometry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounds(self) -> numpy.ndarray:
|
||||||
|
if self._bounds is None:
|
||||||
|
if self._base_result is not None:
|
||||||
|
dx, dy = self._offset
|
||||||
|
self._bounds = self._base_result.bounds + [dx, dy, dx, dy]
|
||||||
|
return self._bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_bounds(self) -> numpy.ndarray:
|
||||||
|
if self._total_bounds is None:
|
||||||
|
if self._base_result is not None:
|
||||||
|
dx, dy = self._offset
|
||||||
|
self._total_bounds = self._base_result.total_bounds + [dx, dy, dx, dy]
|
||||||
|
return self._total_bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dilated_bounds(self) -> numpy.ndarray | None:
|
||||||
|
if self._dilated_bounds is None:
|
||||||
|
if self._base_result is not None and self._base_result.dilated_bounds is not None:
|
||||||
|
dx, dy = self._offset
|
||||||
|
self._dilated_bounds = self._base_result.dilated_bounds + [dx, dy, dx, dy]
|
||||||
|
return self._dilated_bounds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_dilated_bounds(self) -> numpy.ndarray | None:
|
||||||
|
if self._total_dilated_bounds is None:
|
||||||
|
if self._base_result is not None and self._base_result.total_dilated_bounds is not None:
|
||||||
|
dx, dy = self._offset
|
||||||
|
self._total_dilated_bounds = self._base_result.total_dilated_bounds + [dx, dy, dx, dy]
|
||||||
|
return self._total_dilated_bounds
|
||||||
|
|
||||||
|
def translate(self, dx: float, dy: float, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None) -> ComponentResult:
|
||||||
"""
|
"""
|
||||||
Create a new ComponentResult translated by (dx, dy).
|
Create a new ComponentResult translated by (dx, dy).
|
||||||
"""
|
"""
|
||||||
|
|
@ -102,44 +233,27 @@ class ComponentResult:
|
||||||
if (dxr, dyr) in self._t_cache:
|
if (dxr, dyr) in self._t_cache:
|
||||||
return self._t_cache[(dxr, dyr)]
|
return self._t_cache[(dxr, dyr)]
|
||||||
|
|
||||||
# FASTEST TRANSLATE
|
|
||||||
new_coords = self._coords_cache + [dx, dy]
|
|
||||||
new_total_arr = shapely.set_coordinates(list(self._total_geom_list), new_coords)
|
|
||||||
new_total = new_total_arr.tolist()
|
|
||||||
|
|
||||||
o = self._offsets
|
|
||||||
new_geom = new_total[:o[0]]
|
|
||||||
new_dil = new_total[o[0]:o[1]] if self.dilated_geometry is not None else None
|
|
||||||
new_proxy = new_total[o[1]:o[2]] if self.proxy_geometry is not None else None
|
|
||||||
new_actual = new_total[o[2]:o[3]] if self.actual_geometry is not None else None
|
|
||||||
new_dil_actual = new_total[o[3]:] if self.dilated_actual_geometry is not None else None
|
|
||||||
|
|
||||||
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
|
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
|
||||||
|
|
||||||
# Fast bypass of __init__
|
# LAZY TRANSLATE
|
||||||
res = self.__class__.__new__(self.__class__)
|
if self._base_result:
|
||||||
res.geometry = new_geom
|
base = self._base_result
|
||||||
res.dilated_geometry = new_dil
|
current_offset = self._offset
|
||||||
res.proxy_geometry = new_proxy
|
new_offset = [current_offset[0] + dx, current_offset[1] + dy]
|
||||||
res.actual_geometry = new_actual
|
|
||||||
res.dilated_actual_geometry = new_dil_actual
|
|
||||||
res.end_port = new_port
|
|
||||||
res.length = self.length
|
|
||||||
res.move_type = self.move_type
|
|
||||||
res._t_cache = {}
|
|
||||||
res._total_geom_list = new_total
|
|
||||||
res._offsets = o
|
|
||||||
res._coords_cache = new_coords
|
|
||||||
|
|
||||||
db = [dx, dy, dx, dy]
|
|
||||||
res.bounds = self.bounds + db
|
|
||||||
res.total_bounds = self.total_bounds + db
|
|
||||||
if self.dilated_bounds is not None:
|
|
||||||
res.dilated_bounds = self.dilated_bounds + db
|
|
||||||
res.total_dilated_bounds = self.total_dilated_bounds + db
|
|
||||||
else:
|
else:
|
||||||
res.dilated_bounds = None
|
base = self
|
||||||
res.total_dilated_bounds = None
|
new_offset = [dx, dy]
|
||||||
|
|
||||||
|
res = ComponentResult(
|
||||||
|
end_port=new_port,
|
||||||
|
length=self.length,
|
||||||
|
move_type=self.move_type,
|
||||||
|
_base_result=base,
|
||||||
|
_offset=new_offset,
|
||||||
|
rel_gx=rel_gx,
|
||||||
|
rel_gy=rel_gy,
|
||||||
|
rel_go=rel_go
|
||||||
|
)
|
||||||
|
|
||||||
self._t_cache[(dxr, dyr)] = res
|
self._t_cache[(dxr, dyr)] = res
|
||||||
return res
|
return res
|
||||||
|
|
@ -205,7 +319,7 @@ class Straight:
|
||||||
dilated_geom = [Polygon(poly_points_dil)]
|
dilated_geom = [Polygon(poly_points_dil)]
|
||||||
|
|
||||||
# For straight segments, geom IS the actual geometry
|
# For straight segments, geom IS the actual geometry
|
||||||
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom, actual_geometry=geom, dilated_actual_geometry=dilated_geom, move_type='Straight')
|
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom, actual_geometry=geom, dilated_actual_geometry=dilated_geom, move_type='Straight', snap_size=snap_size)
|
||||||
|
|
||||||
|
|
||||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||||
|
|
@ -432,7 +546,8 @@ class Bend90:
|
||||||
proxy_geometry=proxy_geom,
|
proxy_geometry=proxy_geom,
|
||||||
actual_geometry=arc_polys,
|
actual_geometry=arc_polys,
|
||||||
dilated_actual_geometry=dilated_actual_geom,
|
dilated_actual_geometry=dilated_actual_geom,
|
||||||
move_type='Bend90'
|
move_type='Bend90',
|
||||||
|
snap_size=snap_size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -485,7 +600,7 @@ class SBend:
|
||||||
if abs(theta) < 1e-9:
|
if abs(theta) < 1e-9:
|
||||||
# De-generate to straight
|
# De-generate to straight
|
||||||
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
||||||
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation)
|
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation, snap_size=snap_size)
|
||||||
|
|
||||||
denom = (2 * (1 - numpy.cos(theta)))
|
denom = (2 * (1 - numpy.cos(theta)))
|
||||||
if abs(denom) < 1e-9:
|
if abs(denom) < 1e-9:
|
||||||
|
|
@ -496,7 +611,7 @@ class SBend:
|
||||||
# Limit radius to prevent giant arcs
|
# Limit radius to prevent giant arcs
|
||||||
if actual_radius > 100000.0:
|
if actual_radius > 100000.0:
|
||||||
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
||||||
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation)
|
return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation, snap_size=snap_size)
|
||||||
|
|
||||||
direction = 1 if local_dy > 0 else -1
|
direction = 1 if local_dy > 0 else -1
|
||||||
c1_angle = rad_start + direction * numpy.pi / 2
|
c1_angle = rad_start + direction * numpy.pi / 2
|
||||||
|
|
@ -546,5 +661,6 @@ class SBend:
|
||||||
proxy_geometry=proxy_geom,
|
proxy_geometry=proxy_geom,
|
||||||
actual_geometry=arc_polys,
|
actual_geometry=arc_polys,
|
||||||
dilated_actual_geometry=dilated_actual_geom,
|
dilated_actual_geometry=dilated_actual_geom,
|
||||||
move_type='SBend'
|
move_type='SBend',
|
||||||
|
snap_size=snap_size
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ class AStarRouter:
|
||||||
return self._reconstruct_path(current)
|
return self._reconstruct_path(current)
|
||||||
|
|
||||||
# Expansion
|
# Expansion
|
||||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set, snap, nodes_expanded, skip_congestion=skip_congestion, inv_snap=inv_snap)
|
self._expand_moves(current, target, net_width, net_id, open_set, closed_set, snap, nodes_expanded, skip_congestion=skip_congestion, inv_snap=inv_snap, parent_state=state)
|
||||||
|
|
||||||
return self._reconstruct_path(best_node) if return_partial else None
|
return self._reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
|
|
@ -192,10 +192,14 @@ class AStarRouter:
|
||||||
snap: float = 1.0,
|
snap: float = 1.0,
|
||||||
nodes_expanded: int = 0,
|
nodes_expanded: int = 0,
|
||||||
skip_congestion: bool = False,
|
skip_congestion: bool = False,
|
||||||
inv_snap: float | None = None
|
inv_snap: float | None = None,
|
||||||
|
parent_state: tuple[int, int, int] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
cp = current.port
|
cp = current.port
|
||||||
if inv_snap is None: inv_snap = 1.0 / snap
|
if inv_snap is None: inv_snap = 1.0 / snap
|
||||||
|
if parent_state is None:
|
||||||
|
parent_state = (int(round(cp.x / snap)), int(round(cp.y / snap)), int(round(cp.orientation / 1.0)))
|
||||||
|
|
||||||
dx_t = target.x - cp.x
|
dx_t = target.x - cp.x
|
||||||
dy_t = target.y - cp.y
|
dy_t = target.y - cp.y
|
||||||
dist_sq = dx_t*dx_t + dy_t*dy_t
|
dist_sq = dx_t*dx_t + dy_t*dy_t
|
||||||
|
|
@ -210,7 +214,7 @@ class AStarRouter:
|
||||||
if proj_t > 0 and abs(perp_t) < 1e-3 and abs(cp.orientation - target.orientation) < 0.1:
|
if proj_t > 0 and abs(perp_t) < 1e-3 and abs(cp.orientation - target.orientation) < 0.1:
|
||||||
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, proj_t + 1.0)
|
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, proj_t + 1.0)
|
||||||
if max_reach >= proj_t - 0.01:
|
if max_reach >= proj_t - 0.01:
|
||||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, inv_snap=inv_snap, snap_to_grid=False)
|
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, inv_snap=inv_snap, snap_to_grid=False, parent_state=parent_state)
|
||||||
|
|
||||||
# 2. VISIBILITY JUMPS & MAX REACH
|
# 2. VISIBILITY JUMPS & MAX REACH
|
||||||
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, self.config.max_straight_length)
|
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, self.config.max_straight_length)
|
||||||
|
|
@ -257,7 +261,7 @@ class AStarRouter:
|
||||||
if s_l <= max_reach and s_l > 0.1: straight_lengths.add(s_l)
|
if s_l <= max_reach and s_l > 0.1: straight_lengths.add(s_l)
|
||||||
|
|
||||||
for length in sorted(straight_lengths, reverse=True):
|
for length in sorted(straight_lengths, reverse=True):
|
||||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{length}', 'S', (length,), skip_congestion, inv_snap=inv_snap)
|
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{length}', 'S', (length,), skip_congestion, inv_snap=inv_snap, parent_state=parent_state)
|
||||||
|
|
||||||
# 3. BENDS & SBENDS
|
# 3. BENDS & SBENDS
|
||||||
angle_to_target = numpy.degrees(numpy.arctan2(target.y - cp.y, target.x - cp.x))
|
angle_to_target = numpy.degrees(numpy.arctan2(target.y - cp.y, target.x - cp.x))
|
||||||
|
|
@ -271,7 +275,7 @@ class AStarRouter:
|
||||||
new_diff = (angle_to_target - new_ori + 180) % 360 - 180
|
new_diff = (angle_to_target - new_ori + 180) % 360 - 180
|
||||||
if abs(new_diff) > 135:
|
if abs(new_diff) > 135:
|
||||||
continue
|
continue
|
||||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion, inv_snap=inv_snap)
|
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion, inv_snap=inv_snap, parent_state=parent_state)
|
||||||
|
|
||||||
# 4. SBENDS
|
# 4. SBENDS
|
||||||
max_sbend_r = max(self.config.sbend_radii) if self.config.sbend_radii else 0
|
max_sbend_r = max(self.config.sbend_radii) if self.config.sbend_radii else 0
|
||||||
|
|
@ -294,7 +298,7 @@ class AStarRouter:
|
||||||
for offset in sorted(offsets):
|
for offset in sorted(offsets):
|
||||||
for radius in self.config.sbend_radii:
|
for radius in self.config.sbend_radii:
|
||||||
if abs(offset) >= 2 * radius: continue
|
if abs(offset) >= 2 * radius: continue
|
||||||
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion, inv_snap=inv_snap)
|
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion, inv_snap=inv_snap, parent_state=parent_state)
|
||||||
|
|
||||||
def _process_move(
|
def _process_move(
|
||||||
self,
|
self,
|
||||||
|
|
@ -311,20 +315,25 @@ class AStarRouter:
|
||||||
skip_congestion: bool,
|
skip_congestion: bool,
|
||||||
inv_snap: float | None = None,
|
inv_snap: float | None = None,
|
||||||
snap_to_grid: bool = True,
|
snap_to_grid: bool = True,
|
||||||
|
parent_state: tuple[int, int, int] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
cp = parent.port
|
cp = parent.port
|
||||||
if inv_snap is None: inv_snap = 1.0 / snap
|
if inv_snap is None: inv_snap = 1.0 / snap
|
||||||
base_ori = float(int(cp.orientation + 0.5))
|
base_ori = float(int(cp.orientation + 0.5))
|
||||||
gx = int(round(cp.x / snap))
|
if parent_state is None:
|
||||||
gy = int(round(cp.y / snap))
|
gx = int(round(cp.x / snap))
|
||||||
go = int(round(cp.orientation / 1.0))
|
gy = int(round(cp.y / snap))
|
||||||
state_key = (gx, gy, go)
|
go = int(round(cp.orientation / 1.0))
|
||||||
|
parent_state = (gx, gy, go)
|
||||||
|
else:
|
||||||
|
gx, gy, go = parent_state
|
||||||
|
state_key = parent_state
|
||||||
|
|
||||||
abs_key = (state_key, move_class, params, net_width, self.config.bend_collision_type, snap_to_grid)
|
abs_key = (state_key, move_class, params, net_width, self.config.bend_collision_type, snap_to_grid)
|
||||||
if abs_key in self._move_cache:
|
if abs_key in self._move_cache:
|
||||||
res = self._move_cache[abs_key]
|
res = self._move_cache[abs_key]
|
||||||
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
||||||
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap)
|
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap, parent_state=parent_state)
|
||||||
return
|
return
|
||||||
|
|
||||||
rel_key = (base_ori, move_class, params, net_width, self.config.bend_collision_type, self._self_dilation, snap_to_grid)
|
rel_key = (base_ori, move_class, params, net_width, self.config.bend_collision_type, self._self_dilation, snap_to_grid)
|
||||||
|
|
@ -335,7 +344,7 @@ class AStarRouter:
|
||||||
|
|
||||||
if rel_key in self._move_cache:
|
if rel_key in self._move_cache:
|
||||||
res_rel = self._move_cache[rel_key]
|
res_rel = self._move_cache[rel_key]
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y, rel_gx=res_rel.rel_gx + gx, rel_gy=res_rel.rel_gy + gy, rel_go=res_rel.rel_go)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
p0 = Port(0, 0, base_ori)
|
p0 = Port(0, 0, base_ori)
|
||||||
|
|
@ -348,13 +357,13 @@ class AStarRouter:
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self._move_cache[rel_key] = res_rel
|
self._move_cache[rel_key] = res_rel
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y, rel_gx=res_rel.rel_gx + gx, rel_gy=res_rel.rel_gy + gy, rel_go=res_rel.rel_go)
|
||||||
except (ValueError, ZeroDivisionError):
|
except (ValueError, ZeroDivisionError):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._move_cache[abs_key] = res
|
self._move_cache[abs_key] = res
|
||||||
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None)
|
||||||
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap)
|
self._add_node(parent, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap, parent_state=parent_state)
|
||||||
|
|
||||||
def _add_node(
|
def _add_node(
|
||||||
self,
|
self,
|
||||||
|
|
@ -370,17 +379,21 @@ class AStarRouter:
|
||||||
snap: float = 1.0,
|
snap: float = 1.0,
|
||||||
skip_congestion: bool = False,
|
skip_congestion: bool = False,
|
||||||
inv_snap: float | None = None,
|
inv_snap: float | None = None,
|
||||||
|
parent_state: tuple[int, int, int] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.metrics['moves_generated'] += 1
|
self.metrics['moves_generated'] += 1
|
||||||
end_p = result.end_port
|
state = (result.rel_gx, result.rel_gy, result.rel_go)
|
||||||
state = (int(round(end_p.x / snap)), int(round(end_p.y / snap)), int(round(end_p.orientation / 1.0)))
|
|
||||||
|
|
||||||
if state in closed_set and closed_set[state] <= parent.g_cost + 1e-6:
|
if state in closed_set and closed_set[state] <= parent.g_cost + 1e-6:
|
||||||
self.metrics['pruned_closed_set'] += 1
|
self.metrics['pruned_closed_set'] += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
parent_p = parent.port
|
parent_p = parent.port
|
||||||
pgx, pgy, pgo = int(round(parent_p.x / snap)), int(round(parent_p.y / snap)), int(round(parent_p.orientation / 1.0))
|
end_p = result.end_port
|
||||||
|
if parent_state is None:
|
||||||
|
pgx, pgy, pgo = int(round(parent_p.x / snap)), int(round(parent_p.y / snap)), int(round(parent_p.orientation / 1.0))
|
||||||
|
else:
|
||||||
|
pgx, pgy, pgo = parent_state
|
||||||
cache_key = (pgx, pgy, pgo, move_type, net_width)
|
cache_key = (pgx, pgy, pgo, move_type, net_width)
|
||||||
|
|
||||||
if cache_key in self._hard_collision_set:
|
if cache_key in self._hard_collision_set:
|
||||||
|
|
@ -416,9 +429,9 @@ class AStarRouter:
|
||||||
if move_radius is not None and move_radius > 1e-6: penalty *= (10.0 / move_radius)**0.5
|
if move_radius is not None and move_radius > 1e-6: penalty *= (10.0 / move_radius)**0.5
|
||||||
|
|
||||||
move_cost = self.cost_evaluator.evaluate_move(
|
move_cost = self.cost_evaluator.evaluate_move(
|
||||||
result.geometry, result.end_port, net_width, net_id,
|
None, result.end_port, net_width, net_id,
|
||||||
start_port=parent_p, length=result.length,
|
start_port=parent_p, length=result.length,
|
||||||
dilated_geometry=result.dilated_geometry, penalty=penalty,
|
dilated_geometry=None, penalty=penalty,
|
||||||
skip_static=True, skip_congestion=True
|
skip_static=True, skip_congestion=True
|
||||||
)
|
)
|
||||||
move_cost += total_overlaps * self.cost_evaluator.congestion_penalty
|
move_cost += total_overlaps * self.cost_evaluator.congestion_penalty
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ class CostEvaluator:
|
||||||
|
|
||||||
def evaluate_move(
|
def evaluate_move(
|
||||||
self,
|
self,
|
||||||
geometry: list[Polygon],
|
geometry: list[Polygon] | None,
|
||||||
end_port: Port,
|
end_port: Port,
|
||||||
net_width: float,
|
net_width: float,
|
||||||
net_id: str,
|
net_id: str,
|
||||||
|
|
@ -199,6 +199,9 @@ class CostEvaluator:
|
||||||
# 2. Collision Check
|
# 2. Collision Check
|
||||||
if not skip_static or not skip_congestion:
|
if not skip_static or not skip_congestion:
|
||||||
collision_engine = self.collision_engine
|
collision_engine = self.collision_engine
|
||||||
|
# Ensure geometry is provided if collision checks are enabled
|
||||||
|
if geometry is None:
|
||||||
|
return 1e15
|
||||||
for i, poly in enumerate(geometry):
|
for i, poly in enumerate(geometry):
|
||||||
dil_poly = dilated_geometry[i] if dilated_geometry else None
|
dil_poly = dilated_geometry[i] if dilated_geometry else None
|
||||||
# Hard Collision (Static obstacles)
|
# Hard Collision (Static obstacles)
|
||||||
|
|
|
||||||