| from pathlib import Path |
| from datetime import datetime |
| import heapq |
| import json |
| import time |
| from quantum.utils.validation import is_valid_move, get_position_representation |
| from quantum.utils.logger import get_logger |
|
|
|
|
| class BenchmarkRunner: |
| def __init__( |
| self, |
| qubobuilder, |
| solver, |
| num_runs=10, |
| output_dir="results/benchmarks", |
| level=2, |
| preprocess=True, |
| ): |
| """ |
| Run benchmark on a given solver and problem. |
| |
| Args: |
| qubobuilder (QUBOBuilder): A fully initialized QUBO builder |
| solver (QUBOSolver): A solver implementing `.solve(Q)` |
| num_runs (int): Number of times to run the solver |
| output_dir (str): Where to save results |
| level (int): Benchmark verbosity level (1=Summary, 2=Paths, 3=Full) |
| - Level 1: Only statistics, timing, energy, validation pass/fail |
| - Level 2: Level 1 + robot paths and per-robot validation details |
| - Level 3: Level 2 + raw bit solution for debugging |
| preprocess (bool): Forwarded to solver.solve() each run — toggles |
| each solver's own variable-reduction step (BFS reachability |
| pruning for both QUBO and ILP, plus diagonal fixing/windowing |
| for QUBO specifically). |
| """ |
| self.builder = qubobuilder |
| self.problem = qubobuilder.problem |
| self.penalty_set = qubobuilder.penalties |
| self.solver = solver |
| self.num_runs = num_runs |
| self.preprocess = preprocess |
| self.level = max(1, min(3, level)) |
| self.output_dir = Path(output_dir) |
| self.output_dir.mkdir(parents=True, exist_ok=True) |
| self.logger = get_logger() |
|
|
| |
| self.results = { |
| "metadata": { |
| "problem": self.problem.to_dict(), |
| "solver": self.solver.to_dict(), |
| "penalty_set": self.penalty_set, |
| "benchmark_level": self.level, |
| "num_runs": num_runs, |
| "timestamp": datetime.now().isoformat(), |
| }, |
| "runs": [], |
| } |
|
|
| def run_build(self): |
| """Run the benchmark multiple times and store results""" |
| self.logger.minimal(f"\nBenchmarking Problem: {self.problem.name}") |
| self.logger.minimal(f"Using Solver: {self.solver.name}") |
| self.logger.minimal(f"Penalty Set: {self.penalty_set.get('name', 'unnamed')}") |
| self.logger.minimal( |
| f"Benchmark Level: {self.level} ({'Summary' if self.level == 1 else 'Paths' if self.level == 2 else 'Full'})" |
| ) |
| self.logger.minimal("-" * 60) |
|
|
| valid_count = 0 |
| total_solve_time = 0.0 |
|
|
| |
| for run_id in range(1, self.num_runs + 1): |
| self.builder.reset_problem() |
| build_start = time.time() |
| |
| |
| |
| |
| |
| if not hasattr(self.builder, "local_index"): |
| self.builder.build() |
| build_duration = time.time() - build_start |
|
|
| solve_start = time.time() |
| solution = self.solver.solve(self.builder, preprocess=self.preprocess) |
| solve_duration = time.time() - solve_start |
|
|
| self.logger.minimal( |
| f"Build time: {build_duration:.4f}s, Solve time: {solve_duration:.4f}s" |
| ) |
|
|
| |
| path = self.solver.decode_path(solution["solution"], self.problem) |
| validation = is_solution_valid(path, self.problem) |
|
|
| |
| |
| |
| forced_collisions = solution.get("metadata", {}).get( |
| "forced_collisions", [] |
| ) |
| invalid_cause = _attribute_invalid_cause(validation, forced_collisions) |
| if invalid_cause: |
| validation["invalid_cause"] = invalid_cause |
|
|
| |
| if isinstance(solution["energy"], list): |
| total_energy = sum(solution["energy"]) |
| else: |
| total_energy = solution["energy"] |
|
|
| valid_count += int(validation["valid"]) |
| total_solve_time += solve_duration |
|
|
| |
| result = { |
| "run_id": run_id, |
| "timestamp": datetime.now().isoformat(), |
| "valid": validation["valid"], |
| "energy": total_energy, |
| "execution_time_sec": round(solve_duration, 3), |
| } |
| if invalid_cause: |
| result["invalid_cause"] = invalid_cause |
|
|
| |
| |
| |
| termination_condition = solution.get("metadata", {}).get( |
| "termination_condition" |
| ) |
| if termination_condition is not None: |
| result["termination_condition"] = termination_condition |
|
|
| |
| window_stats = solution.get("metadata", {}).get("window_stats", []) |
| if window_stats: |
| |
| total_initial = sum(ws["initial_variables"] for ws in window_stats) |
| total_reduced = sum(ws["variables_reduced"] for ws in window_stats) |
| total_final = sum(ws["final_variables"] for ws in window_stats) |
| avg_reduction = ( |
| total_reduced / total_initial if total_initial > 0 else 0 |
| ) |
|
|
| |
| result["variable_stats"] = { |
| "total_initial_variables": total_initial, |
| "total_variables_reduced": total_reduced, |
| "total_final_variables": total_final, |
| "average_reduction_ratio": round(avg_reduction, 4), |
| "num_windows": len(window_stats), |
| } |
|
|
| |
| if self.level >= 2: |
| result["window_variable_stats"] = window_stats |
|
|
| |
| if self.level >= 2: |
| |
| robot_paths = {} |
| for robot_id, robot in self.problem.robots.items(): |
| robot_paths[robot_id] = robot.path |
|
|
| result["robot_paths"] = robot_paths |
|
|
| |
| validation_details = {} |
| for key, value in validation.get("details", {}).items(): |
| if key.startswith("robot_"): |
| validation_details[key] = value |
|
|
| result["validation_details"] = validation_details |
|
|
| result["solution_statistics"] = _compute_solution_statistics( |
| self.problem, robot_paths, validation_details, validation["valid"] |
| ) |
|
|
| |
| if isinstance(solution["energy"], list): |
| result["window_energies"] = solution["energy"] |
|
|
| |
| if self.level >= 3: |
| result["raw_solution"] = solution["solution"] |
|
|
| self.results["runs"].append(result) |
|
|
| status = "✅ Valid" if validation["valid"] else "❌ Invalid" |
| if not validation["valid"]: |
| self.logger.standard( |
| "Validation details:", validation.get("details", {}) |
| ) |
| self.logger.standard("Reason:", validation.get("reason", "unknown")) |
| self.logger.standard("Message:", validation.get("message", "")) |
| if invalid_cause: |
| self.logger.standard("Cause:", invalid_cause) |
| self.logger.minimal( |
| f"Run {run_id}: {status} | Time: {solve_duration:.2f}s | " |
| f"Energy: {total_energy:.4f}" |
| ) |
| self.logger.minimal(f"Path: {path}") |
| for robot_id, robot in self.problem.robots.items(): |
| self.logger.minimal(f" Robot {robot_id} path: {robot.path}") |
|
|
| avg_solve_time = total_solve_time / self.num_runs if self.num_runs else 0 |
| self.logger.minimal( |
| f"\nAccuracy: {valid_count}/{self.num_runs} valid " |
| f"({valid_count / self.num_runs:.1%}) | " |
| f"Average solve time: {avg_solve_time:.3f}s" |
| ) |
| self.results["summary"] = { |
| "valid_runs": valid_count, |
| "total_runs": self.num_runs, |
| "accuracy": round(valid_count / self.num_runs, 4) if self.num_runs else 0, |
| "average_solve_time_sec": round(avg_solve_time, 4), |
| } |
|
|
| self.save_results() |
| return self.results |
|
|
| def save_results(self): |
| filename = f"benchmark_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" |
| filepath = self.output_dir / filename |
| with open(filepath, "w") as f: |
| |
| serializable_results = convert_tuple_keys_to_str(self.results) |
| json.dump(serializable_results, f, indent=2, default=str) |
| self.logger.minimal(f"\nBenchmark complete. Results saved to {filepath}") |
|
|
|
|
| _FIX_MECHANISM_LABELS = { |
| "bfs": "bfs_fixing", |
| "diag": "diag_fixing", |
| "locked_inactive": "goal_lock", |
| } |
|
|
|
|
| def _graph_shortest_distance(graph, start_node, goal_node): |
| """Dijkstra shortest-path distance between two node indices over a |
| quantum.map.Graph's adjacency (node -> set of (neighbor, weight)). |
| |
| Used as the "optimal length" baseline for graph-problem path |
| efficiency — abs(goal_node - start_node) only means anything if node |
| IDs happen to be laid out linearly along the path, which isn't true |
| for a real graph topology.""" |
| if start_node == goal_node: |
| return 0.0 |
| best = {start_node: 0.0} |
| visited = set() |
| heap = [(0.0, start_node)] |
| while heap: |
| dist, node = heapq.heappop(heap) |
| if node in visited: |
| continue |
| visited.add(node) |
| if node == goal_node: |
| return dist |
| for neighbor, weight in graph.adjacency.get(node, ()): |
| new_dist = dist + weight |
| if new_dist < best.get(neighbor, float("inf")): |
| best[neighbor] = new_dist |
| heapq.heappush(heap, (new_dist, neighbor)) |
| return float("inf") |
|
|
|
|
| def _compute_solution_statistics(problem, robot_paths, validation_details, overall_valid): |
| """ |
| Per-robot and aggregate path-quality statistics for one run: path |
| length, whether the goal was reached, and path efficiency (optimal |
| length / moves actually taken). Complements is_solution_valid()'s |
| pass/fail check with a continuous quality measure. |
| |
| robot_paths: {robot_id: [(i, j, t), ...]} as stored on |
| problem.robots[...].path. path[0] is always the robot's start |
| position (decode_path() includes it), so "moves taken" is |
| len(path) - 1, not len(path) — dividing optimal_length (a move |
| count) by len(path) directly undercounts efficiency by one step |
| for every real path. |
| validation_details: is_solution_valid()'s per-robot {"robot_N": {...}} |
| entries. Only present for robots is_solution_valid actually |
| reached (it returns early on the first individually-invalid |
| robot) — overall_valid is the fallback for any robot it didn't |
| get to. |
| """ |
| robot_ids = list(problem.robots.keys()) |
| stats = {"total_robots": problem.num_robots, "robot_statistics": {}} |
|
|
| successful = 0 |
| for robot_num, robot_id in enumerate(robot_ids): |
| robot = problem.robots[robot_id] |
| path = robot_paths.get(robot_id, []) |
| detail = validation_details.get(f"robot_{robot_num}") |
| robot_valid = detail["valid"] if detail is not None else overall_valid |
| successful += int(robot_valid) |
|
|
| moves_taken = max(0, len(path) - 1) |
| robot_stats = { |
| "path_length": len(path), |
| "moves_taken": moves_taken, |
| "goal_reached": robot.is_at_goal(), |
| "validation_passed": robot_valid, |
| "priority": robot.priority, |
| "start_time": robot.start_time, |
| "time_horizon": robot.T, |
| } |
|
|
| if problem.grid is not None: |
| optimal_length = problem.manhattan_distance(robot.start, robot.goal) |
| elif isinstance(robot.start, int) and isinstance(robot.goal, int): |
| optimal_length = _graph_shortest_distance( |
| problem.graph, robot.start, robot.goal |
| ) |
| else: |
| optimal_length = problem.euclidean_distance(robot.start, robot.goal) |
| robot_stats["optimal_path_length"] = optimal_length |
|
|
| if optimal_length == 0: |
| robot_stats["path_efficiency"] = 1.0 |
| elif moves_taken > 0: |
| robot_stats["path_efficiency"] = optimal_length / moves_taken |
| else: |
| robot_stats["path_efficiency"] = 0.0 |
|
|
| stats["robot_statistics"][robot_id] = robot_stats |
|
|
| stats["successful_robots"] = successful |
| stats["success_rate"] = ( |
| successful / problem.num_robots if problem.num_robots else 0.0 |
| ) |
|
|
| if problem.num_robots > 1: |
| lengths = [s["path_length"] for s in stats["robot_statistics"].values()] |
| efficiencies = [ |
| s["path_efficiency"] for s in stats["robot_statistics"].values() |
| ] |
| stats["aggregate"] = { |
| "total_path_length": sum(lengths), |
| "avg_path_length": sum(lengths) / len(lengths), |
| "max_path_length": max(lengths), |
| "min_path_length": min(lengths), |
| "avg_efficiency": sum(efficiencies) / len(efficiencies), |
| } |
|
|
| return stats |
|
|
|
|
| def _attribute_invalid_cause(validation, forced_collisions): |
| """ |
| Cross-reference a failed validation's reported vertex conflicts against |
| the solver's pre-processing forced-collision log (BaseSolver._flag_forced_collisions, |
| surfaced via solution["metadata"]["forced_collisions"]). |
| |
| A run can be invalid for two very different reasons that call for |
| different fixes: the conflicting cell/time was already forced before |
| K_crash/K_swap ever ran (a pre-processing routing gap), or the QAOA/ |
| annealing sample itself landed on a degenerate state that violates a |
| penalty which was genuinely present in Q (a solver-convergence issue). |
| |
| Returns None if validation passed, otherwise a dict: |
| {"origin": "solver_sampling"} or |
| {"origin": "pre_processing", "matches": [{"cell", "time", "robots", "fixed_by"}, ...]} |
| where "fixed_by" names the mechanism(s) that forced the conflict — e.g. |
| "diag_fixing" when both robots were forced by the same mechanism, or |
| "diag_fixing + goal_lock" when one robot was still being fixed while the |
| other was an already-finished robot locked at its goal. |
| """ |
| if validation.get("valid", True): |
| return None |
|
|
| forced_by_cell_time = {} |
| for fc in forced_collisions: |
| forced_by_cell_time.setdefault((fc["cell"], fc["time"]), []).append(fc) |
|
|
| matched = [] |
| for conflict in validation.get("details", {}).get("conflicts", []): |
| key = (conflict["cell"], conflict["time"]) |
| if key in forced_by_cell_time: |
| matched.extend(forced_by_cell_time[key]) |
|
|
| if not matched: |
| |
| |
| |
| return {"origin": "solver_sampling"} |
|
|
| matches = [] |
| for fc in matched: |
| labels = { |
| _FIX_MECHANISM_LABELS.get(source, source) for _, source in fc["sources"] |
| } |
| matches.append( |
| { |
| "cell": fc["cell"], |
| "time": fc["time"], |
| "robots": fc["robots"], |
| "fixed_by": " + ".join(sorted(labels)), |
| } |
| ) |
|
|
| return {"origin": "pre_processing", "matches": matches} |
|
|
|
|
| def is_solution_valid(solution, problem): |
| """ |
| Checks if the decoded path represents a valid path from start to goal. |
| Accepts a list of ((i, j, t), robot_num) tuples (decoded path), or a list of such paths. |
| Args: |
| solution (list): List of ((i, j, t), robot_num) tuples, |
| or list of such lists. |
| problem: Problem instance (grid or graph) |
| Returns: |
| dict: Validation result with 'valid' flag and optional error details |
| """ |
| problem_type = problem.get_format_type() |
|
|
| if problem_type != "grid" and problem_type != "graph": |
| return { |
| "valid": False, |
| "reason": "unsupported_problem_type", |
| "message": f"Unsupported problem type: {problem_type}", |
| } |
|
|
| result = {"valid": True, "details": {}} |
|
|
| |
| if solution and isinstance(solution[0], list): |
| return [is_solution_valid(path, problem) for path in solution] |
|
|
| |
| positions = list(solution) |
| if not positions: |
| result["valid"] = False |
| result["reason"] = "empty_path" |
| result["message"] = "❌ No path positions found. Invalid sample" |
| return result |
|
|
| result["details"]["path"] = positions |
|
|
| |
| robot_positions = {} |
| for (i, j, t), robot_num in positions: |
| if robot_num not in robot_positions: |
| robot_positions[robot_num] = [] |
| robot_positions[robot_num].append((i, j, t)) |
|
|
| |
| for robot_num, robot_path in robot_positions.items(): |
| robot_id = list(problem.robots.keys())[robot_num] |
| robot_result = _validate_single_robot_path_unified( |
| robot_path, problem, robot_id, robot_num |
| ) |
| if not robot_result["valid"]: |
| result["valid"] = False |
| result["reason"] = f"robot_{robot_num}_invalid" |
| result["message"] = f"❌ Robot {robot_num}: {robot_result['message']}" |
| result["details"][f"robot_{robot_num}"] = robot_result |
| return result |
| result["details"][f"robot_{robot_num}"] = robot_result |
|
|
| |
| occupancy = {} |
| for robot_num, robot_path in robot_positions.items(): |
| for i, j, t in robot_path: |
| key = (i, j, t) |
| occupancy.setdefault(key, []).append(robot_num) |
|
|
| |
| conflicts = [] |
| for (i, j, t), robots in occupancy.items(): |
| if len(robots) > 1: |
| conflicts.append({"cell": (i, j), "time": t, "robots": sorted(robots)}) |
|
|
| |
| |
| |
| |
| position_by_time = { |
| robot_num: {t: (i, j) for (i, j, t) in robot_path} |
| for robot_num, robot_path in robot_positions.items() |
| } |
| robot_nums_sorted = sorted(position_by_time.keys()) |
| swap_conflicts = [] |
| for idx, r1 in enumerate(robot_nums_sorted): |
| for r2 in robot_nums_sorted[idx + 1 :]: |
| times_r1 = position_by_time[r1] |
| times_r2 = position_by_time[r2] |
| for t in set(times_r1) & set(times_r2): |
| if (t + 1) not in times_r1 or (t + 1) not in times_r2: |
| continue |
| if times_r1[t] == times_r2[t + 1] and times_r2[t] == times_r1[t + 1]: |
| swap_conflicts.append( |
| { |
| "cells": (times_r1[t], times_r2[t]), |
| "time": (t, t + 1), |
| "robots": [r1, r2], |
| } |
| ) |
|
|
| if conflicts or swap_conflicts: |
| conflicts.sort(key=lambda c: (c["time"], c["cell"])) |
| swap_conflicts.sort(key=lambda c: (c["time"], c["robots"])) |
| result["valid"] = False |
| reasons = [] |
| messages = [] |
| if conflicts: |
| reasons.append("vertex_conflict") |
| messages.append(f"{len(conflicts)} vertex collision(s)") |
| result["details"]["conflicts"] = conflicts |
| if swap_conflicts: |
| reasons.append("swap_conflict") |
| messages.append(f"{len(swap_conflicts)} swap collision(s)") |
| result["details"]["swap_conflicts"] = swap_conflicts |
| result["reason"] = "+".join(reasons) |
| result["message"] = f"❌ Multi-robot conflict detected: {', '.join(messages)}" |
| return result |
|
|
| result["valid"] = True |
| result["message"] = "✅ Solution is valid" |
| return result |
|
|
|
|
| def _validate_single_robot_path_unified(positions, problem, robot_id, robot_num): |
| """ |
| Unified validation function for single robot paths in both grid and graph |
| problems. |
| |
| Args: |
| positions: List of (i, j, t) tuples for one robot |
| problem: Problem instance (grid or graph) |
| robot_id: Robot identifier/name |
| robot_num: Robot number for error messages |
| |
| Returns: |
| dict: Validation result |
| """ |
| result = {"valid": True, "details": {}} |
|
|
| |
| notation = _get_notation_abstraction(problem) |
|
|
| |
| robot = problem.robots[robot_id] |
| robot_end_time = robot.T + robot.start_time |
|
|
| if not positions: |
| result["valid"] = False |
| result["reason"] = "empty_path" |
| result["message"] = f"❌ Robot {robot_num}: No path positions found" |
| return result |
|
|
| |
| positions.sort(key=lambda x: x[2]) |
|
|
| |
| expected_times = notation.get_expected_time_range(problem, robot_id, robot_end_time) |
| expected_goal = notation.get_goal_position(problem, robot_id) |
|
|
| |
| goal_times = [ |
| t |
| for i, j, t in positions |
| if get_position_representation(problem, (i, j)) == expected_goal |
| ] |
|
|
| if goal_times: |
| first_goal_time = min(goal_times) |
| |
| expected_times = set(t for t in expected_times if t <= first_goal_time) |
|
|
| |
| later_positions = [(i, j, t) for i, j, t in positions if t > first_goal_time] |
| for i, j, t in later_positions: |
| if get_position_representation(problem, (i, j)) != expected_goal: |
| result["valid"] = False |
| result["reason"] = "left_goal_after_reaching" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Left goal position at time {t} " |
| f"after reaching it at time {first_goal_time}" |
| ) |
| result["details"]["first_goal_time"] = first_goal_time |
| result["details"]["left_at_time"] = t |
| return result |
|
|
| all_times = set(t for _, _, t in positions) |
| missing_times = expected_times - all_times |
| |
| |
| |
| extra_times = set(t for t in all_times if t >= robot_end_time) |
|
|
| if missing_times: |
| result["valid"] = False |
| result["reason"] = "missing_time_steps" |
| result["message"] = f"❌ Robot {robot_num}: Missing time steps: {missing_times}" |
| result["details"]["missing_times"] = list(missing_times) |
| return result |
|
|
| if extra_times: |
| result["valid"] = False |
| result["reason"] = "extra_time_steps" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Timesteps exceed robot's timeline " |
| f"[{robot.start_time}, {robot_end_time - 1}]: {extra_times}" |
| ) |
| result["details"]["extra_times"] = list(extra_times) |
| result["details"]["robot_timeline"] = ( |
| f"[{robot.start_time}, {robot_end_time - 1}]" |
| ) |
| return result |
|
|
| |
| time_to_positions = {} |
| for i, j, t in positions: |
| time_to_positions.setdefault(t, []).append((i, j)) |
|
|
| for t, cells in time_to_positions.items(): |
| if len(cells) > 1: |
| result["valid"] = False |
| result["reason"] = "multiple_positions_per_time" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Multiple positions at time {t}: {cells}" |
| ) |
| result["details"]["conflicts"] = {t: cells} |
| return result |
|
|
| |
| start_time = notation.get_start_time(problem, robot_id) |
| start_positions = [(i, j) for i, j, t in positions if t == start_time] |
| if not start_positions: |
| result["valid"] = False |
| result["reason"] = "wrong_start" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: No start position found at time {start_time}" |
| ) |
| return result |
|
|
| expected_start = notation.get_start_position(problem, robot_id) |
| actual_start = get_position_representation(problem, start_positions[0]) |
| if actual_start != expected_start: |
| result["valid"] = False |
| result["reason"] = "wrong_start" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Wrong start position. Expected {expected_start}, got {actual_start}" |
| ) |
| result["details"]["expected_start"] = expected_start |
| result["details"]["actual_start"] = actual_start |
| return result |
|
|
| |
| |
| if not goal_times: |
| result["valid"] = False |
| result["reason"] = "goal_not_reached" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Goal position {expected_goal} never reached" |
| ) |
| result["details"]["goal"] = expected_goal |
| return result |
|
|
| |
| last_pos = None |
| for i, j, t in positions: |
| current_pos = (i, j) |
|
|
| |
| if notation.has_obstacle_check(): |
| obstacles = notation.get_obstacles(problem) |
| if current_pos in obstacles: |
| result["valid"] = False |
| result["reason"] = "obstacle_collision" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Path goes through obstacle at {current_pos} at time {t}" |
| ) |
| result["details"]["collisions"] = [(*current_pos, t)] |
| return result |
|
|
| if last_pos is not None: |
| last_i, last_j, last_t = last_pos |
|
|
| |
| if ( |
| get_position_representation(problem, (last_i, last_j)) == expected_goal |
| and get_position_representation(problem, current_pos) == expected_goal |
| ): |
| last_pos = (i, j, t) |
| continue |
|
|
| |
| if not is_valid_move(problem, (last_i, last_j), current_pos): |
| result["valid"] = False |
| result["reason"] = "invalid_move" |
| result["message"] = ( |
| f"❌ Robot {robot_num}: Invalid move from ({last_i}, {last_j}, {last_t}) to ({i}, {j}, {t})" |
| ) |
| result["details"]["invalid_moves"] = [(last_i, last_j, last_t, i, j, t)] |
| return result |
| last_pos = (i, j, t) |
|
|
| |
| goal_times = [ |
| t |
| for i, j, t in positions |
| if get_position_representation(problem, (i, j)) == expected_goal |
| ] |
| result["details"]["goal_times"] = goal_times |
|
|
| result["valid"] = True |
| result["reason"] = "valid_path" |
| result["message"] = f"✅ Robot {robot_num}: Solution is valid" |
| return result |
|
|
|
|
| def _get_notation_abstraction(problem): |
| """Get the appropriate notation abstraction for the problem type.""" |
| problem_type = problem.get_format_type() |
|
|
| if problem_type == "grid": |
| return GridNotation() |
| elif problem_type in ["graph", "both"]: |
| return GraphNotation() |
| else: |
| raise ValueError(f"Unsupported problem type: {problem_type}") |
|
|
|
|
| class BaseNotation: |
| """Base notation abstraction with common methods.""" |
|
|
| def get_expected_time_range(self, problem, robot_id, T): |
| """Get expected time range for robot.""" |
| robot = problem.robots[robot_id] |
| return set(range(robot.start_time, T)) |
|
|
| def get_start_time(self, problem, robot_id): |
| """Get start time for robot.""" |
| robot = problem.robots[robot_id] |
| return robot.start_time |
|
|
|
|
| class GridNotation(BaseNotation): |
| """Notation abstraction for grid problems.""" |
|
|
| def get_start_position(self, problem, robot_id): |
| """Get start position for robot.""" |
| robot = problem.robots[robot_id] |
| return robot.start |
|
|
| def get_goal_position(self, problem, robot_id): |
| """Get goal position for robot.""" |
| robot = problem.robots[robot_id] |
| return robot.goal |
|
|
| def has_obstacle_check(self): |
| """Whether this notation checks for obstacles.""" |
| return True |
|
|
| def get_obstacles(self, problem): |
| """Get obstacles for the problem.""" |
| return problem.grid.obstacles |
|
|
|
|
| class GraphNotation(BaseNotation): |
| """Notation abstraction for graph problems.""" |
|
|
| def get_start_position(self, problem, robot_id): |
| """Get start node for robot.""" |
| robot = problem.robots[robot_id] |
| start_node = ( |
| robot.start |
| if isinstance(robot.start, int) |
| else problem.graph.get_node_from_position(robot.start) |
| ) |
| return start_node |
|
|
| def get_goal_position(self, problem, robot_id): |
| """Get goal node for robot.""" |
| _, goal_node = problem.get_graph_robot_current_goal(robot_id) |
| return goal_node |
|
|
| def has_obstacle_check(self): |
| """Whether this notation checks for obstacles (graphs don't).""" |
| return False |
|
|
| def get_obstacles(self, problem): |
| """Get obstacles for the problem (not used for graphs).""" |
| return set() |
|
|
|
|
| def convert_tuple_keys_to_str(obj): |
| """ |
| Recursively convert tuple keys in dictionaries to strings. |
| This is useful for serializing dictionaries with tuple keys to JSON. |
| """ |
| if isinstance(obj, dict): |
| return { |
| str(k) if isinstance(k, tuple) else k: convert_tuple_keys_to_str(v) |
| for k, v in obj.items() |
| } |
| elif isinstance(obj, list): |
| return [convert_tuple_keys_to_str(i) for i in obj] |
| else: |
| return obj |
|
|