import argparse from pathlib import Path import numpy as np import torch from sklearn.neighbors import NearestNeighbors from utils import ensure_dir def build_knn_graph(features: np.ndarray, k: int) -> tuple[torch.Tensor, torch.Tensor]: neighbors = NearestNeighbors(n_neighbors=k + 1, metric="cosine") neighbors.fit(features) distances, indices = neighbors.kneighbors(features) edge_map: dict[tuple[int, int], float] = {} for node_index, (node_distances, node_neighbors) in enumerate(zip(distances, indices)): for distance, neighbor_index in zip(node_distances[1:], node_neighbors[1:]): weight = max(0.0, 1.0 - float(distance)) forward = (node_index, int(neighbor_index)) backward = (int(neighbor_index), node_index) edge_map[forward] = max(edge_map.get(forward, 0.0), weight) edge_map[backward] = max(edge_map.get(backward, 0.0), weight) edge_items = sorted(edge_map.items()) edge_index = torch.tensor([[src, dst] for (src, dst), _ in edge_items], dtype=torch.long).t().contiguous() edge_weight = torch.tensor([weight for _, weight in edge_items], dtype=torch.float32) return edge_index, edge_weight def main() -> None: parser = argparse.ArgumentParser(description="Build a k-NN similarity graph from node features.") parser.add_argument("--features", required=True, help="Path to node_features.npy") parser.add_argument("--output", required=True, help="Path to graph .pt file") parser.add_argument("--k", type=int, default=5) parser.add_argument("--store_features", action="store_true") args = parser.parse_args() features = np.load(args.features) edge_index, edge_weight = build_knn_graph(features, k=args.k) graph = { "edge_index": edge_index, "edge_weight": edge_weight, "num_nodes": int(features.shape[0]), "feature_dim": int(features.shape[1]), "k": args.k, "features_path": str(Path(args.features).resolve()), } if args.store_features: graph["node_features"] = torch.tensor(features, dtype=torch.float32) output_path = Path(args.output) ensure_dir(output_path.parent) torch.save(graph, output_path) print(f"Saved graph to {output_path} with {edge_index.shape[1]} directed edges") if __name__ == "__main__": main()