Skip to content

Commit f8f073c

Browse files
committed
started sketching solution
1 parent 6455a4d commit f8f073c

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
TODO - docstring
3+
4+
Algorithm is defined in this paper: https://cdn.aaai.org/ojs/8140/8140-13-11667-1-2-20201228.pdf
5+
"""
6+
7+
import numpy as np
8+
from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import (
9+
Grid,
10+
Interval,
11+
ObstacleArrangement,
12+
Position,
13+
)
14+
from PathPlanning.TimeBasedPathPlanning.BaseClasses import MultiAgentPlanner, StartAndGoal
15+
from PathPlanning.TimeBasedPathPlanning.Node import NodePath
16+
from PathPlanning.TimeBasedPathPlanning.BaseClasses import SingleAgentPlanner
17+
from PathPlanning.TimeBasedPathPlanning.SafeInterval import SafeIntervalPathPlanner
18+
from PathPlanning.TimeBasedPathPlanning.Plotting import PlotNodePaths
19+
import time
20+
21+
class ConflictBasedSearch(MultiAgentPlanner):
22+
23+
@staticmethod
24+
def plan(grid: Grid, start_and_goals: list[StartAndGoal], single_agent_planner_class: SingleAgentPlanner, verbose: bool = False) -> tuple[list[StartAndGoal], list[NodePath]]:
25+
"""
26+
Generate a path from the start to the goal for each agent in the `start_and_goals` list.
27+
Returns the re-ordered StartAndGoal combinations, and a list of path plans. The order of the plans
28+
corresponds to the order of the `start_and_goals` list.
29+
"""
30+
print(f"Using single-agent planner: {single_agent_planner_class}")
31+
32+
# Reserve initial positions
33+
for start_and_goal in start_and_goals:
34+
grid.reserve_position(start_and_goal.start, start_and_goal.index, Interval(0, 10))
35+
36+
# Plan in descending order of distance from start to goal
37+
# TODO: dont bother doing this
38+
start_and_goals = sorted(start_and_goals,
39+
key=lambda item: item.distance_start_to_goal(),
40+
reverse=True)
41+
42+
# first, plan optimally for each agent
43+
# now in a loop:
44+
# *
45+
46+
# paths = []
47+
# for start_and_goal in start_and_goals:
48+
# if verbose:
49+
# print(f"\nPlanning for agent: {start_and_goal}" )
50+
51+
# grid.clear_initial_reservation(start_and_goal.start, start_and_goal.index)
52+
# path = single_agent_planner_class.plan(grid, start_and_goal.start, start_and_goal.goal, verbose)
53+
54+
# if path is None:
55+
# print(f"Failed to find path for {start_and_goal}")
56+
# return []
57+
58+
# agent_index = start_and_goal.index
59+
# grid.reserve_path(path, agent_index)
60+
# paths.append(path)
61+
62+
return (start_and_goals, paths)
63+
64+
def cbs():
65+
66+
verbose = False
67+
show_animation = True
68+
def main():
69+
grid_side_length = 21
70+
71+
start_and_goals = [StartAndGoal(i, Position(1, i), Position(19, 19-i)) for i in range(1, 16)]
72+
obstacle_avoid_points = [pos for item in start_and_goals for pos in (item.start, item.goal)]
73+
74+
grid = Grid(
75+
np.array([grid_side_length, grid_side_length]),
76+
num_obstacles=250,
77+
obstacle_avoid_points=obstacle_avoid_points,
78+
# obstacle_arrangement=ObstacleArrangement.NARROW_CORRIDOR,
79+
obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1,
80+
# obstacle_arrangement=ObstacleArrangement.RANDOM,
81+
)
82+
83+
start_time = time.time()
84+
start_and_goals, paths = ConflictBasedSearch.plan(grid, start_and_goals, SafeIntervalPathPlanner, verbose)
85+
86+
runtime = time.time() - start_time
87+
print(f"\nPlanning took: {runtime:.5f} seconds")
88+
89+
if verbose:
90+
print(f"Paths:")
91+
for path in paths:
92+
print(f"{path}\n")
93+
94+
if not show_animation:
95+
return
96+
97+
PlotNodePaths(grid, start_and_goals, paths)
98+
99+
if __name__ == "__main__":
100+
main()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from dataclasses import dataclass
2+
from typing import Optional, TypeAlias
3+
import heapq
4+
5+
from PathPlanning.TimeBasedPathPlanning.Node import NodePath, Position
6+
7+
AgentId: TypeAlias = int
8+
9+
@dataclass
10+
class Constraint:
11+
position: Position
12+
time: int
13+
14+
@dataclass
15+
class PathConstraint:
16+
constraint: Constraint
17+
shorter_path_agent: AgentId
18+
longer_path_agent: AgentId
19+
20+
@dataclass
21+
class ConstraintTreeNode:
22+
parent_idx = int
23+
constraint: tuple[AgentId, Constraint]
24+
25+
paths: dict[AgentId, NodePath]
26+
cost: int
27+
28+
def __lt__(self, other) -> bool:
29+
# TODO - this feels jank?
30+
return self.cost + self.constrained_path_cost() < other.cost + other.constrained_path_cost()
31+
32+
def get_constraint_point(self) -> Optional[PathConstraint]:
33+
final_t = max(path.goal_reached_time() for path in self.paths)
34+
positions_at_time: dict[Position, AgentId] = {}
35+
for t in range(final_t + 1):
36+
# TODO: need to be REALLY careful that these agent ids are consitent
37+
for agent_id, path in self.paths.items():
38+
position = path.get_position(t)
39+
if position is None:
40+
continue
41+
if position in positions_at_time:
42+
conflicting_agent_id = positions_at_time[position]
43+
this_agent_shorter = self.paths[agent_id].goal_reached_time() < self.paths[conflicting_agent_id].goal_reached_time()
44+
45+
return PathConstraint(
46+
constraint=Constraint(position=position, time=t),
47+
shorter_path_agent= agent_id if this_agent_shorter else conflicting_agent_id,
48+
longer_path_agent= conflicting_agent_id if this_agent_shorter else agent_id
49+
)
50+
return None
51+
52+
def constrained_path_cost(self) -> int:
53+
constrained_path = self.paths[self.constraint[0]]
54+
return constrained_path.goal_reached_time()
55+
56+
class ConstraintTree:
57+
# Child nodes have been created (Maps node_index to ConstraintTreeNode)
58+
expanded_nodes: dict[int, ConstraintTreeNode]
59+
# Need to solve and generate children
60+
nodes_to_expand: heapq[ConstraintTreeNode]
61+
62+
solution: Optional[ConstraintTreeNode] = None
63+
64+
def __init__(self, initial_solution: dict[AgentId, NodePath]):
65+
initial_cost = sum(path.goal_reached_time() for path in initial_solution.values())
66+
heapq.heappush(self.nodes_to_expand, ConstraintTreeNode(constraints={}, paths=initial_solution, cost=initial_cost, parent_idx=-1))
67+
68+
def get_next_node_to_expand(self) -> Optional[ConstraintTreeNode]:
69+
if not self.nodes_to_expand:
70+
return None
71+
return heapq.heappop(self.nodes_to_expand)
72+
73+
def add_node_to_tree(self, node: ConstraintTreeNode) -> bool:
74+
"""
75+
Add a node to the tree and generate children if needed. Returns true if the node is a solution, false otherwise.
76+
"""
77+
node_index = len(self.expanded_nodes)
78+
self.expanded_nodes[node_index] = node
79+
constraint_point = node.get_constraint_point()
80+
if constraint_point is None:
81+
# Don't need to add any constraints, this is a solution!
82+
self.solution = node
83+
return
84+
85+
child_node1 = node
86+
child_node1.constraint = (constraint_point.shorter_path_agent, constraint_point.constraint)
87+
child_node1.parent_idx = node_index
88+
89+
child_node2 = node
90+
child_node2.constraint = (constraint_point.longer_path_agent, constraint_point.constraint)
91+
child_node2.parent_idx = node_index
92+
93+
heapq.heappush(self.nodes_to_expand, child_node1)
94+
heapq.heappush(self.nodes_to_expand, child_node2)
95+
96+
def get_ancestor_constraints(self, parent_index: int):
97+
"""
98+
Get the constraints that were applied to the parent node to generate this node.
99+
"""
100+
constraints = []
101+
while parent_index != -1:
102+
node = self.expanded_nodes[parent_index]
103+
if node.constraint is not None:
104+
constraints.append(node.constraint)
105+
parent_index = node.parent_idx
106+
return constraints

0 commit comments

Comments
 (0)