add s-bend example and surface config
This commit is contained in:
parent
82aaf066e2
commit
18b2f83a7b
7 changed files with 212 additions and 39 deletions
81
examples/04_sbends_and_radii.py
Normal file
81
examples/04_sbends_and_radii.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.router.astar import AStarRouter
|
||||||
|
from inire.router.config import CostConfig, RouterConfig
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 04: S-Bends and Multiple Radii...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
bounds = (0, 0, 150, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
|
||||||
|
# Create obstacles that force S-bends and turns
|
||||||
|
# Obstacle 1: Forces a vertical jog (S-bend)
|
||||||
|
obs1 = Polygon([(40, 20), (60, 20), (60, 60), (40, 60)])
|
||||||
|
|
||||||
|
# Obstacle 2: Forces a large radius turn
|
||||||
|
obs2 = Polygon([(80, 0), (100, 0), (100, 40), (80, 40)])
|
||||||
|
|
||||||
|
obstacles = [obs1, obs2]
|
||||||
|
for obs in obstacles:
|
||||||
|
engine.add_static_obstacle(obs)
|
||||||
|
|
||||||
|
danger_map.precompute(obstacles)
|
||||||
|
|
||||||
|
# 2. Configure Router with custom parameters (Directly via constructor)
|
||||||
|
evaluator = CostEvaluator(
|
||||||
|
engine,
|
||||||
|
danger_map,
|
||||||
|
unit_length_cost=1.0,
|
||||||
|
greedy_h_weight=1.2,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = AStarRouter(
|
||||||
|
evaluator,
|
||||||
|
node_limit=500000,
|
||||||
|
bend_radii=[10.0, 30.0], # Allow standard and large bends
|
||||||
|
sbend_offsets=[-10.0, -5.0, 5.0, 10.0], # Allow larger S-bend offsets
|
||||||
|
sbend_radii=[20.0, 50.0], # Large S-bends
|
||||||
|
bend_penalty=10.0, # Lower penalty to encourage using the right bend
|
||||||
|
)
|
||||||
|
|
||||||
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
|
# 3. Define Netlist
|
||||||
|
# Net 1: Needs to S-bend around obs1 (gap at y=60-100? No, obs1 is y=20-60).
|
||||||
|
# Start at (10, 40), End at (140, 40).
|
||||||
|
# Obstacle 1 blocks 40-60. Net must go above or below.
|
||||||
|
# Obstacle 2 blocks 80-100 x 0-40.
|
||||||
|
|
||||||
|
# Let's force a path that requires a large bend.
|
||||||
|
netlist = {
|
||||||
|
"large_bend_net": (Port(10, 10, 0), Port(140, 80, 0)),
|
||||||
|
"sbend_net": (Port(10, 50, 0), Port(70, 70, 0)),
|
||||||
|
}
|
||||||
|
net_widths = {"large_bend_net": 2.0, "sbend_net": 2.0}
|
||||||
|
|
||||||
|
# 4. Route
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 5. Check Results
|
||||||
|
for nid, res in results.items():
|
||||||
|
status = "Success" if res.is_valid else "Failed"
|
||||||
|
print(f"{nid}: {status}, collisions={res.collisions}")
|
||||||
|
|
||||||
|
# 6. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, obstacles, bounds)
|
||||||
|
fig.savefig("examples/sbends_radii.png")
|
||||||
|
print("Saved plot to examples/sbends_radii.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/sbends_radii.png
Normal file
BIN
examples/sbends_radii.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -16,9 +16,10 @@ if TYPE_CHECKING:
|
||||||
class CollisionEngine:
|
class CollisionEngine:
|
||||||
"""Manages spatial queries for collision detection."""
|
"""Manages spatial queries for collision detection."""
|
||||||
|
|
||||||
def __init__(self, clearance: float, max_net_width: float = 2.0) -> None:
|
def __init__(self, clearance: float, max_net_width: float = 2.0, safety_zone_radius: float = 0.0021) -> None:
|
||||||
self.clearance = clearance
|
self.clearance = clearance
|
||||||
self.max_net_width = max_net_width
|
self.max_net_width = max_net_width
|
||||||
|
self.safety_zone_radius = safety_zone_radius
|
||||||
self.static_obstacles = rtree.index.Index()
|
self.static_obstacles = rtree.index.Index()
|
||||||
# To store geometries for precise checks
|
# To store geometries for precise checks
|
||||||
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||||
|
|
@ -131,14 +132,14 @@ class CollisionEngine:
|
||||||
|
|
||||||
is_near_start = False
|
is_near_start = False
|
||||||
if start_port:
|
if start_port:
|
||||||
if (abs(ix_minx - start_port.x) < 0.0021 and abs(ix_maxx - start_port.x) < 0.0021 and
|
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) < 0.0021 and abs(ix_maxy - start_port.y) < 0.0021):
|
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_start = True
|
||||||
|
|
||||||
is_near_end = False
|
is_near_end = False
|
||||||
if end_port:
|
if end_port:
|
||||||
if (abs(ix_minx - end_port.x) < 0.0021 and abs(ix_maxx - end_port.x) < 0.0021 and
|
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) < 0.0021 and abs(ix_maxy - end_port.y) < 0.0021):
|
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
|
is_near_end = True
|
||||||
|
|
||||||
if is_near_start or is_near_end:
|
if is_near_start or is_near_end:
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class Straight:
|
||||||
|
|
||||||
ex = start_port.x + dx
|
ex = start_port.x + dx
|
||||||
ey = start_port.y + dy
|
ey = start_port.y + dy
|
||||||
|
|
||||||
if snap_to_grid:
|
if snap_to_grid:
|
||||||
ex = snap_search_grid(ex)
|
ex = snap_search_grid(ex)
|
||||||
ey = snap_search_grid(ey)
|
ey = snap_search_grid(ey)
|
||||||
|
|
@ -89,10 +89,10 @@ class Bend90:
|
||||||
# End port (snapped to lattice)
|
# End port (snapped to lattice)
|
||||||
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
||||||
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
||||||
|
|
||||||
end_orientation = (start_port.orientation + turn_angle) % 360
|
end_orientation = (start_port.orientation + turn_angle) % 360
|
||||||
end_port = Port(ex, ey, float(end_orientation))
|
end_port = Port(ex, ey, float(end_orientation))
|
||||||
|
|
||||||
actual_length = radius * np.pi / 2.0
|
actual_length = radius * np.pi / 2.0
|
||||||
|
|
||||||
# Generate arc geometry
|
# Generate arc geometry
|
||||||
|
|
@ -124,12 +124,12 @@ class SBend:
|
||||||
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
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))
|
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)
|
end_port = Port(ex, ey, start_port.orientation)
|
||||||
|
|
||||||
actual_length = 2 * radius * theta
|
actual_length = 2 * radius * theta
|
||||||
|
|
||||||
# Arc centers and angles (Relative to start orientation)
|
# Arc centers and angles (Relative to start orientation)
|
||||||
direction = 1 if offset > 0 else -1
|
direction = 1 if offset > 0 else -1
|
||||||
|
|
||||||
# Arc 1
|
# Arc 1
|
||||||
c1_angle = rad_start + direction * np.pi / 2
|
c1_angle = rad_start + direction * np.pi / 2
|
||||||
cx1 = start_port.x + radius * np.cos(c1_angle)
|
cx1 = start_port.x + radius * np.cos(c1_angle)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from inire.geometry.components import Bend90, SBend, Straight
|
from inire.geometry.components import Bend90, SBend, Straight
|
||||||
|
from inire.router.config import RouterConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from inire.geometry.components import ComponentResult
|
from inire.geometry.components import ComponentResult
|
||||||
|
|
@ -46,9 +47,46 @@ class AStarNode:
|
||||||
|
|
||||||
|
|
||||||
class AStarRouter:
|
class AStarRouter:
|
||||||
def __init__(self, cost_evaluator: CostEvaluator) -> None:
|
"""Hybrid State-Lattice A* Router."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cost_evaluator: CostEvaluator,
|
||||||
|
node_limit: int = 1000000,
|
||||||
|
straight_lengths: list[float] | None = None,
|
||||||
|
bend_radii: list[float] | None = None,
|
||||||
|
sbend_offsets: list[float] | None = None,
|
||||||
|
sbend_radii: list[float] | None = None,
|
||||||
|
snap_to_target_dist: float = 20.0,
|
||||||
|
bend_penalty: float = 50.0,
|
||||||
|
sbend_penalty: float = 100.0,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the A* Router.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cost_evaluator: The evaluator for path and proximity costs.
|
||||||
|
node_limit: Maximum number of nodes to expand before failing.
|
||||||
|
straight_lengths: List of lengths for straight move expansion.
|
||||||
|
bend_radii: List of radii for 90-degree bend moves.
|
||||||
|
sbend_offsets: List of lateral offsets for S-bend moves.
|
||||||
|
sbend_radii: List of radii for S-bend moves.
|
||||||
|
snap_to_target_dist: Distance threshold for lookahead snapping.
|
||||||
|
bend_penalty: Flat cost penalty for each 90-degree bend.
|
||||||
|
sbend_penalty: Flat cost penalty for each S-bend.
|
||||||
|
"""
|
||||||
self.cost_evaluator = cost_evaluator
|
self.cost_evaluator = cost_evaluator
|
||||||
self.node_limit = 1000000
|
self.config = RouterConfig(
|
||||||
|
node_limit=node_limit,
|
||||||
|
straight_lengths=straight_lengths if straight_lengths is not None else [1.0, 5.0, 25.0],
|
||||||
|
bend_radii=bend_radii if bend_radii is not None else [10.0],
|
||||||
|
sbend_offsets=sbend_offsets if sbend_offsets is not None else [-5.0, -2.0, 2.0, 5.0],
|
||||||
|
sbend_radii=sbend_radii if sbend_radii is not None else [10.0],
|
||||||
|
snap_to_target_dist=snap_to_target_dist,
|
||||||
|
bend_penalty=bend_penalty,
|
||||||
|
sbend_penalty=sbend_penalty,
|
||||||
|
)
|
||||||
|
self.node_limit = self.config.node_limit
|
||||||
self.total_nodes_expanded = 0
|
self.total_nodes_expanded = 0
|
||||||
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
|
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
|
||||||
|
|
||||||
|
|
@ -107,7 +145,7 @@ class AStarRouter:
|
||||||
) -> None:
|
) -> None:
|
||||||
# 1. Snap-to-Target Look-ahead
|
# 1. Snap-to-Target Look-ahead
|
||||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||||
if dist < 30.0:
|
if dist < self.config.snap_to_target_dist:
|
||||||
# A. Try straight exact reach
|
# A. Try straight exact reach
|
||||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||||
rad = np.radians(current.port.orientation)
|
rad = np.radians(current.port.orientation)
|
||||||
|
|
@ -127,30 +165,37 @@ class AStarRouter:
|
||||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||||
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
||||||
try:
|
for radius in self.config.sbend_radii:
|
||||||
res = SBend.generate(current.port, perp, 10.0, net_width)
|
try:
|
||||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
|
res = SBend.generate(current.port, perp, radius, net_width)
|
||||||
except ValueError:
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
|
||||||
pass
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
# 2. Lattice Straights
|
# 2. Lattice Straights
|
||||||
for length in [1.0, 5.0, 25.0]:
|
lengths = self.config.straight_lengths
|
||||||
|
if dist < 5.0:
|
||||||
|
fine_steps = [0.1, 0.5]
|
||||||
|
lengths = sorted(list(set(lengths + fine_steps)))
|
||||||
|
|
||||||
|
for length in lengths:
|
||||||
res = Straight.generate(current.port, length, net_width)
|
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}")
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"S{length}")
|
||||||
|
|
||||||
# 3. Lattice Bends
|
# 3. Lattice Bends
|
||||||
for radius in [10.0]:
|
for radius in self.config.bend_radii:
|
||||||
for direction in ["CW", "CCW"]:
|
for direction in ["CW", "CCW"]:
|
||||||
res = Bend90.generate(current.port, radius, net_width, direction)
|
res = Bend90.generate(current.port, radius, net_width, direction)
|
||||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}")
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}")
|
||||||
|
|
||||||
# 4. Discrete SBends
|
# 4. Discrete SBends
|
||||||
for offset in [-5.0, -2.0, 2.0, 5.0]:
|
for offset in self.config.sbend_offsets:
|
||||||
try:
|
for radius in self.config.sbend_radii:
|
||||||
res = SBend.generate(current.port, offset, 10.0, net_width)
|
try:
|
||||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}")
|
res = SBend.generate(current.port, offset, radius, net_width)
|
||||||
except ValueError:
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}")
|
||||||
pass
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def _add_node(
|
def _add_node(
|
||||||
self,
|
self,
|
||||||
|
|
@ -194,12 +239,10 @@ class AStarRouter:
|
||||||
for move_poly in result.geometry:
|
for move_poly in result.geometry:
|
||||||
dilated_move = move_poly.buffer(dilation)
|
dilated_move = move_poly.buffer(dilation)
|
||||||
curr_p = parent
|
curr_p = parent
|
||||||
# Skip immediate parent
|
|
||||||
seg_idx = 0
|
seg_idx = 0
|
||||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
while curr_p and curr_p.component_result and seg_idx < 100:
|
||||||
if seg_idx > 0:
|
if seg_idx > 0:
|
||||||
for prev_poly in curr_p.component_result.geometry:
|
for prev_poly in curr_p.component_result.geometry:
|
||||||
# Optimization: fast bounding box check
|
|
||||||
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \
|
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \
|
||||||
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \
|
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \
|
||||||
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
|
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
|
||||||
|
|
@ -226,12 +269,10 @@ class AStarRouter:
|
||||||
if move_cost > 1e12:
|
if move_cost > 1e12:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Substantial penalties for turns to favor straights,
|
|
||||||
# but low enough to allow detours in complex environments.
|
|
||||||
if "B" in move_type:
|
if "B" in move_type:
|
||||||
move_cost += 50.0
|
move_cost += self.config.bend_penalty
|
||||||
if "SB" in move_type:
|
if "SB" in move_type:
|
||||||
move_cost += 100.0
|
move_cost += self.config.sbend_penalty
|
||||||
|
|
||||||
g_cost = parent.g_cost + move_cost
|
g_cost = parent.g_cost + move_cost
|
||||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||||
|
|
|
||||||
26
inire/router/config.py
Normal file
26
inire/router/config.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RouterConfig:
|
||||||
|
"""Configuration parameters for the A* Router."""
|
||||||
|
|
||||||
|
node_limit: int = 500000
|
||||||
|
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0])
|
||||||
|
bend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||||
|
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0])
|
||||||
|
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||||
|
snap_to_target_dist: float = 20.0
|
||||||
|
bend_penalty: float = 50.0
|
||||||
|
sbend_penalty: float = 100.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CostConfig:
|
||||||
|
"""Configuration parameters for the Cost Evaluator."""
|
||||||
|
|
||||||
|
unit_length_cost: float = 1.0
|
||||||
|
greedy_h_weight: float = 1.1
|
||||||
|
congestion_penalty: float = 10000.0
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from inire.router.config import CostConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
|
@ -11,16 +13,38 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class CostEvaluator:
|
class CostEvaluator:
|
||||||
"""Calculates total cost f(n) = g(n) + h(n)."""
|
"""Calculates total path and proximity costs."""
|
||||||
|
|
||||||
def __init__(self, collision_engine: CollisionEngine, danger_map: DangerMap) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
collision_engine: CollisionEngine,
|
||||||
|
danger_map: DangerMap,
|
||||||
|
unit_length_cost: float = 1.0,
|
||||||
|
greedy_h_weight: float = 1.1,
|
||||||
|
congestion_penalty: float = 10000.0,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the Cost Evaluator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collision_engine: The engine for intersection checks.
|
||||||
|
danger_map: Pre-computed grid for heuristic proximity costs.
|
||||||
|
unit_length_cost: Cost multiplier per micrometer of path length.
|
||||||
|
greedy_h_weight: Heuristic weighting (A* greedy factor).
|
||||||
|
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
|
||||||
|
"""
|
||||||
self.collision_engine = collision_engine
|
self.collision_engine = collision_engine
|
||||||
self.danger_map = danger_map
|
self.danger_map = danger_map
|
||||||
# Cost weights
|
self.config = CostConfig(
|
||||||
self.unit_length_cost = 1.0
|
unit_length_cost=unit_length_cost,
|
||||||
self.bend_cost_multiplier = 100.0 # Per turn penalty
|
greedy_h_weight=greedy_h_weight,
|
||||||
self.greedy_h_weight = 1.1
|
congestion_penalty=congestion_penalty,
|
||||||
self.congestion_penalty = 10000.0 # Massive multiplier for overlaps
|
)
|
||||||
|
|
||||||
|
# Use config values
|
||||||
|
self.unit_length_cost = self.config.unit_length_cost
|
||||||
|
self.greedy_h_weight = self.config.greedy_h_weight
|
||||||
|
self.congestion_penalty = self.config.congestion_penalty
|
||||||
|
|
||||||
def g_proximity(self, x: float, y: float) -> float:
|
def g_proximity(self, x: float, y: float) -> float:
|
||||||
"""Get proximity cost from the Danger Map."""
|
"""Get proximity cost from the Danger Map."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue