Add ChEMBL3D explicit-H epochs 200/300/400 and implicit-H epochs 100/200/300 with inference bundles
ab9bea9 verified Download sample.py from maltefranke/morph: direct link, hf CLI and curl.
- Browser
- Download file 4.89 kB
-
https://huggingface.co/maltefranke/morph/resolve/main/sample.py
- Command line
-
hf download hf://maltefranke/morph/sample.py
-
curl -L -o sample.py https://huggingface.co/maltefranke/morph/resolve/main/sample.py
4.89 kB
| """Dataset-free sampling from a downloaded maltefranke/morph bundle.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| from pathlib import Path | |
| import sys | |
| def load_bundle(root, variant, epoch, device="cpu", steps=100): | |
| """Load the exact bundled runtime, statistics, current weights and EMA.""" | |
| root = Path(root).resolve() | |
| bundle = root / "chembl3d" / variant | |
| runtime = bundle / "runtime" | |
| if "morph" in sys.modules: | |
| raise RuntimeError("Load each runtime in a fresh Python process.") | |
| sys.path.insert(0, str(runtime / "src")) | |
| os.environ.setdefault("MORPH_COMPILE", "0") | |
| os.environ.setdefault("MORPH_ATTENTION_BACKEND", "sdpa") | |
| os.environ.setdefault("MORPH_FLASH_ATTN", "0") | |
| os.environ.setdefault("MORPH_MUON_EXECUTION", "native") | |
| import torch | |
| from omegaconf import OmegaConf | |
| import morph.config # registers the training config's resolvers | |
| from morph.inference.generate import _build_module, load_checkpoint | |
| if device == "auto": | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| cfg = OmegaConf.load(bundle / "config.yaml") | |
| cfg.data.preprocessing.root = str(bundle) | |
| for field, filename in { | |
| "atom_tokens_path": "atom_tokens.txt", | |
| "edge_tokens_path": "edge_tokens.txt", | |
| "charge_tokens_path": "charge_tokens.txt", | |
| "distributions_path": "distributions.pt", | |
| }.items(): | |
| path = bundle / filename | |
| if not path.is_file(): | |
| raise FileNotFoundError(path) | |
| cfg.data.preprocessing[field] = str(path) | |
| cfg.integrator.integrator.num_integration_steps = steps | |
| module, vocab, distributions = _build_module(cfg) | |
| checkpoint = bundle / f"epoch_{epoch:03d}.ckpt" | |
| load_checkpoint(module, checkpoint) | |
| module.eval().to(device) | |
| return module, vocab, distributions, cfg | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent) | |
| parser.add_argument("--variant", choices=["explicit_h", "implicit_h"], required=True) | |
| parser.add_argument("--epoch", type=int, required=True) | |
| parser.add_argument("--num-mols", type=int, default=16) | |
| parser.add_argument("--batch-size", type=int, default=16) | |
| parser.add_argument("--steps", type=int, default=100) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--device", choices=["auto", "cpu", "cuda"], default="auto") | |
| parser.add_argument("--output", type=Path, default=Path("molecules.sdf")) | |
| args = parser.parse_args() | |
| if min(args.num_mols, args.batch_size, args.steps) < 1: | |
| parser.error("num-mols, batch-size, and steps must be positive") | |
| import torch | |
| torch.manual_seed(args.seed) | |
| module, vocab, _, cfg = load_bundle( | |
| args.root, args.variant, args.epoch, args.device, args.steps | |
| ) | |
| from morph.inference.generate import _prior_loader, final_mol | |
| from rdkit import Chem | |
| loader = _prior_loader( | |
| module.distributions, args.num_mols, args.batch_size, | |
| uniform_n_atoms_padding=int(cfg.data.uniform_n_atoms_padding), | |
| n_atoms_strategy=str(cfg.data.n_atoms_strategy), | |
| ) | |
| records = [] | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| with Chem.SDWriter(str(args.output)) as writer, torch.inference_mode(): | |
| for batch_index, batch in enumerate(loader): | |
| batch = tuple(graph.to(module.device) for graph in batch) | |
| result = module.predict_step(batch, batch_index) | |
| for trajectory, valid in zip(result["all_mols"], result["is_valid"], strict=True): | |
| frame = trajectory[-1] | |
| if not torch.isfinite(frame.x).all(): | |
| raise RuntimeError("Non-finite generated coordinates") | |
| mol = final_mol(trajectory, vocab) | |
| index = len(records) | |
| records.append({ | |
| "index": index, "valid": bool(valid), | |
| "smiles": Chem.MolToSmiles(mol) if mol is not None else None, | |
| "represented_atoms": int(frame.x.shape[0]), | |
| }) | |
| if valid and mol is not None: | |
| mol.SetProp("_Name", f"sample_{index}") | |
| writer.write(mol) | |
| assert len(records) == args.num_mols | |
| report = { | |
| "variant": args.variant, "completed_epochs": args.epoch, | |
| "seed": args.seed, "integration_steps": args.steps, | |
| "weights": "EMA", "coordinate_units": "angstrom", | |
| "num_attempted": len(records), | |
| "num_valid": sum(row["valid"] for row in records), | |
| "samples": records, | |
| } | |
| args.output.with_suffix(".json").write_text(json.dumps(report, indent=2) + "\n") | |
| print(json.dumps({k: v for k, v in report.items() if k != "samples"}), flush=True) | |
| if __name__ == "__main__": | |
| main() | |