Molecules#
Before MolFoundry can rewrite chemistry it needs the chemistry written down. This section covers the three everyday ways to hand it a molecule — a SMILES string, an explicit GML graph, or (for a single complex group) a GML node with an arbitrary label — and what a molecule graph can tell you about itself.
Molecules from SMILES#
The Simplified Molecular Input Line Entry System (SMILES) is a typographical line notation for chemical structures. It is compact because it throws away everything redundant: a SMILES string is essentially a spanning tree of the molecule, with the non-essential hydrogens removed, the rings cut open, and the remaining atoms and bonds written out in a depth-first walk. The main conventions are few:
Atoms are their atomic symbol. The organic subset — B, C, N, O, P, S, F, Cl, Br, I — may be written bare; everything else goes in square brackets. Lower case denotes an aromatic atom, and implicit hydrogens are filled in to satisfy the normal valence.
Bonds are
-(single),=(double),#(triple) and:(aromatic); single and aromatic bonds are usually omitted.Branches are enclosed in parentheses.
Rings are cut, and the two ends of the cut bond carry the same digit.
smiles() parses a SMILES string into a molecule graph; the
optional second argument names it.
pyruvate = smiles("CC(=O)C(=O)O", "pyruvate", add=False)
pyruvate.print()
pyruvate
add=False keeps the molecule out of the global inputGraphs collection; drop
it and every parsed molecule is registered there automatically, ready for a
derivation-graph build.
A molecule graph knows its own canonical SMILES, its InChI, its exact (monoisotopic) mass, and how many atoms or bonds of a given kind it has:
print("canonical SMILES:", pyruvate.smiles)
print("InChI: ", pyruvate.inchi)
print("exact mass: ", round(pyruvate.exactMass, 4))
print("#atoms, #carbons:", pyruvate.numVertices, pyruvate.vLabelCount("C"))
canonical SMILES: O=C(C(=O)O)C
InChI: InChI=1S/C3H4O3/c1-2(4)3(5)6/h1H3,(H,5,6)
exact mass: 88.016
#atoms, #carbons: 10 3
The complete script also renders a more elaborate structure and prints its descriptors:
"""1. Visualizing molecules.
The smallest useful MolFoundry program: turn a SMILES string into a molecule
graph and render it. ``enable_post()`` turns on the HTML summary; every
``.print()`` appends a depiction, and when the process exits a self-contained
``summary/summary.html`` is written next to this script (open it in a browser).
Run: python 01_visualizing_molecules.py
"""
import _bootstrap # noqa: F401 (puts molfoundry on sys.path)
from molfoundry import smiles, enable_post
enable_post()
# Pyruvate, CC(=O)C(=O)O. The optional second argument names the graph.
pyruvate = smiles("CC(=O)C(=O)O", "pyruvate")
pyruvate.print()
# A molecule graph knows its canonical SMILES, InChI and exact mass.
print("SMILES: ", pyruvate.smiles)
print("InChI: ", pyruvate.inchi)
print("InChIKey: ", pyruvate.inchiKey)
print("exact mass: ", round(pyruvate.exactMass, 4))
print("#atoms: ", pyruvate.numVertices, "(#C =", pyruvate.vLabelCount("C"), ")")
print("\nWrote summary/summary.html -- open it in a browser to see the depiction.")
Molecules in GML#
GML (Graph Modeling Language) is the explicit, atom-by-atom description of a
graph — a hierarchy of key/value pairs. A molecule is a single graph [ … ]
block of node and edge entries: each node carries an integer id and a
label (the element symbol), and each edge names its source and target
ids and a bond label. Because a chemical bond is undirected, it is listed
once, conventionally with the smaller id as source. Here is water:
water = graphGMLString('''graph [
node [ id 0 label "O" ]
node [ id 1 label "H" ]
node [ id 2 label "H" ]
edge [ source 0 target 1 label "-" ]
edge [ source 0 target 2 label "-" ]
]''', "water", add=False)
water.print()
water
MolFoundry stores graphs canonically, so getGMLString() re-emits the graph in
a normalized vertex order — the same molecule, described consistently:
print(water.getGMLString())
graph [
node [ id 0 label "H" ]
node [ id 1 label "H" ]
node [ id 2 label "O" ]
edge [ source 0 target 2 label "-" ]
edge [ source 1 target 2 label "-" ]
]
Special atom states — charge and radical — are written straight into the node
label by appending +, - (repeatable, e.g. Fe+++) or . (radical). The
hydronium cation is an oxygen written as "O+":
hydronium = graphGMLString('''graph [
node [ id 0 label "O+" ]
node [ id 1 label "H" ] node [ id 2 label "H" ] node [ id 3 label "H" ]
edge [ source 0 target 1 label "-" ]
edge [ source 0 target 2 label "-" ]
edge [ source 0 target 3 label "-" ]
]''', "hydronium", add=False)
hydronium.print()
print(hydronium.smiles, " exact mass:", round(hydronium.exactMass, 4))
hydronium
[OH3+] exact mass: 19.0178
"""2. Specifying molecules in GML.
GML (Graph Modeling Language) is the explicit, atom-by-atom way to write a
molecule graph: a ``graph [ ... ]`` block of ``node`` and ``edge`` entries.
Each ``node`` has an ``id`` and a ``label`` (the element symbol, with an
optional trailing ``+``, ``-`` or ``.`` for charge / radical state); each
``edge`` names its ``source`` and ``target`` ids and a bond ``label``
(``-`` single, ``=`` double, ``#`` triple, ``:`` aromatic). A bond is listed
once, with the smaller id as source.
See ``gml/water.gml`` and ``gml/hydronium.gml`` for the two graphs loaded here.
Run: python 02_molecules_in_gml.py
"""
import _bootstrap
from _bootstrap import graphGMLFile
from molfoundry import enable_post
enable_post()
water = graphGMLFile("water.gml", "water")
hydronium = graphGMLFile("hydronium.gml", "hydronium")
for g in (water, hydronium):
g.print()
print(f"{g.name:10s} SMILES={g.smiles!r:12} mass={g.exactMass:8.4f} isMolecule={g.isMolecule}")
# The GML is canonical and round-trips: getGMLString() re-emits the graph.
print("\n--- water.gml as MolFoundry sees it ---")
print(water.getGMLString())
Complex group labels#
A bracket atom in a SMILES string must be a real element, so a whole functional
group cannot be hidden behind a made-up symbol like [CoA]. When you want a
single node to stand for a complex group, write the graph in GML and give the
node any label you like. Coenzyme-A, with only its reactive thiol modeled
explicitly:
coa = graphGMLString('''graph [
node [ id 0 label "CoA" ] node [ id 1 label "S" ] node [ id 2 label "H" ]
edge [ source 0 target 1 label "-" ]
edge [ source 1 target 2 label "-" ]
]''', "CoA-SH", add=False)
coa.print()
print("is a molecule:", coa.isMolecule)
CoA-SH
is a molecule: False
isMolecule is False: a "CoA" pseudo-atom is not a real element, so this is
a first-class graph you can still rewrite and put into a network, but not one
MolFoundry can weigh or hand to InChI. A graph that is a real molecule
additionally exposes an inchi, inchiKey, and exactMass.
"""3. Molecules from SMILES (and complex group labels).
``smiles()`` parses the usual line notation: organic-subset atoms
(B, C, N, O, P, S, F, Cl, Br, I) need no brackets, aromaticity is lower-case,
branches are ``( ... )``, and rings are broken with matching digits. Implicit
hydrogens are filled in.
Note on "complex" labels: a bracket atom in SMILES must be a real element, so a
whole functional group cannot be hidden behind a made-up symbol like ``[CoA]``
in a SMILES string. When you want a single node to stand for a complex group,
write the graph in GML and give the node any label you like -- shown here for
coenzyme-A, where only the reactive thiol is modeled explicitly.
Run: python 03_molecules_from_smiles.py
"""
import _bootstrap
from molfoundry import smiles, graphGMLString, enable_post
enable_post()
# Organic-subset atoms, a branch and a ring closure.
acetone = smiles("CC(=O)C", "acetone")
ciprofloxacin = smiles("N1CCN(CC1)C(C(F)=C2)=CC(=C2C4=O)N(C3CC3)C=C4C(=O)O", "ciprofloxacin")
for g in (acetone, ciprofloxacin):
g.print()
print(f"{g.name:14s} canonical SMILES = {g.smiles}")
# A single node standing for a complex group (coenzyme-A), thiol explicit.
coa = graphGMLString(
"""graph [
node [ id 0 label "CoA" ]
node [ id 1 label "S" ]
node [ id 2 label "H" ]
edge [ source 0 target 1 label "-" ]
edge [ source 1 target 2 label "-" ]
]""",
"CoA-SH",
)
coa.print()
print(f"\n{coa.name}: isMolecule={coa.isMolecule} "
f"(a 'CoA' pseudo-atom is not a real element, so it is a graph, not a molecule)")
# A real molecule additionally exposes an InChI (and InChIKey).
print("\nacetone InChI:", acetone.inchi)