Skip to content

Reaction templates

A template describes the reaction centre. Everything else in the molecule comes along on its own, which is why a template can be written small: [C:1][OH:2]>>[C:1][Cl:2] says "a hydroxyl becomes a chloride" and says nothing about the rest of the molecule.

Esterification

The template used here is [C:1](=[O:2])[OH:3].[OH:4][C:5]>>[C:1](=[O:2])[O:4][C:5]. The benzene ring is never mentioned in it; the water on the right is not in it either — that comes from byproduct reconstruction.

>>> acid = omgkit.parse_smiles("CC(=O)O"); acid.sanitize()
>>> amine = omgkit.parse_smiles("CCN"); amine.sanitize()
>>> rxn = omgkit.parse_reaction("[C:1](=[O:2])[OH].[N:3]>>[C:1](=[O:2])[N:3]")
>>> rxn.num_reactant_templates, rxn.num_product_templates
(2, 1)
>>> out = rxn.run([acid, amine])[0]
>>> [p.to_canonical_smiles() for p in out.products]
['CC(NCC)=O']

Each reactant template gets a different input molecule. Mismatched counts give an empty list — see Intramolecular below for the case that needs.

The order you hand the molecules in does not decide whether it runs

Position is not chemistry. rxn.run([amine, acid]) gives the same products as rxn.run([acid, amine]): the engine tries "template i with molecule i" first, and only if that yields nothing does it look for another one-to-one assignment. When your order already lines up, the cost is exactly the same as trying that one assignment alone.

So an empty list means one thing only: there is no reaction site on these molecules.

Atom mapping

>>> out = rxn.run([acid, amine], atom_mapping=True)[0]
>>> [r.to_smiles() for r in out.reactants]
['[CH3:1][C:2](=[O:3])O', '[CH3:4][CH2:5][NH2:6]']
>>> [p.to_smiles() for p in out.products]
['[C:2](=[O:3])([N:6][CH2:5][CH3:4])[CH3:1]']

Outcome.reactants is filled only when you ask. The two sides together are a complete atom-mapped reaction — every product atom carries the number of the reactant atom it came from, including atoms the template never mentioned.

Numbers are assigned to the whole reactant side, not just the template's matched atoms, so the mapping is total rather than partial.

Product count comes from the graph, not from the template

A template rewrites one graph. How many product molecules come out is decided by how many connected components that rewritten graph has — not by how many product templates were written.

For most templates this agrees with the common implementation. They diverge exactly when the template cuts a bond in a ring that extends beyond the template. The other implementation duplicates the atoms outside the template into both products; omgkit gives one molecule, because the rewritten graph is still connected.

This is a deliberate divergence, with the extent measured on a real corpus.

Intramolecular

run gives each template fragment a different molecule. When there are more template fragments than molecules — two fragments landing on the same molecule — that shape cannot be expressed, and run returns nothing.

run_on_substrate treats the whole reactant side as one graph and lets each fragment find its own place, requiring only that the fragments' matched atoms do not overlap:

out = rxn.run_on_substrate([one_molecule])
run run_on_substrate
Intermolecular yes yes
Sensitive to the order you pass molecules in no no
Intramolecular no yes
Salts (cation and anion as components of one molecule) no yes
Cost predictable larger search space, less predictable

Use run when you want predictable timing.

Limiting output

max_products=n caps how many outcomes are generated; 0 means no limit. A template that matches in many places on a symmetric molecule can produce a lot of equivalent outcomes.

Byproducts

Pass byproducts=True to get the discarded fragments closed into molecules. That has its own page: Byproduct reconstruction.