Spooky / quantum /builder /CBSBuilder.py
JavideuS's picture
Deploy a665039
beeea66 verified
Raw
History Blame Contribute Delete
8.57 kB
import networkx as nx
from quantum.utils.logger import get_logger
from quantum.builder.ILPBuilder import bfs_reachable_sets, reverse_adjacency
class BaseCBSBuilder:
"""
Mirrors BaseILPBuilder's shape (penalty-set stub metadata, common constructor shape, local_index(v))
but build() constructs a plain networkx.Graph instead of a Pyomo model:
CBS has no decision variables/constraints to declare, just a graph for
its space-time A* low-level solver to search over. Concrete grid/graph
subclasses implement build(), vars_per_time, and local_index(v).
"""
def __init__(self, problem, name="cbs", verbose_level=2):
self.problem = problem
self.name = name
self.graph = None
self.legal_cells = None
self.verbose_level = verbose_level
self.logger = get_logger()
# No penalty weights — CBS uses hard constraints (the low-level
# search's reservation table) instead
self.penalties = {
"name": "cbs_hard_constraints",
"constraints": ["reservation_table", "crash", "swap"],
}
def build(self, preprocess=True):
"""Build the search graph for the current problem state and return it."""
raise NotImplementedError
def local_index(self, v):
"""Map a builder-native vertex (grid tuple or graph node id) to the
position component of the flat (robot, time, position) variable
index — same contract as BaseILPBuilder.local_index(), and for the
same reason: CBSSolver packs its result into that shared flat index
so decode_path()/BaseSolver post-processing work unchanged."""
raise NotImplementedError
def get_robot_start_goal(self, robot_id):
"""Return (start_node_id, goal_node_id) in self.graph's node space
for the given robot — the uniform lookup CBSSolver uses regardless
of grid vs graph format."""
raise NotImplementedError
def reset_problem(self):
"""Restore every robot to its initial start state (mirrors BaseILPBuilder.reset_problem)."""
for robot in self.problem.robots.values():
robot.reset()
class GridCBSBuilder(BaseCBSBuilder):
"""CBS builder for grid pathfinding problems."""
def __init__(self, problem, name="cbs_grid", verbose_level=2):
if problem.grid is None:
raise ValueError("Grid representation not available in this problem")
super().__init__(problem, name=name, verbose_level=verbose_level)
self.vars_per_time = problem.grid.M * problem.grid.N
def local_index(self, v):
i, j = v
return i * self.problem.grid.N + j
def get_robot_start_goal(self, robot_id):
robot = self.problem.robots[robot_id]
return self.local_index(robot.current_position), self.local_index(robot.goal)
def build(self, preprocess=True):
problem = self.problem
grid = problem.grid
robots = problem.robots
graph = nx.Graph()
for i in range(grid.M):
for j in range(grid.N):
if (i, j) not in grid.obstacles:
graph.add_node(self.local_index((i, j)), pos=(i, j))
for (i, j), neighbors in grid.adjacency.items():
if (i, j) in grid.obstacles:
continue
u = self.local_index((i, j))
for ni, nj in neighbors:
if (ni, nj) in grid.obstacles:
continue
graph.add_edge(u, self.local_index((ni, nj)))
self.graph = graph
# Forward-from-start and backward-from-goal BFS reachability per
# robot, exactly as GridILPBuilder does
# Used to restrict SpaceTimeAStar's neighbor expansion, not to fix
# ILP-style variables (CBS has none), but the reduction is the same
# in spirit: fewer cells the low-level search has to consider.
in_window_vars = 0
final_vars = 0
self.legal_cells = {} if preprocess else None
if preprocess:
reachable = {v: list(graph.neighbors(v)) + [v] for v in graph.nodes}
reverse_reachable = reverse_adjacency(reachable)
for robot_id, robot in robots.items():
start_id = self.local_index(robot.current_position)
goal_id = self.local_index(robot.goal)
max_steps = robot.T - 1
forward = bfs_reachable_sets(reachable, start_id, max_steps)
backward = bfs_reachable_sets(reverse_reachable, goal_id, max_steps)
legal = [forward[k] & backward[max_steps - k] for k in range(robot.T)]
self.legal_cells[robot_id] = legal
in_window_vars += self.vars_per_time * robot.T
final_vars += sum(len(s) for s in legal)
else:
for robot in robots.values():
in_window_vars += self.vars_per_time * robot.T
final_vars = in_window_vars
reduced = in_window_vars - final_vars
self.bfs_stats = {
"window": 0,
"preprocess": preprocess,
"initial_variables": in_window_vars,
"variables_reduced": reduced,
"final_variables": final_vars,
"reduction_ratio": round(reduced / in_window_vars, 4)
if in_window_vars
else 0,
}
self.logger.standard(
f"CBS graph built: {len(robots)} robots, {graph.number_of_nodes()} free cells"
f"\nBFS Stats: {self.bfs_stats}"
)
return self.graph
class GraphCBSBuilder(BaseCBSBuilder):
"""CBS builder for graph pathfinding problems. Same structure as
GridCBSBuilder, written over the graph's native vertex set instead of
(i, j) grid cells — see BaseCBSBuilder's docstring for what differs."""
def __init__(self, problem, name="cbs_graph", verbose_level=2):
if problem.graph is None:
raise ValueError("Graph representation not available in this problem")
super().__init__(problem, name=name, verbose_level=verbose_level)
self.vars_per_time = len(problem.graph.nodes)
def local_index(self, v):
return v
def get_robot_start_goal(self, robot_id):
return self.problem.get_graph_robot_current_goal(robot_id)
def build(self, preprocess=True):
problem = self.problem
graph_data = problem.graph
robots = problem.robots
graph = nx.Graph()
for node_id in range(len(graph_data.nodes)):
graph.add_node(node_id, pos=tuple(graph_data.nodes[node_id]))
for u, neighbors in graph_data.adjacency.items():
for v, _weight in neighbors:
graph.add_edge(u, v)
self.graph = graph
# Robot start/goal may be stored as raw node ids or as coordinates,
# depending on how the problem was built
start_goal = {a: problem.get_graph_robot_current_goal(a) for a in robots}
in_window_vars = 0
final_vars = 0
self.legal_cells = {} if preprocess else None
if preprocess:
reachable = {v: list(graph.neighbors(v)) + [v] for v in graph.nodes}
reverse_reachable = reverse_adjacency(reachable)
for robot_id, robot in robots.items():
start_id, goal_id = start_goal[robot_id]
max_steps = robot.T - 1
forward = bfs_reachable_sets(reachable, start_id, max_steps)
backward = bfs_reachable_sets(reverse_reachable, goal_id, max_steps)
legal = [forward[k] & backward[max_steps - k] for k in range(robot.T)]
self.legal_cells[robot_id] = legal
in_window_vars += self.vars_per_time * robot.T
final_vars += sum(len(s) for s in legal)
else:
for robot in robots.values():
in_window_vars += self.vars_per_time * robot.T
final_vars = in_window_vars
reduced = in_window_vars - final_vars
self.bfs_stats = {
"window": 0,
"preprocess": preprocess,
"initial_variables": in_window_vars,
"variables_reduced": reduced,
"final_variables": final_vars,
"reduction_ratio": round(reduced / in_window_vars, 4)
if in_window_vars
else 0,
}
self.logger.standard(
f"CBS graph built: {len(robots)} robots, {graph.number_of_nodes()} nodes"
f"\nBFS Stats: {self.bfs_stats}"
)
return self.graph