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.read_smiles gives you a graph, SmilesIR gives you the parsed
intermediate representation, SmartsPattern gives you a compiled query,
CGSmilesIR gives you a parsed CGsmiles string.
Polymer notations. CGsmiles is read by
mp.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.read_smiles is the right choice whenever you have a single, fully
specified molecule. It parses the string and returns an Atomistic containing
atoms and bonds.
import molpy as mp
mol = mp.io.read_smiles("CC(=O)OCC") # 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 read_smiles gives you exactly
the heavy-atom skeleton the string names. Ask for the hydrogens when you want
them:
skeleton = mp.io.read_smiles("CCO")
filled = mp.Perceive().find_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 a molecule — ion
pairs and solvent mixtures use this. read_smiles refuses it rather than
silently returning a disconnected graph; SmilesIR.components() takes it
apart.
try:
mp.io.read_smiles("[Li+].[F-]")
except ValueError as exc:
print("refused:", exc)
ions = mp.SmilesIR("[Li+].[F-]").components()
print(f"components: {len(ions)} -> {[len(i.atoms) for i in ions]}")
refused: read_smiles needs one component, '[Li+].[F-]' has 2. Use mp.SmilesIR(smiles).components(), or pass one component at a time.
components: 2 -> [1, 1]
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.
Perceive().find_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.read_smiles("C1=CC=CC=C1")
print("as written: ", [atom.get("is_aromatic") for atom in kekule.atoms])
perceived = mp.Perceive().find_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.SmartsPattern("[C;X4][O;H1]")
print(f"query atoms: {query.num_query_atoms}, max bond depth: {query.max_bond_depth}")
ethanol = mp.Perceive().find_hydrogens(mp.io.read_smiles("CCO"))
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.
Splitting parse from convert¶
mp.io.read_smiles parses and converts in one call, which suits most
workflows. SmilesIR is the step in between, for when you want to know what the
string said before committing to a graph — how many molecules it names, and
whether to take them together or separately.
ir = mp.SmilesIR("CCO.O")
print(f"components: {ir.n_components}")
together = ir.to_atomistic()
print(f"to_atomistic(): one graph of {len(together.atoms)} atoms")
separate = ir.components()
print(
f"components(): {len(separate)} graphs of {[len(m.atoms) for m in separate]} atoms"
)
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.CGSmilesIR("{[#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.read_smiles(s) |
| A SMILES string, several molecules | One graph each | mp.SmilesIR(s).components() |
| A SMILES string | To inspect before converting | mp.SmilesIR(s) |
| A structural rule | To find where it matches | mp.SmartsPattern(p) |
| Missing hydrogens / aromaticity | A perceived structure | mp.Perceive().find_*(mol) |
| A CGsmiles string with fragments | Ported unit templates | mp.CGSmilesIR(s).templates() |
One fragment body such as [<]OCC[>] |
One ported unit template | mp.SmilesIR.from_fragment(body).to_template() |
| A CGsmiles string | A site graph (topology) | mp.CGSmilesIR(s).to_coarsegrain() |
| Unit templates and a site graph | A polymer | mp.Assembler(library, mp.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.Assembler — see
Assembly and Polymer Topologies.