|
import torch |
|
import numpy as np |
|
import networkx as nx |
|
from scipy.sparse.linalg import eigsh |
|
from sklearn.cluster import SpectralClustering |
|
from torch_geometric.utils import to_networkx, get_laplacian |
|
import torch_geometric.utils as pyg_utils |
|
|
|
class GraphSequencer: |
|
""" |
|
Production-ready graph ordering strategies |
|
All methods use real graph data - no hardcoded values |
|
""" |
|
|
|
@staticmethod |
|
def bfs_ordering(edge_index, num_nodes, start_node=None): |
|
"""Breadth-first search ordering""" |
|
|
|
G = nx.Graph() |
|
G.add_nodes_from(range(num_nodes)) |
|
edge_list = edge_index.t().cpu().numpy() |
|
G.add_edges_from(edge_list) |
|
|
|
|
|
if start_node is None: |
|
degrees = dict(G.degree()) |
|
start_node = max(degrees, key=degrees.get) |
|
|
|
|
|
visited = set() |
|
order = [] |
|
queue = [start_node] |
|
|
|
while queue: |
|
node = queue.pop(0) |
|
if node in visited: |
|
continue |
|
|
|
visited.add(node) |
|
order.append(node) |
|
|
|
|
|
neighbors = list(G.neighbors(node)) |
|
neighbors.sort(key=lambda n: G.degree(n), reverse=True) |
|
|
|
for neighbor in neighbors: |
|
if neighbor not in visited: |
|
queue.append(neighbor) |
|
|
|
|
|
for node in range(num_nodes): |
|
if node not in visited: |
|
order.append(node) |
|
|
|
return torch.tensor(order, dtype=torch.long) |
|
|
|
@staticmethod |
|
def spectral_ordering(edge_index, num_nodes): |
|
"""Spectral ordering using graph Laplacian eigenvector""" |
|
try: |
|
|
|
edge_index_np = edge_index.cpu().numpy() |
|
|
|
|
|
A = np.zeros((num_nodes, num_nodes)) |
|
A[edge_index_np[0], edge_index_np[1]] = 1 |
|
A[edge_index_np[1], edge_index_np[0]] = 1 |
|
|
|
|
|
D = np.diag(np.sum(A, axis=1)) |
|
|
|
|
|
D_sqrt_inv = np.diag(1.0 / np.sqrt(np.maximum(np.diag(D), 1e-12))) |
|
L = D_sqrt_inv @ (D - A) @ D_sqrt_inv |
|
|
|
|
|
eigenvals, eigenvecs = eigsh(L, k=min(10, num_nodes-1), which='SM') |
|
fiedler_vector = eigenvecs[:, 1] |
|
|
|
|
|
order = np.argsort(fiedler_vector) |
|
|
|
return torch.tensor(order, dtype=torch.long) |
|
|
|
except Exception as e: |
|
print(f"Spectral ordering failed: {e}, falling back to degree ordering") |
|
return GraphSequencer.degree_ordering(edge_index, num_nodes) |
|
|
|
@staticmethod |
|
def degree_ordering(edge_index, num_nodes): |
|
"""Order nodes by degree (high to low)""" |
|
|
|
degrees = torch.zeros(num_nodes, dtype=torch.long) |
|
degrees.index_add_(0, edge_index[0], torch.ones(edge_index.shape[1], dtype=torch.long)) |
|
degrees.index_add_(0, edge_index[1], torch.ones(edge_index.shape[1], dtype=torch.long)) |
|
|
|
|
|
_, order = torch.sort(-degrees * num_nodes - torch.arange(num_nodes)) |
|
|
|
return order |
|
|
|
@staticmethod |
|
def community_ordering(edge_index, num_nodes, n_clusters=None): |
|
"""Community-aware ordering using spectral clustering""" |
|
try: |
|
if n_clusters is None: |
|
n_clusters = max(2, min(10, num_nodes // 100)) |
|
|
|
|
|
edge_index_np = edge_index.cpu().numpy() |
|
A = np.zeros((num_nodes, num_nodes)) |
|
A[edge_index_np[0], edge_index_np[1]] = 1 |
|
A[edge_index_np[1], edge_index_np[0]] = 1 |
|
|
|
|
|
clustering = SpectralClustering( |
|
n_clusters=n_clusters, |
|
affinity='precomputed', |
|
random_state=42 |
|
) |
|
|
|
labels = clustering.fit_predict(A) |
|
|
|
|
|
degrees = np.sum(A, axis=1) |
|
|
|
order = [] |
|
for cluster in range(n_clusters): |
|
cluster_nodes = np.where(labels == cluster)[0] |
|
cluster_degrees = degrees[cluster_nodes] |
|
cluster_order = cluster_nodes[np.argsort(-cluster_degrees)] |
|
order.extend(cluster_order) |
|
|
|
return torch.tensor(order, dtype=torch.long) |
|
|
|
except Exception as e: |
|
print(f"Community ordering failed: {e}, falling back to BFS ordering") |
|
return GraphSequencer.bfs_ordering(edge_index, num_nodes) |
|
|
|
@staticmethod |
|
def multi_view_ordering(edge_index, num_nodes): |
|
"""Generate multiple orderings for different perspectives""" |
|
orderings = {} |
|
|
|
|
|
orderings['bfs'] = GraphSequencer.bfs_ordering(edge_index, num_nodes) |
|
orderings['degree'] = GraphSequencer.degree_ordering(edge_index, num_nodes) |
|
orderings['spectral'] = GraphSequencer.spectral_ordering(edge_index, num_nodes) |
|
orderings['community'] = GraphSequencer.community_ordering(edge_index, num_nodes) |
|
|
|
return orderings |
|
|
|
class PositionalEncoder: |
|
"""Graph-aware positional encoding""" |
|
|
|
@staticmethod |
|
def encode_positions(x, edge_index, order, max_dist=10): |
|
""" |
|
Create positional encodings that preserve graph structure |
|
""" |
|
num_nodes = x.size(0) |
|
device = x.device |
|
|
|
|
|
seq_pos = torch.zeros(num_nodes, device=device) |
|
seq_pos[order] = torch.arange(num_nodes, device=device, dtype=torch.float) |
|
|
|
|
|
G = nx.Graph() |
|
G.add_edges_from(edge_index.t().cpu().numpy()) |
|
|
|
|
|
distances = torch.full((num_nodes, max_dist), float('inf'), device=device) |
|
|
|
for i, node in enumerate(order): |
|
|
|
start_idx = max(0, i - max_dist) |
|
for j in range(start_idx, i): |
|
prev_node = order[j].item() |
|
try: |
|
dist = nx.shortest_path_length(G, source=node.item(), target=prev_node) |
|
distances[node, j - start_idx] = min(dist, max_dist - 1) |
|
except nx.NetworkXNoPath: |
|
distances[node, j - start_idx] = max_dist - 1 |
|
|
|
|
|
distances[distances == float('inf')] = max_dist - 1 |
|
|
|
|
|
seq_pos = seq_pos / num_nodes |
|
distances = distances / max_dist |
|
|
|
return seq_pos.unsqueeze(1), distances |