molfp 0.2.0__py3-none-any.whl
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/__init__.py +103 -0
- molfp/_pubchem.py +157 -0
- molfp/_smarts.py +105 -0
- molfp/annotate.py +198 -0
- molfp/bioactivity.py +295 -0
- molfp/cliffs.py +91 -0
- molfp/collisions.py +171 -0
- molfp/data/klekota_roth_smarts.txt +4860 -0
- molfp/data/pubchem_fingerprint_definitions.txt +931 -0
- molfp/data/smarts_substructure.txt +983 -0
- molfp/distance.py +150 -0
- molfp/explain.py +202 -0
- molfp/fingerprints.py +277 -0
- molfp/fusion.py +121 -0
- molfp/interpret.py +386 -0
- molfp/isomers.py +103 -0
- molfp/similarity.py +77 -0
- molfp/sklearn.py +223 -0
- molfp/standardize.py +232 -0
- molfp-0.2.0.dist-info/METADATA +216 -0
- molfp-0.2.0.dist-info/RECORD +24 -0
- molfp-0.2.0.dist-info/WHEEL +5 -0
- molfp-0.2.0.dist-info/licenses/LICENSE +21 -0
- molfp-0.2.0.dist-info/top_level.txt +1 -0
molfp/__init__.py
ADDED
|
@@ -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
|
+
)
|
molfp/_pubchem.py
ADDED
|
@@ -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
|
molfp/_smarts.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SMARTS pattern loading, compilation, and matching utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib.resources
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _data_path(filename):
|
|
14
|
+
"""Resolve path to a data file in molfp/data/."""
|
|
15
|
+
return os.path.join(os.path.dirname(__file__), "data", filename)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_smarts_file(filename: str) -> list[tuple[str, str]]:
|
|
19
|
+
"""Load SMARTS patterns from a data file. Returns list of (name, smarts) tuples."""
|
|
20
|
+
path = _data_path(filename)
|
|
21
|
+
patterns = []
|
|
22
|
+
with open(path, encoding="utf-8") as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if not line or line.startswith("#"):
|
|
26
|
+
continue
|
|
27
|
+
if ":" in line and not line.startswith("["):
|
|
28
|
+
name, smarts = line.split(":", 1)
|
|
29
|
+
patterns.append((name.strip(), smarts.strip()))
|
|
30
|
+
else:
|
|
31
|
+
patterns.append((f"bit_{len(patterns)}", line))
|
|
32
|
+
return patterns
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_plain_smarts(filename: str) -> list[tuple[str, str]]:
|
|
36
|
+
"""Load a file where each non-empty, non-comment line is a SMARTS string."""
|
|
37
|
+
path = _data_path(filename)
|
|
38
|
+
patterns = []
|
|
39
|
+
with open(path, encoding="utf-8") as f:
|
|
40
|
+
for i, line in enumerate(f):
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if line and not line.startswith("#"):
|
|
43
|
+
patterns.append((f"bit_{i}", line))
|
|
44
|
+
return patterns
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compile_smarts(patterns: list[tuple[str, str]]) -> tuple[list[tuple[str, Chem.rdchem.Mol | _SaltMatcher]], list[tuple[str, str]]]:
|
|
48
|
+
"""Compile (name, smarts) list. Returns (compiled, failed).
|
|
49
|
+
|
|
50
|
+
Handles special patterns like the Salt disconnected-charge pattern.
|
|
51
|
+
"""
|
|
52
|
+
compiled = []
|
|
53
|
+
failed = []
|
|
54
|
+
for name, smarts in patterns:
|
|
55
|
+
pat = Chem.MolFromSmarts(smarts)
|
|
56
|
+
if pat is not None:
|
|
57
|
+
compiled.append((name, pat))
|
|
58
|
+
elif _is_salt_pattern(smarts):
|
|
59
|
+
compiled.append((name, _SaltMatcher()))
|
|
60
|
+
else:
|
|
61
|
+
failed.append((name, smarts))
|
|
62
|
+
return compiled, failed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_salt_pattern(smarts):
|
|
66
|
+
return "[-1,-2,-3,-4,-5,-6,-7" in smarts and "+1,+2,+3,+4,+5,+6,+7" in smarts
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _SaltMatcher:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _check_salt(mol):
|
|
74
|
+
has_neg = has_pos = False
|
|
75
|
+
for atom in mol.GetAtoms():
|
|
76
|
+
fc = atom.GetFormalCharge()
|
|
77
|
+
if fc < 0:
|
|
78
|
+
has_neg = True
|
|
79
|
+
if fc > 0:
|
|
80
|
+
has_pos = True
|
|
81
|
+
if has_neg and has_pos:
|
|
82
|
+
return True
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def match_binary(mol: Chem.rdchem.Mol, compiled_patterns: list[tuple[str, Chem.rdchem.Mol | _SaltMatcher]]) -> list[int]:
|
|
87
|
+
"""Match patterns, return list of 0/1 ints."""
|
|
88
|
+
result = []
|
|
89
|
+
for _, pat in compiled_patterns:
|
|
90
|
+
if isinstance(pat, _SaltMatcher):
|
|
91
|
+
result.append(1 if _check_salt(mol) else 0)
|
|
92
|
+
else:
|
|
93
|
+
result.append(1 if mol.HasSubstructMatch(pat) else 0)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def match_count(mol: Chem.rdchem.Mol, compiled_patterns: list[tuple[str, Chem.rdchem.Mol | _SaltMatcher]]) -> list[int]:
|
|
98
|
+
"""Count pattern matches, return list of ints."""
|
|
99
|
+
result = []
|
|
100
|
+
for _, pat in compiled_patterns:
|
|
101
|
+
if isinstance(pat, _SaltMatcher):
|
|
102
|
+
result.append(1 if _check_salt(mol) else 0)
|
|
103
|
+
else:
|
|
104
|
+
result.append(len(mol.GetSubstructMatches(pat)))
|
|
105
|
+
return result
|
molfp/annotate.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chemical annotation: human-readable names and descriptions for fingerprint bits.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rdkit import Chem
|
|
10
|
+
from rdkit.Chem import MACCSkeys
|
|
11
|
+
|
|
12
|
+
from . import _smarts
|
|
13
|
+
from .fingerprints import _get_substructure_patterns, _get_kr_patterns, _get_pubchem_computer
|
|
14
|
+
|
|
15
|
+
_annotation_cache = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def annotate(fp_type: str, bit_idx: int) -> dict[str, Any]:
|
|
19
|
+
"""Get human-readable annotation for a specific fingerprint bit.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
fp_type: fingerprint name (e.g. "substructure", "pubchem", "maccs")
|
|
23
|
+
bit_idx: bit index (0-based)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
dict with keys: name, smarts (if applicable), description, category
|
|
27
|
+
"""
|
|
28
|
+
annotations = get_annotations(fp_type)
|
|
29
|
+
if bit_idx not in annotations:
|
|
30
|
+
return {"name": f"bit_{bit_idx}", "description": "Unknown bit", "category": "unknown"}
|
|
31
|
+
return annotations[bit_idx]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_annotations(fp_type: str) -> dict[int, dict[str, Any]]:
|
|
35
|
+
"""Get all annotations for a fingerprint type. Returns {bit_idx: annotation_dict}."""
|
|
36
|
+
if fp_type in _annotation_cache:
|
|
37
|
+
return _annotation_cache[fp_type]
|
|
38
|
+
|
|
39
|
+
dispatch = {
|
|
40
|
+
"maccs": _annotate_maccs,
|
|
41
|
+
"substructure": _annotate_substructure,
|
|
42
|
+
"substructure_count": _annotate_substructure,
|
|
43
|
+
"klekota_roth": _annotate_kr,
|
|
44
|
+
"klekota_roth_count": _annotate_kr,
|
|
45
|
+
"pubchem": _annotate_pubchem,
|
|
46
|
+
"estate": _annotate_estate,
|
|
47
|
+
"ecfp4": _annotate_hashed,
|
|
48
|
+
"atompairs2d": _annotate_hashed,
|
|
49
|
+
"atompairs2d_count": _annotate_hashed,
|
|
50
|
+
"rdk": _annotate_hashed,
|
|
51
|
+
"rdk_ext": _annotate_hashed,
|
|
52
|
+
"rdk_graphonly": _annotate_hashed,
|
|
53
|
+
}
|
|
54
|
+
if fp_type not in dispatch:
|
|
55
|
+
raise KeyError(f"Unknown fp_type: {fp_type}")
|
|
56
|
+
|
|
57
|
+
result = dispatch[fp_type](fp_type)
|
|
58
|
+
_annotation_cache[fp_type] = result
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _annotate_maccs(fp_type):
|
|
63
|
+
annotations = {}
|
|
64
|
+
patts = MACCSkeys.smartsPatts
|
|
65
|
+
for bit_idx in range(166):
|
|
66
|
+
rdkit_bit = bit_idx + 1
|
|
67
|
+
if rdkit_bit in patts:
|
|
68
|
+
entry = patts[rdkit_bit]
|
|
69
|
+
sma = entry[0] if len(entry) > 0 else ""
|
|
70
|
+
desc = entry[1] if len(entry) > 1 else ""
|
|
71
|
+
if not isinstance(desc, str):
|
|
72
|
+
desc = f"MACCS key {rdkit_bit}"
|
|
73
|
+
annotations[bit_idx] = {
|
|
74
|
+
"name": f"MACCS_{rdkit_bit}",
|
|
75
|
+
"smarts": sma,
|
|
76
|
+
"description": desc if desc else f"MACCS key {rdkit_bit}",
|
|
77
|
+
"category": "substructure",
|
|
78
|
+
}
|
|
79
|
+
else:
|
|
80
|
+
annotations[bit_idx] = {
|
|
81
|
+
"name": f"MACCS_{rdkit_bit}",
|
|
82
|
+
"description": f"MACCS key {rdkit_bit} (no public definition)",
|
|
83
|
+
"category": "substructure",
|
|
84
|
+
}
|
|
85
|
+
return annotations
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _annotate_substructure(fp_type):
|
|
89
|
+
patterns = _get_substructure_patterns()
|
|
90
|
+
annotations = {}
|
|
91
|
+
for i, (name, pat) in enumerate(patterns):
|
|
92
|
+
smarts = ""
|
|
93
|
+
if isinstance(pat, _smarts._SaltMatcher):
|
|
94
|
+
smarts = "([-charge]).([+charge])"
|
|
95
|
+
else:
|
|
96
|
+
smarts = Chem.MolToSmarts(pat) if pat else ""
|
|
97
|
+
annotations[i] = {
|
|
98
|
+
"name": name,
|
|
99
|
+
"smarts": smarts,
|
|
100
|
+
"description": f"Inte:Ligand functional group: {name}",
|
|
101
|
+
"category": "functional_group",
|
|
102
|
+
}
|
|
103
|
+
return annotations
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _annotate_kr(fp_type):
|
|
107
|
+
raw = _smarts.load_plain_smarts("klekota_roth_smarts.txt")
|
|
108
|
+
annotations = {}
|
|
109
|
+
for i, (name, smarts_str) in enumerate(raw):
|
|
110
|
+
annotations[i] = {
|
|
111
|
+
"name": f"KRFP{i + 1}",
|
|
112
|
+
"smarts": smarts_str,
|
|
113
|
+
"description": f"Klekota-Roth substructure {i + 1}",
|
|
114
|
+
"category": "bioactivity_substructure",
|
|
115
|
+
}
|
|
116
|
+
return annotations
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _annotate_pubchem(fp_type):
|
|
120
|
+
computer = _get_pubchem_computer()
|
|
121
|
+
computer._ensure_loaded()
|
|
122
|
+
annotations = {}
|
|
123
|
+
|
|
124
|
+
for bit, anum, threshold in computer._s1:
|
|
125
|
+
sym = Chem.GetPeriodicTable().GetElementSymbol(anum) if anum > 0 else "?"
|
|
126
|
+
annotations[bit] = {
|
|
127
|
+
"name": f"PubchemFP{bit}",
|
|
128
|
+
"description": f"Element count: {sym} >= {threshold}",
|
|
129
|
+
"category": "element_count",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for bit, ring_type, ring_size, threshold in computer._s2:
|
|
133
|
+
annotations[bit] = {
|
|
134
|
+
"name": f"PubchemFP{bit}",
|
|
135
|
+
"description": f"Ring: {ring_type} size={ring_size} >= {threshold}",
|
|
136
|
+
"category": "ring_count",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for bit, pat in computer._smarts:
|
|
140
|
+
smarts = Chem.MolToSmarts(pat) if pat else ""
|
|
141
|
+
section = "3-7"
|
|
142
|
+
if 263 <= bit <= 326:
|
|
143
|
+
section = "3 (atom pairs)"
|
|
144
|
+
elif 327 <= bit <= 415:
|
|
145
|
+
section = "4 (nearest neighbors)"
|
|
146
|
+
elif 416 <= bit <= 459:
|
|
147
|
+
section = "5 (detailed neighbors)"
|
|
148
|
+
elif 460 <= bit <= 712:
|
|
149
|
+
section = "6 (bond-specific)"
|
|
150
|
+
elif 713 <= bit <= 880:
|
|
151
|
+
section = "7 (ring substitution)"
|
|
152
|
+
annotations[bit] = {
|
|
153
|
+
"name": f"PubchemFP{bit}",
|
|
154
|
+
"smarts": smarts,
|
|
155
|
+
"description": f"Section {section} substructure",
|
|
156
|
+
"category": "smarts",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return annotations
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _annotate_estate(fp_type):
|
|
163
|
+
from rdkit.Chem.EState import AtomTypes as AT
|
|
164
|
+
AT.BuildPatts()
|
|
165
|
+
patterns = AT.esPatterns
|
|
166
|
+
|
|
167
|
+
atom_type_names = [
|
|
168
|
+
"sLi","ssBe","ssssBe","ssBH","sssB","ssssB",
|
|
169
|
+
"sCH3","dCH2","ssCH2","tCH","dsCH","aaCH","sssCH","ddC","tsC",
|
|
170
|
+
"dssC","aasC","aaaC","ssssC","sNH3","sNH2","ssNH2","dNH","ssNH",
|
|
171
|
+
"aaNH","tN","sssNH","dsN","aaN","sssN","ddsN","aasN","ssssN",
|
|
172
|
+
"sOH","dO","ssO","aaO","sF","sSiH3","ssSiH2","sssSiH","ssssSi",
|
|
173
|
+
"sPH2","ssPH","sssP","dsssP","sssssP","sSH","dS","ssS","aaS",
|
|
174
|
+
"dssS","ddssS","ssssssS","sCl","sGeH3","ssGeH2","sssGeH","ssssGe",
|
|
175
|
+
"sAsH2","ssAsH","sssAs","sssdAs","sssssAs","sSeH","dSe","ssSe",
|
|
176
|
+
"aaSe","dssSe","ddssSe","sBr","sSnH3","ssSnH2","sssSnH","ssssSn",
|
|
177
|
+
"sI","sPbH3","ssPbH2","sssPbH","ssssPb",
|
|
178
|
+
]
|
|
179
|
+
annotations = {}
|
|
180
|
+
for i in range(79):
|
|
181
|
+
name = atom_type_names[i] if i < len(atom_type_names) else f"EState_{i}"
|
|
182
|
+
smarts = ""
|
|
183
|
+
if patterns and i < len(patterns):
|
|
184
|
+
_, pat = patterns[i]
|
|
185
|
+
if pat is not None:
|
|
186
|
+
smarts = Chem.MolToSmarts(pat)
|
|
187
|
+
annotations[i] = {
|
|
188
|
+
"name": name,
|
|
189
|
+
"smarts": smarts,
|
|
190
|
+
"description": f"E-State atom type: {name}",
|
|
191
|
+
"category": "atom_type",
|
|
192
|
+
}
|
|
193
|
+
return annotations
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _annotate_hashed(fp_type):
|
|
197
|
+
"""For hashed fingerprints, bits don't have fixed meanings."""
|
|
198
|
+
return {} # empty — use explain() with a specific molecule instead
|