graph-energy-matching / src /gem /ot_data.py
multimodalart's picture
multimodalart HF Staff
Initial GEM demo
6fe4366 verified
Raw History Blame Contribute Delete
4.45 kB
"""Data utilities for OT diagnostics: dataset context, graph collection, and noise init."""
from __future__ import annotations
from typing import List, Tuple
import torch
from gem import utils
from omegaconf import DictConfig
from gem.datasets.dataset_context import build_dataset_context
from gem.models.extra_features import DummyExtraFeatures, ExtraFeatures
from gem.models.extra_features_molecular import ExtraMolecularFeatures
from gem.flow_matching.noise_distribution import NoiseDistribution
from gem.flow_matching import flow_matching_utils
def build_data_context(cfg: DictConfig):
"""Create datamodule, dataset_infos, and feature builders similar to train/eval."""
datamodule, dataset_infos, dataset_smiles = build_dataset_context(cfg)
extra_features = ExtraFeatures(
cfg.model.extra_features,
cfg.model.rrwp_steps,
dataset_info=dataset_infos,
)
domain_features = (
ExtraMolecularFeatures(dataset_infos=dataset_infos)
if dataset_smiles is not None
else DummyExtraFeatures()
)
dataset_infos.compute_input_output_dims(
datamodule=datamodule,
extra_features=extra_features,
domain_features=domain_features,
)
return datamodule, dataset_infos
def collect_graphs_from_data(datamodule, M: int) -> Tuple[List[torch.Tensor], List[torch.Tensor]]:
"""Collect at least M graphs from the training loader as discrete label tensors.
Returns two lists of length >= M:
- node label tensors of shape (n,)
- edge type tensors of shape (n, n)
"""
node_list: List[torch.Tensor] = []
edge_list: List[torch.Tensor] = []
loader = datamodule.train_dataloader()
it = iter(loader)
while len(node_list) < M:
try:
batch = next(it)
except StopIteration:
it = iter(datamodule.train_dataloader())
batch = next(it)
dense_data, node_mask = utils.to_dense(
batch.x, batch.edge_index, batch.edge_attr, batch.batch
)
graphs = dense_data.mask(node_mask, collapse=True).split(node_mask)
for g in graphs:
node_list.append(g.X.long().cpu())
edge_list.append(g.E.long().cpu())
if len(node_list) >= M:
break
return node_list, edge_list
def train_node_count_distribution(datamodule) -> torch.Tensor:
"""Compute distribution of number of nodes per graph over the training dataset."""
max_nodes = 0
for batch in datamodule.train_dataloader():
_, counts = torch.unique(batch.batch, return_counts=True)
if counts.numel() > 0:
max_nodes = max(max_nodes, int(counts.max().item()))
if max_nodes == 0:
return torch.tensor([1.0])
counts_hist = torch.zeros(max_nodes + 1, dtype=torch.float32)
total = 0.0
for batch in datamodule.train_dataloader():
_, counts = torch.unique(batch.batch, return_counts=True)
for c in counts:
n = int(c.item())
counts_hist[n] += 1.0
total += 1.0
if total > 0:
counts_hist = counts_hist / total
return counts_hist
def initialize_random_graphs_with_counts(
counts: List[int],
dataset_info,
device: torch.device = torch.device("cpu"),
transition: str = "marginal",
) -> List[Tuple[torch.Tensor, torch.Tensor]]:
"""Sample random graphs with exact node counts per graph using the noise distribution."""
if not counts:
return []
B = len(counts)
n_max = max(int(c) for c in counts)
if n_max <= 0:
return []
n_nodes = torch.tensor(counts, device=device, dtype=torch.long)
arange = torch.arange(n_max, device=device).unsqueeze(0).expand(B, -1)
node_mask = arange < n_nodes.unsqueeze(1)
noise_dist = NoiseDistribution(transition, dataset_info)
limit_dist = noise_dist.get_limit_dist()
z_T = flow_matching_utils.sample_discrete_feature_noise(
limit_dist=limit_dist, node_mask=node_mask
)
graphs: List[Tuple[torch.Tensor, torch.Tensor]] = []
for i in range(B):
n = int(counts[i])
if n <= 0:
graphs.append((torch.empty(0, dtype=torch.long), torch.empty(0, 0, dtype=torch.long)))
continue
node_types = torch.argmax(z_T.X[i, :n], dim=-1)
edge_types = torch.argmax(z_T.E[i, :n, :n], dim=-1)
graphs.append((node_types.cpu(), edge_types.cpu()))
return graphs