vqatom / inference.py
Anonymous
Connect live VQAtom inference
3d681a2
Raw
History Blame Contribute Delete
6.11 kB
from pathlib import Path
from types import SimpleNamespace
import re
import torch
from models import EquivariantThreeHopGINE
from smiles_to_npy_discretize import smiles_to_graph_with_labels
class SimpleGraph:
"""The tiny subset of DGLGraph used by EquivariantThreeHopGINE."""
def __init__(self, src, dst, weights, features):
self._src, self._dst = src, dst
self.edata = {"weight": weights}
self.ndata = {"feat": features}
def edges(self):
return self._src, self._dst
def num_edges(self):
return int(self._src.numel())
def to(self, device):
self._src = self._src.to(device)
self._dst = self._dst.to(device)
self.edata = {k: v.to(device) for k, v in self.edata.items()}
self.ndata = {k: v.to(device) for k, v in self.ndata.items()}
return self
def build_args():
return SimpleNamespace(
hidden_dim=16,
codebook_size=10000,
edge_emb_dim=32,
ema_decay=0.8,
dynamic_threshold=True,
epoch_at_mode_shift=0,
ss_max_total_latent_count=40000,
train_or_infer="infer",
use_checkpoint=True,
)
def ensure_codebook_keys(model, state_dict):
cb = model.vq._codebook
prefixes = ["vq._codebook.", ""]
for prefix in prefixes:
ea_pat = re.compile(rf"^{re.escape(prefix)}embed_avg_(.+)$")
cs_pat = re.compile(rf"^{re.escape(prefix)}cluster_size_(.+)$")
emb_pat = re.compile(rf"^{re.escape(prefix)}embed\.(.+)$")
created = False
for key, value in state_dict.items():
match = ea_pat.match(key)
if match and torch.is_tensor(value) and value.ndim == 2:
original = match.group(1)
k, d = value.shape
if not hasattr(cb, f"cluster_size_{original}"):
cb.register_buffer(f"cluster_size_{original}", torch.zeros(k))
if not hasattr(cb, f"embed_avg_{original}"):
cb.register_buffer(f"embed_avg_{original}", torch.zeros(k, d))
cb._get_or_create_safe_key(original, K_e=int(k), D=int(d), device="cpu")
created = True
for key, value in state_dict.items():
match = cs_pat.match(key)
if match and torch.is_tensor(value) and value.ndim == 1:
original = match.group(1)
if not hasattr(cb, f"cluster_size_{original}"):
cb.register_buffer(f"cluster_size_{original}", torch.zeros_like(value, device="cpu"))
created = True
for key, value in state_dict.items():
match = emb_pat.match(key)
if match and torch.is_tensor(value) and value.ndim == 2:
safe = match.group(1)
if safe not in cb.embed:
cb.embed[safe] = torch.nn.Parameter(torch.zeros_like(value, device="cpu"))
created = True
if created:
return
def make_graph(adj, features, device):
x = torch.as_tensor(features, dtype=torch.float32, device=device)
w1 = torch.as_tensor(adj, dtype=torch.float32, device=device)
n = x.shape[0]
a1 = w1 > 0
a1 = a1 | a1.T
a1.fill_diagonal_(True)
w1 = w1.clone()
w1.fill_diagonal_(1.0)
a1f = a1.float()
a2 = (a1f @ a1f) > 0
a3 = (a2.float() @ a1f) > 0
two_only = a2 & ~a1
three_only = a3 & ~(a1 | a2)
full_w = w1 + two_only.float() * 0.5 + three_only.float() * 0.3
full_w.fill_diagonal_(1.0)
src, dst = (full_w > 0).nonzero(as_tuple=True)
return SimpleGraph(src, dst, full_w[src, dst], x), n
def make_masks(features, device):
masks = {}
for i, row in enumerate(features):
key = "_".join(str(int(row[j])) for j in (0, 2, 3, 4, 5, 6))
masks.setdefault(key, []).append(i)
return {k: torch.tensor(v, dtype=torch.long, device=device) for k, v in masks.items()}
class VQAtomTokenizer:
def __init__(self, checkpoint="data/model_epoch_3.pt", device=None):
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
args = build_args()
self.model = EquivariantThreeHopGINE(
in_feats=64, hidden_feats=16, out_feats=16, args=args
)
ckpt = torch.load(Path(checkpoint), map_location="cpu", weights_only=False)
state = ckpt.get("model", ckpt.get("state_dict", ckpt)) if isinstance(ckpt, dict) else ckpt
drop = (
"vq._codebook.usage_ema_k_", "vq._codebook.split_cd_k_",
"vq._codebook.ever_used_k_", "vq._codebook.last_used_ep_k_",
)
state = {k: v for k, v in state.items() if not any(k.startswith(p) for p in drop)}
ensure_codebook_keys(self.model, state)
missing, unexpected = self.model.load_state_dict(state, strict=False)
essential_missing = [k for k in missing if not any(x in k for x in ("usage_ema", "split_cd", "ever_used", "last_used"))]
if essential_missing:
raise RuntimeError(f"Checkpoint is missing required weights: {essential_missing[:8]}")
self.model.to(self.device).eval()
@torch.inference_mode()
def encode(self, smiles):
adj, features, _ = smiles_to_graph_with_labels(smiles, 0)
if features.shape[1] != 79:
raise RuntimeError(f"Expected 79 atom features, got {features.shape[1]}")
if features.shape[0] >= 100:
raise ValueError("This demo supports molecules with fewer than 100 heavy atoms.")
graph, _ = make_graph(adj, features, self.device)
masks = make_masks(features, self.device)
out = self.model(
graph, graph.ndata["feat"], 0, masks, None, 0,
None, "infer", [graph.ndata["feat"]],
)
_, ids, _ = out
key_ids, cluster_ids, global_ids, id2safe = ids
return {
"tokens": global_ids.detach().cpu().long().tolist(),
"key_ids": key_ids.detach().cpu().long().tolist(),
"cluster_ids": cluster_ids.detach().cpu().long().tolist(),
"id2safe": id2safe,
}