ZeroPM: the global chemical inventory database¶
The ZeroPM project (EU Horizon 2020) combined the chemical inventories of
regulators around the world into one database. It records which substances
are registered where, their identifiers, which are multicomponent
substances, and, for many of them, estimated probabilities of being
persistent and mobile. ZeroPM reads the released SQLite file (0.0.4, 439 MiB).
Install it with datasets.fetch("zeropm"); see
Installing the offline databases.
Because the names come from inventories, not from curated compound records,
they are noisier than those of the other databases. Search therefore leaves
ZeroPM out unless asked (sources="all", or the "recall" preset). The
same noise makes it the one database that finds misspelled and trade names.
Everything here is offline, and the database is only read. The outputs below were produced on 2026-09-23.
import pandas as pd
from provesid import ZeroPM
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 12)
pd.set_option("display.max_colwidth", 60)
zpm = ZeroPM()
zpm.get_database_stats()
{'api_ready_query': 447617,
'api_results': 1349825,
'substances': 359221,
'inventories': 543851,
'inventory_summary': 1045887,
'cleanventory_chemicals': 483388,
'zeropm_chemicals': 126369,
'components': 22380,
'multi_components': 38696,
'unique_cas_numbers': 164513,
'unique_chemical_names': 283104}
A CAS number¶
get_id_table_from_cas returns every structure the CAS number maps to, ranked,
with the synonyms and the inventories that list it:
atrazine = zpm.get_id_table_from_cas("1912-24-9")
atrazine[["cas", "rank", "inchikey", "zeropm_id"]]
| cas | rank | inchikey | zeropm_id | |
|---|---|---|---|---|
| 0 | 1912-24-9 | 1 | MXWJVTOOROXGIU-UHFFFAOYSA-N | 223 |
| 1 | 1912-24-9 | 2 | MXWJVTOOROXGIU-WEQCDQLKSA-N | <NA> |
| 2 | 1912-24-9 | 3 | QLLNXSNPTUSROU-UHFFFAOYSA-N | <NA> |
Rank 1 is ZeroPM's best match for the CAS number, and the only one assigned
a zeropm_id. The others are further candidates that some inventory linked
to the same number: here a stereo variant, and a different structure.
atrazine.loc[0, "sources"].split("; ")
['NITE', 'EC Inventory', 'Inventory of Chemicals and Chemical Substances', 'CLP Annex VI', 'Inventory of Chemicals', ' Chemicals Information System ', 'Substances in Preparations in Nordic Countries Inventory', 'Toxic Substances Control Act (TSCA) Chemical Substance Inventory', 'Chemical Substances Control Law Existing Chemical Substances', 'Hazardous Chemical Information System', 'Domestic Substance List', 'Chemical Substance Inventory', 'Inventory of Existing Chemical Substances and Chemicals']
zpm.get_names("1912-24-9")[:10]
['ATRAZINE', 'ATRAZIN', '2-Chloro-4-ethylamino-6-isopropylamino-1,3,5-triazine', 'Atrazine', '1,3,5-Triazine-2,4-diamine, 6-chloro-N2-ethyl-N4-(1-methylethyl)-', 'atrazine (ISO)', '2-chloro-4-ethylamine-6-isopropylamine-1,3,5-triazine', "1,3,5-Triazine-2,4-diamine, 6-chloro-N-ethyl-N'-(1-methylethyl)-"]
Names, including misspelled ones¶
get_id_table_from_name needs the name exactly as some inventory wrote it,
including its case. get_id_table_from_similar_name matches approximately:
print(zpm.get_id_table_from_name("atrazin"))
zpm.get_id_table_from_similar_name("atrazin", number_of_results=3)[
["name", "matched_name", "match_score", "cas", "inchikey"]]
None
| name | matched_name | match_score | cas | inchikey | |
|---|---|---|---|---|---|
| 0 | atrazin | ATRAZIN | 100.0 | 1912-24-9 | MXWJVTOOROXGIU-UHFFFAOYSA-N |
The exact lookup finds nothing, because the inventory wrote "ATRAZIN" in capitals. The approximate one finds it. "Atrazin" is the German and Scandinavian spelling, and it is in the Nordic countries' list, which is how ZeroPM finds names no curated database has.
Structures to CAS numbers¶
zpm.get_cas_from_inchikey("LFQSCWFLJHTTHZ-UHFFFAOYSA-N")
['64-17-5', '68475-56-9', '68476-78-8', '1516-08-1', '14742-23-5', '925-93-9', '9002-89-5', '90604-31-2']
Ethanol's InChIKey maps to eight CAS numbers: ethanol itself, 64-17-5, and seven other registrations that some inventory recorded with ethanol's structure. One of them is 9002-89-5, polyvinyl alcohol. Treat these as candidates, not as synonyms of ethanol.
A formula maps to every isomer an inventory lists:
zpm.get_cas_from_formula("C2H6O")
['115-10-6', '14742-23-5', '1516-08-1', '4502-04-9', '64-17-5', '68475-56-9', '68476-78-8', '9002-89-5', '90604-31-2', '925-93-9']
Inventories, countries and regions¶
pd.DataFrame(zpm.get_all_regions())
| region_id | region | |
|---|---|---|
| 0 | 4 | Asia |
| 1 | 2 | Europe |
| 2 | 1 | North America |
| 3 | 5 | Oceania |
| 4 | 3 | Scandinavia |
pd.DataFrame(zpm.get_inventories_for_country(country_name="Japan"))[["source_id", "source_name"]]
| source_id | source_name | |
|---|---|---|
| 0 | 8 | Chemical Substances Control Law Existing Chemical Substa... |
| 1 | 9 | Chemical Substances Control Law Newly Announced Chemical... |
| 2 | 10 | Chemical Substances Control Law Newly Announced Chemical... |
| 3 | 11 | NITE |
zpm.count_chemicals_by_inventory(8)
45072
Persistence and mobility¶
For about 130,000 structures ZeroPM estimates the probability of each
persistence class and each mobility class from the models it combined. n is
how many models contributed:
probs = zpm.get_pm_probabilities(cas="1912-24-9")
{key: round(value, 3) for key, value in probs.items()}
{'probability_of_not_p': 0.366,
'probability_of_p_or_vp': 0.634,
'probability_of_p': 0.489,
'probability_of_vp': 0.145,
'probability_of_not_m': 0.014,
'probability_of_m_or_vm': 0.986,
'probability_of_m': 0.542,
'probability_of_vm': 0.444,
'n': 5}
Each set of classes sums to one: probability_of_not_p and
probability_of_p_or_vp sum to one, and probability_of_p (persistent but
not very persistent) and probability_of_vp sum to probability_of_p_or_vp.
The same holds for M. Atrazine is almost certainly mobile, with the odds
roughly even between M and vM.
In the released file, three of the mobility columns hold each other's values.
ZeroPM returns each value under its true name, but a query of the
pm_probabilities table written by hand gets the stored names (see the note in
get_pm_probabilities).
batch_get_pm_probabilities takes a list. Ordered from very mobile to
immobile: trifluoroacetic acid, 1,4-dioxane, atrazine, naphthalene and
benzo[a]pyrene. Formaldehyde was assessed, but ZeroPM published no
probabilities for it, so its row is empty:
cas_list = ["76-05-1", "123-91-1", "1912-24-9", "91-20-3", "50-32-8", "50-00-0"]
mobility = zpm.batch_get_pm_probabilities(cas_list=cas_list).set_index("cas").loc[cas_list]
mobility[["probability_of_not_m", "probability_of_m", "probability_of_vm",
"probability_of_p_or_vp"]].round(3)
| probability_of_not_m | probability_of_m | probability_of_vm | probability_of_p_or_vp | |
|---|---|---|---|---|
| cas | ||||
| 76-05-1 | 0.000 | 0.005 | 0.995 | 0.611 |
| 123-91-1 | 0.000 | 0.010 | 0.990 | 0.611 |
| 1912-24-9 | 0.014 | 0.542 | 0.444 | 0.634 |
| 91-20-3 | 0.493 | 0.502 | 0.005 | 0.365 |
| 50-32-8 | 1.000 | 0.000 | 0.000 | 0.736 |
| 50-00-0 | NaN | NaN | NaN | NaN |
The mobility estimates follow the substances' sorption: TFA and dioxane are very mobile, naphthalene sits on the M threshold, and benzo[a]pyrene binds to soil. The persistence estimates are much less decisive: all five fall between 0.36 and 0.74, and TFA, which does not degrade in the environment, gets 0.61, the same as dioxane.
zpm.is_in_zeropm(cas="1912-24-9"), zpm.is_in_cleanventory(cas="1912-24-9")
(True, True)
is_in_zeropm says whether ZeroPM assessed the substance (it has a
zeropm_id). A CAS number can be in the inventories without being assessed.
is_in_cleanventory says whether its rank-1 structure is in the project's
cleaned inventory, "CLEANventory".
Multicomponent substances¶
Salts and mixtures are recorded as multicomponent substances:
zpm.get_all_multicomponent_substances(limit=3)
| mc_id | inchi_id | inchi | inchikey | component_count | |
|---|---|---|---|---|---|
| 0 | 1 | 5 | InChI=1S/C8H10N3.ClH/c1-11(2)8-5-3-7(10-9)4-6-8;/h3-6H,1... | CCIAVEMREXZXAK-UHFFFAOYSA-M | 2 |
| 1 | 2 | 6 | InChI=1S/C6H4N3O2.ClH/c7-8-5-1-3-6(4-2-5)9(10)11;/h1-4H;... | GAVPOWTXUZVFML-UHFFFAOYSA-M | 2 |
| 2 | 3 | 32 | InChI=1S/C6H5N2.ClH/c7-8-6-4-2-1-3-5-6;/h1-5H;1H/q+1;/p-1 | CLRSZXHOSMKUIB-UHFFFAOYSA-M | 2 |
get_components lists a substance's components, the most frequent first:
pd.DataFrame(zpm.get_components(1))[["component_id", "component_frequency", "inchikey", "inchi"]]
| component_id | component_frequency | inchikey | inchi | |
|---|---|---|---|---|
| 0 | 21801 | 1 | VEXZGXHMUGYJMC-UHFFFAOYSA-M | InChI=1S/ClH/h1H/p-1 |
| 1 | 20043 | 1 | MOXBCYIWIODTKI-UHFFFAOYSA-N | InChI=1S/C8H10N3/c1-11(2)8-5-3-7(10-9)4-6-8/h3-6H,1-2H3/q+1 |
Substructure search¶
get_cas_by_substructure takes a SMARTS pattern and stops after
max_results matches:
hits = zpm.get_cas_by_substructure("c1ccc2ccccc2c1", max_results=5) # naphthalene
pd.DataFrame(hits)[["cas", "smiles"]]
[08:16:21] WARNING: Problems/mismatches: Mobile-H( Mobile-H groups: Attachment points, Number)
| cas | smiles | |
|---|---|---|
| 0 | 10000-56-3 | CCCCN=C(O)c1cc2ccccc2c(N=Nc2cc(OC)ccc2OC)c1O |
| 1 | 100063-59-0 | Cc1ccc(N=Nc2c(O)ccc3ccccc23)c(S(=O)(=O)O)c1.[Ca] |
| 2 | [10008-12-5, 10049-24-8] | Cc1ccc2ccccc2c1C(=O)Cl |
| 3 | 10019-03-1 | O=[N+]([O-])c1cccc(N=C(O)c2cc3ccccc3cc2OP(=O)(O)O)c1 |
| 4 | 100208-44-4 | O=S(=O)(O)c1cc(NC2C(Cl)C2Cl)c2c(O)c(N=Nc3ccccc3)c(S(=O)(... |
Closing¶
ZeroPM holds an SQLite connection. Close it when you are done, or use it as
a context manager (with ZeroPM() as zpm:).
zpm.close()