Converting a Chemical Name to a Structure With AI
11 min read · updated August 11, 2026
You have a spreadsheet column of chemical names from a supplier catalogue, a paper, or a laboratory notebook, and you need SMILES. The working answer is not a model — it is a deterministic parser, then a database, then a model only for the residue, with every result validated before it is accepted.
Three kinds of name, three different tools
- Systematic IUPAC names —
2-acetyloxybenzoic acid,(2S)-2-amino-3-phenylpropanoic acid. These are generated by a grammar and can be parsed by one. No model needed and none wanted: a parser either succeeds correctly or fails loudly. - Trivial, trade and common names —
aspirin,paracetamol,Tylenol,vitamin B12. There is no grammar; these are dictionary entries, and the tool is a synonym database. - Damaged and informal strings — OCR errors, missing stereo descriptors, lab shorthand,
4-hydroxyacetanilide (para)with a stray annotation. Nothing deterministic resolves these, and this is the only place a language model earns its keep.
The ordering follows from that. Try the deterministic thing first, because it is free, fast and correct; use the database next, because it covers the dictionary; and reach for a model only for the leftovers, where you have already decided that a guess needing verification beats nothing.
Build the resolver
- Install the pieces. OPSIN is the open-source IUPAC name parser described by Daniel Lowe and colleagues in “Chemical Name to Structure: OPSIN, an Open Source Solution” (Journal of Chemical Information and Modeling, 2011). It is a Java library with Python wrappers; RDKit does the validation.
pip install rdkit py2opsin requests
- Normalise the input string. Strip surrounding whitespace and quotes, collapse internal runs of spaces, normalise Unicode dashes to ASCII hyphens and Greek letters written as words, and remove trailing parenthetical annotations that are not part of the name.
- Try OPSIN. It returns SMILES for a systematic name and nothing for anything else. A hit here is authoritative — the name was parsed by its own grammar, not matched.
- Fall back to PubChem. PUG-REST resolves synonyms, trade names and registry numbers.
https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/aspirin/property/CanonicalSMILES/TXT
- Fall back to a model. Only for names that neither resolved. Ask for a structured response containing the proposed SMILES and a confidence, with an explicit instruction to return null rather than guess.
- Validate every result, whatever produced it, before it leaves the function.
import re, requests
from rdkit import Chem
from py2opsin import py2opsin
PUBCHEM = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{name}/property/CanonicalSMILES/TXT"
def normalise(name: str) -> str:
name = name.strip().strip('"').replace("\u2013", "-").replace("\u2014", "-")
return re.sub(r"\s+", " ", name)
def canonical(smiles: str | None) -> str | None:
"""Accept a SMILES only if RDKit can parse and sanitise it."""
if not smiles:
return None
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# keep the largest fragment: catalogue names often denote salts
frags = Chem.GetMolFrags(mol, asMols=True, sanitizeFrags=True)
mol = max(frags, key=lambda m: m.GetNumHeavyAtoms())
return Chem.MolToSmiles(mol)
def from_opsin(name: str) -> str | None:
out = py2opsin(name) # "" when the name is not systematic
return canonical(out or None)
def from_pubchem(name: str, session: requests.Session) -> str | None:
r = session.get(PUBCHEM.format(name=requests.utils.quote(name)), timeout=15)
if r.status_code != 200:
return None
return canonical(r.text.strip().splitlines()[0])
def resolve(name: str, session: requests.Session, model_fn=None) -> dict:
n = normalise(name)
for source, fn in (("opsin", from_opsin),
("pubchem", lambda x: from_pubchem(x, session))):
smiles = fn(n)
if smiles:
return {"name": name, "smiles": smiles, "source": source,
"needs_review": False}
if model_fn:
smiles = canonical(model_fn(n)) # validated like everything else
if smiles:
return {"name": name, "smiles": smiles, "source": "model",
"needs_review": True}
return {"name": name, "smiles": None, "source": None, "needs_review": True}SMILES and a ConnectivitySMILES property now appear alongside the long-standing CanonicalSMILES and IsomericSMILES. Check the current property table in PubChem’s own PUG-REST documentation before you hard-code a field name, and note that a connectivity-only SMILES discards the stereochemistry you may need.The validation step is not optional
A model asked for the SMILES of an obscure compound will produce a plausible string. Plausible is the problem: it parses, it looks like chemistry, and it is a different molecule. Three checks catch most of it.
First, parse and sanitise. Chem.MolFromSmiles returning None rejects valence violations and unmatched ring closures outright, as the SMILES grammar defines them.
Second, round-trip the name. Feed the resulting structure back to a structure-to-name step, or take the molecular formula from RDKit and compare it to any formula the source gave you. For a systematic input name, an even stronger check is available: run the model’s SMILES through a name generator and compare to the input, or run the input through OPSIN once more with relaxed settings.
Third, compare identity keys, not strings. Two correct answers can be different SMILES for the same molecule. Compute the InChIKey with Chem.MolToInchiKey and compare on that; the first 14 characters encode connectivity alone, so you can also detect “right skeleton, wrong stereochemistry”, which is the single most common model error on chiral compounds.
Mark everything the model produced with needs_review and keep the flag in your output table. A resolved-name dataset where you cannot tell which rows were guessed is a dataset you cannot audit later.
Rate limits, caching and batching
PubChem publishes a usage policy: no more than 5 requests per second, no more than 400 requests per minute, and no more than 300 seconds of total request time per minute, with dynamic throttling when the service is busy. A naive loop over 50,000 names will be throttled and then blocked.
Three mitigations, in order of value. Cache on the normalised name — catalogue files repeat names constantly, and a local key-value store removes most of the traffic on the second run. Deduplicate before you start, resolving the unique set and joining back. And respect the limit deliberately with a token bucket at four requests per second rather than five, with exponential backoff on any 503, and a descriptive user-agent so the service can identify you.
For genuinely large jobs, download rather than query. PubChem publishes bulk files including synonym-to-CID mappings, and a local lookup table turns a rate-limited network call into a hash lookup. That is the right shape above roughly a hundred thousand names.
What to do with what is left
A realistic run over a messy catalogue leaves a residue that nothing resolved: mistyped names, internal codes, mixtures, polymers, and names that denote a class rather than a compound.
- Do not let them become nulls in a modelling table. Keep the unresolved names in their own file with the reason each failed. A silently dropped 8% of a dataset is a selection effect.
- Cluster the failures. They are rarely independent: one supplier’s formatting convention or one OCR error usually accounts for many at once, and a single regular expression fixes the batch.
- Mixtures and polymers are not resolution failures. A name denoting a formulation has no single structure, and forcing one in is worse than leaving the cell empty.
- Route the rest to a human. A chemist resolves ambiguous names quickly when given the name and the model’s rejected suggestion side by side, which is a much better interface than a blank field.