Upload qads/planner/graph.py
Browse files- qads/planner/graph.py +194 -0
qads/planner/graph.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""World State Graph Builder."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Dict, Any, List, Tuple, Optional
|
| 4 |
+
import networkx as nx
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class WorldGraph:
|
| 8 |
+
"""
|
| 9 |
+
Probabilistic graph representing the environment.
|
| 10 |
+
Each node stores: position, risk, traversal cost, energy cost, uncertainty, obstacle probability
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, resolution: float = 0.5):
|
| 14 |
+
self.resolution = resolution
|
| 15 |
+
self.graph = nx.DiGraph()
|
| 16 |
+
self.node_positions: Dict[int, Tuple[float, ...]] = {}
|
| 17 |
+
self.node_metadata: Dict[int, Dict[str, Any]] = {}
|
| 18 |
+
self.next_id = 0
|
| 19 |
+
|
| 20 |
+
def add_node(self,
|
| 21 |
+
position: Tuple[float, ...],
|
| 22 |
+
risk: float = 0.0,
|
| 23 |
+
cost: float = 1.0,
|
| 24 |
+
energy: float = 1.0,
|
| 25 |
+
uncertainty: float = 0.0,
|
| 26 |
+
obstacle_prob: float = 0.0) -> int:
|
| 27 |
+
"""Add a node to the graph."""
|
| 28 |
+
node_id = self.next_id
|
| 29 |
+
self.next_id += 1
|
| 30 |
+
|
| 31 |
+
self.graph.add_node(node_id)
|
| 32 |
+
self.node_positions[node_id] = position
|
| 33 |
+
self.node_metadata[node_id] = {
|
| 34 |
+
'risk': float(risk),
|
| 35 |
+
'cost': float(cost),
|
| 36 |
+
'energy': float(energy),
|
| 37 |
+
'uncertainty': float(uncertainty),
|
| 38 |
+
'obstacle_prob': float(obstacle_prob),
|
| 39 |
+
'traversal_prob': float(1.0 - obstacle_prob),
|
| 40 |
+
'entropy': float(-uncertainty * np.log2(uncertainty + 1e-10) if uncertainty > 0 else 0.0)
|
| 41 |
+
}
|
| 42 |
+
return node_id
|
| 43 |
+
|
| 44 |
+
def add_edge(self,
|
| 45 |
+
node_a: int,
|
| 46 |
+
node_b: int,
|
| 47 |
+
weight: Optional[float] = None,
|
| 48 |
+
risk: float = 0.0):
|
| 49 |
+
"""Add an edge between nodes."""
|
| 50 |
+
if weight is None:
|
| 51 |
+
pos_a = self.node_positions[node_a]
|
| 52 |
+
pos_b = self.node_positions[node_b]
|
| 53 |
+
weight = np.linalg.norm(np.array(pos_a) - np.array(pos_b))
|
| 54 |
+
|
| 55 |
+
meta_a = self.node_metadata[node_a]
|
| 56 |
+
meta_b = self.node_metadata[node_b]
|
| 57 |
+
|
| 58 |
+
# Composite edge cost
|
| 59 |
+
composite_cost = (
|
| 60 |
+
0.3 * weight +
|
| 61 |
+
0.2 * (meta_a['risk'] + meta_b['risk']) / 2 +
|
| 62 |
+
0.2 * (meta_a['cost'] + meta_b['cost']) / 2 +
|
| 63 |
+
0.15 * (meta_a['uncertainty'] + meta_b['uncertainty']) / 2 +
|
| 64 |
+
0.15 * (meta_a['obstacle_prob'] + meta_b['obstacle_prob']) / 2
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
self.graph.add_edge(node_a, node_b,
|
| 68 |
+
weight=composite_cost,
|
| 69 |
+
distance=weight,
|
| 70 |
+
risk=risk)
|
| 71 |
+
|
| 72 |
+
def build_grid(self,
|
| 73 |
+
bounds: Tuple[Tuple[float, float], ...],
|
| 74 |
+
obstacle_map: Optional[np.ndarray] = None,
|
| 75 |
+
uncertainty_map: Optional[np.ndarray] = None):
|
| 76 |
+
"""Build grid graph from bounds and maps."""
|
| 77 |
+
if len(bounds) == 2:
|
| 78 |
+
(x_min, x_max), (y_min, y_max) = bounds
|
| 79 |
+
nx_nodes = int((x_max - x_min) / self.resolution)
|
| 80 |
+
ny_nodes = int((y_max - y_min) / self.resolution)
|
| 81 |
+
|
| 82 |
+
# Create nodes
|
| 83 |
+
for i in range(nx_nodes):
|
| 84 |
+
for j in range(ny_nodes):
|
| 85 |
+
x = x_min + i * self.resolution
|
| 86 |
+
y = y_min + j * self.resolution
|
| 87 |
+
|
| 88 |
+
# Get map values
|
| 89 |
+
obs_prob = 0.0
|
| 90 |
+
unc = 0.0
|
| 91 |
+
if obstacle_map is not None:
|
| 92 |
+
mi = min(int(i * obstacle_map.shape[0] / nx_nodes), obstacle_map.shape[0]-1)
|
| 93 |
+
mj = min(int(j * obstacle_map.shape[1] / ny_nodes), obstacle_map.shape[1]-1)
|
| 94 |
+
obs_prob = obstacle_map[mi, mj]
|
| 95 |
+
if uncertainty_map is not None:
|
| 96 |
+
mi = min(int(i * uncertainty_map.shape[0] / nx_nodes), uncertainty_map.shape[0]-1)
|
| 97 |
+
mj = min(int(j * uncertainty_map.shape[1] / ny_nodes), uncertainty_map.shape[1]-1)
|
| 98 |
+
unc = uncertainty_map[mi, mj]
|
| 99 |
+
|
| 100 |
+
self.add_node(
|
| 101 |
+
position=(x, y),
|
| 102 |
+
risk=obs_prob * 0.5 + unc * 0.3,
|
| 103 |
+
cost=1.0 + obs_prob,
|
| 104 |
+
energy=1.0,
|
| 105 |
+
uncertainty=unc,
|
| 106 |
+
obstacle_prob=obs_prob
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Add edges (4-connectivity)
|
| 110 |
+
for i in range(nx_nodes):
|
| 111 |
+
for j in range(ny_nodes):
|
| 112 |
+
idx = i * ny_nodes + j
|
| 113 |
+
# Right
|
| 114 |
+
if i < nx_nodes - 1:
|
| 115 |
+
self.add_edge(idx, idx + ny_nodes)
|
| 116 |
+
self.add_edge(idx + ny_nodes, idx)
|
| 117 |
+
# Up
|
| 118 |
+
if j < ny_nodes - 1:
|
| 119 |
+
self.add_edge(idx, idx + 1)
|
| 120 |
+
self.add_edge(idx + 1, idx)
|
| 121 |
+
|
| 122 |
+
def get_entropy(self) -> float:
|
| 123 |
+
"""Compute graph-level entropy."""
|
| 124 |
+
probs = []
|
| 125 |
+
for node_id, meta in self.node_metadata.items():
|
| 126 |
+
p = meta.get('traversal_prob', 1.0)
|
| 127 |
+
if p > 0:
|
| 128 |
+
probs.append(p)
|
| 129 |
+
|
| 130 |
+
if not probs:
|
| 131 |
+
return 0.0
|
| 132 |
+
|
| 133 |
+
probs = np.array(probs)
|
| 134 |
+
probs = probs / probs.sum()
|
| 135 |
+
entropy = -np.sum(probs * np.log2(probs + 1e-10))
|
| 136 |
+
return float(entropy)
|
| 137 |
+
|
| 138 |
+
def get_uncertainty(self) -> float:
|
| 139 |
+
"""Compute average node uncertainty."""
|
| 140 |
+
uncertainties = [meta['uncertainty'] for meta in self.node_metadata.values()]
|
| 141 |
+
return float(np.mean(uncertainties)) if uncertainties else 0.0
|
| 142 |
+
|
| 143 |
+
def get_obstacle_density(self) -> float:
|
| 144 |
+
"""Compute obstacle density."""
|
| 145 |
+
obs = [meta['obstacle_prob'] for meta in self.node_metadata.values()]
|
| 146 |
+
return float(np.mean(obs)) if obs else 0.0
|
| 147 |
+
|
| 148 |
+
def find_node_at(self, position: Tuple[float, ...], tolerance: float = None) -> Optional[int]:
|
| 149 |
+
"""Find node closest to position."""
|
| 150 |
+
if tolerance is None:
|
| 151 |
+
tolerance = self.resolution * 2
|
| 152 |
+
|
| 153 |
+
pos = np.array(position)
|
| 154 |
+
best_id = None
|
| 155 |
+
best_dist = float('inf')
|
| 156 |
+
|
| 157 |
+
for node_id, node_pos in self.node_positions.items():
|
| 158 |
+
dist = np.linalg.norm(pos - np.array(node_pos))
|
| 159 |
+
if dist < tolerance and dist < best_dist:
|
| 160 |
+
best_id = node_id
|
| 161 |
+
best_dist = dist
|
| 162 |
+
|
| 163 |
+
return best_id
|
| 164 |
+
|
| 165 |
+
def to_cost_matrix(self) -> np.ndarray:
|
| 166 |
+
"""Convert graph to cost matrix for QAOA."""
|
| 167 |
+
n = len(self.graph.nodes)
|
| 168 |
+
cost = np.zeros((n, n))
|
| 169 |
+
|
| 170 |
+
for u, v, data in self.graph.edges(data=True):
|
| 171 |
+
cost[u, v] = data.get('weight', 1.0)
|
| 172 |
+
|
| 173 |
+
# Add diagonal costs
|
| 174 |
+
for node_id, meta in self.node_metadata.items():
|
| 175 |
+
cost[node_id, node_id] = meta['cost']
|
| 176 |
+
|
| 177 |
+
return cost
|
| 178 |
+
|
| 179 |
+
def update_node(self, node_id: int, **kwargs):
|
| 180 |
+
"""Update node metadata."""
|
| 181 |
+
if node_id in self.node_metadata:
|
| 182 |
+
self.node_metadata[node_id].update(kwargs)
|
| 183 |
+
|
| 184 |
+
def get_state_dict(self) -> Dict[str, Any]:
|
| 185 |
+
"""Export graph state."""
|
| 186 |
+
return {
|
| 187 |
+
'n_nodes': len(self.graph.nodes),
|
| 188 |
+
'n_edges': len(self.graph.edges),
|
| 189 |
+
'entropy': self.get_entropy(),
|
| 190 |
+
'uncertainty': self.get_uncertainty(),
|
| 191 |
+
'obstacle_density': self.get_obstacle_density(),
|
| 192 |
+
'positions': self.node_positions.copy(),
|
| 193 |
+
'metadata': self.node_metadata.copy()
|
| 194 |
+
}
|