fpsketch 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fpsketch-0.1.0/.github/workflows/publish.yml +21 -0
- fpsketch-0.1.0/.gitignore +10 -0
- fpsketch-0.1.0/.pre-commit-config.yaml +33 -0
- fpsketch-0.1.0/LICENSE +21 -0
- fpsketch-0.1.0/PKG-INFO +152 -0
- fpsketch-0.1.0/README.md +131 -0
- fpsketch-0.1.0/examples/similarity_search.py +69 -0
- fpsketch-0.1.0/pyproject.toml +88 -0
- fpsketch-0.1.0/src/fpsketch/__init__.py +19 -0
- fpsketch-0.1.0/src/fpsketch/_splitmix.py +28 -0
- fpsketch-0.1.0/src/fpsketch/mols.py +46 -0
- fpsketch-0.1.0/src/fpsketch/py.typed +0 -0
- fpsketch-0.1.0/src/fpsketch/sketching.py +394 -0
- fpsketch-0.1.0/tests/_reference.py +65 -0
- fpsketch-0.1.0/tests/conftest.py +10 -0
- fpsketch-0.1.0/tests/data/generate_sample.py +39 -0
- fpsketch-0.1.0/tests/data/zinc_sample.smiles +1000 -0
- fpsketch-0.1.0/tests/test_api.py +140 -0
- fpsketch-0.1.0/tests/test_binary_inner_product.py +44 -0
- fpsketch-0.1.0/tests/test_coo.py +80 -0
- fpsketch-0.1.0/tests/test_internals.py +145 -0
- fpsketch-0.1.0/tests/test_real_molecules.py +73 -0
- fpsketch-0.1.0/tests/test_reference_agreement.py +49 -0
- fpsketch-0.1.0/uv.lock +1003 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
contents: read # needed for actions/checkout on a private repo
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0 # required for hatch-vcs to derive the version from tags
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
- run: uv build
|
|
21
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
# Misc pre-commit checks
|
|
3
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
4
|
+
rev: v5.0.0
|
|
5
|
+
hooks:
|
|
6
|
+
- id: check-added-large-files
|
|
7
|
+
- id: check-case-conflict
|
|
8
|
+
- id: check-merge-conflict
|
|
9
|
+
- id: check-toml
|
|
10
|
+
- id: check-yaml
|
|
11
|
+
- id: debug-statements
|
|
12
|
+
- id: destroyed-symlinks
|
|
13
|
+
- id: detect-private-key
|
|
14
|
+
- id: end-of-file-fixer
|
|
15
|
+
- id: name-tests-test
|
|
16
|
+
args: ["--pytest-test-first"]
|
|
17
|
+
exclude: ^tests/(_reference\.py|data/generate_sample\.py)$
|
|
18
|
+
- id: trailing-whitespace
|
|
19
|
+
# Ruff: lint + format + import sorting
|
|
20
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
21
|
+
rev: v0.7.1
|
|
22
|
+
hooks:
|
|
23
|
+
- id: ruff
|
|
24
|
+
args: [--fix]
|
|
25
|
+
- id: ruff-format
|
|
26
|
+
# mypy: static type checking
|
|
27
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
28
|
+
rev: v1.11.2
|
|
29
|
+
hooks:
|
|
30
|
+
- id: mypy
|
|
31
|
+
additional_dependencies: ["numpy>=1.24"]
|
|
32
|
+
args: ["--config-file=pyproject.toml"]
|
|
33
|
+
files: ^src/
|
fpsketch-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the fpsketch contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fpsketch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fpsketch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compress molecular count fingerprints to a fixed low dimension while preserving Tanimoto similarity as a plain dot product.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AustinT/fpsketch
|
|
6
|
+
Project-URL: Repository, https://github.com/AustinT/fpsketch
|
|
7
|
+
Author: Austin Tripp
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cheminformatics,fingerprint,johnson-lindenstrauss,sketching,tanimoto
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
Provides-Extra: chem
|
|
19
|
+
Requires-Dist: rdkit>=2023.9; extra == 'chem'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# fpsketch
|
|
23
|
+
|
|
24
|
+
Compress molecular count fingerprints to a fixed low dimension while
|
|
25
|
+
(approximately) preserving Tanimoto similarity as a plain dot product.
|
|
26
|
+
|
|
27
|
+
Given two count fingerprints `x`, `x'` (e.g. Morgan fingerprints with counts,
|
|
28
|
+
`{feature_id: count}`), the standard chemistry similarity metric is the
|
|
29
|
+
min-max Tanimoto:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
T_MM(x, x') = sum_i min(x_i, x'_i) / sum_i max(x_i, x'_i)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`T_MM` isn't a dot product, so you can't drop count fingerprints straight into
|
|
36
|
+
models (nearest-neighbor search, GPs, kernel methods, ...) that expect a plain
|
|
37
|
+
inner product. fpsketch sketches count fingerprints into a fixed-width dense
|
|
38
|
+
vector `s = encode(x)` such that
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
T_DP(s, s') = s.s' / (||s||^2 + ||s'||^2 - s.s') ≈ T_MM(x, x')
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
i.e. an ordinary dot product on the sketch approximates `T_MM` on the
|
|
45
|
+
original fingerprints.
|
|
46
|
+
|
|
47
|
+
## Why this works
|
|
48
|
+
|
|
49
|
+
A unary encoding turns each count into a set of indicators,
|
|
50
|
+
`psi(x)_{i,k} = 1[x_i > k]` for `k = 0 .. x_i - 1`. Since
|
|
51
|
+
`min(u, v) = sum_k 1[u > k] * 1[v > k]`, this makes `T_DP(psi(x), psi(x'))`
|
|
52
|
+
exactly equal to `T_MM(x, x')` -- no approximation yet, just a reformulation.
|
|
53
|
+
fpsketch then applies a CountSketch (hashing each `(feature_id, level)` pair
|
|
54
|
+
into one of `m` signed buckets) to that unary expansion, which keeps the
|
|
55
|
+
dot product unbiased while collapsing it to a fixed, low dimension.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
pip install fpsketch # encode_sparse only, numpy-only
|
|
61
|
+
pip install fpsketch[chem] # + encode_mols, pulls in rdkit
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from fpsketch import encode_sparse
|
|
68
|
+
|
|
69
|
+
# Sparse count fingerprints you already have.
|
|
70
|
+
fps = [{1: 1, 2: 2, 3: 3}, {4: 4, 5: 5, 6: 6}]
|
|
71
|
+
sketch = encode_sparse(fps, dim=2048, seed=0)
|
|
72
|
+
|
|
73
|
+
# T_DP as a plain dot product / normalized similarity.
|
|
74
|
+
G = sketch @ sketch.T
|
|
75
|
+
sq = (sketch**2).sum(axis=1)
|
|
76
|
+
similarity = G / (sq[:, None] + sq[None, :] - G)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from rdkit import Chem
|
|
81
|
+
from fpsketch import encode_mols
|
|
82
|
+
|
|
83
|
+
mols = [Chem.MolFromSmiles(s) for s in ["CCO"]] # your list of SMILES strings
|
|
84
|
+
sketch = encode_mols(mols, dim=2048, seed=0) # defaults to a Morgan(radius=2) generator
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you already have your counts vectorized as a COO sparse array (molecules x
|
|
88
|
+
features), `encode_coo` skips the per-molecule dict traversal `encode_sparse`
|
|
89
|
+
does internally:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from scipy.sparse import coo_array
|
|
93
|
+
from fpsketch import encode_coo
|
|
94
|
+
|
|
95
|
+
counts = coo_array(...) # shape (n_molecules, n_features)
|
|
96
|
+
sketch = encode_coo(counts, dim=2048, seed=0)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Two sketches are only comparable if built with the same `seed`.
|
|
100
|
+
|
|
101
|
+
## Choosing `dim` and `num_blocks`
|
|
102
|
+
|
|
103
|
+
`dim=2048` is a strong default for typical fingerprint settings. For extra
|
|
104
|
+
safety margin, `dim` at 2-4x the fingerprint's effective (unfolded) dimension
|
|
105
|
+
is a reasonable range to sweep. `num_blocks` (default 4) splits `dim` into
|
|
106
|
+
that many disjoint sub-sketches, each an independent CountSketch; a dot
|
|
107
|
+
product on the concatenated output is equivalent to averaging the
|
|
108
|
+
`num_blocks` per-block dot-product estimates. This trades a small amount of
|
|
109
|
+
raw accuracy for better tail concentration across single-draw sketches, which
|
|
110
|
+
matters when a sketch is computed once and fed straight into a downstream
|
|
111
|
+
model (e.g. a GP) rather than averaged over many random seeds.
|
|
112
|
+
|
|
113
|
+
## The `scale` parameter
|
|
114
|
+
|
|
115
|
+
By default (`scale=True`), the output is divided by `sqrt(num_blocks)` so
|
|
116
|
+
that raw dot products and squared norms directly approximate the true,
|
|
117
|
+
unnormalized dot product / count mass of the original fingerprints -- useful
|
|
118
|
+
if you compare sketches built with different `num_blocks`, or use the sketch
|
|
119
|
+
for anything beyond the `T_DP` ratio above (cosine similarity, nearest
|
|
120
|
+
neighbors on raw dot product, etc). That factor cancels out of the `T_DP`
|
|
121
|
+
ratio itself, so if you only ever compute Tanimoto similarity through that
|
|
122
|
+
ratio, `scale=False` is equivalent and skips one pass over the output array.
|
|
123
|
+
|
|
124
|
+
## Performance note
|
|
125
|
+
|
|
126
|
+
Hashing is vectorized with numpy (a pure-numpy splitmix64 mixer, not
|
|
127
|
+
`hashlib` per element) rather than hashing one `(feature, count-level)` pair
|
|
128
|
+
at a time -- see `src/fpsketch/sketching.py` for details.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
uv sync --extra chem
|
|
134
|
+
uv run pytest
|
|
135
|
+
uv run ruff check .
|
|
136
|
+
uv run ruff format .
|
|
137
|
+
uv run mypy src
|
|
138
|
+
uv run pre-commit install # run the above automatically on each commit
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Releasing
|
|
142
|
+
|
|
143
|
+
Versions are derived from git tags (`hatch-vcs`); there is no version to bump
|
|
144
|
+
by hand. Pushing a tag matching `v*` (e.g. `v0.2.0`) triggers
|
|
145
|
+
`.github/workflows/publish.yml`, which builds and publishes to PyPI via
|
|
146
|
+
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) -- no API
|
|
147
|
+
token needed, but the `pypi` environment must be configured as a trusted
|
|
148
|
+
publisher for this repo in the PyPI project settings first.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT, see [LICENSE](LICENSE).
|
fpsketch-0.1.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# fpsketch
|
|
2
|
+
|
|
3
|
+
Compress molecular count fingerprints to a fixed low dimension while
|
|
4
|
+
(approximately) preserving Tanimoto similarity as a plain dot product.
|
|
5
|
+
|
|
6
|
+
Given two count fingerprints `x`, `x'` (e.g. Morgan fingerprints with counts,
|
|
7
|
+
`{feature_id: count}`), the standard chemistry similarity metric is the
|
|
8
|
+
min-max Tanimoto:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
T_MM(x, x') = sum_i min(x_i, x'_i) / sum_i max(x_i, x'_i)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`T_MM` isn't a dot product, so you can't drop count fingerprints straight into
|
|
15
|
+
models (nearest-neighbor search, GPs, kernel methods, ...) that expect a plain
|
|
16
|
+
inner product. fpsketch sketches count fingerprints into a fixed-width dense
|
|
17
|
+
vector `s = encode(x)` such that
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
T_DP(s, s') = s.s' / (||s||^2 + ||s'||^2 - s.s') ≈ T_MM(x, x')
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
i.e. an ordinary dot product on the sketch approximates `T_MM` on the
|
|
24
|
+
original fingerprints.
|
|
25
|
+
|
|
26
|
+
## Why this works
|
|
27
|
+
|
|
28
|
+
A unary encoding turns each count into a set of indicators,
|
|
29
|
+
`psi(x)_{i,k} = 1[x_i > k]` for `k = 0 .. x_i - 1`. Since
|
|
30
|
+
`min(u, v) = sum_k 1[u > k] * 1[v > k]`, this makes `T_DP(psi(x), psi(x'))`
|
|
31
|
+
exactly equal to `T_MM(x, x')` -- no approximation yet, just a reformulation.
|
|
32
|
+
fpsketch then applies a CountSketch (hashing each `(feature_id, level)` pair
|
|
33
|
+
into one of `m` signed buckets) to that unary expansion, which keeps the
|
|
34
|
+
dot product unbiased while collapsing it to a fixed, low dimension.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pip install fpsketch # encode_sparse only, numpy-only
|
|
40
|
+
pip install fpsketch[chem] # + encode_mols, pulls in rdkit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from fpsketch import encode_sparse
|
|
47
|
+
|
|
48
|
+
# Sparse count fingerprints you already have.
|
|
49
|
+
fps = [{1: 1, 2: 2, 3: 3}, {4: 4, 5: 5, 6: 6}]
|
|
50
|
+
sketch = encode_sparse(fps, dim=2048, seed=0)
|
|
51
|
+
|
|
52
|
+
# T_DP as a plain dot product / normalized similarity.
|
|
53
|
+
G = sketch @ sketch.T
|
|
54
|
+
sq = (sketch**2).sum(axis=1)
|
|
55
|
+
similarity = G / (sq[:, None] + sq[None, :] - G)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from rdkit import Chem
|
|
60
|
+
from fpsketch import encode_mols
|
|
61
|
+
|
|
62
|
+
mols = [Chem.MolFromSmiles(s) for s in ["CCO"]] # your list of SMILES strings
|
|
63
|
+
sketch = encode_mols(mols, dim=2048, seed=0) # defaults to a Morgan(radius=2) generator
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you already have your counts vectorized as a COO sparse array (molecules x
|
|
67
|
+
features), `encode_coo` skips the per-molecule dict traversal `encode_sparse`
|
|
68
|
+
does internally:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from scipy.sparse import coo_array
|
|
72
|
+
from fpsketch import encode_coo
|
|
73
|
+
|
|
74
|
+
counts = coo_array(...) # shape (n_molecules, n_features)
|
|
75
|
+
sketch = encode_coo(counts, dim=2048, seed=0)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Two sketches are only comparable if built with the same `seed`.
|
|
79
|
+
|
|
80
|
+
## Choosing `dim` and `num_blocks`
|
|
81
|
+
|
|
82
|
+
`dim=2048` is a strong default for typical fingerprint settings. For extra
|
|
83
|
+
safety margin, `dim` at 2-4x the fingerprint's effective (unfolded) dimension
|
|
84
|
+
is a reasonable range to sweep. `num_blocks` (default 4) splits `dim` into
|
|
85
|
+
that many disjoint sub-sketches, each an independent CountSketch; a dot
|
|
86
|
+
product on the concatenated output is equivalent to averaging the
|
|
87
|
+
`num_blocks` per-block dot-product estimates. This trades a small amount of
|
|
88
|
+
raw accuracy for better tail concentration across single-draw sketches, which
|
|
89
|
+
matters when a sketch is computed once and fed straight into a downstream
|
|
90
|
+
model (e.g. a GP) rather than averaged over many random seeds.
|
|
91
|
+
|
|
92
|
+
## The `scale` parameter
|
|
93
|
+
|
|
94
|
+
By default (`scale=True`), the output is divided by `sqrt(num_blocks)` so
|
|
95
|
+
that raw dot products and squared norms directly approximate the true,
|
|
96
|
+
unnormalized dot product / count mass of the original fingerprints -- useful
|
|
97
|
+
if you compare sketches built with different `num_blocks`, or use the sketch
|
|
98
|
+
for anything beyond the `T_DP` ratio above (cosine similarity, nearest
|
|
99
|
+
neighbors on raw dot product, etc). That factor cancels out of the `T_DP`
|
|
100
|
+
ratio itself, so if you only ever compute Tanimoto similarity through that
|
|
101
|
+
ratio, `scale=False` is equivalent and skips one pass over the output array.
|
|
102
|
+
|
|
103
|
+
## Performance note
|
|
104
|
+
|
|
105
|
+
Hashing is vectorized with numpy (a pure-numpy splitmix64 mixer, not
|
|
106
|
+
`hashlib` per element) rather than hashing one `(feature, count-level)` pair
|
|
107
|
+
at a time -- see `src/fpsketch/sketching.py` for details.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
uv sync --extra chem
|
|
113
|
+
uv run pytest
|
|
114
|
+
uv run ruff check .
|
|
115
|
+
uv run ruff format .
|
|
116
|
+
uv run mypy src
|
|
117
|
+
uv run pre-commit install # run the above automatically on each commit
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Releasing
|
|
121
|
+
|
|
122
|
+
Versions are derived from git tags (`hatch-vcs`); there is no version to bump
|
|
123
|
+
by hand. Pushing a tag matching `v*` (e.g. `v0.2.0`) triggers
|
|
124
|
+
`.github/workflows/publish.yml`, which builds and publishes to PyPI via
|
|
125
|
+
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) -- no API
|
|
126
|
+
token needed, but the `pypi` environment must be configured as a trusted
|
|
127
|
+
publisher for this repo in the PyPI project settings first.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Nearest-neighbor search over a handful of molecules using fpsketch, compared
|
|
2
|
+
against RDKit's native Tanimoto similarity.
|
|
3
|
+
|
|
4
|
+
Run with:
|
|
5
|
+
uv run --extra chem python examples/similarity_search.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from rdkit import Chem, DataStructs
|
|
12
|
+
from rdkit.Chem import rdFingerprintGenerator
|
|
13
|
+
|
|
14
|
+
from fpsketch import encode_mols
|
|
15
|
+
|
|
16
|
+
SMILES = [
|
|
17
|
+
"CC(=O)Oc1ccccc1C(=O)O", # aspirin
|
|
18
|
+
"CC(=O)Nc1ccc(O)cc1", # paracetamol
|
|
19
|
+
"CC(C)Cc1ccc(cc1)C(C)C(=O)O", # ibuprofen
|
|
20
|
+
"c1ccc2c(c1)ccc1ccccc12", # anthracene
|
|
21
|
+
"c1ccc2ccccc2c1", # naphthalene
|
|
22
|
+
"CCO", # ethanol
|
|
23
|
+
"CCCCO", # butanol
|
|
24
|
+
"OCC(O)CO", # glycerol
|
|
25
|
+
"c1ccccc1", # benzene
|
|
26
|
+
"Cc1ccccc1", # toluene
|
|
27
|
+
"c1ccc(cc1)c1ccccc1", # biphenyl
|
|
28
|
+
"CC(C)(C)c1ccccc1", # tert-butylbenzene
|
|
29
|
+
"CN1CCC[C@H]1c1cccnc1", # nicotine
|
|
30
|
+
"Cn1cnc2c1c(=O)n(C)c(=O)n2C", # caffeine
|
|
31
|
+
"OC(=O)c1ccccc1O", # salicylic acid
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
QUERY_IDX = 0 # aspirin
|
|
35
|
+
TOP_K = 5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def t_dp(X: np.ndarray) -> np.ndarray:
|
|
39
|
+
G = X @ X.T
|
|
40
|
+
sq = np.einsum("ij,ij->i", X, X)
|
|
41
|
+
return G / np.maximum(sq[:, None] + sq[None, :] - G, 1e-12)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
mols = [Chem.MolFromSmiles(s) for s in SMILES]
|
|
46
|
+
|
|
47
|
+
sketch = encode_mols(mols, dim=2048, seed=0)
|
|
48
|
+
k_hat = t_dp(sketch)
|
|
49
|
+
|
|
50
|
+
generator = rdFingerprintGenerator.GetMorganGenerator(radius=2)
|
|
51
|
+
fps = [generator.GetSparseCountFingerprint(m) for m in mols]
|
|
52
|
+
k_true = np.array([DataStructs.BulkTanimotoSimilarity(fp, fps) for fp in fps])
|
|
53
|
+
|
|
54
|
+
query_sims_hat = k_hat[QUERY_IDX]
|
|
55
|
+
query_sims_true = k_true[QUERY_IDX]
|
|
56
|
+
|
|
57
|
+
ranked = sorted(
|
|
58
|
+
(i for i in range(len(mols)) if i != QUERY_IDX),
|
|
59
|
+
key=lambda i: -query_sims_hat[i],
|
|
60
|
+
)[:TOP_K]
|
|
61
|
+
|
|
62
|
+
print(f"query: {SMILES[QUERY_IDX]}")
|
|
63
|
+
print(f"{'SMILES':<40} {'fpsketch T_DP':>15} {'RDKit T_MM':>12}")
|
|
64
|
+
for i in ranked:
|
|
65
|
+
print(f"{SMILES[i]:<40} {query_sims_hat[i]:>15.4f} {query_sims_true[i]:>12.4f}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fpsketch"
|
|
3
|
+
authors = [
|
|
4
|
+
{name = "Austin Tripp"},
|
|
5
|
+
]
|
|
6
|
+
description = "Compress molecular count fingerprints to a fixed low dimension while preserving Tanimoto similarity as a plain dot product."
|
|
7
|
+
keywords = ["cheminformatics", "fingerprint", "sketching", "tanimoto", "johnson-lindenstrauss"]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
dynamic = ["version"] # version is set from git tags, see [tool.hatch.version]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"numpy>=1.24",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/AustinT/fpsketch"
|
|
25
|
+
Repository = "https://github.com/AustinT/fpsketch"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
chem = ["rdkit>=2023.9"]
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0",
|
|
33
|
+
"rdkit>=2023.9",
|
|
34
|
+
"ruff>=0.6",
|
|
35
|
+
"mypy>=1.11",
|
|
36
|
+
"pre-commit>=3.8",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/fpsketch"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
source = "vcs"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version.raw-options]
|
|
50
|
+
local_scheme = "no-local-version" # PyPI rejects local version segments (e.g. +dirty)
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py310"
|
|
58
|
+
src = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.isort]
|
|
64
|
+
known-first-party = ["fpsketch"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
# Pinned to 3.12 (not requires-python's 3.10 floor): numpy's bundled stubs use
|
|
68
|
+
# unconditional PEP 695 `type` aliases that mypy can only parse when targeting
|
|
69
|
+
# >=3.12, regardless of what our own source uses. Our code doesn't use any
|
|
70
|
+
# 3.11+-only syntax, so this only affects stub parsing, not what's accepted.
|
|
71
|
+
python_version = "3.12"
|
|
72
|
+
files = ["src"]
|
|
73
|
+
warn_unused_configs = true
|
|
74
|
+
warn_redundant_casts = true
|
|
75
|
+
warn_unused_ignores = true
|
|
76
|
+
|
|
77
|
+
[[tool.mypy.overrides]]
|
|
78
|
+
module = "rdkit.*"
|
|
79
|
+
ignore_missing_imports = true
|
|
80
|
+
|
|
81
|
+
# numpy's operator-overload stubs collapse chained uint64 array arithmetic
|
|
82
|
+
# (xor/shift/mul with scalars) to narrower-than-actual types in these two
|
|
83
|
+
# modules' low-level bit hashing; the errors are stub-inference artifacts, not
|
|
84
|
+
# real bugs. Silencing only the specific noisy codes here keeps real errors
|
|
85
|
+
# (typos, wrong arg types, etc.) caught in the rest of the package.
|
|
86
|
+
[[tool.mypy.overrides]]
|
|
87
|
+
module = ["fpsketch.sketching", "fpsketch._splitmix"]
|
|
88
|
+
disable_error_code = ["index", "operator", "attr-defined", "return-value"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""fpsketch: compress molecular count fingerprints to a fixed low dimension
|
|
2
|
+
while preserving Tanimoto (T_MM) similarity as a plain dot product.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from .mols import encode_mols
|
|
8
|
+
from .sketching import encode_coo, encode_sparse
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"encode_sparse",
|
|
12
|
+
"encode_coo",
|
|
13
|
+
"encode_mols",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__ = version("fpsketch")
|
|
18
|
+
except PackageNotFoundError: # pragma: no cover - not installed, e.g. running from source
|
|
19
|
+
__version__ = "unknown"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""A numpy implementation of splitmix"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
U64 = np.uint64
|
|
6
|
+
|
|
7
|
+
# Fixed constants for splitmix
|
|
8
|
+
GAMMA = U64(0x9E3779B97F4A7C15)
|
|
9
|
+
MIX_A = U64(0xBF58476D1CE4E5B9)
|
|
10
|
+
MIX_B = U64(0x94D049BB133111EB)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _splitmix64_hash(x: np.ndarray) -> np.ndarray:
|
|
14
|
+
"""
|
|
15
|
+
Splitmix64 function- an 64 bit rng/hash which is simple to implement,
|
|
16
|
+
invertible, and is considered fairly pseudo-random.
|
|
17
|
+
|
|
18
|
+
It is not strong enough for cryptography but doesn't need to be:
|
|
19
|
+
this is just used to encode fingerprints.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Ignore int overflows- this is an intended part of the algorithm
|
|
23
|
+
with np.errstate(over="ignore"):
|
|
24
|
+
z = x + GAMMA
|
|
25
|
+
z = (z ^ (z >> U64(30))) * MIX_A
|
|
26
|
+
z = (z ^ (z >> U64(27))) * MIX_B
|
|
27
|
+
z = z ^ (z >> U64(31))
|
|
28
|
+
return z
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""RDKit molecule entry point. Requires the ``chem`` extra (``pip install fpsketch[chem]``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .sketching import encode_sparse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode_mols(
|
|
14
|
+
mols: Sequence[Any],
|
|
15
|
+
dim: int = 2048,
|
|
16
|
+
generator: Any | None = None,
|
|
17
|
+
num_blocks: int = 4,
|
|
18
|
+
seed: int = 0,
|
|
19
|
+
scale: bool = True,
|
|
20
|
+
) -> np.ndarray:
|
|
21
|
+
"""Sketch RDKit molecules into a dense ``(len(mols), dim)`` array.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
mols: RDKit ``ROMol`` objects.
|
|
25
|
+
dim: output sketch width.
|
|
26
|
+
generator: an ``rdFingerprintGenerator`` generator (Morgan, RDKitFP,
|
|
27
|
+
AtomPair, TopologicalTorsion, ...). Defaults to
|
|
28
|
+
``GetMorganGenerator(radius=2)`` if not given. Any generator
|
|
29
|
+
exposing ``GetSparseCountFingerprint`` works.
|
|
30
|
+
num_blocks: see ``encode_sparse``.
|
|
31
|
+
seed: hash seed; two sketches are only dot-product-comparable if built
|
|
32
|
+
with the same seed.
|
|
33
|
+
scale: see ``encode_sparse``.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
from rdkit.Chem import rdFingerprintGenerator
|
|
37
|
+
except ImportError as e:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"encode_mols requires rdkit. Install it with `pip install fpsketch[chem]`."
|
|
40
|
+
) from e
|
|
41
|
+
|
|
42
|
+
if generator is None:
|
|
43
|
+
generator = rdFingerprintGenerator.GetMorganGenerator(radius=2)
|
|
44
|
+
|
|
45
|
+
fps = [generator.GetSparseCountFingerprint(mol).GetNonzeroElements() for mol in mols]
|
|
46
|
+
return encode_sparse(fps, dim=dim, num_blocks=num_blocks, seed=seed, scale=scale)
|
|
File without changes
|