MolecularDiffusion.modules.tasks.diffusion_goflow¶
GoFlow: transition-state geometry from a reaction’s condensed graph of reaction (CGR), via conditional flow matching and GotenNet.
Galustian, Mark, Karwounopoulos, Kovar & Heid, GoFlow: Efficient
Transition State Geometry Prediction with Flow Matching and E(3)-Equivariant
Neural Networks, ChemRxiv (2025), doi:10.26434/chemrxiv-2025-bk2rh. Ported
from the repo checked out at others/nice/goflow (commit
3ec00a09d9b283e3258ae01fe5d3e35bb3812bff).
Three pieces live here:
GoFlowTaskThe platform task contract around
GotenNet.forwardreproduces upstream’sFlowModule.train_val_step(flow_matching/flow_module.py:94-99);samplereproducesFlowModule.test_step’s ODE integration (:116-134) but returns every draw independently, unranked – see Scope below.ModelTaskFactoryHydra entry point for
configs/tasks/diffusion_goflow.yaml.representation:is a nested Hydra_target_block that Hydra instantiates into a realGotenNetbefore this factory is constructed (mirroring upstream’s ownconfigs/model/flow.yamlnesting), sobuild()is a two-line assembly, not a second-level dict-to-object conversion.GoFlowTSGeneratorA thin
TSGeneratorsubclass. It writes no ``run()`` loop. “Load one reaction, tile it to a batch, sample the transition state into it, write .xyz” is the shared TS layer (ts_generator.py) on top of the shared pocket loop (pocket_generator.py); what is left here is only GoFlow’s own: which corpus (the converted RDB7 pickle + itsfeat_dict.pkl+ split files), which collate, and nothing else – GoFlow has no RePaint knobs, no solver choice, just a fixed-step Euler integrator with one knob (num_steps) the base already carries.
Scope of this integration¶
Transition-state geometry given a reaction’s connectivity (reactant +
product bonds and per-atom RDKit descriptors), and nothing else. Not
ported, each for a stated reason (see INTEGRATION_PLAN.md, Explicitly
out of scope, for the full list): upstream’s GT-anchored Kabsch-rotate-then-
median-consensus ensembling and RDKit-substructure permutation matching
(both require a known transition state, so cannot run on a real, blind
reaction); the vendored tsdiff/ baseline (a separate, complete model);
schedule_free optimizer mode; the model-size ablation configs; trajectory
frames. ``sample()`` returns every ``num_generate`` draw independently,
unranked and unpermuted – same “no RMSD scoring, no best-of-N ranking”
design as the two existing TS generators
(modules/tasks/ts_generator.py’s own stated principle).
Two facts that constrain everything¶
Atom identity is supplied by the input reaction and copied straight through – only coordinates are generated.
sample()’s one-hot encoding reusesN_ELEMENTand_pad_ts()by import (do not duplicate them): the five-wide[H, C, N, O, F]layout is architecture-agnostic padding logic, not something specific to OA-ReactDiff. RDB7 never populates the “F” column (it is a C/H/N/O corpus), but the column costs nothing.GoFlow has no charge concept at all (unlike OA-ReactDiff/React-OT, whose ninth column is the atomic number standing in for a charge slot): the charge column in
sample()’s output is zeros, full stop.
Classes¶
Transition-state generation behind |
|
Task contract around |
|
Hydra entry point for |
Module Contents¶
- class MolecularDiffusion.modules.tasks.diffusion_goflow.GoFlowTSGenerator(task: Any, reaction_index: int = 0, feat_dict_file: str = '', split_path: str = '', split_file: str = 'random_split.pkl', split: str = 'test', num_generate: int = 25, batch_size: int = 4, num_steps: int | None = 25, output_path: str = 'generated_goflow', seed: int = 42, device: str | None = None, **kwargs: Any)¶
Bases:
MolecularDiffusion.modules.tasks.ts_generator.TSGeneratorTransition-state generation behind
interference/gen_goflow_ts.The shape of the request is identical to every other TS run – load one reaction, tile it to a batch, sample the transition state into it, write .xyz – so the loop is
run(), the reaction plumbing isTSGenerator, and what is left here is only GoFlow’s own: which corpus (the converted RDB7 pickle, plus thefeat_dict.pkland split files it needs alongside it) and which collate. No ``PocketGenerator`` hook beyond what ``TSGenerator`` already fills is overridden.Unlike OA-ReactDiff/React-OT, GoFlow’s corpus is not one self-contained pickle:
GoFlowRDB7Datasetalso needs the frozenfeat_dict.pkl(the one-hot vocabulary the convertedReactionSide.featcolumns were built against) and the split directory/file, so this generator adds those three as its own constructor keys rather than folding them intoreaction_source– exactly the same “a subclass may add its own extra kwargs beyond the shared base” pattern as OA-ReactDiff’s RePaint knobs or React-OT’s solver knobs.source_keystays the shared default,"reaction_source": GoFlow’s corpus really is “a reaction source”, a converted pickle, no more honest model-specific name exists (mirrorsINTEGRATION_PLAN.md’s Inference Task Decision table verbatim).Configure the run.
- Parameters:
task – the loaded
GoFlowTask; itssample()is what gets called.reaction_index – which reaction in the split to generate for.
feat_dict_file – the frozen
feat_dict.pklthis corpus’s one-hot columns were built against.split_path – directory holding the split pickle.
split_file – which split pickle, e.g.
random_split.pkl.split –
"train","val"or"test"; defaults to the held-out"test"split.num_generate – how many transition states to sample. 25 mirrors the actual headline run’s ensemble size (
train_test_all_splits.sh’smodel.num_samples=25).batch_size – how many of those to sample at once.
num_steps – Euler steps;
None=> the task’s default (25).output_path – directory for the .xyz files.
seed – torch/random/numpy seed.
device –
None=> cuda if available.**kwargs – forwarded to
TSGenerator(reaction_sourcearrives this way), then rejected by the base if unknown.
- Raises:
ValueError – if
feat_dict_fileorsplit_pathis not set.
- db_required_msg = 'interference.reaction_source is required: GoFlow has no unconditional mode -- it needs a...¶
- feat_dict_file = ''¶
- seed_numpy = True¶
- source_key = 'reaction_source'¶
- split = 'test'¶
- split_file = 'random_split.pkl'¶
- split_path = ''¶
- tag = 'goflow'¶
- class MolecularDiffusion.modules.tasks.diffusion_goflow.GoFlowTask(representation: MolecularDiffusion.modules.models.goflow.GotenNet, output_n_hidden: int = 64, num_steps: int = 25, atom_vocab: List[str] | None = None)¶
Bases:
torch.nn.ModuleTask contract around
GotenNet+Atomwise3DOut.- evaluate(pred: torch.Tensor, target: torch.Tensor) Dict[str, torch.Tensor]¶
Validation metric.
Upstream’s substructure-matched DMAE/RMSD validation (
evaluate_geometry,callbacks.py:182-239) is deliberately not here – same reasoning as oareactdiff/reactot leaving their own heavier upstream validation-time sampling out of scope.
- forward(batch: Dict[str, Any]) Tuple[torch.Tensor, Dict[str, Any]]¶
One training/validation step.
Reproduces
FlowModule.train_val_step(flow_module.py:94-99): drawx_0, Kabsch-align the ground truth onto it, interpolate to a random time, and regress the straight-line velocity underrmsd_loss.- Parameters:
batch –
{"batch": <PyG Batch>}fromgoflow_collate(); the batch must carryts_pos.- Returns:
(loss, stats).- Raises:
ValueError – if the batch carries no
ts_pos(this model cannot train without a known transition state).
- model_output(x_t_N_3: torch.Tensor, batch: Any, t_G: torch.Tensor) torch.Tensor¶
One network call:
(x_t, t, batch) -> predicted velocity.Reproduces
FlowModule.model_output(flow_module.py:101-104).
- predict_and_target(batch: Dict[str, Any]) Tuple[torch.Tensor, torch.Tensor]¶
Pure-generative stub: the loss is the prediction, target is zero.
- sample(batch_size: int | None = None, nodesxsample: torch.Tensor | None = None, num_steps: int | None = None, batch: Dict[str, Any] | None = None, num_generate: int | None = None, **kwargs: Any) Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]¶
Generate transition states for the reactions in
batch.The signature deviates from Section 2.1 the same way every pocket-conditioned task’s does: the conditioning structure arrives as
batch, because there is no channel for “which reaction” insample(batch_size, nodesxsample, ...).One fresh Gaussian draw per call, integrated with the vendored fixed-step Euler stepper (
euler_integrate()) overlinspace(0, 1, num_steps)– reproducesFlowModule.test_step’s per-sample loop (flow_module.py:128-134) MINUS the GT-anchored substructure matching + Kabsch-to-reference + median-consensus collapsing (structurally requires a known TS; seeINTEGRATION_PLAN.md’s Repo Inspection caveat and Explicitly out of scope). Every sample inbatchis returned independently and unranked.- Parameters:
batch_size – ignored; taken from
batch.nodesxsample – accepted and checked, not used to choose. The transition state has exactly as many atoms as the reaction, so a value disagreeing with it is a caller bug worth surfacing rather than silently overriding.
num_steps – Euler steps.
Nonefalls back toself.num_steps(25, the actual headline run’s value).batch –
{"batch": <PyG Batch>}, one graph per requested sample (built by tiling one reactionntimes).num_generate – unused; accepted for signature parity with the platform contract. The number of samples is however many graphs
batchcarries.
- Returns:
(one_hot, charges, coords, node_mask)for the transition state, padded to(B, N, .).chargesis all zeros: GoFlow has no charge concept, unlike OA-ReactDiff/React-OT’s atomic-number-as-charge column.- Raises:
ValueError – if
batchis missing, ornodesxsampledisagrees with the reaction’s atom count.
- atom_vocab¶
- property device: torch.device¶
Device of the first parameter.
- property model: GoFlowTask¶
There is no separate inner module the generation code needs.
- node_dist_model: Any = None¶
- num_steps = 25¶
- output_layer¶
- prop_dist_model: Any = None¶
- representation¶
- split = 'train'¶
- class MolecularDiffusion.modules.tasks.diffusion_goflow.ModelTaskFactory(task_type: str = 'diffusion_goflow', representation: MolecularDiffusion.modules.models.goflow.GotenNet | None = None, output_n_hidden: int = 64, num_steps: int = 25, atom_vocab: List[str] | None = None, **kwargs: Any)¶
Hydra entry point for
configs/tasks/diffusion_goflow.yaml.No
train_setparameter: the only construction-time statistic (n_atom_rdkit_feats) is a fixed config int, verified against the shippedfeat_dict.pklbyGoFlowRDB7Datasetat data-load time, not injected via the Section 2.5train_setseam.cli/train.pyis untouched.Record GoFlow’s own hyperparameters.
- Parameters:
task_type –
"diffusion_goflow".representation – the (already Hydra-instantiated)
GotenNetbackbone – required;configs/tasks/diffusion_goflow.yamlnests it asrepresentation: {_target_: ...GotenNet, ...}, so Hydra builds it before this factory is constructed.output_n_hidden –
Atomwise3DOut’s hidden width (upstream’sconfigs/model/flow.yaml’soutput.n_hidden: 64).num_steps – default Euler steps for
sample()(the actual headline run’s value, 25 – see the plan’s Hyperparameter Provenance table for the config-default-vs-actual-run conflict this resolves).atom_vocab – output vocabulary; defaults to
[H, C, N, O, F](see the module docstring for why “F” is present but never populated by RDB7).**kwargs – swallowed;
cli/train.pyinjectsnode_feature*.
- Raises:
ValueError – if
representationis missing.
- build() GoFlowTask¶
Assemble the representation + output head into the task.
- atom_vocab¶
- generation_time_keys = ('reaction_source',)¶
- num_steps = 25¶
- representation = None¶
- task: GoFlowTask | None = None¶
- task_type = 'diffusion_goflow'¶