Parsing Chemistry¶
From a one-line string to an editable structure. MolPy reads three chemical notations — SMILES for one concrete molecule, SMARTS for a structural query, CGsmiles for units and how they join — all parsed by the chemistry engine, exposed as types rather than helper functions.
Two notations, two purposes¶
Chemical notation is a compression scheme, and each format answers a different question. SMILES asks "what is this exact molecule?" and encodes atoms, bonds and stereochemistry. SMARTS asks "what structural pattern should I match?" and encodes logical constraints rather than physical atoms — it never builds a structure.
There is no parser function to look up: you name the type you want.
mp.io.smiles.SmilesIr gives you the parsed SMILES (to_atomistic() makes it a
graph), SmartsPattern gives you a compiled query, CgSmilesIr gives you a
parsed CGsmiles string.
Polymer notations. CGsmiles is read by
mp.io.cgsmiles.CgSmilesIr(below); it is how units and polymer topologies are written for Assembly. BigSMILES and G-BigSMILES are not parsed.
SMILES describes one specific molecule¶
mp.io.smiles.read_smiles(s).to_atomistic() is the way from a SMILES string to a
structure. SmilesIr parses the string once; to_atomistic() returns an
Atomistic containing its atoms and bonds.
import molpy as mp
mol = mp.io.smiles.read_smiles("CC(=O)OCC").to_atomistic() # ethyl acetate
print(f"atoms: {len(mol.atoms)}, bonds: {len(mol.bonds)}")
elements = [atom.get("element") for atom in mol.atoms]
print(elements)
Hydrogens are not added. A SMILES string states connectivity; filling
open valences is a separate perception step, so to_atomistic() gives you exactly
the heavy-atom skeleton the string names. Ask for the hydrogens when you want
them:
skeleton = mp.io.smiles.read_smiles("CCO").to_atomistic()
filled = mp.perceive.add_hydrogens(skeleton)
print(f"skeleton: {len(skeleton.atoms)} atoms") # C, C, O
print(f"filled: {len(filled.atoms)} atoms") # + 6 H
print("the input is untouched:", len(skeleton.atoms))
A .-separated SMILES names a set of molecules, not one molecule — ion
pairs and solvent mixtures use this. n_components says how many the string
names; to_atomistic() returns them together as one disconnected graph, and
components() takes them apart, one graph each.
ir = mp.io.smiles.read_smiles("[Li+].[F-]")
print(f"components: {ir.n_components}")
together = ir.to_atomistic()
print(f"to_atomistic(): one graph of {len(together.atoms)} atoms")
ions = ir.components()
print(f"components(): {len(ions)} graphs of {[len(i.atoms) for i in ions]} atoms")
Aromaticity comes from the notation, and perception can revise it¶
Aromatic atoms are lowercase in SMILES, and the parser records that as
is_aromatic on each atom. Ring-closure digits must match: the first
occurrence opens the ring, the second closes it.
benzene = mp.io.smiles.read_smiles("c1ccccc1").to_atomistic()
print([atom.get("is_aromatic") for atom in benzene.atoms])
mp.perceive.assign_aromaticity() re-derives the flag from the ring and its
bonds rather than trusting the notation, so a Kekulé structure written with
explicit double bonds comes out aromatic too:
kekule = mp.io.smiles.read_smiles("C1=CC=CC=C1").to_atomistic()
print("as written: ", [atom.get("is_aromatic") for atom in kekule.atoms])
perceived = mp.perceive.assign_aromaticity(kekule)
print("re-perceived:", [atom.get("is_aromatic") for atom in perceived.atoms])
SMARTS: pattern matching, not structure building¶
SMARTS shares SMILES syntax on the surface, but its semantics are entirely
different. Where SMILES encodes one concrete molecule, SMARTS encodes a query:
[C;X4][O;H1] means "an sp3 carbon bonded to a hydroxyl oxygen" and matches
any molecule containing that environment. A SmartsPattern has no atoms to
read — it has matches to find.
query = mp.perceive.SmartsPattern("[C;X4][O;H1]")
print(f"query atoms: {query.n_query_atoms}, max bond depth: {query.max_bond_depth}")
ethanol = mp.perceive.add_hydrogens(mp.io.smiles.read_smiles("CCO").to_atomistic())
print("matches ethanol:", query.has_match(ethanol))
for match in query.find_matches(ethanol):
print(" matched atom handles:", match.atoms)
query atoms: 2, max bond depth: 1
matches ethanol: True
matched atom handles: [4294967298, 4294967299]
Note the pattern is matched against the hydrogen-filled structure:
X4 counts connections and H1 counts hydrogens, so both are answered wrong on
a bare skeleton. Perceive the hydrogens first, query after.
SMARTS is the language of force-field typification: patterns map atom environments to force-field types. See Typifier in this guide.
CGsmiles describes units and how they join¶
CGsmiles writes a molecule at more than one resolution. The first block is a
graph of named beads — here three EO units in a row — and the block after it
says what each name is made of. A bonding descriptor such as [<] or [>]
marks where a fragment may bond: < joins >.
CgSmilesIr parses the string once and reads it three ways. templates()
returns each fragment as an mp.Atomistic template that keeps its descriptors
as ports: an anchor atom plus a hydrogen handle that leaves when the port
bonds. to_coarsegrain() returns the bead graph, one site per unit, which is
the topology Assembly grows. to_atomistic() expands the
whole string into one heavy-atom graph, turning every paired descriptor into a
bond.
ir = mp.io.cgsmiles.read_cgsmiles("{[#EO]|3}.{#EO=[<]OCC[>]}")
eo = ir.templates()["EO"] # the unit: a ported template
print(f"EO template: {eo.n_atoms} atoms, {eo.n_ports} ports")
for port in eo.ports:
anchor, handle = port.anchor.get("element"), port.handle_atom.get("element")
print(f" port {port.get('port_kind')}: anchor {anchor}, leaving {handle}")
sites = ir.to_coarsegrain() # the topology: one site per unit
bead_types = [bead.get("bead_type") for bead in sites.beads]
print(f"site graph: {bead_types}, {len(sites.cgbonds)} bonds")
chain = ir.to_atomistic() # the whole molecule, descriptors consumed as bonds
print(f"to_atomistic(): {chain.n_atoms} heavy atoms, {len(chain.bonds)} bonds")
EO template: 5 atoms, 2 ports
port <: anchor O, leaving H
port >: anchor C, leaving H
site graph: ['EO', 'EO', 'EO'], 2 bonds
to_atomistic(): 9 heavy atoms, 8 bonds
Choosing the right entry point¶
| You have | You want | Use |
|---|---|---|
| A SMILES string, one molecule | An editable graph | mp.io.smiles.read_smiles(s).to_atomistic() |
| A SMILES string, several molecules | One graph each | mp.io.smiles.read_smiles(s).components() |
| A SMILES string | To inspect before converting | mp.io.smiles.read_smiles(s) |
| A structural rule | To find where it matches | mp.perceive.SmartsPattern(p) |
| Missing hydrogens / aromaticity | A perceived structure | mp.perceive.add_hydrogens(mol), mp.perceive.assign_*(mol) |
| A CGsmiles string with fragments | Ported unit templates | mp.io.cgsmiles.read_cgsmiles(s).templates() |
One fragment body such as [<]OCC[>] |
One ported unit template | mp.io.smiles.read_smiles_fragment(body).to_template() |
| A CGsmiles string | A site graph (topology) | mp.io.cgsmiles.read_cgsmiles(s).to_coarsegrain() |
| Unit templates and a site graph | A polymer | mp.builder.Assembler(library, mp.builder.GrowthPlacer()) |
Reach for SmartsPattern only for matching rules that feed the typifier, never
for structure creation. And when the molecule is a polymer, write its units and
its topology in CGsmiles and join them with mp.builder.Assembler — see
Assembly and Polymer Topologies.