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.
@@ -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,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ .venv/
8
+ dist/
9
+ build/
10
+ .uv/
@@ -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.
@@ -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).
@@ -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