Chemical Converter
The chemrof CLI converts SMILES or InChI strings into chemrof-compliant data
records. Given an input like CCO (ethanol), [Ca+2] (calcium ion), or
InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3, it parses the structure with RDKit,
determines the correct chemrof type, and fills in structural properties
automatically.
Quick start
# Install (from the repo root)
uv sync
# Convert a SMILES to YAML
chemrof convert "CCO"
# Multiple molecules, JSON output
chemrof convert "CCO" "c1ccccc1" "[Ca+2]" --format json
# OWL output (OWL Functional Syntax)
chemrof convert "CCO" "[Ca+2]" --format owl
# Pull names from PubChem
chemrof convert "CCO" --enrichers pubchem
# Add ChemOnt/ClassyFire classifications from a local DuckDB store
chemrof convert "CCO" --enrichers chemont --chemont-source chemont.duckdb
What it produces
For each SMILES, the converter outputs a dict with these slots:
| Slot | Source | Example |
|---|---|---|
id |
InChIKey (computed) | INCHIKEY:LFQSCWFLJHTTHZ-UHFFFAOYSA-N |
name |
Molecular formula (or enricher) | C2H6O |
type |
Auto-classified | chemrof:SmallMolecule |
smiles_string |
Canonical SMILES | CCO |
inchi_string |
Computed InChI | InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3 |
inchi_chemical_sublayer |
Parsed from InChI (other sublayers keep their layer letter: c1-2-3, q+2, p-1) |
C2H6O |
empirical_formula |
RDKit | C2H6O |
molecular_mass |
Exact mass (Da) | 46.0419 |
is_organic |
Contains carbon (molecules only) | true |
elemental_charge |
For ions only | 2 |
atomic_number, symbol |
For single atoms | 20, Ca |
neutron_number |
For isotope-labelled atoms | 7 |
has_element |
For monoatomic ions and isotopes | Ca |
classified_by |
ChemOnt enricher | CHEMONTID:0000286 |
Auto-classification
The converter inspects the parsed molecule and picks the right chemrof class:
| Structure | chemrof type |
|---|---|
Single atom, positive charge (e.g. [Ca+2]) |
AtomCation |
Single atom, negative charge (e.g. [Cl-]) |
AtomAnion |
Single atom, neutral (e.g. [He]) |
UnchargedAtom |
Single atom, neutral, mass number given (e.g. [13C]) |
Isotope |
Single atom, charged, mass number given (e.g. [13C+]) |
FullySpecifiedAtom |
Multi-atom, net positive (e.g. [NH4+]) |
MolecularCation |
Multi-atom, net negative (e.g. CC([O-])=O) |
MolecularAnion |
Multi-atom, neutral (e.g. CCO) |
SmallMolecule |
Atoms and isotopes
A single atom is described by atomic_number and symbol, plus
elemental_charge for ions and neutron_number for isotope-labelled atoms:
chemrof convert "[Fe+3]" "[13C]"
id: INCHIKEY:VTLYFUHAOXGGBS-UHFFFAOYSA-N
name: Fe+3
type: chemrof:AtomCation
atomic_number: 26
symbol: Fe
elemental_charge: 3
has_element: Fe
# ...
---
id: INCHIKEY:OKTJSMMVPCPJKN-OUBTZVSYSA-N
name: 13C
type: chemrof:Isotope
atomic_number: 6
symbol: C
neutron_number: 7
has_element: C
# ...
is_organic and is_radical are not set on bare atoms: the first describes
carbon compounds, and the second would only echo RDKit's open-valence count
(nonzero for [C], zero for [Fe]).
CHEBI ids for atoms
--enrichers chebi swaps the InChIKey id for the CHEBI id and names the entity
after its ChEBI class (iron(3+), carbon-13 atom). It works offline, from
src/chemrof/converter/data/atomic_species.tsv: every ChEBI class that is a
single atom — 118 neutral atoms, the monoatomic ions and the isotope-labelled
forms. Because the id changes, links between entities (a salt to its ions, an
enantiomer to its parent) are rewritten to match.
ChEBI stores no atomic numbers; the table is built from the standard InChI that
each of these classes carries (InChI=1S/Fe/q+3, InChI=1S/C/i1+1), read back
through RDKit. To rebuild it from a newer ChEBI release:
curl -L -o chebi.obo http://purl.obolibrary.org/obo/chebi.obo
python scripts/build_atomic_species.py chebi.obo
Siblings
--siblings generates the family of the input: every species of an element
(from an atom, ion or isotope), or every stereoisomer of a molecule with its
mirror-image pairs grouped as racemic mixtures. See
the CLI reference for details and examples.
chemrof convert "[Fe]" --siblings --enrichers chebi
chemrof convert "CC(O)C(C)O" --siblings
from chemrof.converter.convert import ChemConverter
from chemrof.converter.parse import parse_input
from chemrof.converter.siblings import siblings
parsed = parse_input("CC(O)C(C)O")
family = siblings(ChemConverter().convert_parsed(parsed), parsed.mol)
Enrichers
By default, the converter fills in only what RDKit can compute from the
structure. To pull additional data from external databases, use --enrichers:
chemrof convert "CCO" --enrichers pubchem
This adds a PubChem lookup by InChIKey, filling in the preferred IUPAC name and a PubChem CID cross-reference.
Available enrichers
| Name | Status | What it does |
|---|---|---|
pubchem |
Working | Looks up the compound in PubChem by InChIKey. Fills name (IUPAC preferred) and pubchem_cid. |
chemont |
Working | Looks up the compound in a local ChemOnt/ClassyFire store by InChIKey. Fills classified_by with the ordered ChemOnt path. |
openclatura |
Working | Derives a systematic IUPAC name locally from the structure (no network) via openclatura. Fills IUPAC_name. Requires the optional dependency (pip install 'chemrof[openclatura]'). |
chebi |
Working (atoms) | Resolves atoms, monoatomic ions and isotopes to their CHEBI class, offline. Replaces id with the CHEBI id and sets name. Other entities are left unchanged. |
wikidata |
Stub | Will resolve Wikidata QIDs via SPARQL. |
Multiple enrichers run in sequence:
chemrof convert "CCO" --enrichers pubchem,chemont --chemont-source chemont.duckdb
Local IUPAC naming (openclatura)
The openclatura enricher derives a systematic IUPAC name straight from the
structure, with no network lookup, so it also names compounds that are not in
any database. Install the optional dependency first:
pip install 'chemrof[openclatura]'
Then request it like any other enricher:
chemrof convert "CC(=O)Nc1ccccc1" --enrichers openclatura
It fills IUPAC_name (here, N-phenylacetamide) and also sets name when
that slot still holds a placeholder such as the empirical formula. Running it
alongside pubchem gives a database-preferred name where one exists and a
locally-derived name everywhere else:
chemrof convert "CCO" --enrichers pubchem,openclatura
The IUPAC_name slot carries an owl: AnnotationAssertion interpretation, so
the enrichment flows straight through to OWL output — no extra wiring needed:
chemrof convert "CC(=O)Nc1ccccc1" --enrichers openclatura --format owl
AnnotationAssertion(rdfs:label <.../FZERHIULMFGESH-UHFFFAOYSA-N> "N-phenylacetamide")
AnnotationAssertion(chemrof:IUPAC_name <.../FZERHIULMFGESH-UHFFFAOYSA-N> "N-phenylacetamide")
ChemOnt classification examples
Prepare a local DuckDB lookup store directly from the Zenodo release:
chemrof prepare-chemont-from-zenodo chemont.duckdb
Use a temporary download directory by default, or keep the downloaded files:
chemrof prepare-chemont-from-zenodo chemont.duckdb \
--download-dir /tmp/chemrof-chemont-downloads \
--overwrite
Classify a molecule with ChemOnt:
chemrof convert CCO --enrichers chemont --chemont-source chemont.duckdb
The result includes a list of ChemOnt classes:
classified_by:
- CHEMONTID:0000000
- CHEMONTID:0004603
- CHEMONTID:0000323
- CHEMONTID:0000129
- CHEMONTID:0000286
Set the source as an environment variable to avoid repeating the option:
export CHEMROF_CHEMONT_SOURCE="$PWD/chemont.duckdb"
chemrof convert CCO --enrichers chemont
ChemOnt classifications also work in OWL output. Each classified_by value is
emitted as a SubClassOf axiom:
chemrof convert CCO --enrichers chemont --chemont-source chemont.duckdb --format owl
SubClassOf(<http://identifiers.org/inchikey/LFQSCWFLJHTTHZ-UHFFFAOYSA-N> CHEMONTID:0000286)
For scan-heavy workflows, prepare Parquet instead of DuckDB:
chemrof prepare-chemont-from-zenodo chemont-parquet --format parquet
chemrof convert CCO --enrichers chemont --chemont-source chemont-parquet
To use files that were downloaded separately:
chemrof prepare-chemont \
/tmp/chemrof-chemont-downloads/classyfire_dedup_inchikey_smiles.enriched.tsv.zst \
/tmp/chemrof-chemont-downloads/chemont_dictionary.tsv \
chemont.duckdb
Writing a custom enricher
Enrichers follow a simple protocol. Any class with a name attribute and an
enrich(obj, context) method works:
from chemrof.converter.enrichers.base import EnrichmentContext
class MyEnricher:
name = "my-source"
def enrich(self, obj: dict, context: EnrichmentContext) -> dict:
# context.inchikey, context.smiles, context.inchi, context.mol
# are available for lookups
obj["my_custom_field"] = look_up_something(context.inchikey)
return obj
The EnrichmentContext carries the RDKit mol object and computed identifiers
(InChIKey, canonical SMILES, InChI) so enrichers can use whichever key their
data source accepts.
To use a custom enricher programmatically:
from chemrof.converter.convert import ChemConverter
converter = ChemConverter(enrichers=[MyEnricher()])
result = converter.convert("CCO")
OWL output
The --format owl option emits an OWL ontology in Functional Syntax.
Each entity becomes an OWL Class with a SubClassOf axiom linking it to
its chemrof type, plus annotation assertions for each structural property:
Declaration(Class(chemrof:INCHIKEY:LFQSCWFLJHTTHZ-UHFFFAOYSA-N))
SubClassOf(chemrof:INCHIKEY:LFQSCWFLJHTTHZ-UHFFFAOYSA-N chemrof:SmallMolecule)
AnnotationAssertion(rdfs:label chemrof:INCHIKEY:LFQSCWFLJHTTHZ-UHFFFAOYSA-N "C2H6O")
AnnotationAssertion(chemrof:smiles_string chemrof:INCHIKEY:LFQSCWFLJHTTHZ-UHFFFAOYSA-N "CCO")
The Python API equivalent:
from chemrof.converter.convert import ChemConverter
from chemrof.converter.owl_output import dicts_to_owl
converter = ChemConverter()
objs = [converter.convert(s) for s in ["CCO", "[Ca+2]"]]
print(dicts_to_owl(objs))
Python API
The converter is also usable as a library:
from chemrof.converter.convert import ChemConverter
converter = ChemConverter()
result = converter.convert("CCO")
# result is a plain dict with chemrof slots
print(result["type"]) # chemrof:SmallMolecule
print(result["empirical_formula"]) # C2H6O