molfp 0.2.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.
- molfp-0.2.0/LICENSE +21 -0
- molfp-0.2.0/PKG-INFO +216 -0
- molfp-0.2.0/README.md +179 -0
- molfp-0.2.0/molfp/__init__.py +103 -0
- molfp-0.2.0/molfp/_pubchem.py +157 -0
- molfp-0.2.0/molfp/_smarts.py +105 -0
- molfp-0.2.0/molfp/annotate.py +198 -0
- molfp-0.2.0/molfp/bioactivity.py +295 -0
- molfp-0.2.0/molfp/cliffs.py +91 -0
- molfp-0.2.0/molfp/collisions.py +171 -0
- molfp-0.2.0/molfp/data/klekota_roth_smarts.txt +4860 -0
- molfp-0.2.0/molfp/data/pubchem_fingerprint_definitions.txt +931 -0
- molfp-0.2.0/molfp/data/smarts_substructure.txt +983 -0
- molfp-0.2.0/molfp/distance.py +150 -0
- molfp-0.2.0/molfp/explain.py +202 -0
- molfp-0.2.0/molfp/fingerprints.py +277 -0
- molfp-0.2.0/molfp/fusion.py +121 -0
- molfp-0.2.0/molfp/interpret.py +386 -0
- molfp-0.2.0/molfp/isomers.py +103 -0
- molfp-0.2.0/molfp/similarity.py +77 -0
- molfp-0.2.0/molfp/sklearn.py +223 -0
- molfp-0.2.0/molfp/standardize.py +232 -0
- molfp-0.2.0/molfp.egg-info/PKG-INFO +216 -0
- molfp-0.2.0/molfp.egg-info/SOURCES.txt +28 -0
- molfp-0.2.0/molfp.egg-info/dependency_links.txt +1 -0
- molfp-0.2.0/molfp.egg-info/requires.txt +15 -0
- molfp-0.2.0/molfp.egg-info/top_level.txt +1 -0
- molfp-0.2.0/pyproject.toml +70 -0
- molfp-0.2.0/setup.cfg +4 -0
- molfp-0.2.0/tests/test_molfp.py +1379 -0
molfp-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chanin Nantasenamat
|
|
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.
|
molfp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: molfp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Interpretable molecular fingerprints with SHAP-driven chemical explanations
|
|
5
|
+
Author: Chanin Nantasenamat
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dataprofessor/molfp
|
|
8
|
+
Project-URL: Repository, https://github.com/dataprofessor/molfp
|
|
9
|
+
Keywords: cheminformatics,molecular-fingerprints,drug-discovery,QSAR,RDKit,SHAP,interpretability,scikit-learn
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.22
|
|
25
|
+
Requires-Dist: rdkit>=2023.03
|
|
26
|
+
Requires-Dist: scikit-learn>=1.2
|
|
27
|
+
Requires-Dist: scipy>=1.9
|
|
28
|
+
Requires-Dist: joblib>=1.2
|
|
29
|
+
Provides-Extra: shap
|
|
30
|
+
Requires-Dist: shap>=0.42; extra == "shap"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: shap>=0.42; extra == "all"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: shap>=0.42; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# molfp
|
|
39
|
+
|
|
40
|
+
**Interpretable molecular fingerprints with SHAP-driven chemical explanations.**
|
|
41
|
+
|
|
42
|
+
A native Python/RDKit library that computes 13 molecular fingerprint types and provides built-in tools for interpretation, collision detection, activity cliff analysis, bioactivity profiling, and SHAP-based feature explanation — capabilities not found in any other fingerprint library.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install molfp # core (RDKit + scikit-learn)
|
|
48
|
+
pip install molfp[shap] # + SHAP interpretation
|
|
49
|
+
pip install molfp[all] # everything
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import molfp
|
|
56
|
+
|
|
57
|
+
# Compute fingerprints
|
|
58
|
+
fp = molfp.maccs("CCO") # → list of 166 ints
|
|
59
|
+
fp = molfp.ecfp4("c1ccccc1", n_bits=2048) # → list of 2048 ints
|
|
60
|
+
fps = molfp.maccs_batch(["CCO", "c1ccccc1"]) # → numpy (2, 166)
|
|
61
|
+
|
|
62
|
+
# All 13 types
|
|
63
|
+
molfp.available()
|
|
64
|
+
# ['maccs', 'ecfp4', 'pubchem', 'substructure', 'substructure_count',
|
|
65
|
+
# 'klekota_roth', 'klekota_roth_count', 'estate', 'atompairs2d',
|
|
66
|
+
# 'atompairs2d_count', 'rdk', 'rdk_ext', 'rdk_graphonly']
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
### 13 Fingerprint Types
|
|
72
|
+
|
|
73
|
+
| Type | Bits | Function |
|
|
74
|
+
|------|------|----------|
|
|
75
|
+
| MACCS Keys | 166 | `molfp.maccs()` |
|
|
76
|
+
| ECFP4 (Morgan r=2) | 1024 | `molfp.ecfp4()` |
|
|
77
|
+
| PubChem/CACTVS | 881 | `molfp.pubchem()` |
|
|
78
|
+
| SubstructureFP | 307 | `molfp.substructure()` |
|
|
79
|
+
| Substructure Count | 307 | `molfp.substructure_count()` |
|
|
80
|
+
| Klekota-Roth | 4860 | `molfp.klekota_roth()` |
|
|
81
|
+
| Klekota-Roth Count | 4860 | `molfp.klekota_roth_count()` |
|
|
82
|
+
| E-State | 79 | `molfp.estate()` |
|
|
83
|
+
| AtomPairs2D | 780 | `molfp.atompairs2d()` |
|
|
84
|
+
| AtomPairs2D Count | 780 | `molfp.atompairs2d_count()` |
|
|
85
|
+
| RDKit path-based | 1024 | `molfp.rdk()` |
|
|
86
|
+
| RDKit Extended | 1024 | `molfp.rdk_ext()` |
|
|
87
|
+
| RDKit GraphOnly | 1024 | `molfp.rdk_graphonly()` |
|
|
88
|
+
|
|
89
|
+
### Bit-Level Explainability
|
|
90
|
+
|
|
91
|
+
Map each ON bit to the atoms/bonds that triggered it — across all 13 FP types:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
molfp.explain("CC(=O)Oc1ccccc1C(=O)O", "ecfp4")
|
|
95
|
+
# {11: [(1, 1)], 33: [(0, 0)], 64: [(6, 1), (7, 1)], ...}
|
|
96
|
+
# bit 64 → center atoms 6 and 7 at radius 1
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Chemical Annotation
|
|
100
|
+
|
|
101
|
+
Human-readable names for every fingerprint bit:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
molfp.annotate("substructure", 83)
|
|
105
|
+
# {'name': 'Carboxylic_acid', 'smarts': '[CX3...](=[OX1])...', 'category': 'functional_group'}
|
|
106
|
+
|
|
107
|
+
molfp.annotate("pubchem", 9)
|
|
108
|
+
# {'description': 'Element count: C >= 2', 'category': 'element_count'}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### SHAP Interpretation with Chemical Meaning
|
|
112
|
+
|
|
113
|
+
One call: fingerprint → model → SHAP → chemical annotation → per-class SAR:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
result = molfp.interpret(smiles, activities, fp_type="maccs", metric="pchembl")
|
|
117
|
+
|
|
118
|
+
# Per-class drivers with present/absent effect:
|
|
119
|
+
# ACTIVE:
|
|
120
|
+
# ↑ present methyl group (-CH3) SHAP +0.004 (freq 23% vs 19%)
|
|
121
|
+
# ↓ present oxygen atom SHAP -0.003 (freq 13% vs 17%)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hash Collision Detection
|
|
125
|
+
|
|
126
|
+
Detect when different substructures hash to the same bit:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
molfp.detect_collisions("c1ccccc1", "ecfp4", n_bits=256)
|
|
130
|
+
# {63: [2360741695, 3976623167]} → 2 hashes collided at bit 63
|
|
131
|
+
|
|
132
|
+
molfp.collision_rate(smiles_list, "ecfp4", n_bits=1024)
|
|
133
|
+
# {'collision_rate': 0.027, 'collided_bits': 15, ...}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Activity Cliff Detection
|
|
137
|
+
|
|
138
|
+
Find structurally similar molecules with large activity differences:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
cliffs = molfp.activity_cliffs(smiles, activities, "maccs",
|
|
142
|
+
sim_threshold=0.8, activity_threshold=2.0)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Bioactivity Class Profiling
|
|
146
|
+
|
|
147
|
+
Classify molecules, analyze intra/inter-class similarity, find enriched bits:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
labels = molfp.classify(pchembl_values, metric="pchembl")
|
|
151
|
+
result = molfp.class_similarity(smiles, labels, "maccs")
|
|
152
|
+
enriched = molfp.enriched_bits(smiles, labels, "maccs")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Structure Standardization
|
|
156
|
+
|
|
157
|
+
Desalt, neutralize, canonicalize tautomers, deduplicate by InChIKey:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
molfp.standardize_smiles("CC(=O)Oc1ccccc1C(=O)[O-].[Na+]")
|
|
161
|
+
# → "CC(=O)Oc1ccccc1C(=O)O" (desalted + neutralized)
|
|
162
|
+
|
|
163
|
+
result = molfp.preprocess(smiles_list)
|
|
164
|
+
# n_input: 1000 → n_after_standardization: 985 → n_after_deduplication: 923
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Distance Functions
|
|
168
|
+
|
|
169
|
+
Vectorized Tanimoto, Dice, Cosine, Sokal-Michener, Russel-Rao:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
molfp.tanimoto(fp_a, fp_b) # single pair → float
|
|
173
|
+
molfp.pairwise(X, metric="tanimoto") # (N, N_bits) → (N, N) matrix (vectorized)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### scikit-learn Compatible
|
|
177
|
+
|
|
178
|
+
All 13 FP types as sklearn transformers with Pipeline, GridSearchCV, and FeatureUnion support:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from sklearn.pipeline import make_pipeline
|
|
182
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
183
|
+
|
|
184
|
+
pipe = make_pipeline(
|
|
185
|
+
molfp.ECFPTransformer(n_bits=2048, n_jobs=-1),
|
|
186
|
+
RandomForestClassifier()
|
|
187
|
+
)
|
|
188
|
+
pipe.fit(smiles_train, y_train)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Sparse Matrix & Parallelism
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
molfp.klekota_roth_batch(smiles, sparse=True, n_jobs=-1)
|
|
195
|
+
# → scipy.sparse.csr_matrix, 97% memory savings for KR 4860-bit FPs
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Comparison with scikit-fingerprints (skfp)
|
|
199
|
+
|
|
200
|
+
| Feature | molfp | skfp |
|
|
201
|
+
|---------|-------|------|
|
|
202
|
+
| Fingerprint types | 13 | 35 |
|
|
203
|
+
| Bit-level explainability | All 13 FPs | None |
|
|
204
|
+
| Chemical annotation | Built-in | Feature names only |
|
|
205
|
+
| SHAP + chemical meaning | `interpret()` | None |
|
|
206
|
+
| Hash collision detection | `detect_collisions()` | None |
|
|
207
|
+
| Activity cliff detection | `activity_cliffs()` | None |
|
|
208
|
+
| Bioactivity profiling | `classify()`, `enriched_bits()` | None |
|
|
209
|
+
| Consensus fingerprints | `fuse()` | None |
|
|
210
|
+
| Structure standardization | Full pipeline | Basic |
|
|
211
|
+
| sklearn compatible | Yes | Yes |
|
|
212
|
+
| Sparse + parallel | Yes | Yes |
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
molfp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# molfp
|
|
2
|
+
|
|
3
|
+
**Interpretable molecular fingerprints with SHAP-driven chemical explanations.**
|
|
4
|
+
|
|
5
|
+
A native Python/RDKit library that computes 13 molecular fingerprint types and provides built-in tools for interpretation, collision detection, activity cliff analysis, bioactivity profiling, and SHAP-based feature explanation — capabilities not found in any other fingerprint library.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install molfp # core (RDKit + scikit-learn)
|
|
11
|
+
pip install molfp[shap] # + SHAP interpretation
|
|
12
|
+
pip install molfp[all] # everything
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import molfp
|
|
19
|
+
|
|
20
|
+
# Compute fingerprints
|
|
21
|
+
fp = molfp.maccs("CCO") # → list of 166 ints
|
|
22
|
+
fp = molfp.ecfp4("c1ccccc1", n_bits=2048) # → list of 2048 ints
|
|
23
|
+
fps = molfp.maccs_batch(["CCO", "c1ccccc1"]) # → numpy (2, 166)
|
|
24
|
+
|
|
25
|
+
# All 13 types
|
|
26
|
+
molfp.available()
|
|
27
|
+
# ['maccs', 'ecfp4', 'pubchem', 'substructure', 'substructure_count',
|
|
28
|
+
# 'klekota_roth', 'klekota_roth_count', 'estate', 'atompairs2d',
|
|
29
|
+
# 'atompairs2d_count', 'rdk', 'rdk_ext', 'rdk_graphonly']
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 13 Fingerprint Types
|
|
35
|
+
|
|
36
|
+
| Type | Bits | Function |
|
|
37
|
+
|------|------|----------|
|
|
38
|
+
| MACCS Keys | 166 | `molfp.maccs()` |
|
|
39
|
+
| ECFP4 (Morgan r=2) | 1024 | `molfp.ecfp4()` |
|
|
40
|
+
| PubChem/CACTVS | 881 | `molfp.pubchem()` |
|
|
41
|
+
| SubstructureFP | 307 | `molfp.substructure()` |
|
|
42
|
+
| Substructure Count | 307 | `molfp.substructure_count()` |
|
|
43
|
+
| Klekota-Roth | 4860 | `molfp.klekota_roth()` |
|
|
44
|
+
| Klekota-Roth Count | 4860 | `molfp.klekota_roth_count()` |
|
|
45
|
+
| E-State | 79 | `molfp.estate()` |
|
|
46
|
+
| AtomPairs2D | 780 | `molfp.atompairs2d()` |
|
|
47
|
+
| AtomPairs2D Count | 780 | `molfp.atompairs2d_count()` |
|
|
48
|
+
| RDKit path-based | 1024 | `molfp.rdk()` |
|
|
49
|
+
| RDKit Extended | 1024 | `molfp.rdk_ext()` |
|
|
50
|
+
| RDKit GraphOnly | 1024 | `molfp.rdk_graphonly()` |
|
|
51
|
+
|
|
52
|
+
### Bit-Level Explainability
|
|
53
|
+
|
|
54
|
+
Map each ON bit to the atoms/bonds that triggered it — across all 13 FP types:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
molfp.explain("CC(=O)Oc1ccccc1C(=O)O", "ecfp4")
|
|
58
|
+
# {11: [(1, 1)], 33: [(0, 0)], 64: [(6, 1), (7, 1)], ...}
|
|
59
|
+
# bit 64 → center atoms 6 and 7 at radius 1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Chemical Annotation
|
|
63
|
+
|
|
64
|
+
Human-readable names for every fingerprint bit:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
molfp.annotate("substructure", 83)
|
|
68
|
+
# {'name': 'Carboxylic_acid', 'smarts': '[CX3...](=[OX1])...', 'category': 'functional_group'}
|
|
69
|
+
|
|
70
|
+
molfp.annotate("pubchem", 9)
|
|
71
|
+
# {'description': 'Element count: C >= 2', 'category': 'element_count'}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### SHAP Interpretation with Chemical Meaning
|
|
75
|
+
|
|
76
|
+
One call: fingerprint → model → SHAP → chemical annotation → per-class SAR:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
result = molfp.interpret(smiles, activities, fp_type="maccs", metric="pchembl")
|
|
80
|
+
|
|
81
|
+
# Per-class drivers with present/absent effect:
|
|
82
|
+
# ACTIVE:
|
|
83
|
+
# ↑ present methyl group (-CH3) SHAP +0.004 (freq 23% vs 19%)
|
|
84
|
+
# ↓ present oxygen atom SHAP -0.003 (freq 13% vs 17%)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Hash Collision Detection
|
|
88
|
+
|
|
89
|
+
Detect when different substructures hash to the same bit:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
molfp.detect_collisions("c1ccccc1", "ecfp4", n_bits=256)
|
|
93
|
+
# {63: [2360741695, 3976623167]} → 2 hashes collided at bit 63
|
|
94
|
+
|
|
95
|
+
molfp.collision_rate(smiles_list, "ecfp4", n_bits=1024)
|
|
96
|
+
# {'collision_rate': 0.027, 'collided_bits': 15, ...}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Activity Cliff Detection
|
|
100
|
+
|
|
101
|
+
Find structurally similar molecules with large activity differences:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
cliffs = molfp.activity_cliffs(smiles, activities, "maccs",
|
|
105
|
+
sim_threshold=0.8, activity_threshold=2.0)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Bioactivity Class Profiling
|
|
109
|
+
|
|
110
|
+
Classify molecules, analyze intra/inter-class similarity, find enriched bits:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
labels = molfp.classify(pchembl_values, metric="pchembl")
|
|
114
|
+
result = molfp.class_similarity(smiles, labels, "maccs")
|
|
115
|
+
enriched = molfp.enriched_bits(smiles, labels, "maccs")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Structure Standardization
|
|
119
|
+
|
|
120
|
+
Desalt, neutralize, canonicalize tautomers, deduplicate by InChIKey:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
molfp.standardize_smiles("CC(=O)Oc1ccccc1C(=O)[O-].[Na+]")
|
|
124
|
+
# → "CC(=O)Oc1ccccc1C(=O)O" (desalted + neutralized)
|
|
125
|
+
|
|
126
|
+
result = molfp.preprocess(smiles_list)
|
|
127
|
+
# n_input: 1000 → n_after_standardization: 985 → n_after_deduplication: 923
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Distance Functions
|
|
131
|
+
|
|
132
|
+
Vectorized Tanimoto, Dice, Cosine, Sokal-Michener, Russel-Rao:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
molfp.tanimoto(fp_a, fp_b) # single pair → float
|
|
136
|
+
molfp.pairwise(X, metric="tanimoto") # (N, N_bits) → (N, N) matrix (vectorized)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### scikit-learn Compatible
|
|
140
|
+
|
|
141
|
+
All 13 FP types as sklearn transformers with Pipeline, GridSearchCV, and FeatureUnion support:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from sklearn.pipeline import make_pipeline
|
|
145
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
146
|
+
|
|
147
|
+
pipe = make_pipeline(
|
|
148
|
+
molfp.ECFPTransformer(n_bits=2048, n_jobs=-1),
|
|
149
|
+
RandomForestClassifier()
|
|
150
|
+
)
|
|
151
|
+
pipe.fit(smiles_train, y_train)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Sparse Matrix & Parallelism
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
molfp.klekota_roth_batch(smiles, sparse=True, n_jobs=-1)
|
|
158
|
+
# → scipy.sparse.csr_matrix, 97% memory savings for KR 4860-bit FPs
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Comparison with scikit-fingerprints (skfp)
|
|
162
|
+
|
|
163
|
+
| Feature | molfp | skfp |
|
|
164
|
+
|---------|-------|------|
|
|
165
|
+
| Fingerprint types | 13 | 35 |
|
|
166
|
+
| Bit-level explainability | All 13 FPs | None |
|
|
167
|
+
| Chemical annotation | Built-in | Feature names only |
|
|
168
|
+
| SHAP + chemical meaning | `interpret()` | None |
|
|
169
|
+
| Hash collision detection | `detect_collisions()` | None |
|
|
170
|
+
| Activity cliff detection | `activity_cliffs()` | None |
|
|
171
|
+
| Bioactivity profiling | `classify()`, `enriched_bits()` | None |
|
|
172
|
+
| Consensus fingerprints | `fuse()` | None |
|
|
173
|
+
| Structure standardization | Full pipeline | Basic |
|
|
174
|
+
| sklearn compatible | Yes | Yes |
|
|
175
|
+
| Sparse + parallel | Yes | Yes |
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
molfp — Native Python/RDKit molecular fingerprint library.
|
|
3
|
+
|
|
4
|
+
13 fingerprint types + 7 novel analysis modules:
|
|
5
|
+
- explain: bit-level explainability (which atoms set which bits)
|
|
6
|
+
- annotate: chemical names/descriptions for every bit
|
|
7
|
+
- collisions: hash collision detection for hashed FPs
|
|
8
|
+
- similarity: similarity distribution diagnostics
|
|
9
|
+
- cliffs: activity cliff detection
|
|
10
|
+
- fusion: consensus fingerprint construction
|
|
11
|
+
- isomers: structural isomer awareness
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
import molfp
|
|
15
|
+
|
|
16
|
+
# Compute fingerprints
|
|
17
|
+
fp = molfp.maccs("CCO")
|
|
18
|
+
fps = molfp.maccs_batch(["CCO", "c1ccccc1"])
|
|
19
|
+
|
|
20
|
+
# Explain which atoms set which bits
|
|
21
|
+
molfp.explain("CCO", "ecfp4")
|
|
22
|
+
|
|
23
|
+
# Get chemical annotation for a bit
|
|
24
|
+
molfp.annotate("substructure", 1)
|
|
25
|
+
|
|
26
|
+
# Detect hash collisions
|
|
27
|
+
molfp.detect_collisions("c1ccccc1", "ecfp4")
|
|
28
|
+
|
|
29
|
+
# Find activity cliffs
|
|
30
|
+
molfp.activity_cliffs(smiles, activities, "ecfp4")
|
|
31
|
+
|
|
32
|
+
# Build optimal fusion fingerprint
|
|
33
|
+
molfp.fuse(smiles, activities, ["ecfp4", "maccs", "pubchem"])
|
|
34
|
+
|
|
35
|
+
# Check discriminative power
|
|
36
|
+
molfp.discriminative_power(smiles)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__version__ = "0.2.0"
|
|
40
|
+
|
|
41
|
+
# ── Core fingerprint functions ────────────────────────────────────────────────
|
|
42
|
+
from .fingerprints import (
|
|
43
|
+
maccs,
|
|
44
|
+
ecfp4,
|
|
45
|
+
pubchem,
|
|
46
|
+
substructure,
|
|
47
|
+
substructure_count,
|
|
48
|
+
klekota_roth,
|
|
49
|
+
klekota_roth_count,
|
|
50
|
+
estate,
|
|
51
|
+
atompairs2d,
|
|
52
|
+
atompairs2d_count,
|
|
53
|
+
rdk,
|
|
54
|
+
rdk_ext,
|
|
55
|
+
rdk_graphonly,
|
|
56
|
+
maccs_batch,
|
|
57
|
+
ecfp4_batch,
|
|
58
|
+
pubchem_batch,
|
|
59
|
+
substructure_batch,
|
|
60
|
+
substructure_count_batch,
|
|
61
|
+
klekota_roth_batch,
|
|
62
|
+
klekota_roth_count_batch,
|
|
63
|
+
estate_batch,
|
|
64
|
+
atompairs2d_batch,
|
|
65
|
+
atompairs2d_count_batch,
|
|
66
|
+
rdk_batch,
|
|
67
|
+
rdk_ext_batch,
|
|
68
|
+
rdk_graphonly_batch,
|
|
69
|
+
available,
|
|
70
|
+
get,
|
|
71
|
+
compute_batch,
|
|
72
|
+
FINGERPRINTS,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# ── Novel analysis modules ────────────────────────────────────────────────────
|
|
76
|
+
from .explain import explain
|
|
77
|
+
from .annotate import annotate, get_annotations
|
|
78
|
+
from .collisions import detect_collisions, collision_rate, collision_pairs
|
|
79
|
+
from .similarity import similarity_matrix, similarity_profile, compare_fps
|
|
80
|
+
from .cliffs import activity_cliffs, cliff_summary
|
|
81
|
+
from .fusion import fuse, complementarity
|
|
82
|
+
from .isomers import find_identical_fps, discriminative_power, isomer_sensitivity
|
|
83
|
+
from .bioactivity import (
|
|
84
|
+
classify, class_similarity, enriched_bits, depleted_bits, profile as bioactivity_profile,
|
|
85
|
+
)
|
|
86
|
+
from .standardize import (
|
|
87
|
+
standardize_mol, standardize_smiles, get_inchikey,
|
|
88
|
+
desalt, neutralize, canonicalize_tautomer,
|
|
89
|
+
standardize_batch, deduplicate, preprocess,
|
|
90
|
+
)
|
|
91
|
+
from .sklearn import (
|
|
92
|
+
MACCSTransformer, ECFPTransformer, PubChemTransformer,
|
|
93
|
+
SubstructureTransformer, SubstructureCountTransformer,
|
|
94
|
+
KlekotaRothTransformer, KlekotaRothCountTransformer,
|
|
95
|
+
EStateTransformer, AtomPairs2DTransformer, AtomPairs2DCountTransformer,
|
|
96
|
+
RDKTransformer, RDKExtTransformer, RDKGraphOnlyTransformer,
|
|
97
|
+
get_transformer, TRANSFORMERS,
|
|
98
|
+
)
|
|
99
|
+
from .interpret import interpret, interpret_molecule, compare_molecules
|
|
100
|
+
from .distance import (
|
|
101
|
+
tanimoto, dice, cosine, sokal_michener, russel_rao,
|
|
102
|
+
distance, pairwise, pairwise_distance, available_metrics,
|
|
103
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PubChem/CACTVS fingerprint (881 bits) — internal implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rdkit import Chem
|
|
10
|
+
|
|
11
|
+
from . import _smarts
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_ELEMENT_TO_ANUM = {}
|
|
15
|
+
for _z in range(1, 119):
|
|
16
|
+
_sym = Chem.GetPeriodicTable().GetElementSymbol(_z)
|
|
17
|
+
_ELEMENT_TO_ANUM[_sym] = _z
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _classify_rings(mol):
|
|
21
|
+
"""Classify SSSR rings by size and type. Returns {(type, size): count}."""
|
|
22
|
+
ri = mol.GetRingInfo()
|
|
23
|
+
counts = {}
|
|
24
|
+
|
|
25
|
+
for ring in ri.AtomRings():
|
|
26
|
+
size = len(ring)
|
|
27
|
+
atomic_nums = [mol.GetAtomWithIdx(i).GetAtomicNum() for i in ring]
|
|
28
|
+
|
|
29
|
+
all_carbon = all(z == 6 for z in atomic_nums)
|
|
30
|
+
has_nitrogen = any(z == 7 for z in atomic_nums)
|
|
31
|
+
has_hetero = any(z not in (1, 6) for z in atomic_nums)
|
|
32
|
+
|
|
33
|
+
bonds = []
|
|
34
|
+
for i in range(size):
|
|
35
|
+
b = mol.GetBondBetweenAtoms(ring[i], ring[(i + 1) % size])
|
|
36
|
+
if b:
|
|
37
|
+
bonds.append(b)
|
|
38
|
+
|
|
39
|
+
is_aromatic = all(b.GetIsAromatic() for b in bonds)
|
|
40
|
+
is_saturated = all(b.GetBondType() == Chem.BondType.SINGLE for b in bonds)
|
|
41
|
+
is_sat_or_arom = is_saturated or is_aromatic
|
|
42
|
+
is_unsaturated = not is_saturated and not is_aromatic
|
|
43
|
+
|
|
44
|
+
counts[("countAnyRing", size)] = counts.get(("countAnyRing", size), 0) + 1
|
|
45
|
+
|
|
46
|
+
if is_sat_or_arom:
|
|
47
|
+
if all_carbon:
|
|
48
|
+
k = ("countSaturatedOrAromaticCarbonOnlyRing", size)
|
|
49
|
+
counts[k] = counts.get(k, 0) + 1
|
|
50
|
+
if has_nitrogen:
|
|
51
|
+
k = ("countSaturatedOrAromaticNitrogenContainingRing", size)
|
|
52
|
+
counts[k] = counts.get(k, 0) + 1
|
|
53
|
+
if has_hetero:
|
|
54
|
+
k = ("countSaturatedOrAromaticHeteroContainingRing", size)
|
|
55
|
+
counts[k] = counts.get(k, 0) + 1
|
|
56
|
+
|
|
57
|
+
if is_unsaturated:
|
|
58
|
+
if all_carbon:
|
|
59
|
+
k = ("countUnsaturatedCarbonOnlyRing", size)
|
|
60
|
+
counts[k] = counts.get(k, 0) + 1
|
|
61
|
+
if has_nitrogen:
|
|
62
|
+
k = ("countUnsaturatedNitrogenContainingRing", size)
|
|
63
|
+
counts[k] = counts.get(k, 0) + 1
|
|
64
|
+
if has_hetero:
|
|
65
|
+
k = ("countUnsaturatedHeteroContainingRing", size)
|
|
66
|
+
counts[k] = counts.get(k, 0) + 1
|
|
67
|
+
|
|
68
|
+
if is_aromatic:
|
|
69
|
+
counts[("countAromaticRing", "any")] = counts.get(("countAromaticRing", "any"), 0) + 1
|
|
70
|
+
if has_hetero:
|
|
71
|
+
counts[("countHeteroAromaticRing", "any")] = counts.get(("countHeteroAromaticRing", "any"), 0) + 1
|
|
72
|
+
|
|
73
|
+
return counts
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_definitions() -> tuple[list[tuple[int, int, int]], list[tuple[int, str, int | str, int]], list[tuple[int, Chem.rdchem.Mol]]]:
|
|
77
|
+
"""Parse pubchem_fingerprint_definitions.txt.
|
|
78
|
+
|
|
79
|
+
Returns (section1, section2, smarts_bits).
|
|
80
|
+
"""
|
|
81
|
+
path = _smarts._data_path("pubchem_fingerprint_definitions.txt")
|
|
82
|
+
section1, section2, smarts_bits = [], [], []
|
|
83
|
+
current_section = None
|
|
84
|
+
|
|
85
|
+
with open(path, encoding="utf-8") as f:
|
|
86
|
+
for line in f:
|
|
87
|
+
line = line.rstrip()
|
|
88
|
+
if not line or line.startswith("#") or line.startswith("====="):
|
|
89
|
+
continue
|
|
90
|
+
if "SECTION 1" in line:
|
|
91
|
+
current_section = 1; continue
|
|
92
|
+
elif "SECTION 2" in line:
|
|
93
|
+
current_section = 2; continue
|
|
94
|
+
elif any(f"SECTION {i}" in line for i in range(3, 8)):
|
|
95
|
+
current_section = "smarts"; continue
|
|
96
|
+
if line.startswith("Format:") or line.startswith("Ring type") or line.startswith(" "):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
parts = line.split()
|
|
100
|
+
if not parts or not parts[0].isdigit():
|
|
101
|
+
continue
|
|
102
|
+
bit = int(parts[0])
|
|
103
|
+
|
|
104
|
+
if current_section == 1:
|
|
105
|
+
anum = _ELEMENT_TO_ANUM.get(parts[1], 0)
|
|
106
|
+
section1.append((bit, anum, int(parts[3])))
|
|
107
|
+
elif current_section == 2:
|
|
108
|
+
ring_size = parts[2] if parts[2] == "any" else int(parts[2])
|
|
109
|
+
section2.append((bit, parts[1], ring_size, int(parts[4])))
|
|
110
|
+
elif current_section == "smarts" and len(parts) > 1:
|
|
111
|
+
pat = Chem.MolFromSmarts(parts[1])
|
|
112
|
+
if pat is not None:
|
|
113
|
+
smarts_bits.append((bit, pat))
|
|
114
|
+
|
|
115
|
+
return section1, section2, smarts_bits
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PubChemComputer:
|
|
119
|
+
"""Lazy-loaded PubChem FP computer."""
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
self._loaded = False
|
|
123
|
+
self._s1 = self._s2 = self._smarts = None
|
|
124
|
+
|
|
125
|
+
def _ensure_loaded(self):
|
|
126
|
+
if not self._loaded:
|
|
127
|
+
self._s1, self._s2, self._smarts = load_definitions()
|
|
128
|
+
self._loaded = True
|
|
129
|
+
|
|
130
|
+
def compute(self, mol: Chem.rdchem.Mol) -> list[int]:
|
|
131
|
+
"""Compute 881-bit PubChem FP for an RDKit Mol. Returns list of ints."""
|
|
132
|
+
self._ensure_loaded()
|
|
133
|
+
bits = [0] * 881
|
|
134
|
+
|
|
135
|
+
# Section 1: element counts
|
|
136
|
+
elem_counts = {}
|
|
137
|
+
for atom in mol.GetAtoms():
|
|
138
|
+
anum = atom.GetAtomicNum()
|
|
139
|
+
elem_counts[anum] = elem_counts.get(anum, 0) + 1
|
|
140
|
+
elem_counts[1] = sum(a.GetTotalNumHs() for a in mol.GetAtoms())
|
|
141
|
+
|
|
142
|
+
for bit, anum, threshold in self._s1:
|
|
143
|
+
if elem_counts.get(anum, 0) >= threshold:
|
|
144
|
+
bits[bit] = 1
|
|
145
|
+
|
|
146
|
+
# Section 2: ring counts
|
|
147
|
+
ring_counts = _classify_rings(mol)
|
|
148
|
+
for bit, ring_type, ring_size, threshold in self._s2:
|
|
149
|
+
if ring_counts.get((ring_type, ring_size), 0) >= threshold:
|
|
150
|
+
bits[bit] = 1
|
|
151
|
+
|
|
152
|
+
# Sections 3-7: SMARTS
|
|
153
|
+
for bit, pat in self._smarts:
|
|
154
|
+
if mol.HasSubstructMatch(pat):
|
|
155
|
+
bits[bit] = 1
|
|
156
|
+
|
|
157
|
+
return bits
|