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.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. 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))