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,336 @@
1
+ """Differentiable gene regulatory network inference.
2
+
3
+ This module provides a differentiable alternative to GENIE3/SCENIC for gene
4
+ regulatory network (GRN) inference from single-cell expression data. Instead
5
+ of random forest feature importance, it uses GATv2 graph attention on a
6
+ TF-gene bipartite graph to learn regulatory strengths.
7
+
8
+ Key technique: attention weights on a dense bipartite graph between
9
+ transcription factors and target genes serve as a differentiable proxy for
10
+ regulatory importance scores. Soft L1 sparsity via sigmoid gating promotes
11
+ biologically realistic sparse networks.
12
+
13
+ Algorithm:
14
+ 1. Build a TF-gene bipartite graph (every TF connected to every gene).
15
+ 2. Compute per-edge features from expression: concatenation of TF
16
+ expression, gene expression, and absolute expression difference.
17
+ 3. Apply GATv2 attention -- attention weights between TF-gene pairs
18
+ represent regulatory strength.
19
+ 4. Extract attention weights as the GRN adjacency matrix.
20
+ 5. Apply soft L1 sparsity: ``grn * sigmoid(grn / temperature)``.
21
+ 6. Compute TF activity: ``counts[:, tf_indices] @ grn_matrix``.
22
+
23
+ Applications: SCENIC/GENIE3-style regulatory network reconstruction,
24
+ transcription factor activity estimation, regulon discovery.
25
+ """
26
+
27
+ import logging
28
+ from dataclasses import dataclass
29
+ from typing import Any
30
+
31
+ import jax.numpy as jnp
32
+ from datarax.core.config import OperatorConfig
33
+ from datarax.core.operator import OperatorModule
34
+ from flax import nnx
35
+ from jaxtyping import Array, Float, Int, PyTree
36
+
37
+ from diffbio.constants import EPSILON
38
+ from diffbio.core import soft_ops
39
+ from diffbio.core.gnn_components import GATv2Layer
40
+ from diffbio.utils.nn_utils import ensure_rngs
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class GRNInferenceConfig(OperatorConfig):
47
+ """Configuration for differentiable GRN inference.
48
+
49
+ Attributes:
50
+ n_tfs: Number of transcription factors.
51
+ n_genes: Number of genes in the expression matrix.
52
+ hidden_dim: Hidden dimension for GATv2 attention (must be divisible
53
+ by num_heads).
54
+ num_heads: Number of attention heads in the GATv2 layer.
55
+ sparsity_temperature: Temperature for soft L1 sparsity gating.
56
+ Lower values produce sharper thresholding toward zero.
57
+ sparsity_lambda: L1 regularization weight (used by downstream loss
58
+ functions, not directly by the operator).
59
+ """
60
+
61
+ n_tfs: int = 50
62
+ n_genes: int = 2000
63
+ hidden_dim: int = 64
64
+ num_heads: int = 4
65
+ sparsity_temperature: float = 0.1
66
+ sparsity_lambda: float = 0.01
67
+
68
+
69
+ class DifferentiableGRN(OperatorModule):
70
+ """Differentiable gene regulatory network inference operator.
71
+
72
+ Uses GATv2 graph attention on a TF-gene bipartite graph to infer
73
+ regulatory strengths. Each TF is connected to every gene; the attention
74
+ weight on each edge represents how strongly the TF regulates that gene.
75
+
76
+ This is a novel differentiable alternative to GENIE3's random forest
77
+ feature importance scoring. The key insight is that in GENIE3, each
78
+ gene's expression is predicted from TF expression, and feature importance
79
+ measures regulatory strength. Here, GATv2 attention performs an analogous
80
+ role: TF nodes attend to gene nodes, and the learned attention weights
81
+ capture regulatory relationships.
82
+
83
+ Args:
84
+ config: GRNInferenceConfig with model parameters.
85
+ rngs: Flax NNX random number generators.
86
+ name: Optional operator name.
87
+
88
+ Example:
89
+ >>> config = GRNInferenceConfig(n_tfs=5, n_genes=20, hidden_dim=16)
90
+ >>> op = DifferentiableGRN(config, rngs=nnx.Rngs(0))
91
+ >>> data = {"counts": counts, "tf_indices": jnp.arange(5)}
92
+ >>> result, state, meta = op.apply(data, {}, None)
93
+ >>> result["grn_matrix"].shape
94
+ (5, 20)
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ config: GRNInferenceConfig,
100
+ *,
101
+ rngs: nnx.Rngs | None = None,
102
+ name: str | None = None,
103
+ ) -> None:
104
+ """Initialize the GRN inference operator.
105
+
106
+ Args:
107
+ config: GRN inference configuration.
108
+ rngs: Random number generators for parameter initialization.
109
+ name: Optional operator name.
110
+ """
111
+ super().__init__(config, rngs=rngs, name=name)
112
+
113
+ rngs = ensure_rngs(rngs)
114
+
115
+ self.n_tfs = config.n_tfs
116
+ self.n_genes = config.n_genes
117
+ self.hidden_dim = config.hidden_dim
118
+ self.sparsity_temperature = config.sparsity_temperature
119
+
120
+ # Project 1-d expression scalars to hidden_dim for each node
121
+ self.node_proj = nnx.Linear(
122
+ in_features=1,
123
+ out_features=config.hidden_dim,
124
+ rngs=rngs,
125
+ )
126
+
127
+ # GATv2 layer: attention on the bipartite graph
128
+ # Edge features: [tf_expr, gene_expr, |tf_expr - gene_expr|] -> dim 3
129
+ self.gat_layer = GATv2Layer(
130
+ in_features=config.hidden_dim,
131
+ out_features=config.hidden_dim,
132
+ num_heads=config.num_heads,
133
+ edge_features=3,
134
+ dropout_rate=0.0,
135
+ rngs=rngs,
136
+ )
137
+
138
+ # Projection from GATv2 output to scalar regulatory score per edge
139
+ self.score_proj = nnx.Linear(
140
+ in_features=config.hidden_dim * 2,
141
+ out_features=1,
142
+ rngs=rngs,
143
+ )
144
+
145
+ def _build_bipartite_graph(
146
+ self,
147
+ n_tfs: int,
148
+ n_genes: int,
149
+ ) -> Int[Array, "2 n_edges"]:
150
+ """Build dense bipartite edge index between TFs and genes.
151
+
152
+ TF nodes are indexed ``[0, n_tfs)``, gene nodes are indexed
153
+ ``[n_tfs, n_tfs + n_genes)``. Every TF is connected to every gene.
154
+
155
+ Args:
156
+ n_tfs: Number of transcription factors.
157
+ n_genes: Number of genes.
158
+
159
+ Returns:
160
+ Edge index array of shape ``(2, n_tfs * n_genes)`` where row 0
161
+ is source (TF) indices and row 1 is target (gene) indices.
162
+ """
163
+ # TF indices: 0..n_tfs-1, gene indices: n_tfs..n_tfs+n_genes-1
164
+ tf_ids = jnp.arange(n_tfs)
165
+ gene_ids = jnp.arange(n_tfs, n_tfs + n_genes)
166
+
167
+ # Dense bipartite: every TF connected to every gene
168
+ # sources: each TF repeated n_genes times
169
+ sources = jnp.repeat(tf_ids, n_genes)
170
+ # targets: gene_ids tiled n_tfs times
171
+ targets = jnp.tile(gene_ids, n_tfs)
172
+
173
+ return jnp.stack([sources, targets], axis=0)
174
+
175
+ def _compute_edge_features(
176
+ self,
177
+ mean_counts: Float[Array, "n_genes"],
178
+ tf_indices: Int[Array, "n_tfs"],
179
+ ) -> Float[Array, "n_edges 3"]:
180
+ """Compute per-edge expression features for the bipartite graph.
181
+
182
+ For each TF-gene edge, the feature vector is
183
+ ``[tf_mean_expr, gene_mean_expr, |tf_mean_expr - gene_mean_expr|]``.
184
+
185
+ Args:
186
+ mean_counts: Mean expression per gene across cells ``(n_genes,)``.
187
+ tf_indices: Indices of TF genes in the expression matrix.
188
+
189
+ Returns:
190
+ Edge features of shape ``(n_tfs * n_genes, 3)``.
191
+ """
192
+ n_tfs = tf_indices.shape[0]
193
+ n_genes = mean_counts.shape[0]
194
+
195
+ tf_expr = mean_counts[tf_indices] # (n_tfs,)
196
+
197
+ # Expand to edge level: each TF expression repeated n_genes times
198
+ tf_expr_edges = jnp.repeat(tf_expr, n_genes) # (n_tfs * n_genes,)
199
+ gene_expr_edges = jnp.tile(mean_counts, n_tfs) # (n_tfs * n_genes,)
200
+
201
+ abs_diff = jnp.abs(tf_expr_edges - gene_expr_edges)
202
+
203
+ return jnp.stack([tf_expr_edges, gene_expr_edges, abs_diff], axis=-1)
204
+
205
+ def _extract_grn_from_attention(
206
+ self,
207
+ node_features_updated: Float[Array, "n_nodes hidden_dim"],
208
+ edge_index: Int[Array, "2 n_edges"],
209
+ n_tfs: int,
210
+ n_genes: int,
211
+ ) -> Float[Array, "n_tfs n_genes"]:
212
+ """Extract GRN matrix from updated node representations.
213
+
214
+ Computes a regulatory score for each TF-gene pair by concatenating
215
+ the updated TF and gene node features and projecting to a scalar.
216
+
217
+ Args:
218
+ node_features_updated: Updated node features from GATv2.
219
+ edge_index: Bipartite edge index ``(2, n_edges)``.
220
+ n_tfs: Number of TFs.
221
+ n_genes: Number of genes.
222
+
223
+ Returns:
224
+ Raw GRN matrix of shape ``(n_tfs, n_genes)``.
225
+ """
226
+ sources = edge_index[0] # TF node indices
227
+ targets = edge_index[1] # Gene node indices
228
+
229
+ # Concatenate source (TF) and target (gene) features per edge
230
+ src_features = node_features_updated[sources] # (n_edges, hidden_dim)
231
+ tgt_features = node_features_updated[targets] # (n_edges, hidden_dim)
232
+ edge_repr = jnp.concatenate([src_features, tgt_features], axis=-1)
233
+
234
+ # Project to scalar score per edge
235
+ scores = self.score_proj(edge_repr).squeeze(-1) # (n_edges,)
236
+
237
+ # Reshape to (n_tfs, n_genes)
238
+ return scores.reshape(n_tfs, n_genes)
239
+
240
+ def _apply_soft_sparsity(
241
+ self,
242
+ grn_matrix: Float[Array, "n_tfs n_genes"],
243
+ ) -> Float[Array, "n_tfs n_genes"]:
244
+ """Apply soft L1 sparsity via sigmoid gating.
245
+
246
+ Implements ``grn * sigmoid(grn / temperature)`` which pushes small
247
+ values toward zero while preserving strong regulatory signals.
248
+
249
+ Args:
250
+ grn_matrix: Raw GRN scores.
251
+
252
+ Returns:
253
+ Sparsified GRN matrix.
254
+ """
255
+ gate = soft_ops.greater(grn_matrix, 0.0, softness=self.sparsity_temperature + EPSILON)
256
+ return grn_matrix * gate
257
+
258
+ def apply(
259
+ self,
260
+ data: PyTree,
261
+ state: PyTree,
262
+ metadata: dict[str, Any] | None,
263
+ random_params: Any = None,
264
+ stats: dict[str, Any] | None = None,
265
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
266
+ """Apply differentiable GRN inference.
267
+
268
+ Args:
269
+ data: Dictionary containing:
270
+ - ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
271
+ - ``"tf_indices"``: Indices of TF genes ``(n_tfs,)``
272
+ state: Element state (passed through unchanged).
273
+ metadata: Element metadata (passed through unchanged).
274
+ random_params: Not used (non-stochastic operator).
275
+ stats: Not used.
276
+
277
+ Returns:
278
+ Tuple of (transformed_data, state, metadata):
279
+ - transformed_data contains all original keys plus:
280
+
281
+ - ``"grn_matrix"``: Sparse regulatory matrix ``(n_tfs, n_genes)``
282
+ - ``"tf_activity"``: Per-cell TF activity ``(n_cells, n_tfs)``
283
+ - state is passed through unchanged
284
+ - metadata is passed through unchanged
285
+ """
286
+ counts = data["counts"] # (n_cells, n_genes)
287
+ tf_indices = data["tf_indices"] # (n_tfs,)
288
+
289
+ n_tfs = tf_indices.shape[0]
290
+ n_genes = counts.shape[1]
291
+
292
+ # Step 1: Build bipartite graph
293
+ edge_index = self._build_bipartite_graph(n_tfs, n_genes)
294
+
295
+ # Step 2: Compute mean expression per gene across cells
296
+ mean_counts = jnp.mean(counts, axis=0) # (n_genes,)
297
+
298
+ # Step 3: Build node features -- one node per TF + one per gene
299
+ # TF nodes get mean TF expression, gene nodes get mean gene expression
300
+ tf_expr = mean_counts[tf_indices] # (n_tfs,)
301
+ all_expr = jnp.concatenate([tf_expr, mean_counts], axis=0) # (n_tfs + n_genes,)
302
+
303
+ # Project scalar expression to hidden_dim
304
+ node_features = self.node_proj(all_expr[:, None]) # (n_tfs + n_genes, hidden_dim)
305
+
306
+ # Step 4: Compute edge features
307
+ edge_features = self._compute_edge_features(mean_counts, tf_indices)
308
+
309
+ # Step 5: Apply GATv2 on bipartite graph
310
+ node_features_updated = self.gat_layer(
311
+ node_features,
312
+ edge_index,
313
+ edge_features,
314
+ deterministic=True,
315
+ )
316
+
317
+ # Step 6: Extract GRN matrix from updated node features
318
+ raw_grn = self._extract_grn_from_attention(
319
+ node_features_updated, edge_index, n_tfs, n_genes
320
+ )
321
+
322
+ # Step 7: Apply soft L1 sparsity
323
+ grn_matrix = self._apply_soft_sparsity(raw_grn)
324
+
325
+ # Step 8: Compute TF activity per cell
326
+ # Each TF's activity in a cell is the sum of all gene expressions
327
+ # weighted by that TF's regulatory strengths: activity_tj = sum_g(expr_g * grn_tg)
328
+ tf_activity = counts @ grn_matrix.T # (n_cells, n_genes) @ (n_genes, n_tfs)
329
+
330
+ transformed_data = {
331
+ **data,
332
+ "grn_matrix": grn_matrix,
333
+ "tf_activity": tf_activity,
334
+ }
335
+
336
+ return transformed_data, state, metadata