git:20260428.81ea2d2 to git:20260615.2df60d4

273 added, 268 removed. Audit A to A.

---
name: "zinc-database"
- description: "Query ZINC15/ZINC22 virtual compound libraries (1.4B compounds, 750M purchasable). Search lead/fragment/drug-like compounds by MW, logP, reactivity, or SMILES similarity; download 3D sets for docking. For bioactivity use chembl-database-bioactivity; for approved drugs use drugbank-database-access."
+ description: "Query the ZINC22 virtual compound library (CartBlanche API, billions of make-on-demand + purchasable molecules). Look up substances by ZINC ID, resolve a SMILES to its ZINC ID (exact match), inspect purchasability/catalogs, and assemble compound sets for docking. Property (MW/logP) filtering is done locally with RDKit. For bioactivity use chembl-database-bioactivity; for approved drugs use drugbank-database-access."
license: "CC-BY-4.0"
---
- # ZINC Chemical Library Database
+ # ZINC Chemical Library Database (ZINC22 / CartBlanche)
## Overview
- ZINC (ZINC Is Not Commercial) is a free database of commercially available compounds curated for virtual screening. ZINC22 contains over 1.4 billion compounds (ZINC20: 1.4B, including purchasable 3D conformers), organized by molecular property filters (lead-like, fragment-like, drug-like) and reactivity class. The REST API enables SMILES-based searches, property-filtered downloads, and compound subset exports for docking campaigns.
+ ZINC (ZINC Is Not Commercial) is a free database of commercially available compounds curated for virtual screening. The current generation, **ZINC22**, holds billions of make-on-demand and in-stock molecules and is served through the **CartBlanche** web application at `cartblanche22.docking.org`. CartBlanche exposes a small JSON API to look up substances by ZINC ID, resolve a SMILES to its ZINC identifier, and inspect supplier/catalog purchasability.
+ > **Important — the old ZINC15 REST API no longer works for automated access.**
+ > `zinc15.docking.org` (and `zinc.docking.org`) now sits behind a site-wide CAPTCHA: every request to `/substances.json`, `/tranches/...`, `/substances/{id}.json`, etc. is redirected to a `/captcha/` page or returns `403 Forbidden`. Any script using the `mwt__gte` / `availability` / `similarity` query parameters against `zinc15.docking.org` will fail. Use the ZINC22 CartBlanche endpoints documented below instead.
+
+ Three things about ZINC22/CartBlanche that change how you use this skill:
+
+ 1. **There is no server-side property-range query.** ZINC22/CartBlanche has no `mwt__gte` / `logp__lte` / `hbd__lte` search. You filter by molecular property by either (a) selecting **tranches** (a 2D MW x logP grid) for bulk download, or (b) retrieving compounds and filtering locally with RDKit. This skill shows the local-RDKit approach.
+ 2. **SMILES lookup is asynchronous.** You POST a SMILES, receive a `task` id, and poll for the result. Exact-match lookup completes in seconds.
+ 3. **Reliable programmatic search = exact match.** The API's broader analog search (`dist` > 0) is slow and frequently times out on the public server, and the Smallworld similarity search is a website-only flow. Treat exact SMILES → ZINC ID lookup as the dependable primitive; see the "Analog / Similarity Search" note for the (limited) alternatives.
+
## When to Use
- - Downloading a purchasable, drug-like or lead-like compound library for virtual screening or docking campaigns
- - Filtering compounds by Lipinski/lead-like properties (MW, logP, HBD, HBA) to build focused screening sets
- - Searching ZINC for commercially available analogs of a query molecule via SMILES similarity
- - Retrieving purchasable fragments (MW < 300, logP < 3) for fragment-based drug discovery
- - Building compound diversity libraries for high-throughput screening (HTS) campaigns
- - For known drug bioactivity data use `chembl-database-bioactivity`; for approved drug structures use `drugbank-database-access`; for RDKit property calculation use `rdkit-cheminformatics`
+ - Looking up a known ZINC ID to get its SMILES, computed properties, and purchasability/suppliers
+ - Resolving a molecule you have as a SMILES to its ZINC22 identifier(s) to check availability
+ - Checking whether a hit compound is purchasable and from which catalogs before ordering
+ - Assembling a SMILES/ZINC-ID set to feed into a docking campaign
+ - For property-filtered library building, combine SMILES/tranche retrieval here with local RDKit filtering (`rdkit-cheminformatics`)
+ - For known drug bioactivity data use `chembl-database-bioactivity`; for approved drug structures use `drugbank-database-access`
## Prerequisites
- - **Python packages**: `requests`, `pandas`
- - **Data requirements**: SMILES strings, MW/logP ranges, or ZINC subset IDs
- - **Environment**: internet connection; no API key needed for ZINC15; large downloads may take minutes
- - **Rate limits**: reasonable use; avoid crawling all 1.4B records in automated loops
+ - **Python packages**: `requests`, `pandas` (and `rdkit` for local property filtering)
+ - **Data requirements**: a ZINC ID, or a SMILES string
+ - **Environment**: internet connection; no API key needed; SMILES lookups are async (poll for the result)
+ - **Rate limits**: be courteous — serialize searches, cache results, and do not poll the task endpoint faster than once every few seconds
```bash
pip install requests pandas
+ # optional, for local property filtering:
+ pip install rdkit
```
## Quick Start
```python
import requests
- # Search ZINC15 REST API for drug-like compounds
- BASE = "https://zinc15.docking.org"
+ BASE = "https://cartblanche22.docking.org"
+ HEADERS = {"User-Agent": "sciagent-zinc-skill/1.0"}
- r = requests.get(f"{BASE}/substances.json",
- params={"mwt__gte": 250, "mwt__lte": 350,
- "logp__gte": 0, "logp__lte": 3,
- "availability": "for-sale", "count": 5})
+ # Look up a substance by ZINC ID (synchronous, returns JSON immediately)
+ r = requests.get(f"{BASE}/substance/ZINC000000029632.json", headers=HEADERS, timeout=30)
r.raise_for_status()
- compounds = r.json()
- print(f"Returned {len(compounds)} compounds")
- for c in compounds[:3]:
- print(f" ZINC: {c['zinc_id']:20s} MW: {c['mwt']:.1f} logP: {c['logp']:.2f} SMILES: {c['smiles'][:40]}")
+ c = r.json()
+
+ td = c["tranche_details"]
+ print(f"ZINC ID : {c['zinc_id']} (db: {c['db']})")
+ print(f"SMILES : {c['smiles']}")
+ print(f"MW : {td['mwt']:.2f} logP: {td['logp']:.2f} heavy atoms: {td['heavy_atoms']}")
+ print(f"InChIKey: {td['inchikey']}")
+ print(f"Catalogs: {len(c.get('catalogs', []))} supplier entries")
```
- ## Core API
+ Expected output (abridged):
- ### Query 1: Property-Filtered Compound Search
+ ```
+ ZINC ID : ZINC000000029632 (db: zinc20)
+ SMILES : O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1
+ MW : 387.48 logP: 3.29 heavy atoms: 29
+ InChIKey: QCEMBLKSDIWPBK-JOCHJYFZSA-N
+ Catalogs: 4 supplier entries
+ ```
- Search ZINC15 by molecular property ranges (Lipinski, lead-like, fragment-like criteria).
+ ## Core API
- ```python
- import requests, pandas as pd
+ The CartBlanche base URL and two reusable helpers used throughout:
- BASE = "https://zinc15.docking.org"
+ ```python
+ import requests, time
- def zinc_search(params, max_results=500):
- """Search ZINC15 with property filters. Returns DataFrame."""
- all_results = []
- params = dict(params)
- params["count"] = min(100, max_results)
+ BASE = "https://cartblanche22.docking.org"
+ HEADERS = {"User-Agent": "sciagent-zinc-skill/1.0"}
- r = requests.get(f"{BASE}/substances.json", params=params)
+ def get_substance(zinc_id):
+ """Fetch full record for one ZINC ID (synchronous)."""
+ r = requests.get(f"{BASE}/substance/{zinc_id}.json", headers=HEADERS, timeout=30)
r.raise_for_status()
- compounds = r.json()
- all_results.extend(compounds)
- return pd.DataFrame(all_results)
+ return r.json()
- # Lead-like set: MW 250-350, logP 1-3, HBD ≤ 3
- df_leads = zinc_search({
- "mwt__gte": 250, "mwt__lte": 350,
- "logp__gte": 1, "logp__lte": 3,
- "hbd__lte": 3, "hba__lte": 7,
- "availability": "for-sale",
- })
- print(f"Lead-like compounds: {len(df_leads)}")
- print(df_leads[["zinc_id", "mwt", "logp", "smiles"]].head())
- ```
+ def smiles_lookup(smiles, dist=0, adist=0, database="zinc22", timeout=120, poll=5):
+ """Resolve a SMILES to ZINC22 substance dicts via the async task API.
- ```python
- # Fragment-like set: MW < 300, logP < 3 (Rule of Three)
- df_frags = zinc_search({
- "mwt__lte": 300,
- "logp__lte": 3,
- "hbd__lte": 3,
- "availability": "for-sale",
- })
- print(f"\nFragment-like compounds: {len(df_frags)}")
- print(df_frags[["zinc_id", "mwt", "logp", "smiles"]].head())
+ dist=0, adist=0 -> exact match (fast, reliable; use this).
+ dist/adist > 0 -> near-neighbor analog search (slow; may time out — see note).
+ Returns a list of substance dicts (possibly empty).
+ """
+ files = {"smiles": (None, smiles), "dist": (None, str(dist)),
+ "adist": (None, str(adist)), "database": (None, database)}
+ sub = requests.post(f"{BASE}/smiles.json", files=files, headers=HEADERS, timeout=60)
+ sub.raise_for_status()
+ task = sub.json()["task"] # async: returns {"task": "<uuid>"}
+
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ res = requests.get(f"{BASE}/search/result/{task}", headers=HEADERS, timeout=30).json()
+ if res.get("status") == "SUCCESS":
+ data = res.get("result")
+ # result is {"zinc22": [...], "zinc20": [...]} when hits exist,
+ # or an empty list when there are none.
+ if isinstance(data, dict):
+ return data.get("zinc22", []) + data.get("zinc20", [])
+ return data or []
+ time.sleep(poll) # status == "PROGRESS" until done
+ raise TimeoutError(f"ZINC22 search task {task} did not finish in {timeout}s")
```
- ### Query 2: Retrieve Compound by ZINC ID
+ ### Query 1: Retrieve a Compound by ZINC ID
- Fetch full compound data for a known ZINC identifier.
+ Fetch the full record (SMILES, computed properties, formula, ring/heteroatom counts, supplier catalogs) for a known ZINC identifier.
```python
- import requests
-
- BASE = "https://zinc15.docking.org"
-
- zinc_id = "ZINC000000029632"
-
- r = requests.get(f"{BASE}/substances/{zinc_id}.json")
- r.raise_for_status()
- c = r.json()
+ c = get_substance("ZINC000000029632")
+ td = c["tranche_details"]
- print(f"ZINC ID : {c['zinc_id']}")
- print(f"SMILES : {c['smiles']}")
- print(f"MW : {c['mwt']:.2f}")
- print(f"logP : {c['logp']:.2f}")
- print(f"HBD : {c['hbd']}")
- print(f"HBA : {c['hba']}")
- print(f"TPSA : {c.get('tpsa', 'n/a')}")
- print(f"Rotatable: {c.get('rotatable_bonds', 'n/a')}")
- print(f"Suppliers: {len(c.get('suppliers', []))}")
+ print(f"ZINC ID : {c['zinc_id']}")
+ print(f"SMILES : {c['smiles']}")
+ print(f"Formula : {c['mol_formula']}")
+ print(f"MW : {td['mwt']:.2f}")
+ print(f"logP : {td['logp']:.2f}")
+ print(f"Heavy atoms: {td['heavy_atoms']}")
+ print(f"Rings : {c['rings']} Hetero atoms: {c['hetero_atoms']}")
+ print(f"InChIKey : {td['inchikey']}")
+ print(f"Suppliers : {len(c.get('catalogs', []))}")
```
- ### Query 3: Download Compound Subsets (Tranches)
+ Both the modern `ZINCbq...`/`ZINCbj...` style IDs and the legacy 12-digit `ZINC000...` IDs resolve through this endpoint.
- ZINC organizes compounds into "tranches" by MW and logP. Download pre-built SDF/SMILES files.
+ ### Query 2: Resolve a ZINC ID from a SMILES (Exact Match)
+ Find the ZINC22 identifier(s) for a molecule you have as a SMILES string. This is the dependable search primitive.
+
```python
- import requests
+ hits = smiles_lookup("CC(=O)Nc1ccc(O)cc1") # acetaminophen, exact (dist=0)
+ print(f"Exact matches: {len(hits)}")
+ for h in hits[:5]:
+ td = h["tranche_details"]
+ print(f" {h['zinc_id']:22s} MW {td['mwt']:.1f} logP {td['logp']:.2f} {h['smiles']}")
+ ```
- # ZINC15 tranche download (MW 200-250, logP 1-2 range)
- # Tranche naming: letters encode MW range (A-K) and logP range (A-J)
- # See http://zinc15.docking.org/tranches/home
+ A single molecule can map to several ZINC IDs across ZINC22 sub-databases (e.g. `ZINCbq...` and `ZINCbj...`) — each represents the same structure in a different build.
- def download_zinc_tranche(tranche_name, dest_file, fmt="smi"):
- """Download a ZINC tranche SMILES file."""
- url = f"https://zinc15.docking.org/tranches/{tranche_name}.{fmt}"
- r = requests.get(url, stream=True)
- r.raise_for_status()
- with open(dest_file, "wb") as f:
- for chunk in r.iter_content(chunk_size=8192):
- f.write(chunk)
- print(f"Downloaded {dest_file}")
+ ### Query 3: Purchasability and Supplier Catalogs
- # Download one tranche as SMILES
- download_zinc_tranche("AABA", "zinc_AABA.smi", fmt="smi")
+ Purchasability is embedded in the substance record under `catalogs` — no separate request needed.
+
+ ```python
+ c = get_substance("ZINC000000029632")
+ catalogs = c.get("catalogs", [])
+ print(f"Supplier entries: {len(catalogs)}")
+ for cat in catalogs:
+ print(f" {cat.get('catalog_name', 'n/a'):24s} | "
+ f"purchasable: {cat.get('purchase')} | "
+ f"code: {cat.get('supplier_code', 'n/a')} | "
+ f"price: {cat.get('price', 'n/a')} {cat.get('unit', '')}")
```
- ### Query 4: SMILES Similarity Search
+ `purchase: 1` indicates the compound is orderable from that catalog; entries may include price, quantity, and lead time (`shipping`).
- Find ZINC compounds similar to a query molecule.
+ ### Query 4: Property-Filtered Library (Local RDKit Filtering)
+ ZINC22 has no server-side MW/logP query. The portable pattern is: gather candidate SMILES (resolved ZINC IDs, a downloaded tranche file, or your own enumeration), then filter locally with RDKit.
+
```python
- import requests, pandas as pd
+ import pandas as pd
+ from rdkit import Chem
+ from rdkit.Chem import Descriptors, Lipinski
- BASE = "https://zinc15.docking.org"
+ # Candidate SMILES (here: a small hand list; in practice load a tranche .smi or your own set)
+ smiles_list = [
+ "CC(=O)Nc1ccc(O)cc1", # acetaminophen (MW 151 - too small, filtered out)
+ "CC(C)NCC(O)COc1cccc2ccccc12", # propranolol (MW 259, logP 2.6 - lead-like)
+ "CN(C)CCOC(c1ccccc1)c1ccccc1", # diphenhydramine
+ "c1ccc(NC(=O)c2ccccc2)cc1", # benzanilide (MW 197 - too small, filtered out)
+ ]
- query_smiles = "c1ccc(NC(=O)c2ccccc2)cc1" # benzanilide analog
+ def lead_like(smi):
+ m = Chem.MolFromSmiles(smi)
+ if m is None:
+ return None
+ mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)
+ hbd, hba = Lipinski.NumHDonors(m), Lipinski.NumHAcceptors(m)
+ ok = 250 <= mw <= 350 and 1 <= logp <= 3 and hbd <= 3 and hba <= 7
+ return {"smiles": Chem.MolToSmiles(m), "mw": round(mw, 1), "logp": round(logp, 2),
+ "hbd": hbd, "hba": hba} if ok else None
- r = requests.get(f"{BASE}/substances.json",
- params={
- "smiles": query_smiles,
- "similarity": 0.6, # Tanimoto similarity threshold
- "count": 20,
- "availability": "for-sale"
- })
- r.raise_for_status()
- results = r.json()
- print(f"Similar compounds found: {len(results)}")
- df = pd.DataFrame(results)[["zinc_id", "smiles", "mwt", "logp"]]
+ rows = [r for r in (lead_like(s) for s in smiles_list) if r]
+ df = pd.DataFrame(rows).drop_duplicates(subset=["smiles"])
+ print(f"Lead-like compounds after RDKit filter: {len(df)}")
print(df.head())
```
- ### Query 5: Catalog and Supplier Information
+ ### Analog / Similarity Search (limited — read before relying on it)
- Retrieve purchasability and supplier catalog data for compounds.
+ The submit endpoint accepts `dist` (topological graph-edit distance, 0–3) and `adist` ("anon" distance, scaffold hops) for a near-neighbor **analog** search:
```python
- import requests
-
- BASE = "https://zinc15.docking.org"
-
- # Check purchasability and catalog info
- zinc_id = "ZINC000000029632"
- r = requests.get(f"{BASE}/substances/{zinc_id}/suppliers.json")
- r.raise_for_status()
- suppliers = r.json()
-
- print(f"Suppliers for {zinc_id}: {len(suppliers)}")
- for sup in suppliers[:5]:
- print(f" {sup.get('name', 'n/a'):30s} | Catalog: {sup.get('catalognum', 'n/a')}")
+ # Near-neighbor analogs. Keep dist small (1) and the timeout generous; expect FEW results.
+ try:
+ analogs = smiles_lookup("c1ccc(NC(=O)c2ccccc2)cc1", dist=1, adist=0, timeout=240)
+ print(f"Analogs: {len(analogs)}")
+ for h in analogs[:10]:
+ print(" ", h["zinc_id"], h["smiles"])
+ except TimeoutError as e:
+ print("Analog search timed out:", e)
```
- ### Query 6: Bulk Download via ZINC Slices
-
- For large-scale virtual screening, download entire ZINC subsets as compressed SMILES.
-
- ```python
- import requests, gzip, io, pandas as pd
+ Caveats verified against the live public server:
+ - `dist=0` (exact) is fast and reliable. `dist >= 1` runs across billions of molecules on a shared cluster and **frequently times out** (e.g. `dist=3` did not return within ~270 s); even when it succeeds it often returns only the query and a handful of near-identical structures.
+ - For broad **whole-molecule similarity** (Tanimoto over graph-edit distance), CartBlanche provides a separate **Smallworld** search on the website (`cartblanche22.docking.org` → Similarity → Smallworld, `/similarity/sw`). It is an interactive flow and is not exposed as a simple GET/POST JSON endpoint here — use the web UI for large similarity jobs.
+ - Do not build automated pipelines that assume analog search returns a rich set. Base workflows on exact lookup + local RDKit/cheminformatics expansion instead.
- # ZINC15 drug-like purchasable slice (public URL pattern)
- # Full drug-like: https://zinc15.docking.org/substances/subsets/drug-like.smi.gz
+ ## Key Concepts
- def download_zinc_subset(subset_name, max_lines=1000):
- """Download a ZINC subset SMILES file and return a DataFrame sample."""
- url = f"https://zinc15.docking.org/substances/subsets/{subset_name}.smi.gz"
- r = requests.get(url, stream=True)
- r.raise_for_status()
+ ### ZINC22 IDs
- lines = []
- with gzip.open(r.raw, "rt") as f:
- for i, line in enumerate(f):
- if i >= max_lines:
- break
- lines.append(line.strip().split())
+ ZINC22 introduced short alphanumeric IDs such as `ZINCbq0000001gLA` (the prefix letters encode the sub-database / tranche). Legacy 12-digit IDs like `ZINC000000029632` (from ZINC15/ZINC20) still resolve through `/substance/{id}.json`. Always pass the ID exactly as given.
- df = pd.DataFrame(lines, columns=["smiles", "zinc_id"] + [f"col{i}" for i in range(max(0, len(lines[0])-2))])
- return df[["smiles", "zinc_id"]]
+ ### Asynchronous Search Model
- # Load first 1000 from lead-like subset
- df_sample = download_zinc_subset("lead-like", max_lines=1000)
- print(f"Loaded {len(df_sample)} compounds from lead-like subset")
- print(df_sample.head())
- ```
+ SMILES searches are queued as tasks:
- ## Key Concepts
+ 1. `POST /smiles.json` (multipart form: `smiles`, `dist`, `adist`, `database`) → `{"task": "<uuid>"}`
+ 2. `GET /search/result/{task}` → `{"status": "PROGRESS"}` while running, then `{"status": "SUCCESS", "result": {...}}`
+ 3. On success, `result` is a dict keyed by sub-database (`zinc22`, sometimes `zinc20`) when there are hits, or an empty list when there are none. The helper above normalizes both shapes.
- ### ZINC Tranches
+ ### Tranches
- Compounds are organized into a 2D grid of "tranches" based on MW (rows A–K: <200 to >600 Da) and logP (columns A–J: <-1 to >5). Each tranche can be downloaded as a SMILES or SDF file. This tranching enables targeted downloads of specific property spaces for docking.
+ ZINC organizes molecules into a 2D grid of "tranches" by heavy-atom count / MW (H-codes, e.g. `H11`) and logP (letter bins). The CartBlanche **Tranches** browser (`cartblanche22.docking.org`, "3D"/"2D" tranche pages) lets you select property regions and bulk-download SMILES/SDF for docking. Tranche bulk download is interactive (and large); for programmatic property filtering prefer the RDKit approach in Query 4, or fetch tranche files from `files.docking.org` and filter locally.
- ### Availability Classes
+ ### Result Fields
- - **for-sale**: Purchasable from ≥1 supplier
- - **in-stock**: Available for immediate purchase
- - **wait-ok**: Longer lead time acceptable
- - **on-demand**: Custom synthesis required
+ Each search/substance record exposes: `zinc_id`, `smiles`, `mol_formula`, `rings`, `hetero_atoms`, `db`, `catalogs[]`, and `tranche_details` (`mwt`, `logp`, `heavy_atoms`, `inchi`, `inchikey`). SMILES search rows additionally include `sub_id`, `matched_smiles`, and the `tranche` code.
## Common Workflows
- ### Workflow 1: Build a Focused Docking Library
+ ### Workflow 1: Check Purchasability of a Hit List
- **Goal**: Curate a purchasable, lead-like compound library within specific property ranges, deduplicate, and export for docking.
+ **Goal**: Given SMILES of docking hits, resolve their ZINC22 IDs and report which are purchasable.
```python
- import requests, pandas as pd
-
- BASE = "https://zinc15.docking.org"
+ import pandas as pd
- # Fetch lead-like purchasable compounds with Lipinski compliance
- params = {
- "mwt__gte": 200, "mwt__lte": 500,
- "logp__gte": -1, "logp__lte": 5,
- "hbd__lte": 5, "hba__lte": 10,
- "rotatable_bonds__lte": 10,
- "availability": "for-sale",
- "count": 200,
- }
- r = requests.get(f"{BASE}/substances.json", params=params)
- r.raise_for_status()
- compounds = r.json()
+ hit_smiles = [
+ "CC(=O)Nc1ccc(O)cc1", # acetaminophen
+ "c1ccc(NC(=O)c2ccccc2)cc1", # benzanilide
+ ]
- df = pd.DataFrame(compounds)[["zinc_id", "smiles", "mwt", "logp", "hbd", "hba"]]
- df = df.drop_duplicates(subset=["smiles"])
- print(f"Curated library: {len(df)} unique compounds")
+ rows = []
+ for smi in hit_smiles:
+ matches = smiles_lookup(smi) # exact
+ if not matches:
+ rows.append({"query_smiles": smi, "zinc_id": None, "purchasable": False})
+ continue
+ for m in matches:
+ cats = m.get("catalogs", [])
+ rows.append({
+ "query_smiles": smi,
+ "zinc_id": m["zinc_id"],
+ "mw": m["tranche_details"]["mwt"],
+ "n_catalogs": len(cats),
+ "purchasable": any(c.get("purchase") for c in cats) or bool(cats),
+ })
- # Export as SMILES for docking input
- df[["smiles", "zinc_id"]].to_csv("docking_library.smi", sep=" ", index=False, header=False)
- print("Saved: docking_library.smi")
- print(df.head())
+ df = pd.DataFrame(rows)
+ print(df)
+ df.to_csv("hit_purchasability.csv", index=False)
+ print("Saved: hit_purchasability.csv")
```
- ### Workflow 2: Fragment Library for FBDD
+ ### Workflow 2: Resolve and Property-Filter a Candidate Set for Docking
- **Goal**: Download fragment-like (Rule of Three) compounds for fragment-based drug discovery.
+ **Goal**: Take a list of candidate SMILES (your own enumeration or a tranche file), keep the ones present/purchasable in ZINC22, filter to lead-like space, and export SMILES for docking.
```python
- import requests, pandas as pd
+ import pandas as pd
+ from rdkit import Chem
+ from rdkit.Chem import Descriptors
- BASE = "https://zinc15.docking.org"
+ candidates = [
+ "CC(=O)Nc1ccc(O)cc1",
+ "O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1",
+ "c1ccc(NC(=O)c2ccccc2)cc1",
+ ]
- # Rule of Three: MW ≤ 300, logP ≤ 3, HBD ≤ 3, HBA ≤ 3, RotB ≤ 3
- params = {
- "mwt__lte": 300,
- "logp__lte": 3,
- "hbd__lte": 3,
- "hba__lte": 3,
- "rotatable_bonds__lte": 3,
- "availability": "for-sale",
- "count": 200,
- }
- r = requests.get(f"{BASE}/substances.json", params=params)
- fragments = r.json()
- df = pd.DataFrame(fragments)[["zinc_id", "smiles", "mwt", "logp"]]
+ rows = []
+ for smi in candidates:
+ m = Chem.MolFromSmiles(smi)
+ if m is None:
+ continue
+ mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)
+ if not (200 <= mw <= 500 and -1 <= logp <= 5): # lead-like local filter
+ continue
+ matches = smiles_lookup(smi) # confirm it exists in ZINC22
+ if not matches:
+ continue
+ m0 = matches[0]
+ rows.append({"zinc_id": m0["zinc_id"], "smiles": Chem.MolToSmiles(m),
+ "mw": round(mw, 1), "logp": round(logp, 2),
+ "purchasable": bool(m0.get("catalogs"))})
- print(f"Fragment library: {len(df)} compounds (Rule of Three)")
- df.to_csv("fragment_library.smi", sep=" ", index=False, header=False)
- print("Saved: fragment_library.smi")
- df.describe()
+ df = pd.DataFrame(rows).drop_duplicates(subset=["smiles"])
+ print(f"ZINC22-confirmed lead-like compounds: {len(df)}")
+ df[["smiles", "zinc_id"]].to_csv("docking_library.smi", sep=" ", index=False, header=False)
+ print("Saved: docking_library.smi")
+ print(df)
```
## Key Parameters
- | Parameter | Module | Default | Range / Options | Effect |
- |-----------|--------|---------|-----------------|--------|
- | `mwt__gte` / `mwt__lte` | Search | — | numeric (Da) | Molecular weight lower/upper bound |
- | `logp__gte` / `logp__lte` | Search | — | numeric | logP (lipophilicity) range |
- | `hbd__lte` | Search | — | integer | Max hydrogen bond donors |
- | `hba__lte` | Search | — | integer | Max hydrogen bond acceptors |
- | `rotatable_bonds__lte` | Search | — | integer | Max rotatable bonds |
- | `availability` | Search | all | `"for-sale"`, `"in-stock"`, `"on-demand"` | Purchasability filter |
- | `count` | Search | 10 | `1`–`1000` | Max compounds returned per request |
- | `similarity` | Similarity | — | `0.0`–`1.0` | Tanimoto similarity threshold |
+ | Parameter | Endpoint | Default | Range / Options | Effect |
+ |-----------|----------|---------|-----------------|--------|
+ | (path) | `GET /substance/{zinc_id}.json` | — | ZINC ID | Synchronous substance lookup |
+ | `smiles` | `POST /smiles.json` | — | valid SMILES | Query molecule |
+ | `dist` | `POST /smiles.json` | `0` | `0`–`3` | Topological distance; `0` = exact (reliable), `>=1` = analog search (slow, may time out) |
+ | `adist` | `POST /smiles.json` | `0` | `0`–`3` | Anonymous-graph distance (scaffold hopping); `0` = off |
+ | `database` | `POST /smiles.json` | `zinc22` | `"zinc22"`, `"zinc20"` | Which ZINC build to search |
+ | `task` | `GET /search/result/{task}` | — | UUID from submit | Poll handle for async result |
## Best Practices
- 1. **Use tranches for large docking campaigns**: Downloading entire MW/logP tranches as pre-built SDF files is faster than paginating the API. Use the ZINC tranches page to identify the subset of property space you need.
-
- 2. **Apply reactivity filters**: ZINC marks reactive compounds with "reactivity" flags. Exclude compounds with reactive groups (`reactivity: "clean"` filter) for cell-based assays.
+ 1. **Use ZINC22 (CartBlanche), not ZINC15.** The `zinc15.docking.org` REST API is CAPTCHA-walled and will not work from scripts. All endpoints here target `cartblanche22.docking.org`.
- 3. **Deduplicate by SMILES**: API results may contain duplicates across supplier catalog entries. Canonical SMILES deduplication with RDKit (`Chem.MolToSmiles(Chem.MolFromSmiles(smi))`) before docking.
+ 2. **Lean on exact lookup; don't depend on analog search.** `dist=0` is the reliable primitive. For analogs/similarity, expand locally (RDKit, `rdkit-cheminformatics`) or use the Smallworld web UI — the analog API times out on broad queries.
- 4. **Combine with RDKit filtering**: After downloading, apply additional filters (PAINS, Brenk alerts) using `rdkit-cheminformatics` or `medchem` before investing compute in docking.
+ 3. **Filter properties locally with RDKit.** There is no server-side MW/logP/HBD query in ZINC22. Retrieve candidates, then filter with RDKit — this is also where you apply PAINS/Brenk alerts.
- 5. **Cache SMILES downloads**: ZINC data is updated periodically. Cache downloads with a date-stamped filename and avoid re-downloading within a project.
+ 4. **Poll politely.** SMILES lookups are async; poll `/search/result/{task}` every few seconds, set a sensible timeout, and handle the `PROGRESS` → `SUCCESS` transition (and `TimeoutError`).
- ## Common Recipes
+ 5. **Deduplicate by canonical SMILES.** A structure can appear under multiple ZINC IDs / catalogs. Canonicalize with RDKit (`Chem.MolToSmiles(Chem.MolFromSmiles(smi))`) before docking.
- ### Recipe: Lookup ZINC ID from SMILES
+ 6. **Verify purchasability via `catalogs`.** Treat `catalogs` entries with `purchase: 1` as orderable; record `supplier_code` and `catalog_name` for procurement.
- When to use: Find the ZINC ID for a known compound to check purchasability.
+ 7. **Cache results.** ZINC data updates periodically; cache substance/search JSON with a date-stamped filename and avoid re-querying within a project.
- ```python
- import requests
+ ## Common Recipes
- BASE = "https://zinc15.docking.org"
- smiles = "CC(=O)Nc1ccc(O)cc1" # paracetamol / acetaminophen
+ ### Recipe: Lookup ZINC22 ID and purchasability from a SMILES
- r = requests.get(f"{BASE}/substances.json",
- params={"smiles": smiles, "count": 3})
- for c in r.json():
- print(f"ZINC: {c['zinc_id']} | MW: {c['mwt']:.1f} | In stock: {c.get('availability')}")
+ ```python
+ matches = smiles_lookup("CC(=O)Nc1ccc(O)cc1") # acetaminophen
+ for m in matches:
+ cats = m.get("catalogs", [])
+ print(f"{m['zinc_id']} | MW {m['tranche_details']['mwt']:.1f} | "
+ f"catalogs {len(cats)} | purchasable {bool(cats)}")
```
- ### Recipe: Export SDF for Docking
-
- When to use: Download 3D SDF conformers for a list of ZINC IDs for use in docking software.
+ ### Recipe: Batch-resolve a list of ZINC IDs
```python
- import requests
-
- BASE = "https://zinc15.docking.org"
- zinc_ids = ["ZINC000000029632", "ZINC000001532592"]
-
+ zinc_ids = ["ZINC000000029632", "ZINCbq0000001gLA"]
for zid in zinc_ids:
- r = requests.get(f"{BASE}/substances/{zid}.sdf")
- if r.ok:
- with open(f"{zid}.sdf", "w") as f:
- f.write(r.text)
- print(f"Downloaded {zid}.sdf")
- else:
- print(f"Not available: {zid}")
+ try:
+ c = get_substance(zid)
+ print(f"{zid}: {c['smiles']} MW {c['tranche_details']['mwt']:.1f}")
+ except requests.HTTPError as e:
+ print(f"{zid}: not found ({e.response.status_code})")
```
### Recipe: Property Distribution of a Library
- When to use: Quickly assess the property coverage of a downloaded compound set.
-
```python
import pandas as pd
+ from rdkit import Chem
+ from rdkit.Chem import Descriptors
df = pd.read_csv("docking_library.smi", sep=" ", names=["smiles", "zinc_id"])
+ df["mw"] = df["smiles"].map(lambda s: Descriptors.MolWt(Chem.MolFromSmiles(s)))
+ df["logp"] = df["smiles"].map(lambda s: Descriptors.MolLogP(Chem.MolFromSmiles(s)))
print(f"Library size: {len(df)}")
-
- # If you have the full ZINC metadata:
- # df = pd.DataFrame(compounds)[["mwt", "logp", "hbd", "hba"]]
- # print(df.describe())
- # import matplotlib.pyplot as plt
- # df[["mwt", "logp"]].hist(bins=30, figsize=(10, 4)); plt.show()
+ print(df[["mw", "logp"]].describe())
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
- | `HTTP 404` for compound ID | ZINC ID format incorrect | Use full 12-digit ZINC ID (e.g., `ZINC000000029632`) |
- | Empty results for property search | Filters too restrictive | Relax ranges; check `mwt__gte < mwt__lte` is not inverted |
- | Similarity search returns nothing | SMILES invalid or unusual scaffold | Validate SMILES with RDKit first; try lower similarity threshold |
- | Tranche file download fails | Tranche code wrong | Verify tranche naming at zinc15.docking.org/tranches/home |
- | API returns HTML error page | Server maintenance | Retry after a few minutes; check ZINC status |
- | Slow large downloads | Large compound sets | Download tranche files via FTP/HTTP bulk download instead of API pagination |
+ | Requests to `zinc15.docking.org` redirect to `/captcha/` or return `403` | The legacy ZINC15 REST API is CAPTCHA-walled and unusable from scripts | Switch all calls to `cartblanche22.docking.org` (ZINC22 / CartBlanche) as shown here |
+ | `GET /substances/{id}.json` (plural) returns HTML | Wrong path — that is the SPA catch-all | Use the **singular** `/substance/{id}.json` |
+ | Endpoint returns `200` with `content-type: application/json` but body is HTML | CartBlanche serves the SPA shell for unknown routes (misleading content-type) | Check the body actually parses as JSON; only the documented endpoints return data |
+ | `POST /smiles.json` returns `{"task": ...}` but you expected results | SMILES search is asynchronous | Poll `GET /search/result/{task}` until `status == "SUCCESS"` |
+ | `400 No Valid SMILES, please try again` | SMILES rejected (or wrong request form) | Validate the SMILES with RDKit; submit via multipart form fields `smiles`/`dist`/`adist`/`database` |
+ | `KeyError: 'data'` / `'zinc22'` on a successful task | `result` is a dict when there are hits, but an empty list when there are none | Handle both shapes (see `smiles_lookup` helper) |
+ | Analog search (`dist >= 1`) times out or returns only the exact match | Very large search space on a shared cluster | Use `dist=0` (exact); for similarity use the Smallworld web UI; expand analogs locally with RDKit |
+ | `/substance/random.json` returns `400`; `/substance/random/{n}.json` returns `{"status":"PENDING"}` | The random endpoint is async/non-trivial | Not needed for scripting; pick known ZINC IDs instead |
+ | `HTTP 404` for a compound ID | ID typo or not present in this build | Verify the ZINC ID; try `database="zinc20"` for legacy IDs |
## Related Skills
- - `rdkit-cheminformatics` — Compute additional properties and apply PAINS filters on downloaded ZINC compounds
- - `autodock-vina-docking` — Use downloaded ZINC SMILES/SDF files for molecular docking campaigns
+ - `rdkit-cheminformatics` — Compute properties and apply PAINS/Brenk filters on retrieved ZINC compounds (required for property filtering here)
+ - `autodock-vina-docking` — Use exported ZINC SMILES/SDF files for molecular docking campaigns
- `chembl-database-bioactivity` — Bioactivity data for compounds identified in ZINC virtual screens
- - `medchem` — Apply medicinal chemistry filters (Lipinski, PAINS, NIBR) on ZINC libraries
+ - `drugbank-database-access` — Approved-drug structures and annotations
## References
- - [ZINC15 website](https://zinc15.docking.org/) — Main ZINC15 database and API
- - [ZINC15 REST API reference](https://zinc15.docking.org/api/) — Query parameters and endpoint documentation
- - [ZINC22 update paper](https://doi.org/10.1021/acs.jcim.2c00852) — Irwin et al., J. Chem. Inf. Model. 2022
- - [ZINC tranches download page](https://zinc15.docking.org/tranches/home) — Bulk compound subset downloads by MW/logP
+ - [CartBlanche / ZINC22 web app](https://cartblanche22.docking.org/) — Substance lookup, SMILES search, tranche browser
+ - [ZINC22 paper](https://doi.org/10.1021/acs.jcim.2c01253) — Tingle et al., "ZINC-22, A Free Multi-Billion-Scale Database of Tangible Compounds for Ligand Discovery," J. Chem. Inf. Model. 2023
+ - [docking.org wiki](https://wiki.docking.org/) — CartBlanche usage and ZINC22 download documentation
+ - [files.docking.org](https://files.docking.org/) — Bulk tranche file downloads for ZINC22