MolecularDiffusion.modules.tasks.diffusion_nextmol¶
NExT-Mol de-novo 3D molecule generation (task_type: diffusion_nextmol).
Two decoupled halves, joined by nothing but a flat list of SMILES – which is upstream’s own architecture, not a simplification of it:
MoLlama (1D) writes a molecule down as a SELFIES string, which is decoded to SMILES and built into a 2D molecular graph by RDKit. Inference only; the published
acharkq/MoLlamacheckpoint is sampled from a bare BOS token. It is not part of this task’sstate_dict– the generator loads it at generation time, so the trained checkpoint stays DMT-only and does not grow by 2 GB.DMT (3D) takes that graph – atoms and bonds, no coordinates – and diffuses coordinates onto it. This is the half that trains here.
So the training data path never sees a SELFIES string, and no new data_type
is needed: graph3d already carries the explicit bond orders DMT conditions
on. See INTEGRATION_PLAN.md, “Representation Routing”.
Only coordinates are generated. Atom types and bonds are fixed conditioning on every forward and every sampling step, which is why:
NextMolTask.sample()raises. The de-novo pipeline is genuinely unconditional, butGenerativeFactory.sample(batch_size, nodesxsample, ...)makes the caller choose the atom count – whereas here the language model chooses the molecule and therefore its size – and the(one_hot, charges, coords, node_mask)return contract throws away the bonds MoLlama decided on. De-novo generation goes throughNextMolGenerator, named byconfigs/interference/gen_nextmol_denovo.yaml. Conformers of molecules you already have go through the sharedConformerFactory(gen_conformer.yaml), which drivesNextMolTask.generate_conformers()– the same route LoQI and DiTMC take.node_dist_model/n_node_distare deliberately absent – they exist to letGenerativeFactorypick a size, and that route is bypassed.
Bond mapping (canonical -> NExT-Mol): nextmol_col = canonical_class - 1, so
1=SINGLE -> 0, 2=DOUBLE -> 1, 3=TRIPLE -> 2, 4=AROMATIC -> 3.
Canonical class 0 (“no bond”) is never an edge; it is the all-zero 4-vector
to_dense_adj leaves on non-bonded pairs, so the platform’s storage rule is a
pass-through. Do not set ``kekulize: true`` – aromatic is a real class in
the 4-wide vocabulary and kekulizing would change the conditioning distribution
the published weights were trained on.
pos_std must match the checkpoint’s dataset (GEOM-QM9 1.4182, QM9-2014/JODO
1.7226, GEOM-Drugs 2.4777 / 2.3860) or every structure comes back mis-scaled.
Attributes¶
Classes¶
One |
|
SMILES-keyed cache of upstream's per-molecule RDKit featurization. |
|
The de-novo pipeline, and the conformer-only mode, in one class. |
|
DMT in the platform's Task contract (docs/adding_new_models.md 2.1). |
|
Factory instantiated by |
Module Contents¶
- class MolecularDiffusion.modules.tasks.diffusion_nextmol.Graph3DBatchToNextMolAdapter(dataset: str = 'qm9', pos_std: float = 1.4182, noise_scheduler: MolecularDiffusion.modules.models.nextmol.NoiseScheduleVPV2 | None = None, aug_rotation: bool = True, t_cond: str = 't', disable_com: bool = True)¶
Bases:
torch.nn.ModuleOne
graph3dPyGBatch-> the containerDGTDiffusionwants.Bond symmetry:
featurize_molemits every bond in both directions with the sameedge_attr, which is upstream’s only symmetry mechanism –to_dense_adjwould silently produce an asymmetric matrix from a one-directional list. The mirroring therefore happens insidefeaturize_mol, on the rebuilt RDKit mol, not on the stored upper-triangularbond_index.- add_noise(data)¶
Port of
QM9Collater.add_noise(diffusion_data_module.py:50).Runs on CPU tensors, as upstream’s collate does, then the whole batch is moved to the device in one go.
- collate(data_list: list, device=None, *, add_noise: bool = True)¶
Batch, then either noise (training) or seed from the prior (sampling).
- forward(batch: Any, device=None)¶
- seed_prior(data)¶
Port of
QM9InferCollater.__call__– coordinates from the prior.
- aug_rotation = True¶
- cache¶
- dataset = 'qm9'¶
- disable_com = True¶
- noise_scheduler¶
- pos_std = 1.4182¶
- t_cond = 't'¶
- class MolecularDiffusion.modules.tasks.diffusion_nextmol.MoleculeFeatureCache(dataset: str = 'qm9', maxsize: int = 200000)¶
SMILES-keyed cache of upstream’s per-molecule RDKit featurization.
Rebuilding the mol and running
featurize_molis a full RDKit round-trip; upstream does it once at dataset-build time, so doing it per batch here would dominate the step. Same pattern asmodules/tasks/diffusion_ditmc.MoleculeFeatureCache.- get(item)¶
itemis one PyGDatafrom the graph3d dataset.
- dataset = 'qm9'¶
- types¶
- class MolecularDiffusion.modules.tasks.diffusion_nextmol.NextMolGenerator(task: Any, mollama_model: str | None = None, num_molecules: int = 100, temperature: float = 1.0, num_beams: int = 1, max_sf_tokens: int = 30, lm_batch_size: int = 200, num_generate: int = 1, batch_size: int = 16, num_steps: int = 100, max_atom: int = 200, seed: int = 42, device: str | None = None, output_path: str = 'generated_nextmol')¶
The de-novo pipeline, and the conformer-only mode, in one class.
cli/generate.pydoesinstantiate(cfg.interference, task=task)then.run(), so every key in the interference YAML lands in__init__.Where the SMILES come from is the ONLY difference between the two modes:
mollama_modelset -> de novo. Sample from the language model untilnum_moleculesvalid molecules accumulate. The fullselfies\tsmiles_chiral\tsmilesTSV is written to<output_path>/sampled_sequences.tsvso the intermediate 1D list is inspectable, exactly as upstream saves it.mollama_model: null-> conformers of molecules you supply, viasmilesordb_path(+indices).
Either way RDKit supplies atoms and bonds ONLY; the coordinates are what the model produces.
- batch_size = 16¶
- device¶
- lm_batch_size = 200¶
- max_atom = 200¶
- max_sf_tokens = 30¶
- mollama_model = None¶
- num_beams = 1¶
- num_generate = 1¶
- num_molecules = 100¶
- num_steps = 100¶
- output_path = 'generated_nextmol'¶
- seed = 42¶
- task¶
- temperature = 1.0¶
- class MolecularDiffusion.modules.tasks.diffusion_nextmol.NextMolTask(dataset: str = 'qm9', model_kwargs: dict | None = None, pos_std: float = 1.4182, noise_schedule: str = 'cosine', continuous_beta_0: float = 0.1, continuous_beta_1: float = 20.0, discrete_schedule: bool = False, sampling_steps: int = 100, t_cond: str = 't', aug_rotation: bool = True, align_loss: bool = True, reduce_node_mean: bool = False, disable_com: bool = True, atom_vocab: list | None = None, task_type: str = 'diffusion_nextmol')¶
Bases:
torch.nn.ModuleDMT in the platform’s Task contract (docs/adding_new_models.md 2.1).
- evaluate(pred: torch.Tensor, target: torch.Tensor)¶
- forward(batch: Any)¶
- generate_conformers(data_list: list, *, num_steps: int | None = None)¶
Ancestral VP sampler (
model/uncond_gen_pl.py:96).data_listholds either upstream-shapedDataitems (what the de-novo path builds from MoLlama’s SMILES) or plaingraph3ditems (whatConformerFactoryhands over fromsample_input); the latter are featurized here through the adapter’s existing seam. No coordinates are needed either way – they are seeded from the prior. Returns(pos, batch_segments),posalready rescaled bypos_std.
- predict_and_target(batch: Any, all_loss=None, metric=None)¶
- sample(*args, **kwargs)¶
- adapter¶
- align_loss = True¶
- atom_vocab¶
- dataset = 'qm9'¶
- property device: torch.device¶
- disable_com = True¶
- property model: NextMolTask¶
- net¶
- noise_scheduler¶
- pos_std = 1.4182¶
- reduce_node_mean = False¶
- sampling_steps = 100¶
- t_cond = 't'¶
- task_type = 'diffusion_nextmol'¶
- class MolecularDiffusion.modules.tasks.diffusion_nextmol.NextMolTaskFactory(task_type: str = 'diffusion_nextmol', dataset: str = 'qm9', model: dict | None = None, pos_std: float = 1.4182, noise_schedule: str = 'cosine', continuous_beta_0: float = 0.1, continuous_beta_1: float = 20.0, discrete_schedule: bool = False, sampling_steps: int = 100, t_cond: str = 't', aug_rotation: bool = True, align_loss: bool = True, reduce_node_mean: bool = False, disable_com: bool = True, atom_vocab: list | None = None, train_set: Any = None, **kwargs: Any)¶
Factory instantiated by
cli/train.py.train_setis declared purely so the documented declarative injection seam (docs/adding_new_models.md 2.5) can hand over the dataset for an optional size histogram; DMT needs no construction-time statistics (pos_stdis a config scalar, not a computed stat).- build() NextMolTask¶
- align_loss = True¶
- atom_vocab¶
- aug_rotation = True¶
- continuous_beta_0 = 0.1¶
- continuous_beta_1 = 20.0¶
- dataset = 'qm9'¶
- disable_com = True¶
- discrete_schedule = False¶
- kwargs¶
- model_kwargs¶
- noise_schedule = 'cosine'¶
- pos_std = 1.4182¶
- reduce_node_mean = False¶
- sampling_steps = 100¶
- t_cond = 't'¶
- task: NextMolTask | None = None¶
- task_type = 'diffusion_nextmol'¶
- train_set = None¶
- MolecularDiffusion.modules.tasks.diffusion_nextmol.ModelTaskFactory¶
- MolecularDiffusion.modules.tasks.diffusion_nextmol.N_BOND_CLASSES = 4¶
- MolecularDiffusion.modules.tasks.diffusion_nextmol.logger¶