NCI/CADD Chemical Identifier Resolver (CACTUS)¶
The Chemical Identifier Resolver of the NCI/CADD group, known as CACTUS,
converts one chemical identifier into another. It reads names, CAS numbers,
SMILES, InChI and InChIKeys, and returns structures, names, CAS numbers,
formulas and a few computed properties. NCIChemicalIdentifierResolver is a
client for it.
It is a free web service with no key. Every call here needs the network, and
the service is sometimes slow or drops connections. The client retries those
with back-off. Search(online_fallback=True) asks CACTUS about names that no
offline database holds (see the Search tutorial).
The outputs below were produced on 2026-09-23.
import pandas as pd
from provesid import NCIChemicalIdentifierResolver, NCIResolverNotFoundError
pd.set_option("display.width", 200)
pd.set_option("display.max_colwidth", 70)
resolver = NCIChemicalIdentifierResolver()
One conversion¶
resolve(identifier, representation) makes one request. CACTUS works out
what kind of identifier it was given:
for identifier in ["aspirin", "50-78-2", "CC(=O)Oc1ccccc1C(=O)O"]:
print(f"{identifier:24} -> {resolver.resolve(identifier, 'stdinchikey')}")
aspirin -> InChIKey=BSYNRYMUTXBXSQ-UHFFFAOYSA-N 50-78-2 -> InChIKey=BSYNRYMUTXBXSQ-UHFFFAOYSA-N CC(=O)Oc1ccccc1C(=O)O -> InChIKey=BSYNRYMUTXBXSQ-UHFFFAOYSA-N
The answer is CACTUS's text as it sends it, so the InChIKey keeps its
InChIKey= prefix. The representations the client knows:
resolver.representations
{'stdinchi': 'Standard InChI',
'stdinchikey': 'Standard InChIKey',
'smiles': 'Unique SMILES',
'ficts': 'NCI/CADD FICTS identifier',
'ficus': 'NCI/CADD FICuS identifier',
'uuuuu': 'NCI/CADD uuuuu identifier',
'hashisy': 'CACTVS HASHISY hashcode',
'sdf': 'SD file format',
'names': 'Chemical names list',
'iupac_name': 'IUPAC name',
'cas': 'CAS Registry Number',
'mw': 'Molecular weight',
'formula': 'Molecular formula',
'image': 'Chemical structure image',
'exactmass': 'Exact mass',
'charge': 'Formal charge',
'h_bond_acceptor_count': 'Hydrogen bond acceptor count',
'h_bond_donor_count': 'Hydrogen bond donor count',
'rotor_count': 'Rotatable bond count',
'effective_rotor_count': 'Effective rotor count',
'ring_count': 'Ring count',
'ringsys_count': 'Ring system count'}
CAS numbers come as an unranked list¶
Asked for cas, CACTUS returns every CAS number it associates with the
structure, one per line, in no meaningful order:
resolver.resolve("ethanol", "cas").splitlines()
['121182-78-3', '64-17-5', '8024-45-1', '8000-16-6', '68475-56-9', '71076-86-3', '71329-38-9']
Ethanol's CAS number is 64-17-5. The first line is 121182-78-3, which is not
it. Split the answer and treat it as a set of candidates. Do not take the
first line as the CAS number. Search can decide between candidates with
the offline databases, which carry each compound's primary number.
Several representations at once¶
get_molecular_data asks for the common representations in one call and
reports what it found. Unlike resolve, it parses the text: names and
cas are lists, stdinchikey has no InChIKey= prefix and mw is a
float.
caffeine = resolver.get_molecular_data("caffeine")
{key: caffeine[key] for key in ("success", "smiles", "stdinchikey", "cas",
"iupac_name", "formula", "mw")}
{'success': True,
'smiles': 'Cn1cnc2N(C)C(=O)N(C)C(=O)c12',
'stdinchikey': 'RYYVLZVUVIJVGH-UHFFFAOYSA-N',
'cas': ['71701-02-5', '95789-13-2', '58-08-2'],
'iupac_name': '1,3,7-trimethylpurine-2,6-dione',
'formula': 'C8H10N4O2',
'mw': 194.1926}
caffeine["names"][:8]
['1,3,7-trimethylpurine-2,6-dione', '1,3,7-Trimethylxanthine', '71701-02-5', '95789-13-2', '58-08-2', 'SDCCGMLS-0064595.P002', 'PDSP2_001000', 'KBioGR_002325']
resolve_multiple asks for a chosen set of representations:
resolver.resolve_multiple("ibuprofen", ["smiles", "formula", "mw", "h_bond_donor_count",
"rotor_count"])
{'smiles': 'CC(C)Cc1ccc(cc1)C(C)C(O)=O',
'formula': 'C13H18O2',
'mw': '206.284',
'h_bond_donor_count': '1',
'rotor_count': '4'}
Several identifiers at once¶
batch_resolve makes one request per identifier, paced for the service:
smiles = resolver.batch_resolve(["aspirin", "paracetamol", "naproxen", "diclofenac",
"not-a-chemical-xyz"], "smiles")
pd.Series(smiles, name="smiles")
aspirin CC(=O)Oc1ccccc1C(O)=O paracetamol CC(=O)Nc1ccc(O)cc1 naproxen COc1ccc2cc(ccc2c1)C(C)C(O)=O diclofenac OC(=O)Cc1ccccc1Nc2c(Cl)cccc2Cl not-a-chemical-xyz NaN Name: smiles, dtype: str
An identifier CACTUS does not know maps to None in the batch.
When a lookup fails¶
resolve raises NCIResolverNotFoundError when CACTUS has no answer, and
ValueError for a representation it does not offer:
try:
resolver.resolve("not-a-chemical-xyz", "smiles")
except NCIResolverNotFoundError as e:
print("not found:", e)
try:
resolver.resolve("aspirin", "boiling_point")
except ValueError as e:
print("ValueError:", str(e)[:80], "...")
not found: No data for https://cactus.nci.nih.gov/chemical/structure/not-a-chemical-xyz/smiles (HTTP 500) ValueError: Unsupported representation 'boiling_point'. Available: stdinchi, stdinchikey, sm ...
The module-level helpers (nci_name_to_smiles, nci_cas_to_inchi,
nci_get_molecular_weight, …) wrap the same calls and return None instead
of raising:
from provesid import nci_name_to_smiles, nci_get_molecular_weight
nci_name_to_smiles("naproxen"), nci_name_to_smiles("not-a-chemical-xyz"), nci_get_molecular_weight("caffeine")
('COc1ccc2cc(ccc2c1)C(C)C(O)=O', None, 194.1926)
NCIResolverNotFoundError derives from NCIResolverError, which derives from
provesid.http.ServiceError. Answers, but not failures, are cached on disk,
so a lookup that failed because CACTUS was down is asked again next time.
See Network behaviour and
Caching.
Structure images¶
get_image_url builds the URL of a structure drawing without making a
request, and download_image saves it:
resolver.get_image_url("caffeine", image_format="png", width=300, height=300)
'https://cactus.nci.nih.gov/chemical/structure/caffeine/image?format=png&width=300&height=300'