Skip to content

About

Resources

Contributing

Stars

96 stars

Watchers

3 watching

Forks

Repository files navigation

openclatura

Deterministic structure-to-IUPAC naming, with structural auditing and atom-level explanations.

PyPI Python CI License: MIT

openclatura demo

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 py2opsin installed: result.opsin_check.status == "skipped_no_opsin"
  • py2opsin installed 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 — describe the molecular graph using natural language

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).
The typed graph descriptors record carbon center atom 6 and a double bonded oxygen ligand at atom 7 and carbon center atom 9 and a double bonded oxygen ligand at atom 10.
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}
}

About

Resources

Contributing

Stars

96 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages