diffbio 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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Differentiable molecular fingerprint operators.
|
|
2
|
+
|
|
3
|
+
This module implements neural graph fingerprints that provide
|
|
4
|
+
differentiable alternatives to traditional molecular fingerprints.
|
|
5
|
+
|
|
6
|
+
Operators:
|
|
7
|
+
DifferentiableMolecularFingerprint: General neural graph fingerprint
|
|
8
|
+
CircularFingerprintOperator: Differentiable ECFP/Morgan fingerprints
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import jax.numpy as jnp
|
|
16
|
+
import numpy as np
|
|
17
|
+
from datarax.core.config import OperatorConfig
|
|
18
|
+
from datarax.core.operator import OperatorModule
|
|
19
|
+
from flax import nnx
|
|
20
|
+
|
|
21
|
+
from diffbio.core import soft_ops
|
|
22
|
+
from diffbio.operators.drug_discovery._graph_utils import (
|
|
23
|
+
attach_fingerprint,
|
|
24
|
+
build_encoder,
|
|
25
|
+
ensure_rngs,
|
|
26
|
+
graph_sum_readout,
|
|
27
|
+
initialize_graph_encoder,
|
|
28
|
+
stabilize_operator_id,
|
|
29
|
+
unpack_graph_inputs,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class MolecularFingerprintConfig(OperatorConfig):
|
|
37
|
+
"""Configuration for molecular fingerprint operator.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
fingerprint_dim: Dimension of output fingerprint vector.
|
|
41
|
+
hidden_dim: Hidden dimension for graph convolutions.
|
|
42
|
+
num_layers: Number of graph convolution layers.
|
|
43
|
+
in_features: Number of input node features (default: DEFAULT_ATOM_FEATURES=34).
|
|
44
|
+
normalize: Whether to L2-normalize the fingerprint.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
fingerprint_dim: int = 256
|
|
48
|
+
hidden_dim: int = 128
|
|
49
|
+
num_layers: int = 3
|
|
50
|
+
in_features: int = 4 # Default for tests; use DEFAULT_ATOM_FEATURES for real molecules
|
|
51
|
+
normalize: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DifferentiableMolecularFingerprint(OperatorModule):
|
|
55
|
+
"""Neural graph fingerprint operator.
|
|
56
|
+
|
|
57
|
+
Computes learned molecular fingerprints using graph neural networks.
|
|
58
|
+
Unlike traditional fingerprints (e.g., ECFP/Morgan), these are fully
|
|
59
|
+
differentiable and can be optimized for specific tasks.
|
|
60
|
+
|
|
61
|
+
The fingerprint is computed by:
|
|
62
|
+
1. Message passing to compute atom representations
|
|
63
|
+
2. Sum pooling to get graph-level representation
|
|
64
|
+
3. Linear projection to fingerprint dimension
|
|
65
|
+
4. Optional L2 normalization
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
```python
|
|
69
|
+
config = MolecularFingerprintConfig(fingerprint_dim=128)
|
|
70
|
+
fp_op = DifferentiableMolecularFingerprint(config, rngs=nnx.Rngs(42))
|
|
71
|
+
data = {"node_features": nodes, "adjacency": adj, "node_mask": mask}
|
|
72
|
+
result, _, _ = fp_op.apply(data, {}, None)
|
|
73
|
+
fingerprint = result["fingerprint"] # shape: (128,)
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
config: MolecularFingerprintConfig,
|
|
80
|
+
*,
|
|
81
|
+
rngs: nnx.Rngs | None = None,
|
|
82
|
+
name: str | None = None,
|
|
83
|
+
):
|
|
84
|
+
"""Initialize fingerprint operator.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Fingerprint configuration.
|
|
88
|
+
rngs: Flax NNX random number generators.
|
|
89
|
+
name: Optional name for the operator.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
92
|
+
|
|
93
|
+
rngs = initialize_graph_encoder(
|
|
94
|
+
self,
|
|
95
|
+
rngs=rngs,
|
|
96
|
+
in_features=config.in_features,
|
|
97
|
+
num_layers=config.num_layers,
|
|
98
|
+
hidden_dim=config.hidden_dim,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Projection to fingerprint dimension
|
|
102
|
+
self.projection = nnx.Linear(
|
|
103
|
+
in_features=config.hidden_dim,
|
|
104
|
+
out_features=config.fingerprint_dim,
|
|
105
|
+
rngs=rngs,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def apply(
|
|
109
|
+
self,
|
|
110
|
+
data: dict[str, Any],
|
|
111
|
+
state: dict[str, Any],
|
|
112
|
+
metadata: dict[str, Any] | None,
|
|
113
|
+
random_params: Any = None,
|
|
114
|
+
stats: dict[str, Any] | None = None,
|
|
115
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
|
|
116
|
+
"""Compute molecular fingerprint.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
data: Input data containing:
|
|
120
|
+
- node_features: (num_nodes, num_features) atom features
|
|
121
|
+
- adjacency: (num_nodes, num_nodes) adjacency matrix
|
|
122
|
+
- node_mask: (num_nodes,) mask for valid nodes
|
|
123
|
+
state: Per-element state (passed through).
|
|
124
|
+
metadata: Optional metadata.
|
|
125
|
+
random_params: Unused random parameters.
|
|
126
|
+
stats: Optional statistics dictionary.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple of:
|
|
130
|
+
- data with added "fingerprint" key
|
|
131
|
+
- unchanged state
|
|
132
|
+
- unchanged metadata
|
|
133
|
+
"""
|
|
134
|
+
graph_repr = graph_sum_readout(data, self.encoder)
|
|
135
|
+
|
|
136
|
+
# Project to fingerprint dimension
|
|
137
|
+
fingerprint = self.projection(graph_repr)
|
|
138
|
+
|
|
139
|
+
# Optional normalization
|
|
140
|
+
if self.config.normalize:
|
|
141
|
+
fingerprint = fingerprint / (jnp.linalg.norm(fingerprint) + 1e-8)
|
|
142
|
+
|
|
143
|
+
return attach_fingerprint(data, fingerprint), state, metadata
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_fingerprint_operator(
|
|
147
|
+
fingerprint_dim: int = 256,
|
|
148
|
+
num_layers: int = 3,
|
|
149
|
+
normalize: bool = False,
|
|
150
|
+
seed: int = 42,
|
|
151
|
+
) -> DifferentiableMolecularFingerprint:
|
|
152
|
+
"""Create a molecular fingerprint operator.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
fingerprint_dim: Output fingerprint dimension.
|
|
156
|
+
num_layers: Number of message passing layers.
|
|
157
|
+
normalize: Whether to L2-normalize output.
|
|
158
|
+
seed: Random seed.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Configured DifferentiableMolecularFingerprint.
|
|
162
|
+
"""
|
|
163
|
+
config = MolecularFingerprintConfig(
|
|
164
|
+
fingerprint_dim=fingerprint_dim,
|
|
165
|
+
num_layers=num_layers,
|
|
166
|
+
normalize=normalize,
|
|
167
|
+
)
|
|
168
|
+
return DifferentiableMolecularFingerprint(config, rngs=nnx.Rngs(seed))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# =============================================================================
|
|
172
|
+
# Circular Fingerprint (ECFP/Morgan) Operator
|
|
173
|
+
# =============================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class CircularFingerprintConfig(OperatorConfig):
|
|
178
|
+
# pylint: disable=too-many-instance-attributes
|
|
179
|
+
"""Configuration for circular fingerprint operator (ECFP/Morgan).
|
|
180
|
+
|
|
181
|
+
Attributes:
|
|
182
|
+
radius: Fingerprint radius. ECFP4 = radius 2, ECFP6 = radius 3.
|
|
183
|
+
n_bits: Number of bits in fingerprint (default: 2048).
|
|
184
|
+
use_chirality: Include chirality in fingerprint (default: False).
|
|
185
|
+
use_bond_types: Include bond type information (default: True).
|
|
186
|
+
use_features: Use pharmacophoric features (FCFP variant, default: False).
|
|
187
|
+
differentiable: Use learned hash functions for gradients (default: True).
|
|
188
|
+
hash_hidden_dim: Hidden dimension for hash network (default: 128).
|
|
189
|
+
temperature: Temperature for soft bit assignment (default: 1.0).
|
|
190
|
+
in_features: Number of input node features (default: 4).
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
radius: int = 2 # ECFP4 = radius 2, ECFP6 = radius 3
|
|
194
|
+
n_bits: int = 2048
|
|
195
|
+
use_chirality: bool = False
|
|
196
|
+
use_bond_types: bool = True
|
|
197
|
+
use_features: bool = False # FCFP variant if True
|
|
198
|
+
differentiable: bool = True # Use learned hash functions
|
|
199
|
+
hash_hidden_dim: int = 128
|
|
200
|
+
temperature: float = 1.0 # For soft bit assignment
|
|
201
|
+
in_features: int = 4 # Number of input node features
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CircularFingerprintOperator(OperatorModule):
|
|
205
|
+
"""Differentiable circular fingerprints (ECFP/Morgan).
|
|
206
|
+
|
|
207
|
+
For differentiable=True:
|
|
208
|
+
Uses message passing to aggregate substructure information,
|
|
209
|
+
then learned "soft hash" functions for bit assignment.
|
|
210
|
+
Gradients flow through the entire computation.
|
|
211
|
+
|
|
212
|
+
For differentiable=False:
|
|
213
|
+
Wraps RDKit implementation for exact ECFP.
|
|
214
|
+
No gradient flow (useful for inference/comparison).
|
|
215
|
+
|
|
216
|
+
The differentiable version approximates ECFP behavior while
|
|
217
|
+
enabling end-to-end optimization of the fingerprint representation.
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
```python
|
|
221
|
+
config = CircularFingerprintConfig(radius=2, n_bits=1024)
|
|
222
|
+
fp_op = CircularFingerprintOperator(config, rngs=nnx.Rngs(0))
|
|
223
|
+
data = {"node_features": node_feats, "adjacency": adj}
|
|
224
|
+
result, state, meta = fp_op.apply(data, {}, None)
|
|
225
|
+
fingerprint = result["fingerprint"] # Shape: (n_bits,)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
References:
|
|
229
|
+
Rogers, David, and Mathew Hahn. "Extended-connectivity fingerprints."
|
|
230
|
+
Journal of chemical information and modeling 50.5 (2010): 742-754.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
config: CircularFingerprintConfig,
|
|
236
|
+
*,
|
|
237
|
+
rngs: nnx.Rngs | None = None,
|
|
238
|
+
):
|
|
239
|
+
"""Initialize CircularFingerprintOperator.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
config: Circular fingerprint configuration.
|
|
243
|
+
rngs: Flax NNX random number generators.
|
|
244
|
+
"""
|
|
245
|
+
super().__init__(config, rngs=rngs)
|
|
246
|
+
|
|
247
|
+
stabilize_operator_id(self)
|
|
248
|
+
rngs = ensure_rngs(rngs)
|
|
249
|
+
|
|
250
|
+
if config.differentiable:
|
|
251
|
+
# Message passing layers for substructure aggregation
|
|
252
|
+
# Each layer corresponds to one radius step
|
|
253
|
+
self.message_passing = build_encoder(
|
|
254
|
+
hidden_dim=config.hash_hidden_dim,
|
|
255
|
+
num_layers=config.radius,
|
|
256
|
+
in_features=config.in_features,
|
|
257
|
+
rngs=rngs,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Learned hash function: maps substructure embedding to bit indices
|
|
261
|
+
self.hash_network = nnx.Sequential(
|
|
262
|
+
nnx.Linear(config.hash_hidden_dim, config.hash_hidden_dim, rngs=rngs),
|
|
263
|
+
nnx.relu,
|
|
264
|
+
nnx.Linear(config.hash_hidden_dim, config.n_bits, rngs=rngs),
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
# RDKit mode - no learnable parameters needed
|
|
268
|
+
try:
|
|
269
|
+
from rdkit import Chem
|
|
270
|
+
from rdkit.Chem import AllChem
|
|
271
|
+
|
|
272
|
+
self._Chem = Chem
|
|
273
|
+
self._AllChem = AllChem
|
|
274
|
+
except ImportError as e:
|
|
275
|
+
raise ImportError(
|
|
276
|
+
"CircularFingerprintOperator with differentiable=False "
|
|
277
|
+
"requires RDKit: pip install rdkit"
|
|
278
|
+
) from e
|
|
279
|
+
|
|
280
|
+
@nnx.jit
|
|
281
|
+
def _compute_differentiable_fp(
|
|
282
|
+
self,
|
|
283
|
+
node_features: jnp.ndarray,
|
|
284
|
+
adjacency: jnp.ndarray,
|
|
285
|
+
edge_features: jnp.ndarray | None = None,
|
|
286
|
+
node_mask: jnp.ndarray | None = None,
|
|
287
|
+
) -> jnp.ndarray:
|
|
288
|
+
"""Compute differentiable circular fingerprint (JIT compiled).
|
|
289
|
+
|
|
290
|
+
Uses message passing to aggregate local substructure information,
|
|
291
|
+
then applies learned hash functions for soft bit assignment.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
node_features: (num_nodes, num_features) atom features
|
|
295
|
+
adjacency: (num_nodes, num_nodes) adjacency matrix
|
|
296
|
+
edge_features: Optional edge features
|
|
297
|
+
node_mask: Optional mask for valid nodes
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Fingerprint vector of shape (n_bits,)
|
|
301
|
+
"""
|
|
302
|
+
# Message passing to compute atom representations with substructure info
|
|
303
|
+
# After 'radius' layers, each atom embedding contains info about
|
|
304
|
+
# atoms within 'radius' bonds
|
|
305
|
+
node_hidden = self.message_passing(node_features, adjacency, edge_features)
|
|
306
|
+
|
|
307
|
+
# Apply node mask if provided
|
|
308
|
+
if node_mask is not None:
|
|
309
|
+
node_hidden = node_hidden * node_mask[:, None]
|
|
310
|
+
|
|
311
|
+
# Compute soft hash for each atom's environment
|
|
312
|
+
# hash_logits: (num_nodes, n_bits)
|
|
313
|
+
hash_logits = self.hash_network(node_hidden)
|
|
314
|
+
|
|
315
|
+
# Apply temperature-scaled softmax for soft bit assignment
|
|
316
|
+
# Higher temperature = softer bits, lower = sharper (more binary-like)
|
|
317
|
+
soft_bits = soft_ops.greater(hash_logits, 0.0, softness=self.config.temperature)
|
|
318
|
+
|
|
319
|
+
# Aggregate across atoms using max (OR-like) operation
|
|
320
|
+
# This mimics how ECFP sets bits based on any substructure match
|
|
321
|
+
fingerprint = soft_ops.max(soft_bits, axis=0, softness=self.config.temperature)
|
|
322
|
+
|
|
323
|
+
return fingerprint
|
|
324
|
+
|
|
325
|
+
def _compute_rdkit_fp(self, smiles: str) -> jnp.ndarray:
|
|
326
|
+
"""Compute exact ECFP using RDKit.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
smiles: SMILES string
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Binary fingerprint vector of shape (n_bits,)
|
|
333
|
+
"""
|
|
334
|
+
mol = self._Chem.MolFromSmiles(smiles)
|
|
335
|
+
if mol is None:
|
|
336
|
+
# Return zero fingerprint for invalid SMILES
|
|
337
|
+
return jnp.zeros(self.config.n_bits, dtype=jnp.float32)
|
|
338
|
+
|
|
339
|
+
# Compute Morgan/ECFP fingerprint
|
|
340
|
+
if self.config.use_features:
|
|
341
|
+
# FCFP variant - uses pharmacophoric features
|
|
342
|
+
fp = self._AllChem.GetMorganFingerprintAsBitVect(
|
|
343
|
+
mol,
|
|
344
|
+
self.config.radius,
|
|
345
|
+
nBits=self.config.n_bits,
|
|
346
|
+
useChirality=self.config.use_chirality,
|
|
347
|
+
useBondTypes=self.config.use_bond_types,
|
|
348
|
+
useFeatures=True,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
# Standard ECFP
|
|
352
|
+
fp = self._AllChem.GetMorganFingerprintAsBitVect(
|
|
353
|
+
mol,
|
|
354
|
+
self.config.radius,
|
|
355
|
+
nBits=self.config.n_bits,
|
|
356
|
+
useChirality=self.config.use_chirality,
|
|
357
|
+
useBondTypes=self.config.use_bond_types,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Convert to numpy first (faster), then to JAX
|
|
361
|
+
# RDKit provides efficient conversion to numpy
|
|
362
|
+
arr = np.zeros(self.config.n_bits, dtype=np.float32)
|
|
363
|
+
self._AllChem.DataStructs.ConvertToNumpyArray(fp, arr)
|
|
364
|
+
return jnp.asarray(arr)
|
|
365
|
+
|
|
366
|
+
def apply(
|
|
367
|
+
self,
|
|
368
|
+
data: dict[str, Any],
|
|
369
|
+
state: dict[str, Any],
|
|
370
|
+
metadata: dict[str, Any] | None,
|
|
371
|
+
random_params: Any = None,
|
|
372
|
+
stats: dict[str, Any] | None = None,
|
|
373
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
|
|
374
|
+
"""Compute circular fingerprint.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
data: Input data containing either:
|
|
378
|
+
For differentiable=True:
|
|
379
|
+
- node_features: (num_nodes, num_features) atom features
|
|
380
|
+
- adjacency: (num_nodes, num_nodes) adjacency matrix
|
|
381
|
+
- node_mask: (num_nodes,) optional mask for valid nodes
|
|
382
|
+
For differentiable=False:
|
|
383
|
+
- smiles: SMILES string
|
|
384
|
+
state: Per-element state (passed through).
|
|
385
|
+
metadata: Optional metadata.
|
|
386
|
+
random_params: Unused random parameters.
|
|
387
|
+
stats: Optional statistics dictionary.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Tuple of:
|
|
391
|
+
- data with added "fingerprint" key
|
|
392
|
+
- unchanged state
|
|
393
|
+
- unchanged metadata
|
|
394
|
+
"""
|
|
395
|
+
if self.config.differentiable:
|
|
396
|
+
node_features, adjacency, edge_features, node_mask = unpack_graph_inputs(data)
|
|
397
|
+
|
|
398
|
+
fp = self._compute_differentiable_fp(
|
|
399
|
+
node_features,
|
|
400
|
+
adjacency,
|
|
401
|
+
edge_features, # pyright: ignore[reportArgumentType]
|
|
402
|
+
node_mask, # pyright: ignore[reportArgumentType]
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
smiles = data["smiles"]
|
|
406
|
+
fp = self._compute_rdkit_fp(smiles)
|
|
407
|
+
|
|
408
|
+
return attach_fingerprint(data, fp), state, metadata
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# =============================================================================
|
|
412
|
+
# Factory Functions for Common ECFP Configurations
|
|
413
|
+
# =============================================================================
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def create_ecfp4_operator(
|
|
417
|
+
n_bits: int = 2048,
|
|
418
|
+
differentiable: bool = True,
|
|
419
|
+
rngs: nnx.Rngs | None = None,
|
|
420
|
+
) -> CircularFingerprintOperator:
|
|
421
|
+
"""Create ECFP4 (radius=2) fingerprint operator.
|
|
422
|
+
|
|
423
|
+
ECFP4 captures substructures within 4 bonds (radius 2).
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
n_bits: Number of fingerprint bits (default: 2048).
|
|
427
|
+
differentiable: Use learned hash functions (default: True).
|
|
428
|
+
rngs: Random number generators.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Configured CircularFingerprintOperator.
|
|
432
|
+
"""
|
|
433
|
+
config = CircularFingerprintConfig(
|
|
434
|
+
radius=2,
|
|
435
|
+
n_bits=n_bits,
|
|
436
|
+
differentiable=differentiable,
|
|
437
|
+
)
|
|
438
|
+
return CircularFingerprintOperator(config, rngs=rngs or nnx.Rngs(0))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def create_ecfp6_operator(
|
|
442
|
+
n_bits: int = 2048,
|
|
443
|
+
differentiable: bool = True,
|
|
444
|
+
rngs: nnx.Rngs | None = None,
|
|
445
|
+
) -> CircularFingerprintOperator:
|
|
446
|
+
"""Create ECFP6 (radius=3) fingerprint operator.
|
|
447
|
+
|
|
448
|
+
ECFP6 captures substructures within 6 bonds (radius 3).
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
n_bits: Number of fingerprint bits (default: 2048).
|
|
452
|
+
differentiable: Use learned hash functions (default: True).
|
|
453
|
+
rngs: Random number generators.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Configured CircularFingerprintOperator.
|
|
457
|
+
"""
|
|
458
|
+
config = CircularFingerprintConfig(
|
|
459
|
+
radius=3,
|
|
460
|
+
n_bits=n_bits,
|
|
461
|
+
differentiable=differentiable,
|
|
462
|
+
)
|
|
463
|
+
return CircularFingerprintOperator(config, rngs=rngs or nnx.Rngs(0))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def create_fcfp4_operator(
|
|
467
|
+
n_bits: int = 2048,
|
|
468
|
+
differentiable: bool = True,
|
|
469
|
+
rngs: nnx.Rngs | None = None,
|
|
470
|
+
) -> CircularFingerprintOperator:
|
|
471
|
+
"""Create FCFP4 (feature-based, radius=2) fingerprint operator.
|
|
472
|
+
|
|
473
|
+
FCFP4 uses pharmacophoric atom features instead of atomic properties.
|
|
474
|
+
Better for finding molecules with similar biological activity.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
n_bits: Number of fingerprint bits (default: 2048).
|
|
478
|
+
differentiable: Use learned hash functions (default: True).
|
|
479
|
+
rngs: Random number generators.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Configured CircularFingerprintOperator.
|
|
483
|
+
"""
|
|
484
|
+
config = CircularFingerprintConfig(
|
|
485
|
+
radius=2,
|
|
486
|
+
n_bits=n_bits,
|
|
487
|
+
use_features=True,
|
|
488
|
+
differentiable=differentiable,
|
|
489
|
+
)
|
|
490
|
+
return CircularFingerprintOperator(config, rngs=rngs or nnx.Rngs(0))
|