MolecularDiffusion.modules.tasks.diffusion_loqi¶
LoQI – low-energy conformer generation on a fixed molecular graph.
One module, two task types, because upstream is one model with two configs:
|
interpolant |
weights |
|---|---|---|
|
VDM diffusion, 25 discrete steps, cosine-adaptive, self-conditioned |
|
|
continuous flow matching, velocity prediction, rigid (Kabsch) OT, linear |
|
This is not de-novo generation. Only x (coordinates) is ever noised.
Atom types, formal charges, bond orders and stereochemistry are supplied
un-noised as network input at every step and come straight back out of
sample() unchanged. Every sample therefore needs an input molecule; see
sample_input below.
Data path: data_type: graph3d with bond_collate: raw (a PyG Batch)
and kekulize: true.
Bond representation mapping¶
LoQI’s network consumes a fully-connected directed edge list with a 9-class
label: 0=none 1=SINGLE 2=DOUBLE 3=TRIPLE 4=AROMATIC 5=E 6=Z
7=chirality(sym) 8=chirality(directed). Classes 5-8 are not bonds – they are
a stereochemistry encoding laid over the bond graph, and the class-8 edges are
deliberately asymmetric (the direction is the R/S signal), so they cannot be
stored in the platform’s symmetric upper-triangular bond_index/bond_type.
So storage keeps the canonical five, and Graph3DToLoQIAdapter
reconstructs the rest per batch:
mirror the stored upper-triangular bonds into a directed list (reproducing upstream’s
adj.nonzero());rebuild the molecule with
build_rdkit_mol– which callsAssignStereochemistryFrom3D, the same source of truth upstream’sadd_stereo_bonds(from_3D=True)used – and derive classes 5-8 from it;run upstream’s own
make_graph_fully_connectedto materialize class 0.
Class 4 never appears: the LoQI pipeline kekulizes (process_chembl3d.py:216,
sample_conformers.py:84), so the released weights have never seen one.
There is no per-item cache for step 2 in this first pass. If RDKit dominates step time, the fix is a cache keyed on the dataset index, which needs the dataset to carry one – a separate change.
Attributes¶
Classes¶
|
|
LoQI wrapped in the platform's duck-typed Task contract (§2.1). |
|
Histogram sampler over |
|
Instantiated by |
Module Contents¶
- class MolecularDiffusion.modules.tasks.diffusion_loqi.Graph3DToLoQIAdapter(atom_vocab: list[str])¶
Bases:
torch.nn.Modulegraph3dPyGBatch-> LoQI’s flat conditioning dict.Output keys:
batch(N,),x(N,3) COM-free,h(N,17) one-hot inFULL_ATOM_ENCODERorder,charges(N,6) one-hot offc + 2,edge_index(2,E) fully connected directed,edge_attr(E,9) one-hot.- build_edges(pyg: Any, device) tuple[torch.Tensor, torch.Tensor]¶
Mirror stored bonds, append derived stereo edges, then densify.
- vocab_map(device) torch.Tensor¶
Platform
atom_idx-> LoQIFULL_ATOM_ENCODERindex.Built by SYMBOL, never by position: the platform’s
atom_vocaborder is a data-config choice, LoQI’s is fixed by the pretrainedatom_embedder’s input columns. Getting this wrong loads silently and mislabels every atom.
- atom_vocab¶
- class MolecularDiffusion.modules.tasks.diffusion_loqi.LoQIConformerTask(task_type: str, atom_vocab: list[str], interpolant_config: dict, dynamics_config: dict, self_cond_config: dict | None, timesteps: int, loss_scale: float, loss_clamp: float | None, return_step_output: bool, sample_input: str | collections.abc.Sequence[str] | None, sample_pool_limit: int, n_atoms_hist: dict, conditioning_pool: list)¶
Bases:
torch.nn.ModuleLoQI wrapped in the platform’s duck-typed Task contract (§2.1).
- evaluate(pred: torch.Tensor, target: torch.Tensor)¶
- forward(batch: Any) tuple[torch.Tensor, dict]¶
- predict_and_target(batch: Any)¶
- sample(batch_size: int | None = None, nodesxsample: torch.Tensor | None = None, num_steps: int | None = None, batch: dict | None = None, mode: str | None = None, n_frames: int = 0, mols: list | None = None, **kwargs: Any)¶
Generate conformers for a batch of conditioning molecules.
Returns the platform tuple
(one_hot (B,N,V), charges (B,N), coords (B,N,3), node_mask (B,N))withV = len(atom_vocab). Atom types and charges are the conditioning molecule’s – inputs, not predictions.nodesxsampleis honoured as a count, and molecules are size-matched to it where the pool allows.modeis accepted and ignored.molsnames the conditioning molecules EXPLICITLY (a list of the pool’sDataitems, repeats allowed) and bypasses the size-matched draw entirely – that is how the conformer mode asks for “k conformers of THIS molecule”, which a size draw cannot express. When it is None (every pre-existing caller) nothing changes.
- T¶
- adapter¶
- atom_vocab¶
- property device: torch.device¶
- dynamics¶
- interpolant¶
- loss_clamp¶
- loss_scale¶
- property model: LoQIConformerTask¶
- node_dist_model¶
- prop_dist_model = None¶
- return_step_output¶
- sample_input¶
- sample_pool_limit¶
- self_cond¶
- task_type¶
- timesteps¶
- class MolecularDiffusion.modules.tasks.diffusion_loqi.LoQINodeDistribution(histogram: dict)¶
Histogram sampler over
train_set.graph3d_stats.n_atoms_hist.Only used so
mol_size: [0, 0]works in a generate config; the actual molecule sizes always come from the conditioning molecules.- sample(n_samples: int) torch.Tensor¶
- histogram¶
- n_node_dist¶
- class MolecularDiffusion.modules.tasks.diffusion_loqi.ModelTaskFactory(task_type: str = 'diffusion_loqi', interpolant: dict | None = None, dynamics: dict | None = None, self_conditioning: dict | None = None, timesteps: int = 25, loss_scale: float = 3.0, loss_clamp: float | None = 10.0, return_step_output: bool = False, sample_input: str | collections.abc.Sequence[str] | None = None, sample_pool_limit: int = 2000, dataset_stats: dict | None = None, atom_vocab: list | None = None, train_set: torch.utils.data.Dataset | None = None, **kwargs: Any)¶
Instantiated by
cli/train.py/cli/generate.pyfrom Hydra.train_setis a declared parameter, which is what makes the declarative injection seam (docs/adding_new_models.md §2.5) hand over the dataset – needed for the size histogram and the default conditioning pool.- build() LoQIConformerTask¶
- atom_vocab¶
- dataset_stats¶
- dynamics¶
- generation_time_keys = ('sample_input', 'sample_pool_limit')¶
- interpolant¶
- kwargs¶
- loss_clamp = 10.0¶
- loss_scale¶
- return_step_output = False¶
- sample_input = None¶
- sample_pool_limit = 2000¶
- self_conditioning = None¶
- task: LoQIConformerTask | None = None¶
- task_type = 'diffusion_loqi'¶
- timesteps = 25¶
- train_set = None¶
- MolecularDiffusion.modules.tasks.diffusion_loqi.CHARGE_OFFSET = 2¶
- MolecularDiffusion.modules.tasks.diffusion_loqi.N_CHARGE_CLASSES = 6¶
- MolecularDiffusion.modules.tasks.diffusion_loqi.logger¶