Skip to content

Commit 7d5b85c

Browse files
committed
working, but slow. maybe duplicate constraint sets in the outer tree?
1 parent f8f073c commit 7d5b85c

File tree

7 files changed

+169
-87
lines changed

7 files changed

+169
-87
lines changed

PathPlanning/TimeBasedPathPlanning/BaseClasses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SingleAgentPlanner(ABC):
2020

2121
@staticmethod
2222
@abstractmethod
23-
def plan(grid: Grid, start: Position, goal: Position, verbose: bool = False) -> NodePath:
23+
def plan(grid: Grid, start: Position, goal: Position, agent_idx: int, verbose: bool = False) -> NodePath:
2424
pass
2525

2626
@dataclass

PathPlanning/TimeBasedPathPlanning/ConflictBasedSearch.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
import numpy as np
88
from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import (
99
Grid,
10-
Interval,
1110
ObstacleArrangement,
1211
Position,
1312
)
13+
from copy import deepcopy
1414
from PathPlanning.TimeBasedPathPlanning.BaseClasses import MultiAgentPlanner, StartAndGoal
1515
from PathPlanning.TimeBasedPathPlanning.Node import NodePath
1616
from PathPlanning.TimeBasedPathPlanning.BaseClasses import SingleAgentPlanner
1717
from PathPlanning.TimeBasedPathPlanning.SafeInterval import SafeIntervalPathPlanner
18+
from PathPlanning.TimeBasedPathPlanning.SpaceTimeAStar import SpaceTimeAStar
1819
from PathPlanning.TimeBasedPathPlanning.Plotting import PlotNodePaths
20+
from PathPlanning.TimeBasedPathPlanning.ConstraintTree import AgentId, AppliedConstraint, ConstraintTree, ConstraintTreeNode, ForkingConstraint
1921
import time
2022

2123
class ConflictBasedSearch(MultiAgentPlanner):
@@ -29,59 +31,90 @@ def plan(grid: Grid, start_and_goals: list[StartAndGoal], single_agent_planner_c
2931
"""
3032
print(f"Using single-agent planner: {single_agent_planner_class}")
3133

34+
initial_solution: dict[AgentId, NodePath] = {}
35+
3236
# 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))
37+
for agent_idx, start_and_goal in enumerate(start_and_goals):
38+
# grid.reserve_position(start_and_goal.start, start_and_goal.index, Interval(0, 10))
39+
path = single_agent_planner_class.plan(grid, start_and_goal.start, start_and_goal.goal, agent_idx, verbose)
40+
initial_solution[AgentId(agent_idx)] = path
41+
42+
constraint_tree = ConstraintTree(initial_solution)
43+
44+
while constraint_tree.nodes_to_expand:
45+
constraint_tree_node = constraint_tree.get_next_node_to_expand()
46+
ancestor_constraints = constraint_tree.get_ancestor_constraints(constraint_tree_node.parent_idx)
47+
print(f"Expanded node: {constraint_tree_node.constraint} with parent: {constraint_tree_node.parent_idx}")
48+
print(f"\tAncestor constraints: {ancestor_constraints}")
49+
50+
if verbose:
51+
print(f"Expanding node with constraint {constraint_tree_node.constraint} and parent {constraint_tree_node.parent_idx} ")
52+
53+
if constraint_tree_node is None:
54+
raise RuntimeError("No more nodes to expand in the constraint tree.")
55+
if not constraint_tree_node.constraint:
56+
# This means we found a solution!
57+
return (start_and_goals, [constraint_tree_node.paths[AgentId(i)] for i in range(len(start_and_goals))])
58+
59+
if not isinstance(constraint_tree_node.constraint, ForkingConstraint):
60+
raise ValueError(f"Expected a ForkingConstraint, but got: {constraint_tree_node.constraint}")
61+
62+
# TODO: contents of this loop should probably be in a helper?
63+
for constrained_agent in constraint_tree_node.constraint.constrained_agents:
64+
paths: dict[AgentId, NodePath] = {}
3565

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)
66+
if verbose:
67+
print(f"\nOuter loop step for agent {constrained_agent}")
4168

42-
# first, plan optimally for each agent
43-
# now in a loop:
44-
# *
69+
applied_constraint = AppliedConstraint(constraint_tree_node.constraint.constraint, constrained_agent)
70+
all_constraints = deepcopy(ancestor_constraints)
71+
all_constraints.append(applied_constraint)
4572

46-
# paths = []
47-
# for start_and_goal in start_and_goals:
48-
# if verbose:
49-
# print(f"\nPlanning for agent: {start_and_goal}" )
73+
if verbose:
74+
print(f"\tall constraints: {all_constraints}")
5075

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)
76+
grid.clear_constraint_points()
77+
grid.apply_constraint_points(all_constraints)
5378

54-
# if path is None:
55-
# print(f"Failed to find path for {start_and_goal}")
56-
# return []
79+
for agent_idx, start_and_goal in enumerate(start_and_goals):
80+
path = single_agent_planner_class.plan(grid, start_and_goal.start, start_and_goal.goal, agent_idx, verbose)
81+
if path is None:
82+
raise RuntimeError(f"Failed to find path for {start_and_goal}")
83+
paths[AgentId(start_and_goal.index)] = path
5784

58-
# agent_index = start_and_goal.index
59-
# grid.reserve_path(path, agent_index)
60-
# paths.append(path)
85+
applied_constraint_parent = deepcopy(constraint_tree_node) #TODO: not sure if deepcopy is actually needed
86+
applied_constraint_parent.constraint = applied_constraint
87+
parent_idx = constraint_tree.add_expanded_node(applied_constraint_parent)
6188

62-
return (start_and_goals, paths)
89+
new_constraint_tree_node = ConstraintTreeNode(paths, parent_idx)
90+
if new_constraint_tree_node.constraint is None:
91+
# This means we found a solution!
92+
return (start_and_goals, [paths[AgentId(i)] for i in range(len(start_and_goals))])
6393

64-
def cbs():
94+
if verbose:
95+
print(f"Adding new constraint tree node with constraint: {new_constraint_tree_node.constraint}")
96+
constraint_tree.add_node_to_tree(new_constraint_tree_node)
6597

6698
verbose = False
6799
show_animation = True
68100
def main():
69101
grid_side_length = 21
70102

71-
start_and_goals = [StartAndGoal(i, Position(1, i), Position(19, 19-i)) for i in range(1, 16)]
103+
# start_and_goals = [StartAndGoal(i, Position(1, i), Position(19, 19-i)) for i in range(1, 16)]
104+
start_and_goals = [StartAndGoal(i, Position(1, 8+i), Position(19, 19-i)) for i in range(5)]
72105
obstacle_avoid_points = [pos for item in start_and_goals for pos in (item.start, item.goal)]
73106

74107
grid = Grid(
75108
np.array([grid_side_length, grid_side_length]),
76109
num_obstacles=250,
77110
obstacle_avoid_points=obstacle_avoid_points,
78-
# obstacle_arrangement=ObstacleArrangement.NARROW_CORRIDOR,
79-
obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1,
111+
obstacle_arrangement=ObstacleArrangement.NARROW_CORRIDOR,
112+
# obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1,
80113
# obstacle_arrangement=ObstacleArrangement.RANDOM,
81114
)
82115

83116
start_time = time.time()
84-
start_and_goals, paths = ConflictBasedSearch.plan(grid, start_and_goals, SafeIntervalPathPlanner, verbose)
117+
start_and_goals, paths = ConflictBasedSearch.plan(grid, start_and_goals, SpaceTimeAStar, verbose)
85118

86119
runtime = time.time() - start_time
87120
print(f"\nPlanning took: {runtime:.5f} seconds")

PathPlanning/TimeBasedPathPlanning/ConstraintTree.py

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,80 @@
22
from typing import Optional, TypeAlias
33
import heapq
44

5-
from PathPlanning.TimeBasedPathPlanning.Node import NodePath, Position
5+
from PathPlanning.TimeBasedPathPlanning.Node import NodePath, Position, PositionAtTime
66

77
AgentId: TypeAlias = int
88

9-
@dataclass
9+
@dataclass(frozen=True)
1010
class Constraint:
1111
position: Position
1212
time: int
1313

1414
@dataclass
15-
class PathConstraint:
15+
class ForkingConstraint:
16+
constraint: Constraint
17+
constrained_agents: tuple[AgentId, AgentId]
18+
19+
@dataclass(frozen=True)
20+
class AppliedConstraint:
1621
constraint: Constraint
17-
shorter_path_agent: AgentId
18-
longer_path_agent: AgentId
22+
constrained_agent: AgentId
1923

2024
@dataclass
2125
class ConstraintTreeNode:
2226
parent_idx = int
23-
constraint: tuple[AgentId, Constraint]
27+
constraint: Optional[ForkingConstraint | AppliedConstraint]
2428

2529
paths: dict[AgentId, NodePath]
2630
cost: int
2731

32+
def __init__(self, paths: dict[AgentId, NodePath], parent_idx: int):
33+
self.paths = paths
34+
self.cost = sum(path.goal_reached_time() for path in paths.values())
35+
self.parent_idx = parent_idx
36+
self.constraint = self.get_constraint_point()
37+
2838
def __lt__(self, other) -> bool:
29-
# TODO - this feels jank?
30-
return self.cost + self.constrained_path_cost() < other.cost + other.constrained_path_cost()
39+
return self.cost < other.cost
40+
41+
def get_constraint_point(self, verbose = False) -> Optional[ForkingConstraint]:
42+
if verbose:
43+
print(f"\tpath for {agent_id}: {path}\n")
3144

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] = {}
45+
final_t = max(path.goal_reached_time() for path in self.paths.values())
46+
positions_at_time: dict[PositionAtTime, AgentId] = {}
3547
for t in range(final_t + 1):
3648
# TODO: need to be REALLY careful that these agent ids are consitent
3749
for agent_id, path in self.paths.items():
3850
position = path.get_position(t)
3951
if position is None:
4052
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(
53+
# print(f"reserving pos/t for {agent_id}: {position} @ {t}")
54+
position_at_time = PositionAtTime(position, t)
55+
if position_at_time in positions_at_time:
56+
conflicting_agent_id = positions_at_time[position_at_time]
57+
58+
if verbose:
59+
print(f"found constraint: {position_at_time} for agents {agent_id} & {conflicting_agent_id}")
60+
return ForkingConstraint(
4661
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
62+
constrained_agents=(AgentId(agent_id), AgentId(conflicting_agent_id))
4963
)
64+
else:
65+
positions_at_time[position_at_time] = AgentId(agent_id)
5066
return None
5167

52-
def constrained_path_cost(self) -> int:
53-
constrained_path = self.paths[self.constraint[0]]
54-
return constrained_path.goal_reached_time()
5568

5669
class ConstraintTree:
5770
# Child nodes have been created (Maps node_index to ConstraintTreeNode)
5871
expanded_nodes: dict[int, ConstraintTreeNode]
5972
# Need to solve and generate children
60-
nodes_to_expand: heapq[ConstraintTreeNode]
61-
62-
solution: Optional[ConstraintTreeNode] = None
73+
nodes_to_expand: heapq #[ConstraintTreeNode]
6374

6475
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))
76+
self.nodes_to_expand = []
77+
self.expanded_nodes = {}
78+
heapq.heappush(self.nodes_to_expand, ConstraintTreeNode(initial_solution, -1))
6779

6880
def get_next_node_to_expand(self) -> Optional[ConstraintTreeNode]:
6981
if not self.nodes_to_expand:
@@ -74,33 +86,26 @@ def add_node_to_tree(self, node: ConstraintTreeNode) -> bool:
7486
"""
7587
Add a node to the tree and generate children if needed. Returns true if the node is a solution, false otherwise.
7688
"""
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)
89+
heapq.heappush(self.nodes_to_expand, node)
9590

96-
def get_ancestor_constraints(self, parent_index: int):
91+
def get_ancestor_constraints(self, parent_index: int) -> list[AppliedConstraint]:
9792
"""
98-
Get the constraints that were applied to the parent node to generate this node.
93+
Get the constraints that were applied to all parent nodes starting with the node at the provided parent_index.
9994
"""
100-
constraints = []
95+
constraints: list[AppliedConstraint] = []
10196
while parent_index != -1:
10297
node = self.expanded_nodes[parent_index]
103-
if node.constraint is not None:
98+
if node.constraint and isinstance(node.constraint, AppliedConstraint):
10499
constraints.append(node.constraint)
100+
else:
101+
print(f"Aha!!! {node.constraint}")
105102
parent_index = node.parent_idx
106103
return constraints
104+
105+
def add_expanded_node(self, node: ConstraintTreeNode) -> int:
106+
"""
107+
Add an expanded node to the tree. Returns the index of this node in the expanded nodes dictionary.
108+
"""
109+
parent_idx = len(self.expanded_nodes)
110+
self.expanded_nodes[parent_idx] = node
111+
return parent_idx

PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from enum import Enum
99
from dataclasses import dataclass
1010
from PathPlanning.TimeBasedPathPlanning.Node import NodePath, Position
11+
from PathPlanning.TimeBasedPathPlanning.ConstraintTree import AppliedConstraint
1112

1213
@dataclass
1314
class Interval:
@@ -31,6 +32,12 @@ def empty_2d_array_of_lists(x: int, y: int) -> np.ndarray:
3132
arr[:] = [[[] for _ in range(y)] for _ in range(x)]
3233
return arr
3334

35+
def empty_3d_array_of_sets(x: int, y: int, z: int) -> np.ndarray:
36+
arr = np.empty((x, y, z), dtype=object)
37+
# assign each element individually - np.full creates references to the same list
38+
arr[:] = [[[set() for _ in range(z)] for _ in range(y)] for _ in range(x)]
39+
return arr
40+
3441
class Grid:
3542
# Set in constructor
3643
grid_size: np.ndarray
@@ -39,6 +46,9 @@ class Grid:
3946
# Obstacles will never occupy these points. Useful to avoid impossible scenarios
4047
obstacle_avoid_points: list[Position] = []
4148

49+
# TODO: do i want this as part of grid?
50+
constraint_points: np.ndarray
51+
4252
# Number of time steps in the simulation
4353
time_limit: int
4454

@@ -58,6 +68,8 @@ def __init__(
5868
self.grid_size = grid_size
5969
self.reservation_matrix = np.zeros((grid_size[0], grid_size[1], self.time_limit))
6070

71+
self.constraint_points = empty_3d_array_of_sets(grid_size[0], grid_size[1], self.time_limit)
72+
6173
if num_obstacles > self.grid_size[0] * self.grid_size[1]:
6274
raise Exception("Number of obstacles is greater than grid size!")
6375

@@ -185,6 +197,19 @@ def generate_narrow_corridor_obstacles(self, obs_count: int) -> list[list[Positi
185197

186198
return obstacle_paths
187199

200+
def apply_constraint_points(self, constraints: list[AppliedConstraint], verbose = False):
201+
for constraint in constraints:
202+
if verbose:
203+
print(f"Applying {constraint=}")
204+
if constraint not in self.constraint_points[constraint.constraint.position.x, constraint.constraint.position.y, constraint.constraint.time]:
205+
self.constraint_points[constraint.constraint.position.x, constraint.constraint.position.y, constraint.constraint.time].add(constraint)
206+
207+
if verbose:
208+
print(f"\tExisting constraints: {self.constraint_points[constraint.constraint.position.x, constraint.constraint.position.y, constraint.constraint.time]}")
209+
210+
def clear_constraint_points(self):
211+
self.constraint_points = empty_3d_array_of_sets(self.grid_size[0], self.grid_size[1], self.time_limit)
212+
188213
"""
189214
Check if the given position is valid at time t
190215
@@ -195,11 +220,19 @@ def generate_narrow_corridor_obstacles(self, obs_count: int) -> list[list[Positi
195220
output:
196221
bool: True if position/time combination is valid, False otherwise
197222
"""
198-
def valid_position(self, position: Position, t: int) -> bool:
223+
def valid_position(self, position: Position, t: int, agent_idx: int) -> bool:
199224
# Check if position is in grid
200225
if not self.inside_grid_bounds(position):
201226
return False
202227

228+
constraints = self.constraint_points[position.x, position.y, t]
229+
for constraint in constraints:
230+
if constraint.constrained_agent == agent_idx:
231+
return False
232+
233+
# if any([constraint.constrained_agent == agent_idx for constraint in constraints]):
234+
# return False
235+
203236
# Check if position is not occupied at time t
204237
return self.reservation_matrix[position.x, position.y, t] == 0
205238

0 commit comments

Comments
 (0)