Metadata-Version: 2.5
Name: openclatura
Version: 0.4.0
Summary: Deterministic SMILES-to-IUPAC name generator based on the IUPAC Blue Book
Project-URL: Homepage, https://github.com/lamalab-org/openclatura
Project-URL: Repository, https://github.com/lamalab-org/openclatura
Project-URL: Issues, https://github.com/lamalab-org/openclatura/issues
Project-URL: Changelog, https://github.com/lamalab-org/openclatura/blob/main/CHANGELOG.md
Author: Adrian Mirza, Kevin Maik Jablonka, Rostislav Fedorov
License-Expression: MIT
License-File: LICENSE
Keywords: chemistry,iupac,nomenclature,rdkit,smiles
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Chemistry
Requires-Python: >=3.11
Requires-Dist: rdkit>=2023.09
Provides-Extra: datasets
Requires-Dist: datasets>=2.0; extra == 'datasets'
Requires-Dist: huggingface-hub>=0.20; extra == 'datasets'
Requires-Dist: pandas>=2.0; extra == 'datasets'
Requires-Dist: tqdm>=4.65; extra == 'datasets'
Provides-Extra: dev
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: hypothesis>=6.92; extra == 'dev'
Requires-Dist: pre-commit>=3.5; extra == 'dev'
Requires-Dist: py2opsin>=1.2; extra == 'dev'
Requires-Dist: pydantic>=2.5; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff==0.15.15; extra == 'dev'
Provides-Extra: opsin
Requires-Dist: py2opsin>=1.2; extra == 'opsin'
Provides-Extra: web
Requires-Dist: fastapi>=0.110; extra == 'web'
Requires-Dist: pydantic>=2.5; extra == 'web'
Requires-Dist: uvicorn[standard]>=0.27; extra == 'web'
Description-Content-Type: text/markdown

<div align="center">

# openclatura

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

[![PyPI](https://img.shields.io/pypi/v/openclatura.svg)](https://pypi.org/project/openclatura/)
[![Python](https://img.shields.io/pypi/pyversions/openclatura.svg)](https://pypi.org/project/openclatura/)
[![CI](https://github.com/lamalab-org/openclatura/actions/workflows/ci.yml/badge.svg)](https://github.com/lamalab-org/openclatura/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/lamalab-org/openclatura/blob/main/LICENSE)

![openclatura demo](https://raw.githubusercontent.com/lamalab-org/openclatura/main/assets/openclatura-demo.gif)

</div>

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](https://github.com/dan2097/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/`](https://github.com/lamalab-org/openclatura/tree/main/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

```bash
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        |

```bash
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:

```bash
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

```python
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`:

```python
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:

```python
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:

```python
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, …):

```python
from openclatura import analyze_smiles

analysis = analyze_smiles("CC(=O)Nc1ccccc1")
for step in analysis.decisions:
    print(step.phase, step.decision, step.reason)
```

## CLI

```bash
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`:

```bash
openclatura name "CC(=O)Nc1ccccc1" --no-verify
```

If OPSIN support is unavailable, the command still prints the generated name and
reports the verification status:

```text
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:

```python
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 ...')
```

```text
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:

```python
from openclatura import describe_human

d = describe_human("CN1C=NC2=C1C(=O)N(C(=O)N2C)C")   # caffeine
print(d.text)
```

```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

```bash
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.

```bash
# 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:

```bash
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](https://github.com/lamalab-org/openclatura/blob/main/CHANGELOG.md)
for release notes.

## License

MIT. See `LICENSE`.

## How to cite

If you use Openclatura in your research, please cite the Openclatura preprint:

```bibtex
@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:

```bibtex
@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}
}
```
