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,285 @@
1
+ """Multi-task ADMET property prediction operator.
2
+
3
+ This module implements a ChemProp-style multi-task ADMET predictor
4
+ for predicting Absorption, Distribution, Metabolism, Excretion, and
5
+ Toxicity properties of drug candidates.
6
+
7
+ The implementation follows the TDC ADMET Benchmark with 22 standard endpoints.
8
+
9
+ References:
10
+ - https://tdcommons.ai/benchmark/admet_group/overview/
11
+ - https://github.com/chemprop/chemprop
12
+ - Swanson et al. "ADMET-AI" Bioinformatics 2024
13
+ """
14
+
15
+ import logging
16
+ from dataclasses import dataclass
17
+ from typing import Any
18
+
19
+ import jax.numpy as jnp
20
+ from artifex.generative_models.core.base import MLP
21
+ from datarax.core.config import OperatorConfig
22
+ from datarax.core.operator import OperatorModule
23
+ from flax import nnx
24
+
25
+ from diffbio.operators.drug_discovery._graph_utils import (
26
+ build_optional_dropout,
27
+ graph_sum_readout,
28
+ initialize_graph_encoder_from_config,
29
+ )
30
+ from diffbio.utils.nn_utils import ARTIFEX_RELU_MLP_KWARGS
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Standard TDC ADMET benchmark task names (22 tasks)
35
+ ADMET_TASK_NAMES: list[str] = [
36
+ # Absorption (6)
37
+ "Caco2_Wang",
38
+ "HIA_Hou",
39
+ "Pgp_Broccatelli",
40
+ "Bioavailability_Ma",
41
+ "Lipophilicity_AstraZeneca",
42
+ "Solubility_AqSolDB",
43
+ # Distribution (3)
44
+ "BBB_Martins",
45
+ "PPBR_AZ",
46
+ "VDss_Lombardo",
47
+ # Metabolism (6)
48
+ "CYP2C9_Veith",
49
+ "CYP2D6_Veith",
50
+ "CYP3A4_Veith",
51
+ "CYP2C9_Substrate_CarbonMangels",
52
+ "CYP2D6_Substrate_CarbonMangels",
53
+ "CYP3A4_Substrate_CarbonMangels",
54
+ # Excretion (3)
55
+ "Half_Life_Obach",
56
+ "Clearance_Hepatocyte_AZ",
57
+ "Clearance_Microsome_AZ",
58
+ # Toxicity (4)
59
+ "LD50_Zhu",
60
+ "hERG",
61
+ "AMES",
62
+ "DILI",
63
+ ]
64
+
65
+ # Task types: classification or regression
66
+ ADMET_TASK_TYPES: dict[str, str] = {
67
+ # Absorption
68
+ "Caco2_Wang": "regression",
69
+ "HIA_Hou": "classification",
70
+ "Pgp_Broccatelli": "classification",
71
+ "Bioavailability_Ma": "classification",
72
+ "Lipophilicity_AstraZeneca": "regression",
73
+ "Solubility_AqSolDB": "regression",
74
+ # Distribution
75
+ "BBB_Martins": "classification",
76
+ "PPBR_AZ": "regression",
77
+ "VDss_Lombardo": "regression",
78
+ # Metabolism
79
+ "CYP2C9_Veith": "classification",
80
+ "CYP2D6_Veith": "classification",
81
+ "CYP3A4_Veith": "classification",
82
+ "CYP2C9_Substrate_CarbonMangels": "classification",
83
+ "CYP2D6_Substrate_CarbonMangels": "classification",
84
+ "CYP3A4_Substrate_CarbonMangels": "classification",
85
+ # Excretion
86
+ "Half_Life_Obach": "regression",
87
+ "Clearance_Hepatocyte_AZ": "regression",
88
+ "Clearance_Microsome_AZ": "regression",
89
+ # Toxicity
90
+ "LD50_Zhu": "regression",
91
+ "hERG": "classification",
92
+ "AMES": "classification",
93
+ "DILI": "classification",
94
+ }
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class ADMETConfig(OperatorConfig):
99
+ # pylint: disable=too-many-instance-attributes
100
+ """Configuration for ADMET property predictor.
101
+
102
+ Attributes:
103
+ hidden_dim: Hidden dimension for message passing (default: 300).
104
+ num_message_passing_steps: Number of D-MPNN iterations (default: 3).
105
+ num_tasks: Number of ADMET prediction tasks (default: 22).
106
+ dropout_rate: Dropout rate for regularization (default: 0.0).
107
+ in_features: Number of input node features (default: 4).
108
+ num_edge_features: Number of edge features (default: 4).
109
+ ffn_hidden_dim: FFN hidden dimension (default: same as hidden_dim).
110
+ ffn_num_layers: Number of FFN layers (default: 2).
111
+ apply_task_activations: Apply sigmoid for classification tasks (default: False).
112
+ """
113
+
114
+ hidden_dim: int = 300
115
+ num_message_passing_steps: int = 3
116
+ num_tasks: int = 22
117
+ dropout_rate: float = 0.0
118
+ in_features: int = 4
119
+ num_edge_features: int = 4
120
+ ffn_hidden_dim: int | None = None
121
+ ffn_num_layers: int = 2
122
+ apply_task_activations: bool = False
123
+
124
+
125
+ class ADMETPredictor(OperatorModule):
126
+ """Multi-task ADMET property predictor.
127
+
128
+ Implements a ChemProp-style directed message passing neural network
129
+ for predicting multiple ADMET properties simultaneously. The architecture
130
+ uses a shared molecular encoder with task-specific prediction heads.
131
+
132
+ Architecture:
133
+ 1. Message passing encoder (D-MPNN style)
134
+ 2. Graph-level readout via sum pooling
135
+ 3. Shared feed-forward layers
136
+ 4. Task-specific output heads
137
+
138
+ The 22 standard TDC ADMET endpoints cover:
139
+ - Absorption: Caco2, HIA, Pgp, Bioavailability, Lipophilicity, Solubility
140
+ - Distribution: BBB, PPBR, VDss
141
+ - Metabolism: CYP enzymes (2C9, 2D6, 3A4) inhibition and substrate
142
+ - Excretion: Half-life, Hepatocyte clearance, Microsome clearance
143
+ - Toxicity: LD50, hERG, AMES, DILI
144
+
145
+ Example:
146
+ ```python
147
+ config = ADMETConfig(hidden_dim=256, num_tasks=22)
148
+ predictor = ADMETPredictor(config, rngs=nnx.Rngs(42))
149
+ data = {"node_features": nodes, "adjacency": adj, "node_mask": mask}
150
+ result, _, _ = predictor.apply(data, {}, None)
151
+ predictions = result["predictions"] # shape: (22,)
152
+ ```
153
+
154
+ References:
155
+ - https://tdcommons.ai/benchmark/admet_group/overview/
156
+ - Yang et al. "Analyzing Learned Molecular Representations" JCIM 2019
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ config: ADMETConfig,
162
+ *,
163
+ rngs: nnx.Rngs | None = None,
164
+ name: str | None = None,
165
+ ):
166
+ """Initialize ADMET predictor.
167
+
168
+ Args:
169
+ config: ADMET configuration.
170
+ rngs: Flax NNX random number generators.
171
+ name: Optional name for the operator.
172
+ """
173
+ super().__init__(config, rngs=rngs, name=name)
174
+
175
+ rngs = initialize_graph_encoder_from_config(self, config, rngs=rngs)
176
+
177
+ # FFN hidden dim defaults to hidden_dim
178
+ ffn_hidden = config.ffn_hidden_dim or config.hidden_dim
179
+
180
+ if config.ffn_num_layers > 1:
181
+ self.ffn_backbone = MLP(
182
+ hidden_dims=[ffn_hidden] * (config.ffn_num_layers - 1),
183
+ in_features=config.hidden_dim,
184
+ dropout_rate=config.dropout_rate,
185
+ rngs=rngs,
186
+ **ARTIFEX_RELU_MLP_KWARGS,
187
+ )
188
+ else:
189
+ self.ffn_backbone = None
190
+
191
+ # Task-specific output heads (one per ADMET task)
192
+ last_hidden = ffn_hidden if config.ffn_num_layers > 1 else config.hidden_dim
193
+ task_heads = [nnx.Linear(last_hidden, 1, rngs=rngs) for _ in range(config.num_tasks)]
194
+ self.task_heads = nnx.List(task_heads)
195
+
196
+ # Dropout
197
+ self.dropout = build_optional_dropout(config.dropout_rate, rngs=rngs)
198
+
199
+ def apply(
200
+ self,
201
+ data: dict[str, Any],
202
+ state: dict[str, Any],
203
+ metadata: dict[str, Any] | None,
204
+ random_params: Any = None, # noqa: ARG002
205
+ stats: dict[str, Any] | None = None, # noqa: ARG002
206
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
207
+ """Predict ADMET properties from molecular graph.
208
+
209
+ Args:
210
+ data: Input data containing:
211
+ - node_features: (num_nodes, num_features) atom features
212
+ - adjacency: (num_nodes, num_nodes) adjacency matrix
213
+ - edge_features: Optional (num_nodes, num_nodes, num_edge_features)
214
+ - node_mask: (num_nodes,) mask for valid nodes
215
+ state: Per-element state (passed through).
216
+ metadata: Optional metadata.
217
+ random_params: Unused random parameters.
218
+ stats: Optional statistics dictionary.
219
+
220
+ Returns:
221
+ Tuple of:
222
+ - data with added "predictions" and "task_predictions" keys
223
+ - unchanged state
224
+ - unchanged metadata
225
+ """
226
+ del random_params, stats # Unused
227
+
228
+ graph_repr = graph_sum_readout(data, self.encoder, dropout=self.dropout)
229
+
230
+ h = graph_repr
231
+ if self.ffn_backbone is not None:
232
+ ffn_output = self.ffn_backbone(h)
233
+ if isinstance(ffn_output, tuple):
234
+ raise TypeError("ADMETPredictor shared FFN must return a single tensor output.")
235
+ h = ffn_output
236
+
237
+ # Task-specific predictions
238
+ task_predictions = []
239
+ for i, head in enumerate(self.task_heads):
240
+ pred = head(h).squeeze(-1)
241
+
242
+ # Apply activation for classification tasks if configured
243
+ if self.config.apply_task_activations:
244
+ task_name = ADMET_TASK_NAMES[i] if i < len(ADMET_TASK_NAMES) else f"task_{i}"
245
+ if ADMET_TASK_TYPES.get(task_name) == "classification":
246
+ pred = nnx.sigmoid(pred)
247
+
248
+ task_predictions.append(pred)
249
+
250
+ # Stack all predictions
251
+ predictions = jnp.stack(task_predictions)
252
+
253
+ result = {
254
+ **data,
255
+ "predictions": predictions,
256
+ "task_predictions": task_predictions,
257
+ "graph_representation": graph_repr,
258
+ }
259
+
260
+ return result, state, metadata
261
+
262
+
263
+ def create_admet_predictor(
264
+ hidden_dim: int = 300,
265
+ num_layers: int = 3,
266
+ dropout_rate: float = 0.0,
267
+ seed: int = 42,
268
+ ) -> ADMETPredictor:
269
+ """Create an ADMET predictor with standard configuration.
270
+
271
+ Args:
272
+ hidden_dim: Hidden dimension for message passing.
273
+ num_layers: Number of message passing steps.
274
+ dropout_rate: Dropout rate.
275
+ seed: Random seed.
276
+
277
+ Returns:
278
+ Configured ADMETPredictor.
279
+ """
280
+ config = ADMETConfig(
281
+ hidden_dim=hidden_dim,
282
+ num_message_passing_steps=num_layers,
283
+ dropout_rate=dropout_rate,
284
+ )
285
+ return ADMETPredictor(config, rngs=nnx.Rngs(seed))
@@ -0,0 +1,411 @@
1
+ """AttentiveFP: Attention-based graph fingerprint for molecular property prediction.
2
+
3
+ This module implements the AttentiveFP architecture from Xiong et al. 2019,
4
+ which combines graph attention mechanisms with GRU cells for molecular
5
+ representation learning.
6
+
7
+ The architecture provides:
8
+
9
+ - Interpretable attention weights showing atom importance
10
+ - Two-level aggregation (atom-level and molecule-level)
11
+ - GRU-based state updates for iterative refinement
12
+
13
+ References:
14
+ - Xiong et al. "Pushing the Boundaries of Molecular Representation for
15
+ Drug Discovery with the Graph Attention Mechanism" JCIM 2019
16
+ - https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.nn.models.AttentiveFP.html
17
+ """
18
+
19
+ import logging
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ import jax
24
+ import jax.numpy as jnp
25
+ from datarax.core.config import OperatorConfig
26
+ from datarax.core.operator import OperatorModule
27
+ from flax import nnx
28
+
29
+ from diffbio.operators.drug_discovery._graph_utils import (
30
+ build_optional_dropout,
31
+ stabilize_operator_id,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class _AttentiveFPArchitectureConfig:
39
+ """Architecture configuration for AttentiveFP."""
40
+
41
+ hidden_dim: int = 200
42
+ out_dim: int = 200
43
+ num_layers: int = 2
44
+ num_timesteps: int = 2
45
+ dropout_rate: float = 0.0
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class _AttentiveFPInputConfig:
50
+ """Graph input configuration for AttentiveFP."""
51
+
52
+ in_features: int = 39
53
+ edge_dim: int = 10
54
+ negative_slope: float = 0.2
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class AttentiveFPConfig(
59
+ _AttentiveFPArchitectureConfig,
60
+ _AttentiveFPInputConfig,
61
+ OperatorConfig,
62
+ ):
63
+ """Configuration for AttentiveFP operator."""
64
+
65
+ def __post_init__(self) -> None:
66
+ """Validate the AttentiveFP configuration."""
67
+ super().__post_init__()
68
+
69
+ if self.hidden_dim <= 0:
70
+ raise ValueError("hidden_dim must be positive.")
71
+ if self.out_dim <= 0:
72
+ raise ValueError("out_dim must be positive.")
73
+ if self.num_layers <= 0:
74
+ raise ValueError("num_layers must be positive.")
75
+ if self.num_timesteps <= 0:
76
+ raise ValueError("num_timesteps must be positive.")
77
+ if not 0.0 <= self.dropout_rate < 1.0:
78
+ raise ValueError("dropout_rate must be in [0.0, 1.0).")
79
+ if self.in_features <= 0:
80
+ raise ValueError("in_features must be positive.")
81
+ if self.edge_dim < 0:
82
+ raise ValueError("edge_dim must be non-negative.")
83
+ if self.negative_slope < 0.0:
84
+ raise ValueError("negative_slope must be non-negative.")
85
+
86
+
87
+ class GATEConv(nnx.Module):
88
+ """Graph Attention with Edge features (GATE) convolution layer.
89
+
90
+ Combines node features with edge features using attention mechanism.
91
+ This is the core building block for AttentiveFP's atom-level processing.
92
+
93
+ The attention mechanism computes:
94
+ alpha_ij = softmax_j(LeakyReLU(a^T [Wh_i || Wh_j || We_ij]))
95
+
96
+ where || denotes concatenation and e_ij are edge features.
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ in_dim: int,
102
+ out_dim: int,
103
+ edge_dim: int,
104
+ *,
105
+ negative_slope: float = 0.2,
106
+ rngs: nnx.Rngs,
107
+ ):
108
+ """Initialize GATE convolution.
109
+
110
+ Args:
111
+ in_dim: Input feature dimension.
112
+ out_dim: Output feature dimension.
113
+ edge_dim: Edge feature dimension.
114
+ negative_slope: LeakyReLU negative slope.
115
+ rngs: Random number generators.
116
+ """
117
+ super().__init__()
118
+ self.negative_slope = negative_slope
119
+
120
+ # Linear transformations
121
+ self.linear_src = nnx.Linear(in_dim, out_dim, rngs=rngs)
122
+ self.linear_dst = nnx.Linear(in_dim, out_dim, rngs=rngs)
123
+
124
+ # Edge feature projection (if edge_dim > 0)
125
+ self.use_edge_features = edge_dim > 0
126
+ if self.use_edge_features:
127
+ self.linear_edge = nnx.Linear(edge_dim, out_dim, rngs=rngs)
128
+
129
+ # Attention coefficients
130
+ # Attention is computed as: a^T [src || dst || edge]
131
+ attn_dim = out_dim * 3 if self.use_edge_features else out_dim * 2
132
+ self.attn = nnx.Linear(attn_dim, 1, use_bias=False, rngs=rngs)
133
+
134
+ def __call__(
135
+ self,
136
+ node_features: jnp.ndarray,
137
+ adjacency: jnp.ndarray,
138
+ edge_features: jnp.ndarray | None = None,
139
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
140
+ """Apply GATE convolution.
141
+
142
+ Args:
143
+ node_features: (num_nodes, in_dim) node features
144
+ adjacency: (num_nodes, num_nodes) adjacency matrix
145
+ edge_features: Optional (num_nodes, num_nodes, edge_dim)
146
+
147
+ Returns:
148
+ Tuple of (updated_features, attention_weights)
149
+ """
150
+ num_nodes = node_features.shape[0]
151
+
152
+ # Transform source and destination features
153
+ h_src = self.linear_src(node_features) # (N, out_dim)
154
+ h_dst = self.linear_dst(node_features) # (N, out_dim)
155
+
156
+ # Expand for pairwise computation
157
+ h_src_exp = jnp.expand_dims(h_src, axis=1) # (N, 1, out_dim)
158
+ h_dst_exp = jnp.expand_dims(h_dst, axis=0) # (1, N, out_dim)
159
+
160
+ # Broadcast to (N, N, out_dim)
161
+ h_src_broad = jnp.broadcast_to(h_src_exp, (num_nodes, num_nodes, h_src.shape[-1]))
162
+ h_dst_broad = jnp.broadcast_to(h_dst_exp, (num_nodes, num_nodes, h_dst.shape[-1]))
163
+
164
+ # Concatenate for attention
165
+ if self.use_edge_features and edge_features is not None:
166
+ h_edge = self.linear_edge(edge_features) # (N, N, out_dim)
167
+ attn_input = jnp.concatenate([h_src_broad, h_dst_broad, h_edge], axis=-1)
168
+ else:
169
+ attn_input = jnp.concatenate([h_src_broad, h_dst_broad], axis=-1)
170
+
171
+ # Compute attention logits
172
+ attn_logits = self.attn(attn_input).squeeze(-1) # (N, N)
173
+
174
+ # Apply LeakyReLU
175
+ attn_logits = jnp.where(
176
+ attn_logits >= 0,
177
+ attn_logits,
178
+ self.negative_slope * attn_logits,
179
+ )
180
+
181
+ # Mask non-edges with large negative value
182
+ attn_logits = jnp.where(adjacency > 0, attn_logits, -1e9)
183
+
184
+ # Softmax over neighbors
185
+ attn_weights = jax.nn.softmax(attn_logits, axis=-1) # (N, N)
186
+
187
+ # Mask attention weights for non-edges
188
+ attn_weights = attn_weights * adjacency
189
+
190
+ # Aggregate: weighted sum of transformed features
191
+ out = jnp.einsum("ij,jd->id", attn_weights, h_dst) # (N, out_dim)
192
+
193
+ return out, attn_weights
194
+
195
+
196
+ class AttentiveFP(OperatorModule):
197
+ """AttentiveFP: Attention-based molecular fingerprint.
198
+
199
+ Implements the AttentiveFP architecture with:
200
+ 1. Atom-level attention layers with GRU refinement
201
+ 2. Molecule-level aggregation with attention and GRU
202
+ 3. Final projection to fingerprint dimension
203
+
204
+ The model provides interpretable attention weights that indicate
205
+ which atoms contribute most to the molecular representation.
206
+
207
+ Example:
208
+ ```python
209
+ config = AttentiveFPConfig(hidden_dim=128, out_dim=256)
210
+ afp = AttentiveFP(config, rngs=nnx.Rngs(42))
211
+ data = {"node_features": nodes, "adjacency": adj, "edge_features": edges}
212
+ result, _, _ = afp.apply(data, {}, None)
213
+ fingerprint = result["fingerprint"] # (256,)
214
+ attn = result["attention_weights"] # interpretability
215
+ ```
216
+
217
+ References:
218
+ - Xiong et al. JCIM 2019
219
+ """
220
+
221
+ def __init__(
222
+ self,
223
+ config: AttentiveFPConfig,
224
+ *,
225
+ rngs: nnx.Rngs | None = None,
226
+ ):
227
+ """Initialize AttentiveFP.
228
+
229
+ Args:
230
+ config: AttentiveFP configuration.
231
+ rngs: Flax NNX random number generators.
232
+ """
233
+ super().__init__(config, rngs=rngs)
234
+
235
+ stabilize_operator_id(self)
236
+
237
+ if rngs is None:
238
+ rngs = nnx.Rngs(0)
239
+
240
+ # Initial linear projection
241
+ self.input_proj = nnx.Linear(config.in_features, config.hidden_dim, rngs=rngs)
242
+
243
+ # Atom-level: GATE convolutions with GRU
244
+ atom_convs = []
245
+ atom_grus = []
246
+
247
+ for _ in range(config.num_layers):
248
+ in_dim = config.hidden_dim
249
+ atom_convs.append(
250
+ GATEConv(
251
+ in_dim=in_dim,
252
+ out_dim=config.hidden_dim,
253
+ edge_dim=config.edge_dim,
254
+ negative_slope=config.negative_slope,
255
+ rngs=rngs,
256
+ )
257
+ )
258
+ atom_grus.append(
259
+ nnx.GRUCell(
260
+ in_features=config.hidden_dim,
261
+ hidden_features=config.hidden_dim,
262
+ rngs=rngs,
263
+ )
264
+ )
265
+ self.atom_convs = nnx.List(atom_convs)
266
+ self.atom_grus = nnx.List(atom_grus)
267
+
268
+ # Molecule-level aggregation
269
+ # Attention for global pooling
270
+ self.mol_attn = nnx.Linear(config.hidden_dim, 1, rngs=rngs)
271
+
272
+ # GRU for molecule-level refinement
273
+ mol_grus = []
274
+ for _ in range(config.num_timesteps):
275
+ mol_grus.append(
276
+ nnx.GRUCell(
277
+ in_features=config.hidden_dim,
278
+ hidden_features=config.hidden_dim,
279
+ rngs=rngs,
280
+ )
281
+ )
282
+ self.mol_grus = nnx.List(mol_grus)
283
+
284
+ # Final projection
285
+ self.output_proj = nnx.Linear(config.hidden_dim, config.out_dim, rngs=rngs)
286
+
287
+ # Dropout
288
+ self.dropout = build_optional_dropout(config.dropout_rate, rngs=rngs)
289
+
290
+ def apply(
291
+ self,
292
+ data: dict[str, Any],
293
+ state: dict[str, Any],
294
+ metadata: dict[str, Any] | None,
295
+ random_params: Any = None, # noqa: ARG002
296
+ stats: dict[str, Any] | None = None, # noqa: ARG002
297
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
298
+ """Compute AttentiveFP molecular fingerprint.
299
+
300
+ Args:
301
+ data: Input data containing:
302
+ - node_features: (num_nodes, in_features) atom features
303
+ - adjacency: (num_nodes, num_nodes) adjacency matrix
304
+ - edge_features: Optional (num_nodes, num_nodes, edge_dim)
305
+ - node_mask: (num_nodes,) optional mask for valid nodes
306
+ state: Per-element state (passed through).
307
+ metadata: Optional metadata.
308
+ random_params: Unused random parameters.
309
+ stats: Optional statistics dictionary.
310
+
311
+ Returns:
312
+ Tuple of:
313
+ - data with "fingerprint" and "attention_weights" keys
314
+ - unchanged state
315
+ - unchanged metadata
316
+ """
317
+ del random_params, stats # Unused
318
+
319
+ node_features = data["node_features"]
320
+ adjacency = data["adjacency"]
321
+ edge_features = data.get("edge_features")
322
+ node_mask = data.get("node_mask")
323
+
324
+ # Initial projection
325
+ h = self.input_proj(node_features) # (N, hidden_dim)
326
+
327
+ if self.dropout is not None:
328
+ h = self.dropout(h)
329
+
330
+ # Collect attention weights for interpretability
331
+ all_attention_weights = []
332
+
333
+ # Atom-level message passing with GRU
334
+ for conv, gru in zip(self.atom_convs, self.atom_grus):
335
+ # Graph attention convolution
336
+ h_new, attn_weights = conv(h, adjacency, edge_features)
337
+ all_attention_weights.append(attn_weights)
338
+
339
+ if self.dropout is not None:
340
+ h_new = self.dropout(h_new)
341
+
342
+ # GRU update: h_new is input, h is hidden state
343
+ # GRUCell returns (new_carry, output) tuple - we use new_carry as next h
344
+ h, _ = gru(h, h_new)
345
+
346
+ # Apply node mask
347
+ if node_mask is not None:
348
+ h = h * node_mask[:, None]
349
+
350
+ # Molecule-level aggregation with attention
351
+ # Compute attention scores for global pooling
352
+ attn_scores = self.mol_attn(h).squeeze(-1) # (N,)
353
+ if node_mask is not None:
354
+ attn_scores = jnp.where(node_mask > 0, attn_scores, -1e9)
355
+ attn_probs = jax.nn.softmax(attn_scores) # (N,)
356
+
357
+ # Weighted sum for initial molecule representation
358
+ mol_repr = jnp.einsum("n,nd->d", attn_probs, h) # (hidden_dim,)
359
+
360
+ # GRU refinement at molecule level
361
+ for mol_gru in self.mol_grus:
362
+ # Use atom representations as context
363
+ context = jnp.einsum("n,nd->d", attn_probs, h)
364
+ # GRUCell returns (new_carry, output) tuple
365
+ mol_repr, _ = mol_gru(mol_repr, context)
366
+
367
+ # Final projection
368
+ fingerprint = self.output_proj(mol_repr)
369
+
370
+ if self.dropout is not None:
371
+ fingerprint = self.dropout(fingerprint)
372
+
373
+ result = {
374
+ **data,
375
+ "fingerprint": fingerprint,
376
+ "attention_weights": all_attention_weights,
377
+ "molecule_attention": attn_probs,
378
+ }
379
+
380
+ return result, state, metadata
381
+
382
+
383
+ def create_attentive_fp(
384
+ hidden_dim: int = 200,
385
+ out_dim: int = 200,
386
+ num_layers: int = 2,
387
+ num_timesteps: int = 2,
388
+ dropout_rate: float = 0.0,
389
+ seed: int = 42,
390
+ ) -> AttentiveFP:
391
+ """Create an AttentiveFP operator.
392
+
393
+ Args:
394
+ hidden_dim: Hidden dimension for GNN layers.
395
+ out_dim: Output fingerprint dimension.
396
+ num_layers: Number of atom-level attention layers.
397
+ num_timesteps: Number of molecule-level GRU iterations.
398
+ dropout_rate: Dropout rate.
399
+ seed: Random seed.
400
+
401
+ Returns:
402
+ Configured AttentiveFP.
403
+ """
404
+ config = AttentiveFPConfig(
405
+ hidden_dim=hidden_dim,
406
+ out_dim=out_dim,
407
+ num_layers=num_layers,
408
+ num_timesteps=num_timesteps,
409
+ dropout_rate=dropout_rate,
410
+ )
411
+ return AttentiveFP(config, rngs=nnx.Rngs(seed))