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,361 @@
1
+ """RNA velocity estimation via Neural ODEs.
2
+
3
+ This module provides differentiable RNA velocity estimation using neural
4
+ networks to learn splicing kinetics and integrate ODEs.
5
+
6
+ Key technique: Uses neural networks to learn per-gene kinetics parameters
7
+ (transcription, splicing, degradation rates) and integrates the splicing
8
+ ODE differentiably using Euler method.
9
+
10
+ Applications: Inferring cell state transitions and developmental trajectories
11
+ from single-cell RNA-seq data with spliced/unspliced counts.
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ import jax
19
+ import jax.numpy as jnp
20
+ from datarax.core.config import OperatorConfig
21
+ from datarax.core.operator import OperatorModule
22
+ from flax import nnx
23
+ from jaxtyping import Array, Float, PyTree
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class VelocityConfig(OperatorConfig):
30
+ """Configuration for DifferentiableVelocity.
31
+
32
+ Attributes:
33
+ n_genes: Number of genes.
34
+ hidden_dim: Hidden dimension for neural networks.
35
+ dt: Time step for ODE integration.
36
+ n_steps: Number of integration steps.
37
+ kinetics_model: Type of kinetics model ("standard" or "dynamical").
38
+ """
39
+
40
+ n_genes: int = 2000
41
+ hidden_dim: int = 64
42
+ dt: float = 0.1
43
+ n_steps: int = 10
44
+ kinetics_model: str = "standard"
45
+
46
+
47
+ class TimeEncoder(nnx.Module):
48
+ """Encoder for estimating latent time from expression."""
49
+
50
+ def __init__(
51
+ self,
52
+ n_genes: int,
53
+ hidden_dim: int,
54
+ *,
55
+ rngs: nnx.Rngs,
56
+ ):
57
+ """Initialize the time encoder.
58
+
59
+ Args:
60
+ n_genes: Number of input genes.
61
+ hidden_dim: Hidden dimension.
62
+ rngs: Random number generators.
63
+ """
64
+ super().__init__()
65
+
66
+ # Encode spliced + unspliced to latent time
67
+ self.linear1 = nnx.Linear(
68
+ in_features=n_genes * 2,
69
+ out_features=hidden_dim,
70
+ rngs=rngs,
71
+ )
72
+ self.linear2 = nnx.Linear(
73
+ in_features=hidden_dim,
74
+ out_features=hidden_dim,
75
+ rngs=rngs,
76
+ )
77
+ self.time_proj = nnx.Linear(
78
+ in_features=hidden_dim,
79
+ out_features=1,
80
+ rngs=rngs,
81
+ )
82
+ self.norm = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
83
+
84
+ def __call__(
85
+ self,
86
+ spliced: Float[Array, "n_cells n_genes"],
87
+ unspliced: Float[Array, "n_cells n_genes"],
88
+ ) -> Float[Array, "n_cells"]:
89
+ """Estimate latent time for each cell.
90
+
91
+ Args:
92
+ spliced: Spliced (mature) mRNA counts.
93
+ unspliced: Unspliced (nascent) mRNA counts.
94
+
95
+ Returns:
96
+ Latent time estimates per cell (bounded 0-1).
97
+ """
98
+ # Concatenate and log-transform
99
+ x = jnp.concatenate([jnp.log1p(spliced), jnp.log1p(unspliced)], axis=-1)
100
+
101
+ # Encode
102
+ x = nnx.gelu(self.linear1(x))
103
+ x = self.norm(nnx.gelu(self.linear2(x)))
104
+
105
+ # Project to time (sigmoid for 0-1 range)
106
+ time = jax.nn.sigmoid(self.time_proj(x)).squeeze(-1)
107
+
108
+ return time
109
+
110
+
111
+ class KineticsEncoder(nnx.Module):
112
+ """Encoder for learning per-gene kinetics parameters."""
113
+
114
+ def __init__(
115
+ self,
116
+ n_genes: int,
117
+ *,
118
+ rngs: nnx.Rngs,
119
+ ):
120
+ """Initialize the kinetics encoder.
121
+
122
+ Args:
123
+ n_genes: Number of genes.
124
+ rngs: Random number generators.
125
+ """
126
+ super().__init__()
127
+
128
+ # Learnable per-gene kinetics (alpha, beta, gamma)
129
+ # Initialize with reasonable defaults
130
+ key = rngs.params()
131
+ k1, k2, k3 = jax.random.split(key, 3)
132
+
133
+ # Transcription rate (alpha): typically 0.1-10
134
+ self.log_alpha = nnx.Param(jax.random.normal(k1, (n_genes,)) * 0.5)
135
+
136
+ # Splicing rate (beta): typically 0.1-1
137
+ self.log_beta = nnx.Param(jax.random.normal(k2, (n_genes,)) * 0.5 - 1.0)
138
+
139
+ # Degradation rate (gamma): typically 0.01-0.5
140
+ self.log_gamma = nnx.Param(jax.random.normal(k3, (n_genes,)) * 0.5 - 2.0)
141
+
142
+ def __call__(
143
+ self,
144
+ ) -> tuple[
145
+ Float[Array, "n_genes"],
146
+ Float[Array, "n_genes"],
147
+ Float[Array, "n_genes"],
148
+ ]:
149
+ """Get kinetics parameters.
150
+
151
+ Returns:
152
+ Tuple of (alpha, beta, gamma) - all positive via softplus.
153
+ """
154
+ alpha = jax.nn.softplus(self.log_alpha[...])
155
+ beta = jax.nn.softplus(self.log_beta[...])
156
+ gamma = jax.nn.softplus(self.log_gamma[...])
157
+
158
+ return alpha, beta, gamma
159
+
160
+
161
+ class DifferentiableVelocity(OperatorModule):
162
+ """Differentiable RNA velocity estimation via Neural ODEs.
163
+
164
+ This operator estimates RNA velocity from spliced and unspliced
165
+ counts using learned kinetics parameters and differentiable ODE
166
+ integration.
167
+
168
+ Algorithm:
169
+ 1. Encode expression to latent time per cell
170
+ 2. Learn per-gene kinetics (alpha, beta, gamma)
171
+ 3. Compute velocity from splicing ODE:
172
+ ds/dt = beta * u - gamma * s
173
+ du/dt = alpha - beta * u
174
+ 4. Integrate ODE using Euler method
175
+
176
+ Args:
177
+ config: VelocityConfig with model parameters.
178
+ rngs: Flax NNX random number generators.
179
+ name: Optional operator name.
180
+
181
+ Example:
182
+ ```python
183
+ config = VelocityConfig(n_genes=2000)
184
+ velocity = DifferentiableVelocity(config, rngs=nnx.Rngs(42))
185
+ data = {"spliced": spliced, "unspliced": unspliced}
186
+ result, state, meta = velocity.apply(data, {}, None)
187
+ ```
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ config: VelocityConfig,
193
+ *,
194
+ rngs: nnx.Rngs | None = None,
195
+ name: str | None = None,
196
+ ):
197
+ """Initialize the velocity operator.
198
+
199
+ Args:
200
+ config: Velocity configuration.
201
+ rngs: Random number generators for initialization.
202
+ name: Optional operator name.
203
+ """
204
+ super().__init__(config, rngs=rngs, name=name)
205
+
206
+ if rngs is None:
207
+ rngs = nnx.Rngs(0)
208
+
209
+ self.n_genes = config.n_genes
210
+ self.dt = config.dt
211
+ self.n_steps = config.n_steps
212
+
213
+ # Time encoder
214
+ self.time_encoder = TimeEncoder(
215
+ n_genes=config.n_genes,
216
+ hidden_dim=config.hidden_dim,
217
+ rngs=rngs,
218
+ )
219
+
220
+ # Kinetics encoder
221
+ self.kinetics_encoder = KineticsEncoder(
222
+ n_genes=config.n_genes,
223
+ rngs=rngs,
224
+ )
225
+
226
+ def _compute_velocity(
227
+ self,
228
+ spliced: Float[Array, "n_cells n_genes"],
229
+ unspliced: Float[Array, "n_cells n_genes"],
230
+ beta: Float[Array, "n_genes"],
231
+ gamma: Float[Array, "n_genes"],
232
+ ) -> Float[Array, "n_cells n_genes"]:
233
+ """Compute RNA velocity from splicing dynamics.
234
+
235
+ The standard RNA velocity model:
236
+ ds/dt = beta * u - gamma * s
237
+
238
+ Args:
239
+ spliced: Spliced counts.
240
+ unspliced: Unspliced counts.
241
+ beta: Splicing rate.
242
+ gamma: Degradation rate.
243
+
244
+ Returns:
245
+ Velocity (ds/dt) for each cell-gene pair.
246
+ """
247
+ # Velocity is the rate of change of spliced mRNA
248
+ # ds/dt = splicing_in - degradation_out
249
+ # ds/dt = beta * unspliced - gamma * spliced
250
+ velocity = beta[None, :] * unspliced - gamma[None, :] * spliced
251
+
252
+ return velocity
253
+
254
+ def _euler_step(
255
+ self,
256
+ s: Float[Array, "n_cells n_genes"],
257
+ u: Float[Array, "n_cells n_genes"],
258
+ alpha: Float[Array, "n_genes"],
259
+ beta: Float[Array, "n_genes"],
260
+ gamma: Float[Array, "n_genes"],
261
+ dt: float,
262
+ ) -> tuple[Float[Array, "n_cells n_genes"], Float[Array, "n_cells n_genes"]]:
263
+ """Single Euler integration step.
264
+
265
+ Args:
266
+ s: Current spliced counts.
267
+ u: Current unspliced counts.
268
+ alpha: Transcription rate.
269
+ beta: Splicing rate.
270
+ gamma: Degradation rate.
271
+ dt: Time step.
272
+
273
+ Returns:
274
+ Tuple of (new_spliced, new_unspliced).
275
+ """
276
+ # ODE system:
277
+ # du/dt = alpha - beta * u
278
+ # ds/dt = beta * u - gamma * s
279
+ du_dt = alpha[None, :] - beta[None, :] * u
280
+ ds_dt = beta[None, :] * u - gamma[None, :] * s
281
+
282
+ # Euler update
283
+ u_new = u + dt * du_dt
284
+ s_new = s + dt * ds_dt
285
+
286
+ # Ensure non-negative
287
+ u_new = jnp.maximum(u_new, 0.0)
288
+ s_new = jnp.maximum(s_new, 0.0)
289
+
290
+ return s_new, u_new
291
+
292
+ def apply(
293
+ self,
294
+ data: PyTree,
295
+ state: PyTree,
296
+ metadata: dict[str, Any] | None,
297
+ random_params: Any = None,
298
+ stats: dict[str, Any] | None = None,
299
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
300
+ """Apply RNA velocity estimation.
301
+
302
+ Args:
303
+ data: Dictionary containing:
304
+ - "spliced": Spliced mRNA counts (n_cells, n_genes)
305
+ - "unspliced": Unspliced mRNA counts (n_cells, n_genes)
306
+ state: Element state (passed through unchanged)
307
+ metadata: Element metadata (passed through unchanged)
308
+ random_params: Not used
309
+ stats: Not used
310
+
311
+ Returns:
312
+ Tuple of (transformed_data, state, metadata):
313
+ - transformed_data contains:
314
+
315
+ - "spliced": Original spliced counts
316
+ - "unspliced": Original unspliced counts
317
+ - "velocity": RNA velocity estimates
318
+ - "latent_time": Estimated latent time per cell
319
+ - "alpha": Transcription rate per gene
320
+ - "beta": Splicing rate per gene
321
+ - "gamma": Degradation rate per gene
322
+ - "projected_spliced": Projected future spliced
323
+ - state is passed through unchanged
324
+ - metadata is passed through unchanged
325
+ """
326
+ spliced = data["spliced"]
327
+ unspliced = data["unspliced"]
328
+
329
+ # Estimate latent time per cell
330
+ latent_time = self.time_encoder(spliced, unspliced)
331
+
332
+ # Get kinetics parameters
333
+ alpha, beta, gamma = self.kinetics_encoder()
334
+
335
+ # Compute velocity
336
+ velocity = self._compute_velocity(spliced, unspliced, beta, gamma)
337
+
338
+ # Project forward using learned dynamics via jax.lax.scan
339
+ def _euler_body(
340
+ carry: tuple[jax.Array, jax.Array], _: None
341
+ ) -> tuple[tuple[jax.Array, jax.Array], None]:
342
+ s, u = carry
343
+ s_new, u_new = self._euler_step(s, u, alpha, beta, gamma, self.dt)
344
+ return (s_new, u_new), None
345
+
346
+ (s_proj, u_proj), _ = jax.lax.scan(
347
+ _euler_body, (spliced, unspliced), None, length=self.n_steps
348
+ )
349
+
350
+ transformed_data = {
351
+ "spliced": spliced,
352
+ "unspliced": unspliced,
353
+ "velocity": velocity,
354
+ "latent_time": latent_time,
355
+ "alpha": alpha,
356
+ "beta": beta,
357
+ "gamma": gamma,
358
+ "projected_spliced": s_proj,
359
+ }
360
+
361
+ return transformed_data, state, metadata
@@ -0,0 +1,35 @@
1
+ """Differentiable statistical model operators.
2
+
3
+ This module provides operators for:
4
+
5
+ - Hidden Markov Models with differentiable forward algorithm
6
+ - Negative Binomial GLM for differential expression
7
+ - Unrolled EM for transcript quantification
8
+
9
+ All operators maintain gradient flow for end-to-end training.
10
+ """
11
+
12
+ from diffbio.operators.statistical.em_quantification import (
13
+ DifferentiableEMQuantifier,
14
+ EMQuantifierConfig,
15
+ )
16
+ from diffbio.operators.statistical.hmm import (
17
+ DifferentiableHMM,
18
+ HMMConfig,
19
+ )
20
+ from diffbio.operators.statistical.nb_glm import (
21
+ DifferentiableNBGLM,
22
+ NBGLMConfig,
23
+ )
24
+
25
+ __all__ = [
26
+ # HMM
27
+ "HMMConfig",
28
+ "DifferentiableHMM",
29
+ # Negative Binomial GLM
30
+ "NBGLMConfig",
31
+ "DifferentiableNBGLM",
32
+ # EM Quantification
33
+ "EMQuantifierConfig",
34
+ "DifferentiableEMQuantifier",
35
+ ]
@@ -0,0 +1,260 @@
1
+ """Differentiable EM-based transcript quantification operator.
2
+
3
+ This module provides an unrolled EM algorithm for transcript
4
+ quantification, inspired by Salmon and Kallisto.
5
+
6
+ Key technique: Fixed number of EM iterations enables gradient flow
7
+ through all steps of the algorithm.
8
+
9
+ Applications: RNA-seq transcript quantification, isoform abundance estimation.
10
+
11
+ Inherits from TemperatureOperator to get:
12
+
13
+ - _temperature property for temperature-controlled smoothing
14
+ - soft_max() for logsumexp-based smooth maximum
15
+ - soft_argmax() for soft position selection
16
+ """
17
+
18
+ import logging
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ import jax
23
+ import jax.numpy as jnp
24
+ from datarax.core.config import OperatorConfig
25
+ from flax import nnx
26
+ from jaxtyping import Array, Float, PyTree
27
+
28
+ from diffbio.core.base_operators import TemperatureOperator
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class EMQuantifierConfig(OperatorConfig):
35
+ """Configuration for DifferentiableEMQuantifier.
36
+
37
+ Attributes:
38
+ n_transcripts: Number of transcripts to quantify.
39
+ n_iterations: Fixed number of EM iterations (for unrolling).
40
+ temperature: Temperature for softmax in E-step.
41
+ """
42
+
43
+ n_transcripts: int = 1000
44
+ n_iterations: int = 10
45
+ temperature: float = 1.0
46
+
47
+
48
+ class DifferentiableEMQuantifier(TemperatureOperator):
49
+ """Differentiable EM for transcript quantification.
50
+
51
+ This operator implements the EM algorithm for estimating transcript
52
+ abundances from read-to-transcript compatibility data. The fixed
53
+ number of iterations enables gradient flow through the entire
54
+ quantification process.
55
+
56
+ Algorithm:
57
+ 1. Initialize abundances (learnable prior)
58
+ 2. E-step: Probabilistic assignment of reads to transcripts
59
+ weights = softmax(compatibility * abundances / temperature)
60
+ 3. M-step: Update abundances from weighted counts
61
+ abundances = sum(weights) / effective_lengths
62
+ abundances = abundances / sum(abundances)
63
+ 4. Repeat for n_iterations
64
+
65
+ Inherits from TemperatureOperator to get:
66
+
67
+ - _temperature property for temperature-controlled smoothing
68
+ - soft_max() for logsumexp-based smooth maximum
69
+ - soft_argmax() for soft position selection
70
+
71
+ Args:
72
+ config: EMQuantifierConfig with model parameters.
73
+ rngs: Flax NNX random number generators.
74
+ name: Optional operator name.
75
+
76
+ Example:
77
+ ```python
78
+ config = EMQuantifierConfig(n_transcripts=1000, n_iterations=10)
79
+ quantifier = DifferentiableEMQuantifier(config, rngs=nnx.Rngs(42))
80
+ data = {"compatibility": compat_matrix, "effective_lengths": eff_lens}
81
+ result, state, meta = quantifier.apply(data, {}, None)
82
+ ```
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ config: EMQuantifierConfig,
88
+ *,
89
+ rngs: nnx.Rngs | None = None,
90
+ name: str | None = None,
91
+ ):
92
+ """Initialize the EM quantifier operator.
93
+
94
+ Args:
95
+ config: EM quantifier configuration.
96
+ rngs: Random number generators for initialization.
97
+ name: Optional operator name.
98
+ """
99
+ super().__init__(config, rngs=rngs, name=name)
100
+
101
+ if rngs is None:
102
+ rngs = nnx.Rngs(0)
103
+
104
+ self.n_transcripts = config.n_transcripts
105
+ self.n_iterations = config.n_iterations
106
+ # Temperature is now managed by TemperatureOperator via self._temperature
107
+
108
+ # Initialize log abundances (will be normalized via softmax)
109
+ # Shape: (n_transcripts,)
110
+ key = rngs.params()
111
+ init_log_abundances = jax.random.normal(key, (config.n_transcripts,)) * 0.01
112
+ self.log_initial_abundances = nnx.Param(init_log_abundances)
113
+
114
+ def get_initial_abundances(self) -> Float[Array, "n_transcripts"]:
115
+ """Get normalized initial abundances.
116
+
117
+ Returns:
118
+ Initial abundance distribution (n_transcripts,), sums to 1.
119
+ """
120
+ return jax.nn.softmax(self.log_initial_abundances[...])
121
+
122
+ def em_step(
123
+ self,
124
+ abundances: Float[Array, "n_transcripts"],
125
+ compatibility: Float[Array, "n_reads n_transcripts"],
126
+ effective_lengths: Float[Array, "n_transcripts"],
127
+ ) -> Float[Array, "n_transcripts"]:
128
+ """Perform one EM iteration.
129
+
130
+ Args:
131
+ abundances: Current abundance estimates.
132
+ compatibility: Read-transcript compatibility matrix.
133
+ effective_lengths: Effective transcript lengths.
134
+
135
+ Returns:
136
+ Updated abundance estimates.
137
+ """
138
+ # E-step: Compute read assignment probabilities
139
+ # P(transcript | read) propto compatibility * abundance / length
140
+ rate = abundances / (effective_lengths + 1e-8) # (n_transcripts,)
141
+
142
+ # Score for each read-transcript pair
143
+ scores = compatibility * rate # (n_reads, n_transcripts)
144
+
145
+ # Normalize per read (softmax with temperature)
146
+ # Use inherited _temperature property from TemperatureOperator
147
+ weights = jax.nn.softmax(
148
+ jnp.log(scores + 1e-10) / self._temperature, axis=1
149
+ ) # (n_reads, n_transcripts)
150
+
151
+ # M-step: Update abundances
152
+ # Expected count for each transcript
153
+ expected_counts = jnp.sum(weights, axis=0) # (n_transcripts,)
154
+
155
+ # Normalize by effective length
156
+ new_abundances = expected_counts / (effective_lengths + 1e-8)
157
+
158
+ # Normalize to sum to 1
159
+ new_abundances = new_abundances / (jnp.sum(new_abundances) + 1e-8)
160
+
161
+ return new_abundances
162
+
163
+ def quantify(
164
+ self,
165
+ compatibility: Float[Array, "n_reads n_transcripts"],
166
+ effective_lengths: Float[Array, "n_transcripts"],
167
+ ) -> Float[Array, "n_transcripts"]:
168
+ """Run EM algorithm for quantification.
169
+
170
+ Args:
171
+ compatibility: Read-transcript compatibility matrix.
172
+ effective_lengths: Effective transcript lengths.
173
+
174
+ Returns:
175
+ Final transcript abundance estimates.
176
+ """
177
+ # Initialize abundances
178
+ abundances = self.get_initial_abundances()
179
+
180
+ # Run fixed number of EM iterations
181
+ def em_iteration(abundances, _):
182
+ new_abundances = self.em_step(abundances, compatibility, effective_lengths)
183
+ return new_abundances, None
184
+
185
+ final_abundances, _ = jax.lax.scan(em_iteration, abundances, None, length=self.n_iterations)
186
+
187
+ return final_abundances
188
+
189
+ def compute_tpm(
190
+ self,
191
+ abundances: Float[Array, "n_transcripts"],
192
+ effective_lengths: Float[Array, "n_transcripts"],
193
+ ) -> Float[Array, "n_transcripts"]:
194
+ """Convert abundances to TPM (Transcripts Per Million).
195
+
196
+ Args:
197
+ abundances: Normalized abundance estimates.
198
+ effective_lengths: Effective transcript lengths.
199
+
200
+ Returns:
201
+ TPM values (sum to 1 million).
202
+ """
203
+ # TPM = (abundance / length) / sum(abundance / length) * 1e6
204
+ rate = abundances / (effective_lengths + 1e-8)
205
+ tpm = rate / (jnp.sum(rate) + 1e-8) * 1e6
206
+ return tpm
207
+
208
+ def apply(
209
+ self,
210
+ data: PyTree,
211
+ state: PyTree,
212
+ metadata: dict[str, Any] | None,
213
+ random_params: Any = None,
214
+ stats: dict[str, Any] | None = None,
215
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
216
+ """Apply EM quantification to read assignment data.
217
+
218
+ This method runs the EM algorithm to estimate transcript
219
+ abundances from read-transcript compatibility data.
220
+
221
+ Args:
222
+ data: Dictionary containing:
223
+ - "compatibility": Read-transcript compatibility matrix
224
+ (n_reads, n_transcripts)
225
+ - "effective_lengths": Effective transcript lengths
226
+ (n_transcripts,)
227
+ state: Element state (passed through unchanged)
228
+ metadata: Element metadata (passed through unchanged)
229
+ random_params: Not used (deterministic operator)
230
+ stats: Not used
231
+
232
+ Returns:
233
+ Tuple of (transformed_data, state, metadata):
234
+ - transformed_data contains:
235
+
236
+ - "compatibility": Original compatibility matrix
237
+ - "effective_lengths": Original effective lengths
238
+ - "abundances": Estimated transcript abundances
239
+ - "tpm": TPM (Transcripts Per Million) values
240
+ - state is passed through unchanged
241
+ - metadata is passed through unchanged
242
+ """
243
+ compatibility = data["compatibility"]
244
+ effective_lengths = data["effective_lengths"]
245
+
246
+ # Run EM quantification
247
+ abundances = self.quantify(compatibility, effective_lengths)
248
+
249
+ # Compute TPM
250
+ tpm = self.compute_tpm(abundances, effective_lengths)
251
+
252
+ # Build output data
253
+ transformed_data = {
254
+ "compatibility": compatibility,
255
+ "effective_lengths": effective_lengths,
256
+ "abundances": abundances,
257
+ "tpm": tpm,
258
+ }
259
+
260
+ return transformed_data, state, metadata