MolecularDiffusion.modules.tasks.diffusion_syncogen¶
SynCoGen task: joint masked graph diffusion + coordinate flow matching.
SynCoGen generates molecules the way a chemist would make them: nodes are
catalogue building blocks, edges are reaction templates, and the 3D coordinates
are flow-matched alongside. Chemical bonds are never diffused – they are a
consequence of the sampled reaction graph, recovered by RDKit assembly at the
end (BBRxnGraph.build_rdkit -> RDKitMoleculeAssembly).
Two things follow from that, and both shape this file.
The batch is not a point cloud. Training consumes upstream’s own PyG
Data/Batch end to end, produced by the task-owned
modules/models/syncogen/datamodule.py. No platform data_type is involved
and nothing under MolecularDiffusion/data/ is touched.
Generation is. GenerativeFactory’s unconditional loop wants
(one_hot, charges, coords, node_mask) plus a dense bond matrix on
last_bond_types. So the only adapters in this integration live in
sample(), converting the sampled (BBRxnGraph, Coordinates) pair into
those tensors – see _assemble and _to_pointcloud.
Two traps worth naming here rather than rediscovering:
``charges`` carries signed formal charges, not atomic numbers. The channel is overloaded in the platform:
save_xyz_filewould read it as atomic numbers, but only when the model setsuse_unknown_fallback(which this one does not, so the value is unused there), whileGenerativeFactory._write_molecule_sdfpasses it straight through asformal_charge=(runmodes/generate/tasks_generate.py:344). Atomic numbers here would silently stamp every SDF atom with a +6/+7/+8 charge.Atom ordering is fixed by ``set_mol_coordinates``. Upstream assigns the masked coordinate rows positionally onto the assembled molecule’s atoms, so the point-cloud order and the RDKit atom order are the same order by construction.
one_hot,charges,coordsand the bond matrix are all built in that one order, or the.xyzand the.sdfwould describe different molecules.
Vocabulary loading is process-global and must happen before any other import
from the ported package – see modules/models/syncogen/vocab.py. That is why
every syncogen import in this file is deferred into a function body.
Attributes¶
Classes¶
Instantiated by |
|
The duck-typed |
|
Fragment-count prior, and the small surface |
Module Contents¶
- class MolecularDiffusion.modules.tasks.diffusion_syncogen.ModelTaskFactory(task_type: str = 'diffusion_syncogen', vocab_dir: str | None = None, backbone: str = 'semla', d_model: int = 384, d_message: int = 128, n_coord_sets: int = 64, n_layers: int = 12, n_head: int = 32, d_message_hidden: int = 128, d_edge: int = 128, coord_norm: str = 'length', size_emb: int = 64, pos_emb: int = 64, max_bbs: int = 5, self_conditioning: bool = True, pharm_subset: int = 7, noise_sigma_min: float = 0.001, noise_sigma_max: float = 7.0, num_sample_steps: int = 100, sampling_eps: float = 0.001, antithetic_sampling: bool = True, importance_sampling: bool = False, sampling_noise_removal: bool = True, use_compat: bool = True, time_conditioning: bool = True, train_rot_align: bool = True, augmentations: Sequence[str] = ('center', 'random_rotate', 'normalize'), scale_noise: bool = True, scale_noise_factor: float = 0.2, inference_annealing: bool = True, annealing_coef: float = 10.0, ema_decay: float = 0.0, nll_coef: float = 1.0, mse_coef: float = 1.0, bond_length_coef: float = 0.2, pairwise_distance_coef: float = 0.4, pairwise_distance_threshold: float = 5.0, smooth_lddt_coef: float = 0.4, loss_t_threshold: float = 0.25, load_bonds: bool = True, load_pharmacophores: bool = False, num_fragments_probs: dict | None = None, eval_batch_size: int = 4, reference_ligand: str | None = None, sdf_output_path: str | None = None, max_sample_attempts: int = 5, atom_vocab: list | None = None, **kwargs: Any)¶
Instantiated by
cli/train.py/cli/generate.pyfrom Hydra.No
train_setparameter is declared, so the §2.5 injection seam stays inert: SynCoGen’s only construction-time statistic is the fragment-count histogram, and that is config-owned (num_fragments_probs) precisely so generation needs no dataset.- build() SyncogenDiffusionTask¶
- annealing_coef¶
- antithetic_sampling = True¶
- atom_vocab¶
- augmentations¶
- backbone = 'semla'¶
- backbone_kwargs¶
- ema_decay¶
- eval_batch_size = 4¶
- generation_time_keys = ('vocab_dir', 'reference_ligand', 'sdf_output_path', 'num_fragments_probs', 'eval_batch_size',...¶
- importance_sampling = False¶
- inference_annealing = True¶
- load_bonds = True¶
- load_pharmacophores = False¶
- loss_coefs¶
- max_bbs = 5¶
- max_sample_attempts = 5¶
- noise_sigma_max¶
- noise_sigma_min¶
- num_fragments_probs¶
- num_sample_steps = 100¶
- pharm_subset = 7¶
- reference_ligand = None¶
- sampling_eps¶
- sampling_noise_removal = True¶
- scale_noise = True¶
- scale_noise_factor¶
- sdf_output_path = None¶
- self_conditioning = True¶
- task: SyncogenDiffusionTask | None = None¶
- task_type = 'diffusion_syncogen'¶
- time_conditioning = True¶
- train_rot_align = True¶
- use_compat = True¶
- vocab_dir = None¶
- class MolecularDiffusion.modules.tasks.diffusion_syncogen.SyncogenDiffusionTask(core: torch.nn.Module, size_prior: SyncogenSizePrior, atom_vocab: list, task_type: str, num_sample_steps: int, pharm_subset: int, max_bbs: int, reference_ligand: str | None = None, sdf_output_path: str | None = None, max_sample_attempts: int = 5)¶
Bases:
torch.nn.ModuleThe duck-typed
Taskobject (docs/adding_new_models.md §2.1).- evaluate(pred: torch.Tensor, target: torch.Tensor) dict¶
- forward(batch) tuple[torch.Tensor, dict]¶
- predict_and_target(batch) tuple[torch.Tensor, torch.Tensor]¶
Pure-generative stub: the loss is both prediction and target.
- sample(batch_size: int | None = None, nodesxsample: torch.Tensor | None = None, num_steps: int | None = None, mode: str | None = None, n_frames: int = 0, **kwargs: Any) tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]¶
Return
(one_hot, charges, coords, node_mask)forbatch_sizemolecules.nodesxsampleis a fragment count per molecule, not an atom count (seeSyncogenSizePrior).chargescarries signed formal charges.A sampled graph can come back with a reaction the assembler rejects, so a round is retried up to
max_sample_attemptstimes rather than emitting an empty.xyzfor the failed rows. All attempts exhausted -> raise, so the generation loop counts the batch as failed instead of writing nothing and exiting 0.
- T¶
- atom_vocab¶
- core¶
- property device: torch.device¶
- last_bond_types: torch.Tensor | None = None¶
- max_bbs¶
- max_sample_attempts = 5¶
- property model: SyncogenDiffusionTask¶
- property node_dist_model: SyncogenSizePrior¶
- pharm_subset¶
- prop_dist_model = None¶
- reference_ligand = None¶
- sdf_output_path = None¶
- size_prior¶
- task_type¶
- class MolecularDiffusion.modules.tasks.diffusion_syncogen.SyncogenSizePrior(num_fragments_probs: dict, max_bbs: int = 5, batch_size: int = 4, eval_batch_size: int = 4, load_bonds: bool = True, load_pharmacophores: bool = False)¶
Fragment-count prior, and the small surface
core.Diffusionreads.Serves three roles at once, which is why it is one object and not three:
task.node_dist_model–.sample(n) -> LongTensorof per-molecule sizes. For SynCoGen a “size” is a fragment count (typically 2-5), not an atom count, somol_sizein a generate config must be[0, 0]or a range inside[min, max_bbs].GenerativeFactory.__init__clamps againstmax(n_node_dist)and raises with a clear message otherwise.task.n_node_dist– the{n_fragments: count}histogram that clamp reads.The stand-in for upstream’s
SyncogenDataManagerinsidecore.Diffusion, which reaches formax_bbs,batch_size,eval_batch_size,load_bonds,load_pharmacophoresandsample_n_nodes. Keeping it config-owned rather than dataset-owned is what lets generation run with no dataset present at all – which is the whole point for the released checkpoints, where the 6.3 GB SynSpace download is not needed.
- sample(n: int) torch.Tensor¶
- sample_n_nodes(batch_size: int) torch.Tensor¶
- batch_size = 4¶
- eval_batch_size = 4¶
- forced_n_nodes: torch.Tensor | None = None¶
- load_bonds = True¶
- load_pharmacophores = False¶
- max_bbs = 5¶
- property n_node_dist: dict¶
{n_fragments: count}. Counts are a scaled histogram; only the keys and their relative sizes matter toGenerativeFactory.
- train_length_probs¶
- train_length_values¶
- MolecularDiffusion.modules.tasks.diffusion_syncogen.logger¶