Metadata-Version: 2.4
Name: scigantic-chembl
Version: 0.5.0
Summary: Query ChEMBL directly from a public S3 mirror with DuckDB, including a pre-joined potency table, similarity search, and substructure search.
Author: Scigantic
License: MIT-0
Project-URL: Homepage, https://scigantic.com
Project-URL: Repository, https://github.com/Scigantic/scigantic-chembl
Project-URL: Issues, https://github.com/Scigantic/scigantic-chembl/issues
Keywords: chembl,cheminformatics,duckdb,drug-discovery,bioactivity
Classifier: License :: OSI Approved :: MIT No Attribution License (MIT-0)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Chemistry
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: duckdb<2,>=0.10
Requires-Dist: pyarrow<26,>=14
Requires-Dist: numpy<3,>=2.0
Requires-Dist: pandas<4,>=1.5
Provides-Extra: similarity
Requires-Dist: rdkit>=2023.9; extra == "similarity"
Provides-Extra: patents
Requires-Dist: scigantic-surechembl<1,>=0.2; extra == "patents"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pandas-stubs; extra == "dev"
Dynamic: license-file

<h1 align="center">scigantic-chembl</h1>

<p align="center">
    <a href="https://github.com/Scigantic/scigantic-chembl/actions/workflows/ci.yml">
        <img alt="CI" src="https://github.com/Scigantic/scigantic-chembl/actions/workflows/ci.yml/badge.svg" /></a>
    <a href="https://pypi.org/project/scigantic-chembl/">
        <img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-chembl" /></a>
    <a href="https://pypi.org/project/scigantic-chembl/">
        <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-chembl" /></a>
    <a href="https://github.com/Scigantic/scigantic-chembl/blob/main/LICENSE">
        <img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-chembl" /></a>
</p>

Query ChEMBL directly from a public S3 mirror with DuckDB.

```python
import scigantic_chembl as chembl

df = chembl.query("""
    SELECT chembl_id, pref_name
    FROM molecule_dictionary
    WHERE pref_name IS NOT NULL
    LIMIT 5
""")
```

That query runs against `s3://scigantic-chembl` over DuckDB's httpfs extension. Nothing is downloaded first, and there's no local database file sitting on disk afterward.

## Installation

```console
$ pip install scigantic-chembl
```

## Compared to chembl-downloader

[chembl-downloader](https://github.com/cthoyt/chembl-downloader) is the standard way to work with ChEMBL in Python. It covers every release back to chembl_1, and once the SQLite dump is downloaded it works fully offline. It can also do similarity and substructure search, through `chemfp` and an RDKit `SubstructLibrary` it builds locally. This package trades that release range for less setup: a pre-joined potency table, and similarity and substructure search over fingerprints the mirror already precomputes, so there's no local index to build before a query runs. `enable_cache()` closes some of the offline gap, see below, but the mirror here only carries chembl_35 through chembl_37, and an older release is still a job for chembl-downloader.

## Potency data, pre-joined

`activities` needs a five-table join and a few correctness filters before it's usable for structure-activity work. That join is already done, stored as `derived/activities_enriched.parquet`:

```python
df = chembl.activities(target_chembl_id="CHEMBL203")  # EGFR: 18,998 rows, 11,202 compounds
```

The filters already applied are about correctness, not taste: `pchembl_value` present, `standard_relation = '='`, no `data_validity_comment`, not a `potential_duplicate`. `confidence_score` and `target_type` stay as columns rather than filters, since which rows count as usable SAR data is an analysis choice:

```python
df = chembl.activities(min_confidence=8, limit=50_000)
```

`min_confidence` won't do anything combined with a single `target_chembl_id`: ChEMBL curates `confidence_score` per target entry, not per measurement, so every row for one target shares the same confidence class (verified against the live corpus: zero targets have a mixed score). It narrows results when querying across targets, like the example above.

`chembl.query()` still reaches the raw tables directly for anything the join leaves out.

## Similarity search

```python
gefitinib = "COC1=C(C=C2C(=C1)N=CN=C2NC3=CC(=C(C=C3)F)Cl)OCCCN4CCOCC4"
hits = chembl.similar_compounds(gefitinib, top_k=5)
```

```
    chembl_id  tanimoto
    CHEMBL939  1.000000
  CHEMBL14699  0.919355
CHEMBL4165375  0.916667
 CHEMBL299672  0.857143
CHEMBL4448162  0.857143
```

CHEMBL939 is gefitinib itself. Every compound with a comparable potency measurement (1.68M of ChEMBL's 2.9M structures) has a precomputed 2048-bit Morgan fingerprint, kept packed in memory and compared with numpy's `bitwise_count` rather than unpacked bit by bit. The corpus loads once per process, about 18 seconds on a typical home connection and faster from inside AWS; every call after that in the same process is under half a second. chembl-downloader can do similarity search too, through `chemfp`, but its own docs put building that index at tens of minutes.

Similarity search needs rdkit to encode the query molecule, so it's kept as an optional extra:

```console
$ pip install "scigantic-chembl[similarity]"
```

## Substructure search

Similarity search answers "what's like this molecule." This answers a different question: "what actually contains this fragment."

```python
from rdkit import Chem

gefitinib = "COC1=C(C=C2C(=C1)N=CN=C2NC3=CC(=C(C=C3)F)Cl)OCCCN4CCOCC4"
gefitinib_scaffold = Chem.MolToSmarts(Chem.MolFromSmiles(gefitinib))
hits = chembl.substructure_search(gefitinib_scaffold, limit=50)
```

```
    chembl_id                                                        canonical_smiles
CHEMBL4165375   Fc1ccc(Nc2ncnc3cc(OCCCN4CCOCC4)c(OCCCN4CCOCC4)cc23)cc1Cl
CHEMBL1788321   COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OC[C@H](O)CN1CCOCC1
    CHEMBL939   COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OCCCN1CCOCC1
```

(9 matches total, truncated for space here; CHEMBL939 is gefitinib itself, the rest are close analogues and prodrugs.)

RDKit's exact `HasSubstructMatch` is too slow to run against 1.68M compounds per query, so this prescreens first with a precomputed `PatternFingerprint`: a molecule can only contain the query fragment if every bit the query sets is also set in the candidate, a cheap containment test over packed bytes. Only prescreen survivors get the expensive exact check, stopping as soon as `limit` confirmed matches are found.

**How narrow the prescreen is depends on how specific the query fragment is, not on this package.** A small, generic ring system (a bare quinazoline, say) is a weak filter, over a million of the 1.68M compounds pass it, because PatternFingerprint discriminates on structural complexity, and small fragments have little of it. A large, specific fragment like the full example above prescreens to 34 candidates before the exact stage even starts. `max_candidates` (default 20,000) bounds how many prescreen survivors get exact-matched, so an overly generic query can't turn into an unbounded scan; hitting that cap before finding `limit` matches raises a warning and sets `result.attrs["truncated"] = True`, rather than silently returning a partial answer that looks complete.

## Patents, via scigantic-surechembl

```python
chembl.surechembl_ids("CHEMBL25")           # [1353, 29350479], SureChEMBL's ids for this structure
chembl.patents("CHEMBL25", max_results=50)  # [PatentHit(doc_id='CN-105308118-B', title=..., publication_date=..., assignee=...), ...]
```

The patents a compound appears in come from [SureChEMBL](https://www.surechembl.org/) (EMBL-EBI, 31M compounds extracted from 45M patents) through [scigantic-surechembl](https://github.com/Scigantic/scigantic-surechembl), which maps the ChEMBL id to SureChEMBL's ids via UniChem and takes the union of their patent lists. That package also has the patent text, the chemistry extracted from each document, Solr and structure search, and the join back into this mirror (`chembl_matches_for_patent`, every compound in a patent with its ChEMBL record and activity count). Needs the `patents` extra:

```console
$ pip install "scigantic-chembl[patents]"
```

## Working offline

Off by default, since zero setup is the whole point. Turn it on when you want to run the same queries repeatedly without re-fetching from S3, or work with no network at all after the first pull:

```python
import scigantic_chembl as chembl

chembl.enable_cache()
df = chembl.activities(target_chembl_id="CHEMBL203")  # downloads once, then reads from disk
```

`activities()`, `similar_compounds()` and `substructure_search()` each need exactly one derived file, so caching downloads that one file to `~/.cache/scigantic-chembl` (override with `enable_cache(cache_dir=...)` or the `SCIGANTIC_CHEMBL_CACHE` environment variable) and reuses it after that.

`connect()` and `query()` don't participate in this. `connect()` registers ten core tables as views on every call, several over 1 GB, so caching them there would mean any call eagerly downloads everything regardless of what the query actually touches. Cache one table yourself if you want it locally: `chembl.cache_resolve("chembl_37/parquet/molecule_dictionary.parquet")` downloads it and returns the local path, usable directly in `read_parquet(...)`.

## What's mirrored

```python
chembl.releases()
```

| release | raw tables | pre-joined activities | similarity search | substructure search | CYP training set |
|---|---|---|---|---|---|
| chembl_37 | yes | yes | yes | yes | yes |
| chembl_36 | yes | no | no | no | yes |
| chembl_35 | yes | no | no | no | yes |

chembl_36 and chembl_35 are raw-table access only: their `activities` table is missing a column chembl_37's has (`modality`), so calling `activities()`, `similar_compounds()`, or `substructure_search()` on either one raises `ReleaseCapabilityError` up front instead of failing partway through a join with a confusing error.

This table isn't hardcoded. `releases()` reads a small manifest that the mirror's own weekly cron regenerates by probing the bucket directly, so a new ChEMBL release shows up here without waiting on a new version of this package. If the manifest can't be reached, calls fall back to the snapshot shipped with whatever version you have installed and print a warning, rather than failing outright.

## Command line

```console
$ scigantic-chembl info
$ scigantic-chembl query "SELECT count(*) FROM activities" --release chembl_37
```

## License

MIT-0. See [LICENSE](LICENSE). This covers the code in this package only.

## Data license

ChEMBL's underlying data is licensed [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) by EMBL-EBI, separately from this package's own MIT-0 code license and not superseded by it. If you redistribute ChEMBL data (including derived tables you build from queries against this mirror), CC BY-SA 3.0 requires attribution to ChEMBL and requires that the redistributed data itself stay under a compatible share-alike license. See the [ChEMBL data license page](https://chembl.gitbook.io/chembl-interface-documentation/about#data-licensing) for details.
