Quickstart¶
Every output on this page was produced by running the code. If yours differs, that is a bug — please report it.
Parse and write¶
>>> m = omgkit.parse_smiles("OC(=O)c1ccccc1N")
>>> m.num_atoms, m.num_bonds
(10, 10)
>>> m.to_smiles()
'OC(=O)c1ccccc1N'
to_smiles writes in the current atom storage order, so it gives back what you
put in. To get a form that depends only on the structure, sanitize first and
ask for the canonical one:
Canonical means canonical. Two ways of writing the same molecule land on the same string:
>>> a = omgkit.parse_smiles("OC(=O)c1ccccc1N"); a.sanitize()
>>> b = omgkit.parse_smiles("Nc1ccccc1C(O)=O"); b.sanitize()
>>> a.to_canonical_smiles() == b.to_canonical_smiles()
True
That is the invariant the whole library is built to keep: a molecule's properties are decided by the molecule, not by how someone chose to write it down.
Sanitize¶
sanitize() runs the whole pipeline — valence, implicit hydrogens, ring
perception, kekulization, aromaticity, conjugation, hybridization — and then
converts directional bonds into the double bonds' own cis/trans property.
It modifies in place, and a failure can leave a partial result
The binding does not make that copy for you on purpose — a hidden deep copy would double the cost of every batch, and you would have no way to know.
Match a substructure¶
>>> m = omgkit.parse_smiles("OC(=O)c1ccccc1N"); m.sanitize()
>>> q = omgkit.parse_smarts("[CX3](=O)[OX2H1]")
>>> q.match(m)
[[1, 2, 0]]
Each inner list is one match, giving molecule atom indices in query atom
order. Here query atom 0 (the carbonyl carbon) is molecule atom 1, query
atom 1 (the =O) is molecule atom 2, and query atom 2 (the OH) is molecule
atom 0.
uniquify=False keeps symmetry-equivalent duplicates. Benzene shows the
difference — six aromatic bonds, each matchable in two directions:
>>> bz = omgkit.parse_smiles("c1ccccc1"); bz.sanitize()
>>> len(omgkit.parse_smarts("cc").match(bz))
6
>>> len(omgkit.parse_smarts("cc").match(bz, uniquify=False))
12
max_matches=n stops after n matches; 0 (the default) means no limit.
Run a reaction template¶
>>> 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)
>>> acid = omgkit.parse_smiles("CC(=O)O"); acid.sanitize()
>>> amine = omgkit.parse_smiles("CCN"); amine.sanitize()
>>> out = rxn.run([acid, amine])[0]
>>> [p.to_canonical_smiles() for p in out.products]
['CC(NCC)=O']
reactants[i] goes with reactant template i. If the counts do not match you
get an empty list back — see run_on_substrate
for the case where two template fragments land on one molecule.
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]']
The two sides together are a complete atom-mapped reaction: every product atom carries the number of the reactant atom it came from.
Byproducts¶
The template above throws away the acid's OH. Ask for it back:
>>> out = rxn.run([acid, amine], byproducts=True)[0]
>>> [p.to_canonical_smiles() for p in out.byproducts]
['O']
>>> out.byproduct_verdict
'capped'
>>> out.discarded
[[3], []]
discarded is the fact: atom 3 of the first input went into no product.
byproducts is the inference: those atoms, closed into a real molecule.
byproduct_verdict says how it was closed, and it can say it could not be:
| Verdict | Meaning |
|---|---|
'off' |
you did not pass byproducts=True |
'nothing' |
no atoms were discarded |
'capped' |
closed by adding hydrogens only — no choices, so high confidence |
'bonded(n)' |
n extra bonds were formed; which atoms they join is a heuristic |
'unresolved(...)' |
could not be closed, with the reason |
When the verdict is unresolved, byproducts is empty. Making one up would
be worse than giving nothing: it would be a well-formed molecule with nothing
wrong on its face.
See Byproduct reconstruction for what the budget means and when the answer is trustworthy.
Generate a 3D structure¶
>>> conf = omgkit.parse_smiles("C[C@H](N)C(=O)O").conformer()
>>> conf
<omgkit.Conformer atoms=13 energy=0.000e0 converged=True chiral=1/1>
>>> conf.coords[0]
(-1.1906..., -0.8985..., -0.0731...)
No random seed and no retry loop — the same molecule always gives the same
coordinates. chiral=1/1 says every stereocentre came out with the right sign.
The extra atoms are the explicit hydrogens generation adds; conf.coords lines
up with conf.mol, not with the molecule you called it on. See
3D structures.
Read and write .mol files¶
>>> m = omgkit.parse_smiles("C[C@H](N)C(=O)O"); m.sanitize()
>>> block = m.to_molblock_2d(title="L-alanine") # layout and wedges computed for you
>>> back = omgkit.parse_molblock(block)
>>> back.mol.to_canonical_smiles() == m.to_canonical_smiles()
True
read_sdf reads a whole multi-record file, and a record it cannot read keeps
its place in the list instead of raising or vanishing. See
Reading and writing .mol/.sdf.
Featurize for a model¶
>>> m = omgkit.parse_smiles("CC(=O)Oc1ccccc1C(=O)O"); m.sanitize()
>>> m.atom_descriptors()[1]
{'atomic_num': 6, 'total_degree': 3, 'formal_charge': 0,
'chiral_tag': 'unspecified', 'total_num_hs': 0, 'hybridization': 'sp2',
'is_aromatic': False, 'is_in_ring': False, 'mass': 12.011,
'electronegativity': 2.55, 'gasteiger_charge': 0.3075...,
'gasteiger_valid': True}
>>> m.bond_descriptors()[1]
{'begin': 1, 'end': 2, 'order': 'double', 'is_conjugated': True,
'is_in_ring': False, 'stereo': 'none', 'stereo_atoms': None}
Twelve values per atom and seven per bond, including Gasteiger partial charges.
Categorical ones come back as names, not one-hot vectors — the vocabulary is
your featurizer's decision. electronegativity is None where the element has
no accepted Pauling value and gasteiger_valid is False where the charge
could not be computed; neither is filled in with a default. See
Descriptors for ML.
Where to go next¶
- Guides — one page per capability
- Python API — every callable, with signatures