| from .base_qubo import BaseQUBO |
| import numpy as np |
|
|
|
|
| class GraphQUBO(BaseQUBO): |
| def __init__( |
| self, |
| problem, |
| penalties, |
| name="graph", |
| var_limit=156, |
| window_max_steps=None, |
| distance_scaling="enhanced_linear", |
| robot_window_limits=None, |
| log_reductions=True, |
| verbose_level=2, |
| ): |
| self.graph = problem.graph |
| self.num_nodes = len(self.graph.nodes) |
| super().__init__( |
| problem, |
| penalties, |
| name=name, |
| var_limit=var_limit, |
| window_max_steps=window_max_steps, |
| distance_scaling=distance_scaling, |
| robot_window_limits=robot_window_limits, |
| log_reductions=log_reductions, |
| verbose_level=verbose_level, |
| ) |
| |
| |
| self.initial_num_vars = ( |
| self.num_nodes * self.problem.T * self.problem.num_robots |
| ) |
|
|
| |
| |
| |
| self.P_connectivity_per_robot = {} |
| self.P_obs = self.compute_spatial_obstacle_potential() |
|
|
| self.logger.standard("Window max steps:", self.max_window_size()) |
|
|
| def build(self, constraints_to_apply=None): |
| """Build the QUBO dictionary for graph-based pathfinding.""" |
| if constraints_to_apply is None: |
| penalty_to_constraint = { |
| "K_hot": "one_hot", |
| "K_adj": "adjacency_reward", |
| "K_start": "start", |
| "K_goal": "goal", |
| "K_lock": "lock", |
| "K_bt": "backtracking", |
| "K_crash": "crash", |
| "K_swap": "swap", |
| } |
| constraints_to_apply = [ |
| v for k, v in penalty_to_constraint.items() if k in self.penalties |
| ] |
| |
| |
| |
| if "swap" in constraints_to_apply and "crash" in constraints_to_apply: |
| constraints_to_apply.remove("crash") |
|
|
| self.Q = {} |
|
|
| if "one_hot" in constraints_to_apply: |
| self.apply_one_hot() |
| if "start" in constraints_to_apply: |
| self.apply_start_penalty() |
| if "goal" in constraints_to_apply: |
| self.apply_goal_penalty() |
| if "adjacency" in constraints_to_apply: |
| self.apply_adjacency_constraint() |
| if "lock" in constraints_to_apply: |
| self.apply_lock_after_goal() |
| if "adjacency_reward" in constraints_to_apply: |
| self.apply_adjacency_reward() |
| if "backtracking" in constraints_to_apply: |
| self.apply_backtracking_penalty() |
| if "crash" in constraints_to_apply: |
| self.apply_crash_penalty() |
| if "swap" in constraints_to_apply: |
| self.apply_swap_penalty() |
| if "multi_robot_proximity" in constraints_to_apply: |
| self.apply_multi_robot_proximity_penalty() |
|
|
| return self.Q |
|
|
| def _nodes(self, robot_id, t): |
| """Return active node IDs for (robot_id, t), falling back to all nodes.""" |
| if self._active_cells is not None: |
| return self._active_cells.get((robot_id, t), []) |
| return range(self.num_nodes) |
|
|
| def get_logical_variables(self): |
| """ |
| Returns (fixed_ones, active_cells): |
| - fixed_ones: {flat_idx: 1} for variables known to be 1. |
| Absent entries are implicitly 0 — no zeros stored. |
| - active_cells: {(robot_id, t): [node_id, ...]} built directly from BFS. |
| """ |
| fixed_ones = {} |
| active_cells = {} |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + robot.start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| start_node, goal_node = self.problem.get_graph_robot_current_goal(robot_id) |
| start_idx = start_node + (self.num_nodes * start) + robot_offset |
| fixed_ones[start_idx] = 1 |
| self.logger.debug(start_idx, "fixed to 1 for robot", robot_id) |
|
|
| reachable = self.reachable_positions_aggressive( |
| robot, start_node, start, end |
| ) |
|
|
| if goal_node in reachable.get(start + 1, set()): |
| self.logger.standard( |
| f"Goal is reachable at timestep 1 for robot {robot_id}. Fixing instantaneous path." |
| ) |
| active_cells[(robot_id, start)] = [start_node] |
| for t in range(start + 1, end): |
| goal_idx = goal_node + (self.num_nodes * t) + robot_offset |
| fixed_ones[goal_idx] = 1 |
| active_cells[(robot_id, t)] = [goal_node] |
| else: |
| active_cells[(robot_id, start)] = [start_node] |
| for t in range(start + 1, end): |
| active_cells[(robot_id, t)] = list(reachable.get(t, {goal_node})) |
|
|
| return fixed_ones, active_cells |
|
|
| def apply_one_hot(self): |
| """Apply one-hot constraint: exactly one node per time step per robot.""" |
| K_hot = self.penalties["K_hot"] |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| for t in range(start, end): |
| indices = [ |
| node_id + (self.num_nodes * t) + robot_offset |
| for node_id in self._nodes(robot_id, t) |
| ] |
|
|
| for n in indices: |
| self.Q[(n, n)] = self.Q.get((n, n), 0) - K_hot |
|
|
| for i, n in enumerate(indices): |
| for m in indices[i + 1 :]: |
| self.Q[(n, m)] = self.Q.get((n, m), 0) + 2 * K_hot |
|
|
| def apply_start_penalty(self): |
| """Apply start node penalty: must start at the given node for each robot.""" |
| K_start = self.penalties["K_start"] |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
|
|
| start_time = self.problem.robots[robot_id].start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| |
| start_node = self.problem.get_graph_robot_current_goal(robot_id)[0] |
| start_idx = start_node + (start * self.num_nodes) + robot_offset |
| self.Q[(start_idx, start_idx)] = ( |
| self.Q.get((start_idx, start_idx), 0) - K_start |
| ) |
|
|
| def apply_goal_penalty(self): |
| """Apply goal node penalty: encourage reaching the goal for each robot.""" |
| K_goal = self.penalties["K_goal"] |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot = self.problem.robots[robot_id] |
| start_time = robot.start_time |
| end_time = robot.T + start_time |
|
|
| if end_time > self.current_T + self.t_max: |
| |
| self.apply_goal_approximation_penalty(robot_id) |
| else: |
| |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
|
|
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end = end_time - self.current_T |
| goal_node = self.problem.get_graph_robot_current_goal(robot_id)[1] |
| window_constant = 1 + (0.6 / end) |
|
|
| for t in range(start + 1, end): |
| goal_idx = goal_node + (self.num_nodes * t) + robot_offset |
| time_factor = 1 + ((t - start) / (end - start)) |
| self.Q[(goal_idx, goal_idx)] = ( |
| self.Q.get((goal_idx, goal_idx), 0) |
| - K_goal * time_factor * window_constant |
| ) |
|
|
| def apply_lock_after_goal(self): |
| """Apply lock-after-goal constraint: once at goal, stay there for each robot.""" |
| K_lock = self.penalties["K_lock"] |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| |
| goal_node = self.problem.get_graph_robot_current_goal(robot_id)[1] |
|
|
| for t in range(start, end - 1): |
| if goal_node not in self._nodes(robot_id, t): |
| continue |
| goal_idx_t = goal_node + (self.num_nodes * t) + robot_offset |
| goal_idx_t1 = goal_node + (self.num_nodes * (t + 1)) + robot_offset |
| self.Q[(goal_idx_t, goal_idx_t)] = ( |
| self.Q.get((goal_idx_t, goal_idx_t), 0) + K_lock |
| ) |
| self.Q[(goal_idx_t, goal_idx_t1)] = ( |
| self.Q.get((goal_idx_t, goal_idx_t1), 0) - K_lock |
| ) |
|
|
| def apply_adjacency_constraint(self): |
| """ |
| Apply adjacency constraint: only move between connected nodes for each robot. |
| It enforces edge movements and penalizes non-adjacent moves. |
| """ |
| K_adj = self.penalties["K_adj"] |
| adjacency = self.graph.adjacency |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| for t in range(start, end - 1): |
| next_active = set(self._nodes(robot_id, t + 1)) |
| for node_i in self._nodes(robot_id, t): |
| n = node_i + (self.num_nodes * t) + robot_offset |
| for node_j, weight in adjacency[node_i]: |
| if self._active_cells is not None and node_j not in next_active: |
| continue |
| m = node_j + (self.num_nodes * (t + 1)) + robot_offset |
| self.Q[(n, m)] = self.Q.get((n, m), 0) - K_adj * weight |
|
|
| for node_j in next_active: |
| if node_j != node_i and (node_j, 1.0) not in adjacency[node_i]: |
| m = node_j + (self.num_nodes * (t + 1)) + robot_offset |
| self.Q[(n, m)] = self.Q.get((n, m), 0) + K_adj |
|
|
| def apply_adjacency_reward(self): |
| """Apply adjacency reward: encourage moving to adjacent nodes for each robot.""" |
| K_adj = self.penalties["K_adj"] |
| adjacency = self.graph.adjacency |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| for t in range(start, end - 1): |
| next_active = set(self._nodes(robot_id, t + 1)) |
| for node_i in self._nodes(robot_id, t): |
| n = node_i + (self.num_nodes * t) + robot_offset |
| self.Q[(n, n)] = self.Q.get((n, n), 0) + K_adj |
| for node_j, weight in adjacency[node_i]: |
| if self._active_cells is not None and node_j not in next_active: |
| continue |
| m = node_j + (self.num_nodes * (t + 1)) + robot_offset |
| self.Q[(n, m)] = self.Q.get((n, m), 0) - K_adj * weight |
|
|
| def calculate_euclidean_penalty(self, raw_dist, K_goal_approx, time_factor): |
| """ |
| Calculate Euclidean distance penalty using the specified scaling method. |
| |
| Args: |
| raw_dist: Raw Euclidean distance |
| K_goal_approx: Goal approximation penalty coefficient |
| time_factor: Time-based scaling factor |
| |
| Returns: |
| K_dis: Calculated distance penalty |
| """ |
| if self.distance_scaling == "enhanced_linear": |
| dist_to_goal = raw_dist * 0.165 |
| K_dis = K_goal_approx * (1 / (0.7 + dist_to_goal)) * time_factor |
|
|
| elif self.distance_scaling == "exponential": |
| dist_to_goal = raw_dist * 1.2 |
| K_dis = K_goal_approx * (1 / (1 + dist_to_goal)) * time_factor |
|
|
| elif self.distance_scaling == "quadratic": |
| dist_to_goal = raw_dist**1.3 |
| K_dis = K_goal_approx * (1 / (1 + dist_to_goal)) * time_factor |
|
|
| elif self.distance_scaling == "logarithmic": |
| import numpy as np |
|
|
| dist_to_goal = np.log(1 + raw_dist * 2) |
| K_dis = K_goal_approx * (1 / (1 + dist_to_goal)) * time_factor |
|
|
| elif self.distance_scaling == "adaptive": |
| num_nodes = self.num_nodes |
| if num_nodes <= 9: |
| dist_to_goal = raw_dist * 0.4 |
| K_dis = K_goal_approx * (1 / (0.2 + dist_to_goal)) * time_factor |
| elif num_nodes <= 25: |
| dist_to_goal = raw_dist * 0.8 |
| K_dis = K_goal_approx * (1 / (0.4 + dist_to_goal)) * time_factor |
| else: |
| dist_to_goal = raw_dist * 1.2 |
| K_dis = K_goal_approx * (1 / (0.8 + dist_to_goal)) * time_factor |
|
|
| else: |
| dist_to_goal = raw_dist * 2 |
| K_dis = K_goal_approx * (1 / (1 + dist_to_goal)) * time_factor |
|
|
| return K_dis |
|
|
| def _compute_node_connectivity_potential(self, goal_node=None, start_node=None): |
| """ |
| Compute goal-oriented and directionally-aware connectivity potential for each node. |
| |
| This measures TWO factors: |
| 1. How many neighbors move you CLOSER to the goal (goal-oriented) |
| 2. How many neighbors are DIRECTIONALLY ALIGNED with the path from start to goal |
| |
| Nodes whose neighbors point away from the start→goal direction get higher potential. |
| |
| Args: |
| goal_node: Target node ID (if None, uses simple degree-based) |
| start_node: Current position node ID (if None, ignores directional factor) |
| |
| Returns: |
| Array of potential values, one per node. |
| """ |
| P_nodes = np.zeros(self.num_nodes) |
|
|
| |
| if goal_node is None: |
| |
| degrees = np.zeros(self.num_nodes) |
| for node_id in range(self.num_nodes): |
| degrees[node_id] = len(self.graph.adjacency.get(node_id, [])) |
|
|
| max_degree = max(degrees) if max(degrees) > 0 else 1 |
|
|
| for node_id in range(self.num_nodes): |
| normalized_degree = degrees[node_id] / max_degree |
| P_nodes[node_id] = np.exp(-(normalized_degree**2)) |
| else: |
| |
| goal_pos = self.graph.get_node_position(goal_node) |
| if goal_pos is None: |
| return P_nodes |
|
|
| |
| start_pos = None |
| if start_node is not None: |
| start_pos = self.graph.get_node_position(start_node) |
|
|
| for node_id in range(self.num_nodes): |
| node_pos = self.graph.get_node_position(node_id) |
| if node_pos is None: |
| continue |
|
|
| |
| |
| current_dist = self.problem.euclidean_distance(node_pos, goal_pos) |
|
|
| |
| neighbors = self.graph.adjacency.get(node_id, []) |
| total_neighbors = len(neighbors) |
|
|
| if total_neighbors == 0: |
| P_nodes[node_id] = 1.0 |
| continue |
|
|
| |
| good_neighbors = 0 |
| for neighbor_id, _ in neighbors: |
| neighbor_pos = self.graph.get_node_position(neighbor_id) |
| if neighbor_pos is not None: |
| neighbor_dist = self.problem.euclidean_distance( |
| neighbor_pos, goal_pos |
| ) |
| if neighbor_dist < current_dist: |
| good_neighbors += 1 |
|
|
| |
| |
| |
| |
| |
| P_nodes[node_id] = 1.0 / (1.0 + good_neighbors) |
|
|
| return P_nodes |
|
|
| def compute_spatial_obstacle_potential(self, sigma=1.5, isolation_threshold=3): |
| """ |
| Compute obstacle potential using node positions and connectivity patterns. |
| Nodes with few connections are likely near obstacles/boundaries and create |
| repulsive potential fields similar to the grid-based approach. |
| |
| Args: |
| sigma: Controls spatial decay (higher = wider influence) |
| isolation_threshold: Nodes with fewer neighbors are considered near obstacles |
| |
| Returns: |
| Array of potential values per node |
| """ |
| P_nodes = np.zeros(self.num_nodes) |
|
|
| |
| obstacle_nodes = [] |
| all_positions = {} |
|
|
| for node_id in range(self.num_nodes): |
| pos = self.graph.get_node_position(node_id) |
| if pos is not None: |
| all_positions[node_id] = pos |
|
|
| |
| neighbors = self.graph.adjacency.get(node_id, []) |
| if len(neighbors) <= isolation_threshold: |
| obstacle_nodes.append((node_id, pos)) |
|
|
| if len(obstacle_nodes) == 0 or len(all_positions) == 0: |
| return P_nodes |
|
|
| |
| for node_id, node_pos in all_positions.items(): |
| potential = 0.0 |
|
|
| for obs_id, obs_pos in obstacle_nodes: |
| if node_id == obs_id: |
| |
| potential += 1.0 |
| else: |
| |
| dist = self.problem.euclidean_distance(node_pos, obs_pos) |
| potential += np.exp(-(dist**2) / (2 * sigma**2)) |
|
|
| P_nodes[node_id] = potential |
|
|
| |
| if np.max(P_nodes) > 0: |
| P_nodes /= np.max(P_nodes) |
|
|
| return P_nodes |
|
|
| def apply_goal_approximation_penalty(self, robot_id): |
| """ |
| Apply goal approximation penalty using Euclidean distance heuristic. |
| Encourages getting closer to the goal when it cannot be reached in current window. |
| Includes goal-oriented connectivity potential to avoid dead-ends. |
| """ |
| K_goal_approx = self.penalties.get("K_goal_approx", 0.5) |
| if K_goal_approx == 0: |
| return |
|
|
| K_deadend_repel = ( |
| 0.4 |
| ) |
| K_repel = 1.0 |
|
|
| robot_nums = self.problem.get_robot_nums() |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| goal_node = self.problem.get_graph_robot_current_goal(robot_id)[1] |
| goal_pos = self.graph.get_node_position(goal_node) |
|
|
| |
| start_node = self.problem.get_graph_robot_current_goal(robot_id)[0] |
|
|
| |
| |
| if robot_id not in self.P_connectivity_per_robot: |
| self.P_connectivity_per_robot[robot_id] = ( |
| self._compute_node_connectivity_potential(goal_node, start_node) |
| ) |
|
|
| for t in range(start + 1, self.t_max): |
| time_factor = (1.2) ** (5 * (t - start) / (self.t_max - start)) |
|
|
| for node_id in self._nodes(robot_id, t): |
| node_pos = self.graph.get_node_position(node_id) |
| if node_pos is None or goal_pos is None: |
| continue |
|
|
| |
| raw_dist = self.problem.euclidean_distance(node_pos, goal_pos) |
| K_dis = self.calculate_euclidean_penalty( |
| raw_dist, K_goal_approx, time_factor |
| ) |
|
|
| |
| |
| |
| |
| K_repel_val = self.penalties.get("K_deadend_repel", 0.2) |
| K_deadend = ( |
| K_repel_val * self.P_connectivity_per_robot[robot_id][node_id] |
| ) |
|
|
| K_obs = K_repel |
|
|
| var_idx = node_id + (self.num_nodes * t) + robot_offset |
| |
| self.Q[(var_idx, var_idx)] = ( |
| self.Q.get((var_idx, var_idx), 0) - K_dis + K_obs + K_deadend |
| ) |
|
|
| def apply_backtracking_penalty(self): |
| """Apply backtracking penalty: discourage revisiting nodes for each robot.""" |
| K_bt = self.penalties["K_bt"] |
| robot_nums = self.problem.get_robot_nums() |
|
|
| for robot_id in self.get_active_robot_in_window(): |
| robot_offset = robot_nums[robot_id] * (self.num_nodes * self.total_t) |
| robot = self.problem.robots[robot_id] |
|
|
| start_time = robot.start_time |
| start = 0 |
| if self.current_T < start_time: |
| start = start_time - self.current_T |
|
|
| end_time = robot.T + start_time |
| end = end_time - self.current_T |
| if end_time > self.current_T + self.t_max: |
| end = self.t_max |
|
|
| goal_node = self.problem.get_graph_robot_current_goal(robot_id)[1] |
|
|
| |
| |
| |
| active_sets = {t: set(self._nodes(robot_id, t)) for t in range(start, end)} |
| all_reachable = set() |
| for t in range(start, end): |
| all_reachable.update(active_sets[t]) |
|
|
| for node_i in all_reachable: |
| if node_i == goal_node: |
| continue |
| active_ts = [t for t in range(start, end) if node_i in active_sets[t]] |
| for idx1, t1 in enumerate(active_ts): |
| n1 = node_i + (self.num_nodes * t1) + robot_offset |
| for t2 in active_ts[idx1 + 1 :]: |
| n2 = node_i + (self.num_nodes * t2) + robot_offset |
| self.Q[(n1, n2)] = self.Q.get((n1, n2), 0) + K_bt |
|
|
| if robot.active and robot.path: |
| len_sol = len(robot.path) |
| for t in range(start, end): |
| for p_idx, pos in enumerate(robot.path): |
| node_id = self.graph.get_node_from_position(pos[:2]) |
| if node_id not in active_sets[t]: |
| continue |
| n = node_id + (self.num_nodes * t) + robot_offset |
| time_factor = (1 + (len_sol - p_idx)) / len_sol |
| self.Q[(n, n)] = self.Q.get((n, n), 0) + K_bt * time_factor |
|
|
| def apply_crash_penalty(self): |
| K_crash = self.penalties.get("K_crash", 0) |
| robot_nums = self.problem.get_robot_nums() |
| active_robots_per_timestep = self.get_active_robots_per_timestep_in_window() |
| for t, active_robots in active_robots_per_timestep.items(): |
| if len(active_robots) < 2: |
| continue |
| t_window = t - self.current_T |
| for robot_id1 in active_robots: |
| for robot_id2 in active_robots: |
| if robot_nums[robot_id1] >= robot_nums[robot_id2]: |
| continue |
| robot_offset1 = robot_nums[robot_id1] * ( |
| self.num_nodes * self.total_t |
| ) |
| robot_offset2 = robot_nums[robot_id2] * ( |
| self.num_nodes * self.total_t |
| ) |
| shared_nodes = set(self._nodes(robot_id1, t_window)) & set( |
| self._nodes(robot_id2, t_window) |
| ) |
| for node_i in shared_nodes: |
| idx1 = node_i + (self.num_nodes * t_window) + robot_offset1 |
| idx2 = node_i + (self.num_nodes * t_window) + robot_offset2 |
| self.Q[(idx1, idx2)] = self.Q.get((idx1, idx2), 0) + K_crash |
|
|
| def apply_swap_penalty(self): |
| """ |
| Approximate inter-robot swap-collision penalty (graph equivalent of |
| QUBOBuilder.apply_swap_penalty — cells become nodes). |
| |
| P_swap = K_crash * r1[t]*r2[t] + K_swap * (r1[t+1]*r2[t] + r1[t]*r2[t+1]) |
| |
| Summed over nodes shared by two robots, this penalizes both a true swap |
| (robot1 moves into a node as robot2 moves out, and vice versa at the |
| neighboring node) and same-node/same-time occupancy (the crash case). |
| The two are weighted independently: the same-node/same-time term reuses |
| K_crash as-is (identical to apply_crash_penalty, so crash protection |
| doesn't get diluted), while the cross-time terms get their own K_swap. |
| Those cross-time terms also fire when one robot simply follows another |
| into a just-vacated node, which isn't an actual collision — an accepted |
| overconstraint of this pairwise approximation vs. the exact ancilla-based |
| formulation — so K_swap is the knob to tune down if that false-positive |
| cost outweighs the benefit, independently of crash protection. |
| When K_swap is active it replaces apply_crash_penalty (see build()) so |
| the same-time term isn't double counted. |
| """ |
| K_crash = self.penalties.get("K_crash", 0) |
| K_swap = self.penalties.get("K_swap", 0) |
| robot_nums = self.problem.get_robot_nums() |
| active_robots_per_timestep = self.get_active_robots_per_timestep_in_window() |
|
|
| for t, active_robots in active_robots_per_timestep.items(): |
| if len(active_robots) < 2: |
| continue |
| t_window = t - self.current_T |
| next_active_robots = active_robots_per_timestep.get(t + 1, []) |
|
|
| for robot_id1 in active_robots: |
| for robot_id2 in active_robots: |
| if robot_nums[robot_id1] >= robot_nums[robot_id2]: |
| continue |
|
|
| robot_offset1 = robot_nums[robot_id1] * ( |
| self.num_nodes * self.total_t |
| ) |
| robot_offset2 = robot_nums[robot_id2] * ( |
| self.num_nodes * self.total_t |
| ) |
|
|
| |
| shared_nodes_t = set(self._nodes(robot_id1, t_window)) & set( |
| self._nodes(robot_id2, t_window) |
| ) |
| for node_i in shared_nodes_t: |
| idx1 = node_i + (self.num_nodes * t_window) + robot_offset1 |
| idx2 = node_i + (self.num_nodes * t_window) + robot_offset2 |
| self.Q[(idx1, idx2)] = self.Q.get((idx1, idx2), 0) + K_crash |
|
|
| |
| |
| if ( |
| robot_id1 not in next_active_robots |
| or robot_id2 not in next_active_robots |
| ): |
| continue |
| t_next_window = t_window + 1 |
|
|
| |
| shared_r1_next = set(self._nodes(robot_id1, t_next_window)) & set( |
| self._nodes(robot_id2, t_window) |
| ) |
| for node_i in shared_r1_next: |
| idx1 = node_i + (self.num_nodes * t_next_window) + robot_offset1 |
| idx2 = node_i + (self.num_nodes * t_window) + robot_offset2 |
| self.Q[(idx1, idx2)] = self.Q.get((idx1, idx2), 0) + K_swap |
|
|
| |
| shared_r2_next = set(self._nodes(robot_id1, t_window)) & set( |
| self._nodes(robot_id2, t_next_window) |
| ) |
| for node_i in shared_r2_next: |
| idx1 = node_i + (self.num_nodes * t_window) + robot_offset1 |
| idx2 = node_i + (self.num_nodes * t_next_window) + robot_offset2 |
| self.Q[(idx1, idx2)] = self.Q.get((idx1, idx2), 0) + K_swap |
|
|
| def reachable_positions(self, robot, start_node, start, end): |
| |
| reachable_at_time = {start: {start_node}} |
|
|
| |
| for t in range(start, end - 1): |
| reachable_at_time[t + 1] = set() |
| for node_i in reachable_at_time[t]: |
| for node_j, _ in self.graph.adjacency.get(node_i, []): |
| reachable_at_time[t + 1].add(node_j) |
|
|
| return reachable_at_time |
|
|
| def reachable_positions_aggressive(self, robot, start_node, start_time, end_time): |
|
|
| goal_node = self.problem.get_graph_robot_current_goal(robot.robot_id)[1] |
| reachable_at_time = {start_time: {start_node}} |
| visited = {start_node} |
|
|
| |
| for t in range(start_time + 1, end_time): |
| prev_layer = reachable_at_time[t - 1] |
| curr_layer = set() |
|
|
| for node_i in prev_layer: |
| for node_j, _ in self.graph.adjacency.get(node_i, []): |
| |
| if node_j not in visited: |
| curr_layer.add(node_j) |
| visited.add(node_j) |
| |
| if not curr_layer: |
| break |
|
|
| |
| if goal_node in visited: |
| curr_layer.add(goal_node) |
|
|
| reachable_at_time[t] = curr_layer |
|
|
| |
| return reachable_at_time |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
|
|