summaryrefslogtreecommitdiff
path: root/kaft_mvp/data.py
diff options
context:
space:
mode:
Diffstat (limited to 'kaft_mvp/data.py')
-rw-r--r--kaft_mvp/data.py100
1 files changed, 100 insertions, 0 deletions
diff --git a/kaft_mvp/data.py b/kaft_mvp/data.py
new file mode 100644
index 0000000..2f25d80
--- /dev/null
+++ b/kaft_mvp/data.py
@@ -0,0 +1,100 @@
+"""Dataset and graph-operator utilities for the MVP."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+from urllib.request import urlopen
+
+import torch
+import torch_geometric.transforms as transforms
+from torch_geometric.datasets import Planetoid
+
+
+PLANETOID_FILES = (
+ "x",
+ "tx",
+ "allx",
+ "y",
+ "ty",
+ "ally",
+ "graph",
+ "test.index",
+)
+
+
+def _ensure_planetoid_raw(root: str | Path, name: str) -> None:
+ """Pre-fetch from raw.githubusercontent.com to avoid a flaky redirect."""
+ raw_dir = Path(root) / name / "raw"
+ raw_dir.mkdir(parents=True, exist_ok=True)
+ lower_name = name.lower()
+ base_url = (
+ "https://raw.githubusercontent.com/kimiyoung/"
+ "planetoid/master/data"
+ )
+ for suffix in PLANETOID_FILES:
+ filename = f"ind.{lower_name}.{suffix}"
+ destination = raw_dir / filename
+ if destination.exists() and destination.stat().st_size > 0:
+ continue
+ temporary = destination.with_suffix(destination.suffix + ".part")
+ with urlopen(f"{base_url}/{filename}", timeout=60) as response:
+ temporary.write_bytes(response.read())
+ temporary.replace(destination)
+
+
+def spmm(matrix: torch.Tensor, dense: torch.Tensor) -> torch.Tensor:
+ """Multiply a native PyTorch sparse matrix by a dense matrix."""
+ return torch.sparse.mm(matrix, dense)
+
+
+def normalized_adjacency(
+ edge_index: torch.Tensor,
+ num_nodes: int,
+) -> torch.Tensor:
+ """Return A-hat = D^-1/2 (A + I) D^-1/2 as a coalesced COO tensor."""
+ row, col = edge_index
+ loops = torch.arange(num_nodes, device=edge_index.device)
+ row = torch.cat((row, loops))
+ col = torch.cat((col, loops))
+
+ degree = torch.zeros(num_nodes, device=edge_index.device)
+ degree.scatter_add_(0, row, torch.ones_like(row, dtype=torch.float32))
+ inverse_sqrt = degree.pow(-0.5)
+ inverse_sqrt.masked_fill_(torch.isinf(inverse_sqrt), 0.0)
+ values = inverse_sqrt[row] * inverse_sqrt[col]
+
+ return torch.sparse_coo_tensor(
+ torch.stack((row, col)),
+ values,
+ (num_nodes, num_nodes),
+ ).coalesce()
+
+
+def load_cora(
+ root: str | Path = "data",
+ device: str | torch.device = "cpu",
+) -> dict[str, Any]:
+ """Download Cora and return the tensors used by both training rules."""
+ _ensure_planetoid_raw(root, "Cora")
+ dataset = Planetoid(
+ root=str(root),
+ name="Cora",
+ transform=transforms.NormalizeFeatures(),
+ )
+ graph = dataset[0]
+ adjacency = normalized_adjacency(
+ graph.edge_index,
+ graph.num_nodes,
+ ).to(device)
+ return {
+ "x": graph.x.to(device),
+ "y": graph.y.to(device),
+ "adjacency": adjacency,
+ "train_mask": graph.train_mask.to(device),
+ "val_mask": graph.val_mask.to(device),
+ "test_mask": graph.test_mask.to(device),
+ "num_nodes": graph.num_nodes,
+ "num_features": dataset.num_features,
+ "num_classes": dataset.num_classes,
+ }