MolecularDiffusion.modules.tasks.diffusion_pmdm¶
PMDM task: pocket-conditioned 3D ligand diffusion.
Three objects, mirroring the platform’s usual layout:
PMDMDiffusionTask– the duck-typed Task (docs/adding_new_models.md Section 2.1) wrappingPMDMEpsNet.ModelTaskFactory– the_target_ofconfigs/tasks/diffusion_pmdm.yaml.PMDMPocketGenerator– the_target_ofconfigs/interference/gen_pmdm_pocket.yaml.GenerativeFactory’ssample(batch_size, nodesxsample, ...)has no channel for “which pocket”, so pocket-conditioned models get their own generator behind their own_target_; that is the established in-tree pattern (DiffPharmaPocketGenerator, the DiffSMol shape generator) and needs no core change –cli/generate.pyonly doesinstantiate(cfg.interference, task=task)then.run().
The batch is NOT a PointCloud dict: PMDM needs a diffused ligand cloud plus a
fixed pocket cloud, flat-concatenated with scatter indices. The collate in
data/component/pmdm_data.py already emits PMDM’s own attribute names, so
the adapter here is a one-line SimpleNamespace(**batch).
PMDMConstrainedGenerator adds the two constrained-sampling modes
upstream calls “lead optimisation” and “linker design” (mode: lead_opt
/ mode: linker) – keep part of a starting ligand fixed, regenerate the
rest, inside the same fixed pocket. Both modes read the pocket and the
starting ligand from plain files (pocket_file/mol_file), not a
converted db – see docs/model_integrations/pmdm/INTEGRATION_PLAN.md’s
revision-3 Q1/Q1b. One class for both modes, not two – the same pattern
DiffSBDD already uses for its own de novo/inpaint split.
Out of scope this pass (see the integration plan): the VAE-latent and property-context branches, and CFG / gradient guidance.
Attributes¶
Classes¶
Ligand-atom-count prior, sampled at generation time. |
|
Hydra entry point for |
|
Constrained generation (lead optimisation OR linker design), behind |
|
Task contract around |
|
Pocket-conditioned generation behind |
Module Contents¶
- class MolecularDiffusion.modules.tasks.diffusion_pmdm.LigandSizeDistribution(histogram: Dict[int, float])¶
Ligand-atom-count prior, sampled at generation time.
Same shape of interface as the other in-tree node distributions (
TabascoNodeDistribution,DiffSMolShapeNodeDistribution):.histogram/.n_node_dist/.sample(n). Written here rather than imported fromdiffusion_tabascobecause that module hard-importstensordictand the whole TABASCO flow stack at module scope.- sample(n_samples: int) torch.Tensor¶
- histogram¶
- n_node_dist¶
- class MolecularDiffusion.modules.tasks.diffusion_pmdm.ModelTaskFactory(task_type: str = 'diffusion_pmdm', num_atom: int = 10, protein_feature_dim: int = 31, hidden_dim: int = 128, protein_hidden_dim: int = 128, num_convs: int = 3, num_convs_local: int = 3, protein_num_convs: int = 2, cutoff: float = 3.0, g_cutoff: float = 6.0, encoder_cutoff: float = 6.0, edge_order: int = 3, mlp_act: str = 'relu', edge_encoder: str = 'mlp', soft_edge: bool = True, norm_coors: bool = True, beta_schedule: str = 'sigmoid', beta_start: float = 1e-07, beta_end: float = 0.002, num_diffusion_timesteps: int = 1000, atom_vocab: List[str] | None = None, train_set: Any = None, **kwargs: Any)¶
Hydra entry point for
configs/tasks/diffusion_pmdm.yaml.- build() PMDMDiffusionTask¶
- atom_vocab¶
- model_kwargs¶
- task: PMDMDiffusionTask | None = None¶
- task_type = 'diffusion_pmdm'¶
- train_set = None¶
- class MolecularDiffusion.modules.tasks.diffusion_pmdm.PMDMConstrainedGenerator(task, mode: str = 'lead_opt', pocket_db: str | None = None, pocket_index: int = 0, mol_size: list | None = None, pocket_file: str | None = None, mol_file: str | None = None, fixed_atoms: List[int] | None = None, atoms_to_replace: List[int] | None = None, atoms_to_add: int | None = None, num_generate: int = 20, batch_size: int = 4, num_steps: int | None = None, sampling_type: str = 'generalized', step_lr: float = 1e-06, clip: float = 1000.0, clip_pos: float | None = None, global_start_sigma: float | None = None, w_global_pos: float = 1.0, w_global_node: float = 1.0, w_local_pos: float = 1.0, w_local_node: float = 1.0, output_path: str = 'generated_pmdm_constrained', seed: int = 42, device: str | None = None, validity_filter: bool = True, max_retries: int = 10, **kwargs: Any)¶
Bases:
PMDMPocketGeneratorConstrained generation (lead optimisation OR linker design), behind
interference/gen_pocket(mode: lead_opt/mode: linker) – seepocket_generator.py’s_TASK_TO_GENERATORfor the dispatch.One class for both – following the same pattern DiffSBDD already uses for its own de novo/inpaint split (
gen_diffsbdd_pocket.yamlvsgen_diffsbdd_inpaint.yaml, same_target_): the two upstream sampling methods (inpainting_sample/linker_sample) share their entire loop, differing only in which atoms are held fixed and which internal EGNN masking mode fires (PMDMDiffusionTask.sample’smodedispatch). A second class here would be exactly the “interface with one implementation”gen_diffsbdd_inpaint.yaml’s own header warns against.modepicks the task and which of the two mutually-exclusive atom lists is read:"lead_opt"– keepfixed_atomsof a starting ligand (mol_file) exactly as given, growatoms_to_addbrand-new atoms onto it. Mirrors upstream’ssample_frag.py --keep_index."linker"– deleteatoms_to_replace(typically the gap between two fragments) and regrowatoms_to_addatoms in their place. Mirrors upstream’ssample_linker.py --mask.
Both read the pocket from
pocket_file– a plain PDB, NOT a converted db, unlikePMDMPocketGenerator’s de novo path (which stays a separate class: its input genuinely is a different source, not just a different knob – a converted ASE db row, not a live file).- db_required_msg = None¶
- keep_atoms¶
- mode = 'lead_opt'¶
- mol_file = None¶
- n_new_atoms = 0¶
- pocket_file = None¶
- replace_atoms¶
- tag = 'pmdm-leadopt'¶
- class MolecularDiffusion.modules.tasks.diffusion_pmdm.PMDMDiffusionTask(model: MolecularDiffusion.modules.models.pmdm.PMDMEpsNet, atom_vocab: List[str] | None = None, size_histogram: torch.Tensor | None = None)¶
Bases:
torch.nn.ModuleTask contract around
PMDMEpsNet.- evaluate(pred, target)¶
- forward(batch)¶
- predict_and_target(batch)¶
- sample(batch_size=None, nodesxsample=None, num_steps=None, batch=None, mode: str | None = None, n_new_atoms: int | None = None, **kwargs)¶
Sample ligands inside the pocket carried by
batch.The signature deliberately deviates from Section 2.1: it takes a pocket.
batchmust hold theprotein_*keys of a collated PMDM batch (PMDMPocketGeneratorbuilds it). There is no unconditional mode – the network has no path that runs without a pocket.mode(None|"lead_opt"|"linker"):None(default) is the existing de novo path, unchanged. Either other value is constrained sampling –batchmust ALSO carryfrag_ligand_pos/frag_ligand_atom_feature/frag_element_batch(the kept fragment, tiled once per sample –PMDMConstrainedGeneratorbuilds these), andn_new_atomsis required (how many brand-new atoms to grow, mirroring upstream’s single--num_atom). Dispatches toPMDMEpsNet.inpainting_sample("lead_opt") orlinker_sample("linker") instead oflangevin_dynamics_sample.Returns
(one_hot, charges, coords, node_mask)padded to(B, N, .), in the ORIGINAL pocket frame.chargesis zeros: PMDM’s pocket configuration has no charge channel.
- atom_vocab¶
- property device¶
- model¶
- property node_dist_model: LigandSizeDistribution¶
- prop_dist_model = None¶
- split = 'train'¶
- class MolecularDiffusion.modules.tasks.diffusion_pmdm.PMDMPocketGenerator(task, pocket_db: str | None = None, pocket_index: int = 0, num_generate: int = 20, batch_size: int = 4, num_steps: int | None = None, mol_size: list | None = None, sampling_type: str = 'generalized', step_lr: float = 1e-06, clip: float = 1000.0, clip_pos: float | None = None, global_start_sigma: float | None = None, w_global_pos: float = 1.0, w_global_node: float = 1.0, w_local_pos: float = 1.0, w_local_node: float = 1.0, output_path: str = 'generated_pmdm', seed: int = 42, device: str | None = None, validity_filter: bool = True, max_retries: int = 10, **kwargs: Any)¶
Bases:
MolecularDiffusion.modules.tasks.pocket_generator.PocketGeneratorPocket-conditioned generation behind
interference/gen_pmdm_pocket.The pocket comes from one row of a converted ASE db (
docs/model_integrations/pmdm/scripts/convert_dataset.py), read withcenter=Falseso the sampled ligand comes back in that pocket’s own frame.The only generator that may write FEWER than
num_generate: unusable samples are filtered out and resampled, bounded bymax_retries.The sampling loop itself lives in
PocketGenerator.- db_required_msg = 'interference.pocket_db is required: PMDM has no unconditional mode. Point it at a converted ASE...¶
- max_retries = 10¶
- sampler_kwargs¶
- seed_numpy = False¶
- tag = 'pmdm'¶
- validity_filter = True¶
- MolecularDiffusion.modules.tasks.diffusion_pmdm.ATOMIC_NUMBERS¶
- MolecularDiffusion.modules.tasks.diffusion_pmdm.BOND_TOLERANCE = 0.4¶
- MolecularDiffusion.modules.tasks.diffusion_pmdm.INT_TYPE¶
- MolecularDiffusion.modules.tasks.diffusion_pmdm.MAX_LIGAND_SIZE = 128¶
- MolecularDiffusion.modules.tasks.diffusion_pmdm.MIN_ATOM_SEPARATION = 0.9¶