openclatura
Deterministic structure-to-IUPAC naming, with structural auditing and atom-level explanations.
Openclatura is an open-source, rule-based structure-to-IUPAC naming framework for SMILES and RDKit molecules. It combines IUPAC 2013 nomenclature, atom-linked decision traces, built-in reconstruction auditing, and optional independent verification through OPSIN.
Names are constructed from the molecular graph using nomenclature rules and retained-name vocabulary—not predicted by a neural model. Naming and built-in auditing run locally with Python and RDKit. Java is required only for the optional OPSIN cross-check.
It supports deterministic SMILES-to-IUPAC conversion (smiles2iupac) for
individual structures and molecular datasets.
What that buys you
- Auditable. Each name carries the Blue Book rules it hit (
P-44,P-61, …) and a step-by-step trace of parse → perception → parent → numbering → assembly. - Broad. Chains and rings from 1 to 1000 skeletal atoms (P-14.2.1), plus fused, spiro and bridged systems, and the Blue Book's retained names.
- Verifiable. Optional round-trip through OPSIN: parse the generated name back to a structure and compare.
- Explainable.
describe()renders the trace as prose — useful for teaching material and for building (SMILES, name, description) datasets.
Coverage
Round-trip accuracy against public datasets (details and rerun instructions in
evaluations/):
| dataset | QM9 | PubChem | ZINC22 |
|---|---|---|---|
| coverage | 100.00% | 99.76% | 97.70% |
The package is in beta. Naming is solid across common organic chemistry; exotic corners of the Blue Book — and stereodescriptor edge cases — are still being filled in. Bug reports are very welcome.
Install
pip install openclatura
Optional extras:
| extra | adds |
|---|---|
[opsin] |
py2opsin for OPSIN-based round-trip verification |
[datasets] |
datasets + tqdm for PubChem/QM9-style evaluations |
[web] |
FastAPI + uvicorn for the HTTP service |
[dev] |
pytest, ruff, pre-commit, hypothesis, py2opsin |
pip install "openclatura[opsin,datasets]"
The default install does not include OPSIN verification. Install the
[opsin] extra and make sure Java 8+ is available if you want round-trip
verification through OPSIN:
pip install "openclatura[opsin]"
java -version
If py2opsin is installed but Java is missing or inaccessible, OPSIN
verification is skipped gracefully. The name generation still succeeds and the
verification status is reported as skipped_no_java.
Quick start
from openclatura import name_smiles
name_smiles("CCO") # 'ethanol'
name_smiles("c1ccccc1") # 'benzene'
name_smiles("CC(=O)O") # 'acetic acid'
Typed result with rules hit + OPSIN round-trip
For everything richer than the bare string, use openclatura.name:
from openclatura import name
result = name("CC(=O)Nc1ccccc1", include_trace=True, verify_opsin=True)
result.name # 'N-phenylacetamide'
result.smiles # 'CC(=O)Nc1ccccc1'
result.ok # True
result.rules_hit # ('P-44', 'P-45', 'P-41', 'P-61', 'P-67')
result.rule_hints # ('Parent hydride / parent structure: Blue Book P-44 and P-45.',
# 'Principal characteristic groups: Blue Book P-41, P-44, and P-61-P-67.')
result.opsin_check.status # 'matched' | 'mismatched' | 'skipped_no_java' | ...
result.verified # True when opsin_check is matched
verify_opsin defaults to False. When set to True, verification is
best-effort and does not raise if OPSIN support is unavailable:
- no
py2opsininstalled:result.opsin_check.status == "skipped_no_opsin" py2opsininstalled but Java unavailable:status == "skipped_no_java"- OPSIN parses and round-trips:
status == "matched"or"mismatched"
Errors do not raise — they are captured on result.error, which makes
the batch API safe to point at noisy datasets:
from openclatura import name_many
results = name_many(
["CCO", "c1ccccc1", "definitely-not-a-smiles"],
processes="auto", # or an integer, or 1 for in-process
verify_opsin=False,
)
[r.name for r in results if r.ok] # ['ethanol', 'benzene']
Naming an existing RDKit molecule
If you already hold an rdkit.Chem.rdchem.Mol — from an SD file, a reaction,
or an earlier step in a pipeline — skip the SMILES round-trip:
from rdkit import Chem
from openclatura import name_rdkit_mol, name_mol, name_many
for mol in Chem.SDMolSupplier("compounds.sdf"):
if mol is not None:
print(name_rdkit_mol(mol)) # -> 'benzoic acid'
name_mol(mol) # typed NamingResult, as `name`
name_many([mol, "CCO"]) # batches take either form
The input molecule is never modified, explicit hydrogens (as SD files usually
carry them) are handled, and name_rdkit_mol_with_trace / analyze_rdkit_mol
mirror their SMILES counterparts.
For the full decision trace (one TraceStep per phase: parse, perception,
parent selection, numbering, assembly, …):
from openclatura import analyze_smiles
analysis = analyze_smiles("CC(=O)Nc1ccccc1")
for step in analysis.decisions:
print(step.phase, step.decision, step.reason)
CLI
openclatura name "CC(=O)Nc1ccccc1" # → N-phenylacetamide
openclatura name "CC(=O)Nc1ccccc1" --json # JSON with trace + rules
openclatura batch smiles.txt --output names.jsonl --processes auto
The CLI verifies with OPSIN by default when possible. This is different from the
Python API, where verify_opsin=False by default. Disable CLI verification with
--no-verify:
openclatura name "CC(=O)Nc1ccccc1" --no-verify
If OPSIN support is unavailable, the command still prints the generated name and reports the verification status:
N-phenylacetamide
opsin: skipped_no_java
Other possible skipped statuses include skipped_no_opsin when py2opsin is
not installed. Install openclatura[opsin] and Java 8+ for full CLI
verification.
Explaining a name
Two renderers turn the same decision trace into prose. Both are deterministic — same input, same output, no LLM in the loop.
describe — rule-by-rule
A multi-paragraph account of how the name was built, keyed to the Blue Book rules that fired:
from openclatura import describe
d = describe("CC(=O)Nc1ccccc1")
print(d) # multi-paragraph prose
d.rules_hit # ('P-44', 'P-45', 'P-41', 'P-61', 'P-67')
d.components[0] # DescribedComponent(phase='parse', text='RDKit parsed ...')
The molecule CC(=O)Nc1ccccc1 is named **N-phenylacetamide**.
Processed SMILES: CC(=O)Nc1ccccc1
Atom ids in that SMILES: C{0}C{1}(=O{2})N{3}c{4}1c{5}c{6}c{7}c{8}c{9}1
RDKit parsed the SMILES into a molecular graph with 10 atoms and 10 bonds.
The structure is a single connected component, named in one piece.
Perception identified 1 functional group (amide); principal candidate: amide.
The amide group is selected as the principal group by the registered seniority order.
The parent skeleton is a 2-atom acyclic chain (atoms [0, 1]).
Parent numbering was selected from the final atom-to-locant map (atom 1->1, atom 0->2).
Component and substituent structure:
- Component 1: N-phenylacetamide covers 10 atoms and 10 bonds.
Parent: chain parent with 2 atoms retained as acetamide; locants 1->1, 2->0.
Principal group: amide at 1 (atoms 1,2,3).
- Substituent at N: phenyl covers 6 atoms and 6 bonds.
Parent: ring parent with 6 atoms retained as benzene; locants 1->4, 2->5, 3->6, 4->7, 5->8, 6->9.
Name pieces contributed by the trace:
- parent skeleton: contributes "acetamide" (atoms 0,1; bonds 1).
- amide: contributes "amide" (atoms 1,2,3; bonds 2,3).
- substituent parent skeleton: contributes "benzene" (atoms 4,5,6,7,8,9; bonds 5,6,7,8,9,10).
IUPAC Blue Book rules applied: P-44, P-45, P-41, P-61, P-67.
describe_human — how a chemist would say it
The same information, phrased the way a person would explain the structure at a whiteboard, with every position tied back to an atom index in the SMILES:
from openclatura import describe_human
d = describe_human("CN1C=NC2=C1C(=O)N(C(=O)N2C)C") # caffeine
print(d.text)
Input SMILES: CN1C=NC2=C1C(=O)N(C(=O)N2C)C
Processed SMILES: Cn1cnc2c1c(=O)n(C)c(=O)n2C
Atom ids in that SMILES: C{0}n{1}1c{2}n{3}c{4}2c{5}1c{6}(=O{7})n{8}(C{13})c{9}(=O{10})n{11}2C{12}
The molecule is named 1,3,7-trimethyl-3,7-dihydro-1H-purine-2,6-dione.
The molecule is built around the retained purine parent, 9-atom polycyclic heteroskeleton.
The parent ring basis comprises a 5-membered ring of aromatic atoms containing position 4 (atom id 4), position 5 (atom id 5), position 7 (atom id 1), position 8 (atom id 2), and position 9 (atom id 3) and a 6-membered ring of aromatic atoms containing position 1 (atom id 8), position 2 (atom id 9), position 3 (atom id 11), position 4 (atom id 4), position 5 (atom id 5), and position 6 (atom id 6).
These rings share parent bonds between position 4 (atom id 4) and position 5 (atom id 5).
Within that parent framework, there is nitrogen at positions 1 (atom id 8), 3 (atom id 11), 7 (atom id 1), and 9 (atom id 3).
The principal characteristic feature is oxo groups at positions 2 (atom id 9) and 6 (atom id 6).
Attached to this framework are methyl groups at positions 1 (atom id 8), 3 (atom id 11), and 7 (atom id 1).
Development
git clone https://github.com/lamalab-org/openclatura
cd openclatura
pip install -e ".[dev]"
# run the unit + round-trip tests
pytest
# run only fast tests
pytest -m "not slow and not dataset and not golden"
# strict RDKit-version regression suite (also runs in the rdkit-compat CI job)
pytest -m golden
# lint and format
ruff check --fix src/openclatura
ruff format src/openclatura
Java is required for the OPSIN-based round-trip checks (see py2opsin).
HTTP service (Docker)
The [web] extra ships a FastAPI app with name, batch, describe
and healthz endpoints. The bundled Dockerfile includes a headless JRE
so verify_opsin=True works out of the box.
# build + run
docker build -t openclatura:local .
docker run --rm -p 8000:8000 openclatura:local
# or via compose
docker compose -f docker/compose.yaml up --build
Call the API:
curl -X POST localhost:8000/name -H 'content-type: application/json' \
-d '{"smiles":"CC(=O)Nc1ccccc1","include_trace":true,"verify_opsin":true}'
curl -X POST localhost:8000/batch -H 'content-type: application/json' \
-d '{"smiles":["CCO","c1ccccc1","CC(=O)O"],"processes":1}'
curl -X POST localhost:8000/describe -H 'content-type: application/json' \
-d '{"smiles":"CC(=O)Nc1ccccc1"}'
OpenAPI docs are served at http://localhost:8000/docs.
Changelog
See CHANGELOG.md for release notes.
License
MIT. See LICENSE.
How to cite
If you use Openclatura in your research, please cite the Openclatura preprint:
@article{openclatura2026,
author = {Mirza, Adrian and Jablonka, Kevin Maik and Fedorov, Rostislav},
title = {Openclatura--An Open-Source Nomenclature Framework for Rule-Based Molecule Naming},
journal = {ChemRxiv},
year = {2026},
month = jul,
day = {15},
doi = {10.26434/chemrxiv.15006114/v2},
url = {https://doi.org/10.26434/chemrxiv.15006114/v2},
note = {Preprint}
}
If you are using OPSIN for verification, please cite the original OPSIN publication:
@article{lowe2011opsin,
author = {Lowe, Daniel M. and Corbett, Peter T. and Murray-Rust, Peter and Glen, Robert C.},
title = {Chemical Name to Structure: {OPSIN}, an Open Source Solution},
journal = {Journal of Chemical Information and Modeling},
year = {2011},
volume = {51},
number = {3},
pages = {739--753},
doi = {10.1021/ci100384d},
url = {https://doi.org/10.1021/ci100384d}
}
Metadata
Release files for openclatura 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openclatura-0.4.0.tar.gz | 1.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openclatura-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.1 MB
Release files / openclatura-0.4.0.tar.gz
| Download URL | openclatura-0.4.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
204b8c179d7a6c704e7abf2a3e5b1f51d46d4a882b68917e949bf23fba7b0371
|
|
BLAKE2b-256 checksum How to use checksums |
db3602b75417da8f9834a9640f3eb1328da2ff12f290abda4e1c0a23ff633397
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|
Release files / openclatura-0.4.0-py3-none-any.whl
| Download URL | openclatura-0.4.0-py3-none-any.whl |
|---|---|
| Size | 645.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6d2953099af07f96135a1384b409cbd7ced5a98e73f40216693e962571629b60
|
|
BLAKE2b-256 checksum How to use checksums |
b62f3454531e3dc4a6ae6b916b724e30d71ef4a5e08834b25c0697dd74c6976b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|