Download quantum/solvers/CBS_solver.py from JavideuS/Spooky: direct link, hf CLI and curl.
- Browser
- Download file 6.98 kB
-
https://huggingface.co/spaces/JavideuS/Spooky/resolve/main/quantum/solvers/CBS_solver.py
- Command line
-
hf download hf://spaces/JavideuS/Spooky/quantum/solvers/CBS_solver.py
-
curl -L -o CBS_solver.py https://huggingface.co/spaces/JavideuS/Spooky/resolve/main/quantum/solvers/CBS_solver.py
6.98 kB
| from typing import Any, Dict | |
| from .base_solver import BaseSolver | |
| from quantum.utils import preprocess as preprocess_modes | |
| from .cbs_algorithm import ConflictBasedSearch | |
| class CBSSolver(BaseSolver): | |
| """ | |
| Classical MAPF baseline via Conflict-Based Search (CBS): space-time A* | |
| per robot (cbs_algorithm.SpaceTimeAStar) as the low-level solver, with a | |
| high-level constraint-tree search resolving conflicts by branching. Works | |
| with either GridCBSBuilder or GraphCBSBuilder. This class never branches on grid vs. graph itself, | |
| it just asks the builder for vars_per_time, local_index(v), and | |
| get_robot_start_goal(robot_id) to translate between CBS's plain-graph-node-id paths and the same flat | |
| (robot, time, position) index QUBO/ILP solvers use, so decode_path() and | |
| every other BaseSolver post-processing helper work unchanged. | |
| """ | |
| def __init__( | |
| self, | |
| node_limit=5000, | |
| time_limit=60, | |
| normalize_scale=0, | |
| num_reads=1, | |
| max_corrections=0, | |
| verbose_level=2, | |
| **kwargs, | |
| ): | |
| """ | |
| Args: | |
| node_limit: Max constraint-tree nodes to expand (default 5000). | |
| Safety cap so a pathological conflict-branching case can't | |
| hang a sweep β mirrors ILPSolver's time_limit in spirit. | |
| time_limit: Max wall-clock seconds (default 60). Whichever of | |
| node_limit/time_limit is hit first stops the search β but | |
| unlike ILPSolver's HiGHS time_limit, this does NOT return a | |
| best-incumbent-so-far: CBS has no such concept (see | |
| ConflictBasedSearch.solve()'s docstring β every node it | |
| pops either has zero conflicts, and gets returned | |
| immediately, or at least one, and there's no "good but | |
| unproven" state in between). A budget cutoff here means an | |
| empty solution and energy=inf, not a worse-but-valid answer. | |
| """ | |
| super().__init__( | |
| solver="cbs", | |
| normalize_scale=normalize_scale, | |
| num_reads=num_reads, | |
| max_corrections=max_corrections, | |
| verbose_level=verbose_level, | |
| node_limit=node_limit, # forwarded so to_dict()/the manifest records these too | |
| time_limit=time_limit, | |
| **kwargs, | |
| ) | |
| self.node_limit = node_limit | |
| self.time_limit = time_limit | |
| def solve(self, builder, optimization=False, preprocess=True) -> Dict[str, Any]: | |
| """ | |
| Solve the MAPF problem held by builder via CBS. | |
| Args: | |
| builder: GridCBSBuilder or GraphCBSBuilder instance | |
| optimization: Accepted for interface compatibility; unused (CBS | |
| is exact/deterministic search, no variational step). | |
| preprocess: When True (default), builder.build() computes | |
| forward+backward BFS reachability per robot and CBS's | |
| low-level search only considers those cells β see | |
| BaseCBSBuilder.build(). When False, the search considers | |
| every free cell at every step. Always rebuilds (even if | |
| builder.graph already exists) so this flag reliably takes | |
| effect, same reasoning as ILPSolver.solve(). | |
| Returns: | |
| Dictionary containing solution, energy, and raw response | |
| """ | |
| # coerce the mode to the bool this builder expects; "raw" is | |
| # truthy, so passing the mode through would prune in raw mode | |
| builder.build(preprocess=preprocess_modes.applies_bfs_pruning(preprocess)) | |
| problem = builder.problem | |
| robot_nums = problem.get_robot_nums() | |
| T = problem.T | |
| vars_per_time = builder.vars_per_time | |
| total_vars = vars_per_time * T * problem.num_robots | |
| robots_meta = {} | |
| for robot_id, robot in problem.robots.items(): | |
| start_node, goal_node = builder.get_robot_start_goal(robot_id) | |
| robots_meta[robot_id] = { | |
| "start": start_node, | |
| "goal": goal_node, | |
| "start_time": robot.start_time, | |
| "deadline": robot.start_time + robot.T - 1, | |
| } | |
| cbs = ConflictBasedSearch( | |
| builder.graph, node_limit=self.node_limit, time_limit=self.time_limit | |
| ) | |
| paths, meta = cbs.solve(robots_meta, legal_cells=builder.legal_cells) | |
| solution = {idx: 0 for idx in range(total_vars)} | |
| for robot_id, path in paths.items(): | |
| robot_num = robot_nums[robot_id] | |
| robot_offset = robot_num * (vars_per_time * T) | |
| for node_id, t in path: | |
| solution[robot_offset + t * vars_per_time + node_id] = 1 | |
| # Sum-of-costs: each robot's steps away from goal (arrival_time - | |
| # start_time), not padded path length | |
| if len(paths) < problem.num_robots: | |
| energy = float("inf") # some robot has no path at all | |
| else: | |
| energy = sum( | |
| self._robot_cost( | |
| path, robots_meta[rid]["goal"], robots_meta[rid]["start_time"] | |
| ) | |
| for rid, path in paths.items() | |
| ) | |
| self.logger.standard( | |
| f"CBS solve complete: {meta['termination_condition']}, " | |
| f"energy={energy}, nodes_expanded={meta['nodes_expanded']}" | |
| ) | |
| # Write the solved paths back onto problem.robots β QUBO solvers do | |
| # this as a side effect of their windowed loop; CBS has no windowing | |
| # loop, so it has to happen explicitly here, same as ILPSolver. | |
| decoded = self.decode_path(solution, problem) | |
| num_to_id = {num: rid for rid, num in robot_nums.items()} | |
| for robot_num, coords in self.get_robot_paths(decoded).items(): | |
| if not coords: | |
| continue # robot has no path (infeasible) β leave its state untouched | |
| robot = problem.robots[num_to_id[robot_num]] | |
| robot.path = coords | |
| robot.current_position = coords[-1][:2] | |
| robot.active = False | |
| return { | |
| "solution": solution, | |
| "energy": energy, | |
| "raw_response": meta, | |
| "metadata": { | |
| "termination_condition": meta["termination_condition"], | |
| "solver_config": self.to_dict(), | |
| # Single-entry list, not a per-window loop β CBS solves the | |
| # whole horizon in one shot. Wrapped in a list so | |
| # BenchmarkRunner.run_build()'s existing window_stats | |
| # aggregation (benchmark.py) picks it up unchanged | |
| "window_stats": [builder.bfs_stats], | |
| }, | |
| } | |
| def _robot_cost(path, goal, start_time): | |
| t_arrive = path[-1][1] | |
| for node, t in reversed(path): | |
| if node != goal: | |
| break | |
| t_arrive = t | |
| return t_arrive - start_time | |