fpembed 0.1.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.
- fpembed/__init__.py +32 -0
- fpembed/compression.py +154 -0
- fpembed/compression_blockwise.py +105 -0
- fpembed/compression_projection.py +181 -0
- fpembed/generator.py +348 -0
- fpembed/hashing.py +48 -0
- fpembed/py.typed +1 -0
- fpembed/smiles_utils.py +63 -0
- fpembed-0.1.0.dist-info/METADATA +20 -0
- fpembed-0.1.0.dist-info/RECORD +13 -0
- fpembed-0.1.0.dist-info/WHEEL +5 -0
- fpembed-0.1.0.dist-info/licenses/LICENSE +21 -0
- fpembed-0.1.0.dist-info/top_level.txt +1 -0
fpembed/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""FPembed — Generalized fingerprint embedding library backed by scikit-fingerprints.
|
|
2
|
+
|
|
3
|
+
Provides :class:`EmbeddedFingerprintGenerator` for generating compressed
|
|
4
|
+
molecular fingerprints from SMILES, SELFIES, or RDKit Mol objects, plus
|
|
5
|
+
the standalone :func:`compress_fingerprint` compression function and
|
|
6
|
+
lightweight SMILES helpers.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from fpembed import EmbeddedFingerprintGenerator
|
|
10
|
+
>>> gen = EmbeddedFingerprintGenerator(
|
|
11
|
+
... fp_type="ecfp", fp_size=2048, compression=16, fp_params={"radius": 2}
|
|
12
|
+
... )
|
|
13
|
+
>>> emb = gen.GetFingerprintFromSmiles("CCO")
|
|
14
|
+
>>> emb.shape
|
|
15
|
+
(128,)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from fpembed.generator import EmbeddedFingerprintGenerator
|
|
19
|
+
from fpembed.compression import compress_fingerprint
|
|
20
|
+
from fpembed.smiles_utils import parse_smiles, canonicalize_smiles
|
|
21
|
+
from fpembed.hashing import fp_params_hash
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"EmbeddedFingerprintGenerator",
|
|
27
|
+
"compress_fingerprint",
|
|
28
|
+
"parse_smiles",
|
|
29
|
+
"canonicalize_smiles",
|
|
30
|
+
"fp_params_hash",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
fpembed/compression.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Fingerprint compression orchestrator.
|
|
2
|
+
|
|
3
|
+
Routes ``compress_fingerprint()`` calls to the appropriate sub-module
|
|
4
|
+
based on the ``method`` parameter. Supports block-wise weight schemes
|
|
5
|
+
(geometric, linear, log, uniform) and global projection methods
|
|
6
|
+
(hadamard, random_projection).
|
|
7
|
+
|
|
8
|
+
The default behavior (``method="geometric"``, no ``method_params``)
|
|
9
|
+
produces output identical to the original implementation.
|
|
10
|
+
|
|
11
|
+
Public API
|
|
12
|
+
----------
|
|
13
|
+
compress_fingerprint : Compress a fingerprint array.
|
|
14
|
+
_is_power_of_two : Check whether an integer is a power of two.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import numpy.typing as npt
|
|
23
|
+
|
|
24
|
+
from fpembed.compression_blockwise import compress_blockwise
|
|
25
|
+
from fpembed.compression_projection import (
|
|
26
|
+
build_rp_matrix,
|
|
27
|
+
build_srht_signs,
|
|
28
|
+
compress_hadamard,
|
|
29
|
+
compress_random_projection,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_BLOCKWISE_METHODS = {"geometric", "linear", "log", "uniform"}
|
|
33
|
+
_PROJECTION_METHODS = {"hadamard", "random_projection"}
|
|
34
|
+
_ALL_METHODS = _BLOCKWISE_METHODS | _PROJECTION_METHODS
|
|
35
|
+
|
|
36
|
+
_VALID_PARAMS: dict[str, set[str]] = {
|
|
37
|
+
"geometric": {"interleave"},
|
|
38
|
+
"linear": {"interleave"},
|
|
39
|
+
"log": {"interleave"},
|
|
40
|
+
"uniform": {"interleave"},
|
|
41
|
+
"hadamard": {"seed"},
|
|
42
|
+
"random_projection": {"seed", "sparse"},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_DEFAULT_SEED = 42
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_method_params(method: str, method_params: dict) -> None:
|
|
49
|
+
"""Validate method_params keys and value types for the given method."""
|
|
50
|
+
valid_keys = _VALID_PARAMS[method]
|
|
51
|
+
unrecognized = set(method_params) - valid_keys
|
|
52
|
+
if unrecognized:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Unrecognized method_params keys {unrecognized} for method "
|
|
55
|
+
f"'{method}'. Valid keys: {valid_keys}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if "seed" in method_params:
|
|
59
|
+
seed = method_params["seed"]
|
|
60
|
+
if not isinstance(seed, int) or isinstance(seed, bool) or seed < 0:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"seed must be a non-negative integer, got {seed!r}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if "sparse" in method_params:
|
|
66
|
+
sparse = method_params["sparse"]
|
|
67
|
+
if not isinstance(sparse, bool):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"sparse must be a boolean, got {type(sparse).__name__}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if "interleave" in method_params:
|
|
73
|
+
interleave = method_params["interleave"]
|
|
74
|
+
if not isinstance(interleave, bool):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"interleave must be a boolean, got {type(interleave).__name__}"
|
|
77
|
+
)
|
|
78
|
+
if interleave and method in _PROJECTION_METHODS:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"interleave is not applicable to method '{method}'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def compress_fingerprint(
|
|
85
|
+
vector: npt.NDArray[Any],
|
|
86
|
+
size: int,
|
|
87
|
+
*,
|
|
88
|
+
method: str = "geometric",
|
|
89
|
+
method_params: dict | None = None,
|
|
90
|
+
) -> npt.NDArray[np.float64]:
|
|
91
|
+
"""Compress a binary fingerprint into a dense float embedding.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
vector : ndarray
|
|
96
|
+
Input fingerprint — ``(L,)`` or ``(N, L)``.
|
|
97
|
+
size : int
|
|
98
|
+
Compression factor.
|
|
99
|
+
method : str
|
|
100
|
+
Compression method name.
|
|
101
|
+
method_params : dict or None
|
|
102
|
+
Method-specific parameters.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
ndarray of float64
|
|
107
|
+
Compressed embedding.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
ValueError
|
|
112
|
+
If *size* is invalid, *method* is unsupported, or *method_params*
|
|
113
|
+
contains invalid keys/values.
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(size, int) or size <= 0:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Size must be a positive integer. Try size = 16, or size = 32"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if method not in _ALL_METHODS:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Unsupported method '{method}'. "
|
|
123
|
+
f"Supported: {', '.join(sorted(_ALL_METHODS))}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if method_params is None:
|
|
127
|
+
method_params = {}
|
|
128
|
+
|
|
129
|
+
_validate_method_params(method, method_params)
|
|
130
|
+
|
|
131
|
+
# Block-wise methods
|
|
132
|
+
if method in _BLOCKWISE_METHODS:
|
|
133
|
+
interleave = method_params.get("interleave", False)
|
|
134
|
+
return compress_blockwise(vector, size, scheme=method, interleave=interleave)
|
|
135
|
+
|
|
136
|
+
# Projection methods
|
|
137
|
+
fp_len = vector.shape[-1] if vector.ndim > 1 else vector.shape[0]
|
|
138
|
+
output_dim = fp_len // size
|
|
139
|
+
|
|
140
|
+
if method == "hadamard":
|
|
141
|
+
seed = method_params.get("seed", _DEFAULT_SEED)
|
|
142
|
+
signs = build_srht_signs(fp_len, seed)
|
|
143
|
+
return compress_hadamard(vector, size, signs)
|
|
144
|
+
|
|
145
|
+
# method == "random_projection"
|
|
146
|
+
seed = method_params.get("seed", _DEFAULT_SEED)
|
|
147
|
+
sparse = method_params.get("sparse", False)
|
|
148
|
+
matrix = build_rp_matrix(fp_len, output_dim, seed, sparse)
|
|
149
|
+
return compress_random_projection(vector, matrix)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_power_of_two(n: int) -> bool:
|
|
153
|
+
"""Return True if *n* is a positive power of two."""
|
|
154
|
+
return n > 0 and (n & (n - 1)) == 0
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Block-wise fingerprint compression with pluggable weight schemes.
|
|
2
|
+
|
|
3
|
+
Provides weight vector construction for geometric, linear, logarithmic,
|
|
4
|
+
and uniform schemes, plus block partitioning (contiguous and interleaved)
|
|
5
|
+
with weighted dot-product reduction.
|
|
6
|
+
|
|
7
|
+
Public API
|
|
8
|
+
----------
|
|
9
|
+
build_block_weights : Build a normalized weight vector for a given scheme.
|
|
10
|
+
compress_blockwise : Compress a fingerprint using block-wise weighted dot product.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
19
|
+
|
|
20
|
+
_SUPPORTED_SCHEMES = ("geometric", "linear", "log", "uniform")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_block_weights(
|
|
24
|
+
block_size: int, scheme: str = "geometric"
|
|
25
|
+
) -> npt.NDArray[np.float64]:
|
|
26
|
+
"""Return a normalized weight vector of length *block_size*.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
block_size : int
|
|
31
|
+
Number of bits per block (must be >= 1).
|
|
32
|
+
scheme : str
|
|
33
|
+
One of ``"geometric"``, ``"linear"``, ``"log"``, ``"uniform"``.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
ndarray of float64
|
|
38
|
+
1-D array of length *block_size*, summing to 1.0.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
ValueError
|
|
43
|
+
If *scheme* is not recognized.
|
|
44
|
+
"""
|
|
45
|
+
if scheme == "geometric":
|
|
46
|
+
# Log-space construction for numerical stability (moved from compression.py)
|
|
47
|
+
log_weights = np.arange(block_size, dtype=np.float64) - block_size
|
|
48
|
+
weights = np.exp2(log_weights)
|
|
49
|
+
elif scheme == "linear":
|
|
50
|
+
weights = np.arange(1, block_size + 1, dtype=np.float64)
|
|
51
|
+
elif scheme == "log":
|
|
52
|
+
weights = np.log2(np.arange(2, block_size + 2, dtype=np.float64))
|
|
53
|
+
elif scheme == "uniform":
|
|
54
|
+
return np.full(block_size, 1.0 / block_size, dtype=np.float64)
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Unsupported weight scheme '{scheme}'. "
|
|
58
|
+
f"Supported: {', '.join(_SUPPORTED_SCHEMES)}"
|
|
59
|
+
)
|
|
60
|
+
weights /= weights.sum()
|
|
61
|
+
return weights
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def compress_blockwise(
|
|
65
|
+
vector: npt.NDArray[Any],
|
|
66
|
+
size: int,
|
|
67
|
+
scheme: str = "geometric",
|
|
68
|
+
interleave: bool = False,
|
|
69
|
+
) -> npt.NDArray[np.float64]:
|
|
70
|
+
"""Compress a binary fingerprint using block-wise weighted dot product.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
vector : ndarray
|
|
75
|
+
Input fingerprint — ``(L,)`` for single or ``(N, L)`` for batch.
|
|
76
|
+
size : int
|
|
77
|
+
Block size (compression factor).
|
|
78
|
+
scheme : str
|
|
79
|
+
Weight scheme name.
|
|
80
|
+
interleave : bool
|
|
81
|
+
If True, use strided (interleaved) partitioning; if False, contiguous.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
ndarray of float64
|
|
86
|
+
Compressed embedding — ``(1, L//size)`` or ``(N, L//size)``.
|
|
87
|
+
"""
|
|
88
|
+
single = vector.ndim == 1
|
|
89
|
+
if single:
|
|
90
|
+
vector = vector[np.newaxis, :]
|
|
91
|
+
|
|
92
|
+
rows, cols = vector.shape
|
|
93
|
+
n_blocks = cols // size
|
|
94
|
+
|
|
95
|
+
weights = build_block_weights(size, scheme)
|
|
96
|
+
|
|
97
|
+
if interleave:
|
|
98
|
+
# Strided: bit[i] → block[i % n_blocks]
|
|
99
|
+
blocks = vector.reshape(rows, size, n_blocks).transpose(0, 2, 1)
|
|
100
|
+
else:
|
|
101
|
+
# Contiguous: bit[i] → block[i // size]
|
|
102
|
+
blocks = vector.reshape(rows, n_blocks, size)
|
|
103
|
+
|
|
104
|
+
embedded: npt.NDArray[np.float64] = np.einsum("ijk,k->ij", blocks, weights)
|
|
105
|
+
return embedded
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Global projection compression methods: SRHT and random projection.
|
|
2
|
+
|
|
3
|
+
Provides the Fast Walsh-Hadamard Transform (FWHT), Subsampled Randomized
|
|
4
|
+
Hadamard Transform (SRHT) compression, and seeded random projection
|
|
5
|
+
(dense Gaussian and sparse Achlioptas variants).
|
|
6
|
+
|
|
7
|
+
Public API
|
|
8
|
+
----------
|
|
9
|
+
fwht : In-place Fast Walsh-Hadamard Transform.
|
|
10
|
+
build_srht_signs : Generate random ±1 sign vector for SRHT.
|
|
11
|
+
compress_hadamard : Compress via SRHT (sign flips → FWHT → truncate).
|
|
12
|
+
build_rp_matrix : Build a random projection matrix (dense or sparse).
|
|
13
|
+
compress_random_projection : Compress via matrix multiplication.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import numpy.typing as npt
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fwht(x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
|
|
25
|
+
"""In-place Fast Walsh-Hadamard Transform (butterfly algorithm).
|
|
26
|
+
|
|
27
|
+
Operates in O(L log L) using only additions and subtractions.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
x : ndarray
|
|
32
|
+
1-D float64 array of length L (must be a power of 2).
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
ndarray
|
|
37
|
+
The same array, modified in-place.
|
|
38
|
+
"""
|
|
39
|
+
length = len(x)
|
|
40
|
+
h = 1
|
|
41
|
+
while h < length:
|
|
42
|
+
for i in range(0, length, h * 2):
|
|
43
|
+
for j in range(i, i + h):
|
|
44
|
+
a = x[j]
|
|
45
|
+
b = x[j + h]
|
|
46
|
+
x[j] = a + b
|
|
47
|
+
x[j + h] = a - b
|
|
48
|
+
h *= 2
|
|
49
|
+
return x
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_srht_signs(
|
|
53
|
+
length: int, seed: int
|
|
54
|
+
) -> npt.NDArray[np.float64]:
|
|
55
|
+
"""Generate a random ±1 sign vector for SRHT.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
length : int
|
|
60
|
+
Fingerprint length L.
|
|
61
|
+
seed : int
|
|
62
|
+
Random seed for reproducibility.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
ndarray of float64
|
|
67
|
+
1-D array of ±1 values, length L.
|
|
68
|
+
"""
|
|
69
|
+
rng = np.random.RandomState(seed)
|
|
70
|
+
return rng.choice([-1.0, 1.0], size=length).astype(np.float64)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compress_hadamard(
|
|
74
|
+
vector: npt.NDArray[Any],
|
|
75
|
+
size: int,
|
|
76
|
+
signs: npt.NDArray[np.float64],
|
|
77
|
+
) -> npt.NDArray[np.float64]:
|
|
78
|
+
"""Compress using Subsampled Randomized Hadamard Transform.
|
|
79
|
+
|
|
80
|
+
Applies sign flips, FWHT, normalization by 1/√L, and truncation to
|
|
81
|
+
D = L // size dimensions.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
vector : ndarray
|
|
86
|
+
Input fingerprint — ``(L,)`` or ``(N, L)``.
|
|
87
|
+
size : int
|
|
88
|
+
Compression factor.
|
|
89
|
+
signs : ndarray
|
|
90
|
+
Precomputed ±1 sign vector of length L.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
ndarray of float64
|
|
95
|
+
Compressed embedding — ``(1, D)`` or ``(N, D)``.
|
|
96
|
+
"""
|
|
97
|
+
single = vector.ndim == 1
|
|
98
|
+
if single:
|
|
99
|
+
vector = vector[np.newaxis, :]
|
|
100
|
+
|
|
101
|
+
rows, cols = vector.shape
|
|
102
|
+
output_dim = cols // size
|
|
103
|
+
norm = 1.0 / np.sqrt(cols)
|
|
104
|
+
|
|
105
|
+
result = np.empty((rows, output_dim), dtype=np.float64)
|
|
106
|
+
for i in range(rows):
|
|
107
|
+
row = vector[i].astype(np.float64) * signs
|
|
108
|
+
fwht(row)
|
|
109
|
+
row *= norm
|
|
110
|
+
result[i] = row[:output_dim]
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_rp_matrix(
|
|
116
|
+
fp_length: int,
|
|
117
|
+
output_dim: int,
|
|
118
|
+
seed: int,
|
|
119
|
+
sparse: bool = False,
|
|
120
|
+
) -> npt.NDArray[np.float64]:
|
|
121
|
+
"""Build a random projection matrix R of shape ``(output_dim, fp_length)``.
|
|
122
|
+
|
|
123
|
+
Dense Gaussian: ``R[i,j] ~ N(0, 1/D)``.
|
|
124
|
+
Sparse Achlioptas: ``R[i,j] ∈ {-1, 0, +1}`` with ``P(0) = 2/3``,
|
|
125
|
+
scaled by ``√(3/D)``.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
fp_length : int
|
|
130
|
+
Input dimension L.
|
|
131
|
+
output_dim : int
|
|
132
|
+
Output dimension D.
|
|
133
|
+
seed : int
|
|
134
|
+
Random seed for reproducibility.
|
|
135
|
+
sparse : bool
|
|
136
|
+
If True, use Achlioptas variant.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
ndarray of float64
|
|
141
|
+
2-D array of shape ``(D, L)``.
|
|
142
|
+
"""
|
|
143
|
+
rng = np.random.RandomState(seed)
|
|
144
|
+
|
|
145
|
+
if sparse:
|
|
146
|
+
# Achlioptas: P(-1) = 1/6, P(0) = 2/3, P(+1) = 1/6
|
|
147
|
+
raw = rng.choice([-1, 0, 0, 0, 0, 1], size=(output_dim, fp_length))
|
|
148
|
+
scale = np.sqrt(3.0 / output_dim)
|
|
149
|
+
return raw.astype(np.float64) * scale
|
|
150
|
+
else:
|
|
151
|
+
# Dense Gaussian: N(0, 1/D)
|
|
152
|
+
std = 1.0 / np.sqrt(output_dim)
|
|
153
|
+
return rng.randn(output_dim, fp_length).astype(np.float64) * std
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def compress_random_projection(
|
|
157
|
+
vector: npt.NDArray[Any],
|
|
158
|
+
matrix: npt.NDArray[np.float64],
|
|
159
|
+
) -> npt.NDArray[np.float64]:
|
|
160
|
+
"""Compress using a precomputed random projection matrix.
|
|
161
|
+
|
|
162
|
+
Computes ``embedding = vector @ matrix.T``.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
vector : ndarray
|
|
167
|
+
Input fingerprint — ``(L,)`` or ``(N, L)``.
|
|
168
|
+
matrix : ndarray
|
|
169
|
+
Projection matrix of shape ``(D, L)``.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
ndarray of float64
|
|
174
|
+
Compressed embedding — ``(1, D)`` or ``(N, D)``.
|
|
175
|
+
"""
|
|
176
|
+
single = vector.ndim == 1
|
|
177
|
+
if single:
|
|
178
|
+
vector = vector[np.newaxis, :]
|
|
179
|
+
|
|
180
|
+
embedded = vector.astype(np.float64) @ matrix.T
|
|
181
|
+
return embedded
|
fpembed/generator.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""EmbeddedFingerprintGenerator — unified fingerprint class backed by skfp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from collections import namedtuple
|
|
7
|
+
from typing import Any, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
from rdkit import Chem
|
|
12
|
+
from skfp.fingerprints import (
|
|
13
|
+
AtomPairFingerprint,
|
|
14
|
+
ECFPFingerprint,
|
|
15
|
+
LayeredFingerprint,
|
|
16
|
+
PatternFingerprint,
|
|
17
|
+
RDKitFingerprint,
|
|
18
|
+
TopologicalTorsionFingerprint,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from fpembed.compression import (
|
|
22
|
+
compress_fingerprint,
|
|
23
|
+
_is_power_of_two,
|
|
24
|
+
_ALL_METHODS,
|
|
25
|
+
_validate_method_params,
|
|
26
|
+
_DEFAULT_SEED,
|
|
27
|
+
)
|
|
28
|
+
from fpembed.compression_projection import (
|
|
29
|
+
build_rp_matrix,
|
|
30
|
+
build_srht_signs,
|
|
31
|
+
compress_hadamard,
|
|
32
|
+
compress_random_projection,
|
|
33
|
+
)
|
|
34
|
+
from fpembed.hashing import fp_params_hash
|
|
35
|
+
from fpembed.smiles_utils import canonicalize_smiles, parse_smiles
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Sequence
|
|
39
|
+
|
|
40
|
+
CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
|
|
41
|
+
_EMPTY_CACHE_INFO = CacheInfo(hits=0, misses=0, maxsize=0, currsize=0)
|
|
42
|
+
|
|
43
|
+
_SUPPORTED_FP_TYPES = (
|
|
44
|
+
"ecfp", "atom_pair", "topological_torsion", "rdkit", "layered", "pattern",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_skfp(fp_type: str, fp_size: int, fp_params: dict) -> Any:
|
|
49
|
+
"""Instantiate the appropriate skfp fingerprint object."""
|
|
50
|
+
if fp_type == "ecfp":
|
|
51
|
+
return ECFPFingerprint(
|
|
52
|
+
fp_size=fp_size, radius=fp_params.get("radius", 2),
|
|
53
|
+
count=False, n_jobs=1,
|
|
54
|
+
)
|
|
55
|
+
if fp_type == "atom_pair":
|
|
56
|
+
return AtomPairFingerprint(
|
|
57
|
+
fp_size=fp_size, min_distance=fp_params.get("min_distance", 1),
|
|
58
|
+
max_distance=fp_params.get("max_distance", 30),
|
|
59
|
+
count=False, n_jobs=1,
|
|
60
|
+
)
|
|
61
|
+
if fp_type == "topological_torsion":
|
|
62
|
+
return TopologicalTorsionFingerprint(
|
|
63
|
+
fp_size=fp_size,
|
|
64
|
+
torsion_atom_count=fp_params.get("torsion_atom_count", 4),
|
|
65
|
+
count=False, n_jobs=1,
|
|
66
|
+
)
|
|
67
|
+
if fp_type == "rdkit":
|
|
68
|
+
return RDKitFingerprint(
|
|
69
|
+
fp_size=fp_size, min_path=fp_params.get("min_path", 1),
|
|
70
|
+
max_path=fp_params.get("max_path", 7), count=False, n_jobs=1,
|
|
71
|
+
)
|
|
72
|
+
if fp_type == "layered":
|
|
73
|
+
return LayeredFingerprint(
|
|
74
|
+
fp_size=fp_size, min_path=fp_params.get("min_path", 1),
|
|
75
|
+
max_path=fp_params.get("max_path", 7), n_jobs=1,
|
|
76
|
+
)
|
|
77
|
+
return PatternFingerprint(fp_size=fp_size, n_jobs=1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EmbeddedFingerprintGenerator:
|
|
81
|
+
"""Unified fingerprint generator backed by scikit-fingerprints.
|
|
82
|
+
|
|
83
|
+
Supports six fp_types: ecfp, atom_pair, topological_torsion,
|
|
84
|
+
rdkit, layered, pattern. Compression via log-space weighted mask.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
__slots__ = (
|
|
88
|
+
"_fp_type", "_fp_size", "_compression", "_fp_params",
|
|
89
|
+
"_method", "_method_params", "_projection_cache",
|
|
90
|
+
"_skfp_fp", "_params_hash", "_cache_size", "_cached_compute",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
fp_type: str,
|
|
96
|
+
fp_size: int,
|
|
97
|
+
compression: int | None,
|
|
98
|
+
fp_params: dict | None = None,
|
|
99
|
+
cache_size: int | None = None,
|
|
100
|
+
method: str = "geometric",
|
|
101
|
+
method_params: dict | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
if fp_params is None:
|
|
104
|
+
fp_params = {}
|
|
105
|
+
if method_params is None:
|
|
106
|
+
method_params = {}
|
|
107
|
+
if fp_type not in _SUPPORTED_FP_TYPES:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Unsupported fp_type '{fp_type}'. "
|
|
110
|
+
f"Supported: {', '.join(_SUPPORTED_FP_TYPES)}"
|
|
111
|
+
)
|
|
112
|
+
if method not in _ALL_METHODS:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Unsupported method '{method}'. "
|
|
115
|
+
f"Supported: {', '.join(sorted(_ALL_METHODS))}"
|
|
116
|
+
)
|
|
117
|
+
_validate_method_params(method, method_params)
|
|
118
|
+
|
|
119
|
+
effective: int = 0 if (compression is None or compression == 0) else compression
|
|
120
|
+
if effective != 0:
|
|
121
|
+
if not _is_power_of_two(effective):
|
|
122
|
+
raise ValueError(f"compression must be a power of 2, got {effective}")
|
|
123
|
+
if effective > fp_size / 2:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"compression ({effective}) must be <= fp_size / 2 ({fp_size // 2})"
|
|
126
|
+
)
|
|
127
|
+
if fp_size % effective != 0:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"fp_size ({fp_size}) must be evenly divisible by compression ({effective})"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if method == "hadamard" and not _is_power_of_two(fp_size):
|
|
133
|
+
raise ValueError("Hadamard method requires fp_size to be a power of 2")
|
|
134
|
+
|
|
135
|
+
self._fp_type = fp_type
|
|
136
|
+
self._fp_size = fp_size
|
|
137
|
+
self._compression = effective
|
|
138
|
+
self._fp_params = dict(fp_params)
|
|
139
|
+
self._method = method
|
|
140
|
+
self._method_params = dict(method_params)
|
|
141
|
+
self._cache_size = cache_size
|
|
142
|
+
self._skfp_fp: Any = _build_skfp(fp_type, fp_size, fp_params)
|
|
143
|
+
|
|
144
|
+
# Precompute projection artifacts for projection methods
|
|
145
|
+
self._projection_cache: Any = None
|
|
146
|
+
if effective != 0 and method == "hadamard":
|
|
147
|
+
seed = self._method_params.get("seed", _DEFAULT_SEED)
|
|
148
|
+
self._projection_cache = build_srht_signs(fp_size, seed)
|
|
149
|
+
elif effective != 0 and method == "random_projection":
|
|
150
|
+
seed = self._method_params.get("seed", _DEFAULT_SEED)
|
|
151
|
+
sparse = self._method_params.get("sparse", False)
|
|
152
|
+
output_dim = fp_size // effective
|
|
153
|
+
self._projection_cache = build_rp_matrix(fp_size, output_dim, seed, sparse)
|
|
154
|
+
|
|
155
|
+
self._params_hash = fp_params_hash(
|
|
156
|
+
fp_type, fp_params, method=method, method_params=method_params,
|
|
157
|
+
)
|
|
158
|
+
if cache_size is not None:
|
|
159
|
+
@functools.lru_cache(maxsize=cache_size)
|
|
160
|
+
def _cached(smiles: str) -> npt.NDArray[np.float64] | None:
|
|
161
|
+
mol = parse_smiles(smiles)
|
|
162
|
+
return None if mol is None else self.GetFingerprintAsNumPy(mol)
|
|
163
|
+
self._cached_compute: Any = _cached
|
|
164
|
+
else:
|
|
165
|
+
self._cached_compute = None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def fp_type(self) -> str:
|
|
169
|
+
"""Fingerprint type identifier."""
|
|
170
|
+
return self._fp_type
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def fp_size(self) -> int:
|
|
174
|
+
"""Fingerprint bit-vector length."""
|
|
175
|
+
return self._fp_size
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def compression(self) -> int:
|
|
179
|
+
"""Compression factor (0 means no compression)."""
|
|
180
|
+
return self._compression
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def fp_params(self) -> dict:
|
|
184
|
+
"""Type-specific fingerprint parameters (copy)."""
|
|
185
|
+
return dict(self._fp_params)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def method(self) -> str:
|
|
189
|
+
"""Compression method name."""
|
|
190
|
+
return self._method
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def method_params(self) -> dict:
|
|
194
|
+
"""Method-specific parameters (copy)."""
|
|
195
|
+
return dict(self._method_params)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def params_hash(self) -> str:
|
|
199
|
+
"""Read-only 16-char hex hash of fp_type + fp_params."""
|
|
200
|
+
return self._params_hash
|
|
201
|
+
|
|
202
|
+
def _raw_fp(self, mol: Chem.Mol) -> npt.NDArray[np.float64]:
|
|
203
|
+
return self._skfp_fp.transform([mol]).flatten().astype(np.float64)
|
|
204
|
+
|
|
205
|
+
def GetRawFingerprintAsNumPy(self, mol: Chem.Mol) -> npt.NDArray[np.float64]:
|
|
206
|
+
"""Return the raw (uncompressed) fingerprint for a single Mol."""
|
|
207
|
+
if mol is None:
|
|
208
|
+
raise ValueError("mol must not be None")
|
|
209
|
+
return self._raw_fp(mol)
|
|
210
|
+
|
|
211
|
+
def GetFingerprintAsNumPy(self, mol: Chem.Mol) -> npt.NDArray[np.float64]:
|
|
212
|
+
"""Return the (optionally compressed) fingerprint for a single Mol."""
|
|
213
|
+
if mol is None:
|
|
214
|
+
raise ValueError("mol must not be None")
|
|
215
|
+
raw = self._raw_fp(mol)
|
|
216
|
+
if self._compression == 0:
|
|
217
|
+
return raw
|
|
218
|
+
# Projection methods: use cached artifacts directly
|
|
219
|
+
if self._method == "hadamard":
|
|
220
|
+
return compress_hadamard(raw, self._compression, self._projection_cache).flatten()
|
|
221
|
+
if self._method == "random_projection":
|
|
222
|
+
return compress_random_projection(raw, self._projection_cache).flatten()
|
|
223
|
+
# Block-wise methods: delegate to orchestrator
|
|
224
|
+
return compress_fingerprint(
|
|
225
|
+
raw, self._compression,
|
|
226
|
+
method=self._method, method_params=self._method_params,
|
|
227
|
+
).flatten()
|
|
228
|
+
|
|
229
|
+
def _out_dim(self) -> int:
|
|
230
|
+
return self._fp_size // self._compression if self._compression > 0 else self._fp_size
|
|
231
|
+
|
|
232
|
+
def GetFingerprintsAsNumPy(
|
|
233
|
+
self, mols: Sequence[Chem.Mol | None], chunk_size: int | None = None,
|
|
234
|
+
) -> tuple[npt.NDArray[np.float64], list[int]]:
|
|
235
|
+
"""Batch fingerprints. Returns (embeddings, invalid_indices)."""
|
|
236
|
+
if chunk_size is not None and chunk_size <= 0:
|
|
237
|
+
raise ValueError("chunk_size must be a positive integer or None")
|
|
238
|
+
total = len(mols)
|
|
239
|
+
invalid: list[int] = []
|
|
240
|
+
valid: list[npt.NDArray[np.float64]] = []
|
|
241
|
+
cs = chunk_size or total
|
|
242
|
+
for start in range(0, total, cs):
|
|
243
|
+
for i in range(start, min(start + cs, total)):
|
|
244
|
+
mol = mols[i]
|
|
245
|
+
if mol is None:
|
|
246
|
+
invalid.append(i)
|
|
247
|
+
continue
|
|
248
|
+
try:
|
|
249
|
+
valid.append(self.GetFingerprintAsNumPy(mol))
|
|
250
|
+
except Exception:
|
|
251
|
+
invalid.append(i)
|
|
252
|
+
if valid:
|
|
253
|
+
return np.vstack(valid), invalid
|
|
254
|
+
return np.empty((0, self._out_dim()), dtype=np.float64), invalid
|
|
255
|
+
|
|
256
|
+
def GetFingerprintFromSmiles(
|
|
257
|
+
self, smiles: str, validate: bool = True, canonicalize: bool = True,
|
|
258
|
+
) -> npt.NDArray[np.float64] | None:
|
|
259
|
+
"""Generate a fingerprint from a SMILES string."""
|
|
260
|
+
if validate:
|
|
261
|
+
if canonicalize:
|
|
262
|
+
smiles = canonicalize_smiles(smiles) or ""
|
|
263
|
+
if not smiles:
|
|
264
|
+
return None
|
|
265
|
+
elif parse_smiles(smiles) is None:
|
|
266
|
+
return None
|
|
267
|
+
if self._cached_compute is not None:
|
|
268
|
+
return self._cached_compute(smiles)
|
|
269
|
+
mol = parse_smiles(smiles)
|
|
270
|
+
return None if mol is None else self.GetFingerprintAsNumPy(mol)
|
|
271
|
+
|
|
272
|
+
def GetFingerprintsFromSmiles(
|
|
273
|
+
self, smiles_list: Sequence[str],
|
|
274
|
+
validate: bool = True, canonicalize: bool = True,
|
|
275
|
+
) -> tuple[npt.NDArray[np.float64], list[int]]:
|
|
276
|
+
"""Batch fingerprints from SMILES. Returns (embeddings, invalid_indices)."""
|
|
277
|
+
invalid: list[int] = []
|
|
278
|
+
valid: list[npt.NDArray[np.float64]] = []
|
|
279
|
+
for i, smi in enumerate(smiles_list):
|
|
280
|
+
try:
|
|
281
|
+
r = self.GetFingerprintFromSmiles(smi, validate=validate, canonicalize=canonicalize)
|
|
282
|
+
except Exception:
|
|
283
|
+
r = None
|
|
284
|
+
if r is None:
|
|
285
|
+
invalid.append(i)
|
|
286
|
+
else:
|
|
287
|
+
valid.append(r)
|
|
288
|
+
if valid:
|
|
289
|
+
return np.vstack(valid), invalid
|
|
290
|
+
return np.empty((0, self._out_dim()), dtype=np.float64), invalid
|
|
291
|
+
|
|
292
|
+
def GetFingerprintFromSelfies(
|
|
293
|
+
self, selfies_str: str, validate: bool = True, canonicalize: bool = True,
|
|
294
|
+
) -> npt.NDArray[np.float64] | None:
|
|
295
|
+
"""Generate a fingerprint from a SELFIES string."""
|
|
296
|
+
import selfies as sf # type: ignore[import-untyped]
|
|
297
|
+
try:
|
|
298
|
+
smiles = sf.decoder(selfies_str)
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
if smiles is None:
|
|
302
|
+
return None
|
|
303
|
+
return self.GetFingerprintFromSmiles(smiles, validate=validate, canonicalize=canonicalize)
|
|
304
|
+
|
|
305
|
+
def GetFingerprintsFromSelfies(
|
|
306
|
+
self, selfies_list: Sequence[str],
|
|
307
|
+
validate: bool = True, canonicalize: bool = True,
|
|
308
|
+
) -> tuple[npt.NDArray[np.float64], list[int]]:
|
|
309
|
+
"""Batch fingerprints from SELFIES. Returns (embeddings, invalid_indices)."""
|
|
310
|
+
invalid: list[int] = []
|
|
311
|
+
valid: list[npt.NDArray[np.float64]] = []
|
|
312
|
+
for i, sel in enumerate(selfies_list):
|
|
313
|
+
try:
|
|
314
|
+
r = self.GetFingerprintFromSelfies(sel, validate=validate, canonicalize=canonicalize)
|
|
315
|
+
except Exception:
|
|
316
|
+
r = None
|
|
317
|
+
if r is None:
|
|
318
|
+
invalid.append(i)
|
|
319
|
+
else:
|
|
320
|
+
valid.append(r)
|
|
321
|
+
if valid:
|
|
322
|
+
return np.vstack(valid), invalid
|
|
323
|
+
return np.empty((0, self._out_dim()), dtype=np.float64), invalid
|
|
324
|
+
|
|
325
|
+
def clear_cache(self) -> None:
|
|
326
|
+
"""Clear the LRU cache. No-op if caching is disabled."""
|
|
327
|
+
if self._cached_compute is not None:
|
|
328
|
+
self._cached_compute.cache_clear()
|
|
329
|
+
|
|
330
|
+
def cache_info(self) -> CacheInfo:
|
|
331
|
+
"""Return cache statistics (all zeros when caching is disabled)."""
|
|
332
|
+
if self._cached_compute is not None:
|
|
333
|
+
raw = self._cached_compute.cache_info()
|
|
334
|
+
return CacheInfo(hits=raw.hits, misses=raw.misses, maxsize=raw.maxsize, currsize=raw.currsize)
|
|
335
|
+
return _EMPTY_CACHE_INFO
|
|
336
|
+
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
comp = self._compression if self._compression > 0 else None
|
|
339
|
+
base = (
|
|
340
|
+
f"EmbeddedFingerprintGenerator(fp_type='{self._fp_type}', "
|
|
341
|
+
f"fp_size={self._fp_size}, compression={comp}, "
|
|
342
|
+
f"fp_params={self._fp_params}"
|
|
343
|
+
)
|
|
344
|
+
if self._method != "geometric" or self._method_params:
|
|
345
|
+
base += f", method='{self._method}'"
|
|
346
|
+
base += f", method_params={self._method_params}"
|
|
347
|
+
base += ")"
|
|
348
|
+
return base
|
fpembed/hashing.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Fingerprint parameter hashing for stable cache keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def fp_params_hash(
|
|
10
|
+
fp_type: str,
|
|
11
|
+
fp_params: dict,
|
|
12
|
+
*,
|
|
13
|
+
method: str | None = None,
|
|
14
|
+
method_params: dict | None = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Compute a 16-char SHA-256 prefix of sorted JSON of {fp_type, **fp_params}.
|
|
17
|
+
|
|
18
|
+
Bool values in *fp_params* are normalized to Python ``bool`` before
|
|
19
|
+
serialization to prevent ``True``/``1`` collisions.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
fp_type: Fingerprint type identifier (e.g. ``"ecfp"``).
|
|
23
|
+
fp_params: Type-specific fingerprint parameters.
|
|
24
|
+
method: Compression method name (optional).
|
|
25
|
+
method_params: Method-specific parameters (optional).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A 16-character hexadecimal string.
|
|
29
|
+
"""
|
|
30
|
+
normalized = {
|
|
31
|
+
k: int(v) if isinstance(v, bool) else v
|
|
32
|
+
for k, v in fp_params.items()
|
|
33
|
+
}
|
|
34
|
+
payload: dict = {"fp_type": fp_type, **normalized}
|
|
35
|
+
|
|
36
|
+
effective_params = method_params or {}
|
|
37
|
+
if method is not None and not (method == "geometric" and not effective_params):
|
|
38
|
+
norm_mp = {
|
|
39
|
+
k: int(v) if isinstance(v, bool) else v
|
|
40
|
+
for k, v in effective_params.items()
|
|
41
|
+
}
|
|
42
|
+
payload["method"] = method
|
|
43
|
+
if norm_mp:
|
|
44
|
+
payload["method_params"] = norm_mp
|
|
45
|
+
|
|
46
|
+
return hashlib.sha256(
|
|
47
|
+
json.dumps(payload, sort_keys=True).encode()
|
|
48
|
+
).hexdigest()[:16]
|
fpembed/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
fpembed/smiles_utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""SMILES/molecule helper functions for the FPembed package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rdkit import Chem
|
|
6
|
+
from rdkit import rdBase
|
|
7
|
+
|
|
8
|
+
# Suppress RDKit warnings for invalid SMILES (handled via return values)
|
|
9
|
+
rdBase.DisableLog("rdApp.error")
|
|
10
|
+
rdBase.DisableLog("rdApp.*")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_smiles(smiles: str) -> Chem.Mol | None:
|
|
14
|
+
"""Convert a SMILES string to an RDKit Mol object.
|
|
15
|
+
|
|
16
|
+
Returns ``None`` for any input that RDKit cannot parse, without raising
|
|
17
|
+
an exception.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
smiles: A SMILES string representing a molecule.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
An RDKit Mol object, or ``None`` if the input is invalid.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> parse_smiles("CCO") # ethanol
|
|
27
|
+
<rdkit.Chem.rdchem.Mol object at ...>
|
|
28
|
+
>>> parse_smiles("not_a_molecule") is None
|
|
29
|
+
True
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
return Chem.MolFromSmiles(smiles)
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def canonicalize_smiles(smiles: str) -> str | None:
|
|
38
|
+
"""Canonicalize a SMILES string.
|
|
39
|
+
|
|
40
|
+
Parses the input with RDKit and returns the canonical SMILES form.
|
|
41
|
+
Returns ``None`` for any input that cannot be parsed or canonicalized.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
smiles: A SMILES string to canonicalize.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The canonical SMILES string, or ``None`` if the input is invalid.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> canonicalize_smiles("OCC")
|
|
51
|
+
'CCO'
|
|
52
|
+
>>> canonicalize_smiles("c1ccccc1")
|
|
53
|
+
'c1ccccc1'
|
|
54
|
+
>>> canonicalize_smiles("invalid") is None
|
|
55
|
+
True
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
mol = Chem.MolFromSmiles(smiles)
|
|
59
|
+
if mol is None:
|
|
60
|
+
return None
|
|
61
|
+
return Chem.MolToSmiles(mol)
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fpembed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generalized fingerprint embedding library
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: rdkit==2026.3.1
|
|
9
|
+
Requires-Dist: numpy==2.2.6
|
|
10
|
+
Requires-Dist: selfies==1.0.2
|
|
11
|
+
Requires-Dist: scikit-fingerprints==2.0.0
|
|
12
|
+
Provides-Extra: app
|
|
13
|
+
Requires-Dist: nicegui>=3.8.0; extra == "app"
|
|
14
|
+
Requires-Dist: optuna>=3.1.0; extra == "app"
|
|
15
|
+
Requires-Dist: pyyaml>=6.0; extra == "app"
|
|
16
|
+
Requires-Dist: altair>=5.0; extra == "app"
|
|
17
|
+
Requires-Dist: pandas>=1.5.2; extra == "app"
|
|
18
|
+
Requires-Dist: scikit-learn>=1.2.0; extra == "app"
|
|
19
|
+
Requires-Dist: tqdm>=4.64.1; extra == "app"
|
|
20
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
fpembed/__init__.py,sha256=aicXqkvkMOwA6sU1nnn-7DFBqF_cB27nk4G2CAmqMao,1030
|
|
2
|
+
fpembed/compression.py,sha256=V3WkgeYblVpOMcgJIma1gieE8DFtfIIaTe6QmUf9yQE,4751
|
|
3
|
+
fpembed/compression_blockwise.py,sha256=KmGVsgbaxaT-77pyURGAn3Gdqq_0G81vpgwpNFpXz6U,3134
|
|
4
|
+
fpembed/compression_projection.py,sha256=zx6U-gqqWje4OtXQMovXA8PrBYfZezm0q4PbYycbmxQ,4780
|
|
5
|
+
fpembed/generator.py,sha256=20PikSUcoDVtb0SIWj7qu78yT4zqR2JIf-YSg58LeAs,13275
|
|
6
|
+
fpembed/hashing.py,sha256=l2glvBBgxKNzRRNXlc_6-OdMW1126rvkw_8GFVlih5Y,1424
|
|
7
|
+
fpembed/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
fpembed/smiles_utils.py,sha256=MnvnPRexwhA_-JLKLgXRXk72Cibf3LHaax02jrH3lyg,1670
|
|
9
|
+
fpembed-0.1.0.dist-info/licenses/LICENSE,sha256=_M7NYM9qJY7gueiMrR51K-1DQ4VG6x06T1uR9nEVmCE,1094
|
|
10
|
+
fpembed-0.1.0.dist-info/METADATA,sha256=1Kd0_SOiMAk_cFVH2B6ZZxWCt1dtVf68ygFpU0Ro7rE,681
|
|
11
|
+
fpembed-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
fpembed-0.1.0.dist-info/top_level.txt,sha256=V7Uu_OVnu-a4YO2BjhhuiVz4bJ3Q9_zicZAgpCqhf4c,8
|
|
13
|
+
fpembed-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Markovskii
|
|
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 @@
|
|
|
1
|
+
fpembed
|