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,261 @@
1
+ """Differentiable UMAP dimensionality reduction.
2
+
3
+ This module implements a differentiable version of UMAP (Uniform Manifold
4
+ Approximation and Projection) for dimensionality reduction with end-to-end
5
+ gradient flow.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ import flax.nnx as nnx
12
+ import jax
13
+ import jax.numpy as jnp
14
+ from artifex.generative_models.core.base import MLP
15
+ from datarax.core.config import OperatorConfig
16
+ from datarax.core.operator import OperatorModule
17
+
18
+ from diffbio.constants import DISTANCE_MASK_SENTINEL
19
+ from diffbio.core.graph_utils import (
20
+ compute_fuzzy_membership,
21
+ compute_pairwise_distances,
22
+ symmetrize_graph,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class UMAPConfig(OperatorConfig):
28
+ """Configuration for differentiable UMAP.
29
+
30
+ Attributes:
31
+ n_components: Number of dimensions in the embedding.
32
+ n_neighbors: Number of neighbors for local structure preservation.
33
+ metric: Distance metric ('euclidean' or 'cosine').
34
+ input_features: Number of input features (required for initialization).
35
+ hidden_dim: Hidden dimension for projection network.
36
+ stream_name: Name of the data stream to process.
37
+ """
38
+
39
+ n_components: int = 2
40
+ n_neighbors: int = 15
41
+ metric: str = "euclidean"
42
+ input_features: int = 64
43
+ hidden_dim: int = 32
44
+
45
+ def __post_init__(self) -> None:
46
+ """Validate the supported UMAP configuration surface."""
47
+ super().__post_init__()
48
+
49
+ if self.n_components <= 0:
50
+ raise ValueError(f"n_components must be positive, got {self.n_components}")
51
+ if self.n_neighbors <= 0:
52
+ raise ValueError(f"n_neighbors must be positive, got {self.n_neighbors}")
53
+ if self.input_features <= 0:
54
+ raise ValueError(f"input_features must be positive, got {self.input_features}")
55
+ if self.hidden_dim <= 0:
56
+ raise ValueError(f"hidden_dim must be positive, got {self.hidden_dim}")
57
+ if self.metric not in {"euclidean", "cosine"}:
58
+ raise ValueError(f"metric must be 'euclidean' or 'cosine', got '{self.metric}'")
59
+
60
+
61
+ class ParametricUMAPHead(nnx.Module):
62
+ """Parametric UMAP embedding head with learnable similarity curve."""
63
+
64
+ def __init__(self, config: UMAPConfig, *, rngs: nnx.Rngs):
65
+ """Initialize the parametric embedding head."""
66
+ super().__init__()
67
+ self.curve_params = nnx.Param(jnp.array([1.929, 0.7915], dtype=jnp.float32))
68
+ self.projection_backbone = MLP(
69
+ hidden_dims=[config.hidden_dim, config.n_components],
70
+ in_features=config.input_features,
71
+ activation="relu",
72
+ output_activation=None,
73
+ use_batch_norm=False,
74
+ rngs=rngs,
75
+ )
76
+
77
+ def project(self, features: jax.Array) -> jax.Array:
78
+ """Project high-dimensional features to low-dimensional embedding."""
79
+ embedding = self.projection_backbone(features)
80
+ if isinstance(embedding, tuple):
81
+ raise TypeError("ParametricUMAPHead projection backbone must return a single tensor.")
82
+ return embedding
83
+
84
+ def curve_coefficients(self) -> tuple[jax.Array, jax.Array]:
85
+ """Return positive low-dimensional similarity curve coefficients."""
86
+ a = jnp.abs(self.curve_params[0]) + 1e-6
87
+ b = jnp.abs(self.curve_params[1]) + 1e-6
88
+ return a, b
89
+
90
+
91
+ class DifferentiableUMAP(OperatorModule):
92
+ """Differentiable UMAP for dimensionality reduction.
93
+
94
+ This operator implements a simplified differentiable version of UMAP that
95
+ learns a low-dimensional embedding while preserving local structure.
96
+
97
+ The UMAP loss function is:
98
+ L = sum_edges [p_ij * log(q_ij) + (1 - p_ij) * log(1 - q_ij)]
99
+
100
+ where:
101
+ - p_ij is the high-dimensional similarity (fuzzy set membership)
102
+ - q_ij is the low-dimensional similarity
103
+
104
+ This implementation uses a parametric approach with learnable curve
105
+ parameters (a, b) for the low-dimensional similarity function.
106
+
107
+ Example:
108
+ ```python
109
+ config = UMAPConfig(
110
+ n_components=2,
111
+ n_neighbors=15,
112
+ )
113
+ umap = DifferentiableUMAP(config, rngs=rngs)
114
+
115
+ data = {"features": high_dim_data} # (n_samples, n_features)
116
+ result, state, metadata = umap.apply(data, {}, None)
117
+ embedding = result["embedding"] # (n_samples, n_components)
118
+ ```
119
+ """
120
+
121
+ def __init__(self, config: UMAPConfig, *, rngs: nnx.Rngs | None = None):
122
+ """Initialize the differentiable UMAP.
123
+
124
+ Args:
125
+ config: Configuration for UMAP.
126
+ rngs: Random number generators for initialization.
127
+ """
128
+ super().__init__(config, rngs=rngs)
129
+
130
+ if rngs is None:
131
+ rngs = nnx.Rngs(0)
132
+
133
+ self.embedding_head = ParametricUMAPHead(config, rngs=rngs)
134
+
135
+ def _project(self, features: jax.Array) -> jax.Array:
136
+ """Project high-dimensional features to low-dimensional embedding.
137
+
138
+ Args:
139
+ features: Input features of shape (n_samples, n_features).
140
+
141
+ Returns:
142
+ Embedding of shape (n_samples, n_components).
143
+ """
144
+ return self.embedding_head.project(features)
145
+
146
+ def _compute_high_dim_similarities(self, features: jax.Array) -> jax.Array:
147
+ """Compute high-dimensional fuzzy set membership (p_ij).
148
+
149
+ Uses k-nearest neighbors and Gaussian kernel with local bandwidth.
150
+ Delegates to reusable graph utilities in ``diffbio.core.graph_utils``.
151
+
152
+ Args:
153
+ features: Input features of shape (n_samples, n_features).
154
+
155
+ Returns:
156
+ Similarity matrix of shape (n_samples, n_samples).
157
+ """
158
+ n_samples = features.shape[0]
159
+ n_neighbors = min(self.config.n_neighbors, n_samples - 1)
160
+
161
+ distances = compute_pairwise_distances(features, metric=self.config.metric)
162
+ distances = distances + jnp.eye(n_samples) * DISTANCE_MASK_SENTINEL
163
+
164
+ p_ij = compute_fuzzy_membership(distances, k=n_neighbors)
165
+ return symmetrize_graph(p_ij)
166
+
167
+ def _compute_low_dim_similarities(self, embedding: jax.Array) -> jax.Array:
168
+ """Compute low-dimensional similarities (q_ij).
169
+
170
+ Uses the UMAP student-t like kernel:
171
+ q(d) = 1 / (1 + a * d^(2b))
172
+
173
+ Args:
174
+ embedding: Low-dimensional embedding of shape (n_samples, n_components).
175
+
176
+ Returns:
177
+ Similarity matrix of shape (n_samples, n_samples).
178
+ """
179
+ n_samples = embedding.shape[0]
180
+
181
+ # Compute pairwise distances in embedding space
182
+ diff = embedding[:, None, :] - embedding[None, :, :]
183
+ distances_sq = jnp.sum(diff**2, axis=-1)
184
+
185
+ # UMAP similarity kernel
186
+ a, b = self.embedding_head.curve_coefficients()
187
+
188
+ q_ij = 1.0 / (1.0 + a * jnp.power(distances_sq + 1e-8, b))
189
+
190
+ # Set diagonal to 0
191
+ q_ij = q_ij * (1 - jnp.eye(n_samples))
192
+
193
+ return q_ij
194
+
195
+ def _compute_umap_loss(self, p_ij: jax.Array, q_ij: jax.Array) -> jax.Array:
196
+ """Compute UMAP cross-entropy loss.
197
+
198
+ Args:
199
+ p_ij: High-dimensional similarities.
200
+ q_ij: Low-dimensional similarities.
201
+
202
+ Returns:
203
+ Scalar loss value.
204
+ """
205
+ # Cross-entropy: -sum(p * log(q) + (1-p) * log(1-q))
206
+ eps = 1e-8
207
+ q_ij = jnp.clip(q_ij, eps, 1 - eps)
208
+
209
+ # Attractive term
210
+ attractive = -jnp.sum(p_ij * jnp.log(q_ij))
211
+
212
+ # Repulsive term
213
+ repulsive = -jnp.sum((1 - p_ij) * jnp.log(1 - q_ij))
214
+
215
+ return attractive + repulsive
216
+
217
+ def apply(
218
+ self,
219
+ data: dict[str, Any],
220
+ state: dict[str, Any],
221
+ metadata: dict | None,
222
+ random_params: dict | None = None,
223
+ stats: dict | None = None,
224
+ ) -> tuple[dict, dict, dict | None]:
225
+ """Apply UMAP dimensionality reduction.
226
+
227
+ Args:
228
+ data: Dictionary containing:
229
+ - 'features': High-dimensional features of shape (n_samples, n_features)
230
+ state: Operator state dictionary.
231
+ metadata: Optional metadata dictionary.
232
+ random_params: Optional random parameters (unused).
233
+ stats: Optional statistics dictionary (unused).
234
+
235
+ Returns:
236
+ Tuple of (output_data, state, metadata) where output_data contains:
237
+
238
+ - 'features': Original high-dimensional features
239
+ - 'embedding': Low-dimensional embedding
240
+ - 'high_dim_similarities': Fuzzy set memberships (p_ij)
241
+ - 'low_dim_similarities': Embedding similarities (q_ij)
242
+ """
243
+ del random_params, stats # Unused
244
+
245
+ features = data["features"]
246
+
247
+ # Project to low-dimensional space
248
+ embedding = self._project(features)
249
+
250
+ # Compute similarities
251
+ p_ij = self._compute_high_dim_similarities(features)
252
+ q_ij = self._compute_low_dim_similarities(embedding)
253
+
254
+ output_data = {
255
+ **data,
256
+ "embedding": embedding,
257
+ "high_dim_similarities": p_ij,
258
+ "low_dim_similarities": q_ij,
259
+ }
260
+
261
+ return output_data, state, metadata
@@ -0,0 +1,258 @@
1
+ """VAE-based count normalization operator.
2
+
3
+ This module provides a variational autoencoder for normalizing gene
4
+ expression count data, inspired by scVI (Lopez et al., 2018).
5
+
6
+ Key technique: Learn a latent representation of cell state while
7
+ modeling count data with a configurable likelihood (Poisson or ZINB).
8
+
9
+ The ZINB (Zero-Inflated Negative Binomial) likelihood is particularly
10
+ suited for single-cell RNA-seq data which exhibits both overdispersion
11
+ and excess zeros (dropout events).
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Literal
17
+
18
+ import jax
19
+ import jax.numpy as jnp
20
+ from artifex.generative_models.core.losses.divergence import gaussian_kl_divergence
21
+ from datarax.core.config import OperatorConfig
22
+ from flax import nnx
23
+ from jaxtyping import Array, Float, PyTree
24
+
25
+ from diffbio.core.base_operators import EncoderDecoderOperator
26
+ from diffbio.operators._count_vae import CountReconstructionMixin, CountVAEBackboneMixin
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class VAENormalizerConfig(OperatorConfig):
33
+ """Configuration for VAENormalizer.
34
+
35
+ Attributes:
36
+ latent_dim: Dimension of latent space.
37
+ hidden_dims: Hidden layer dimensions for encoder/decoder.
38
+ n_genes: Number of genes (input/output dimension).
39
+ use_batch_correction: Whether to include batch effects.
40
+ likelihood: Likelihood model for reconstruction loss.
41
+ 'poisson' for standard Poisson NLL, 'zinb' for
42
+ Zero-Inflated Negative Binomial.
43
+ """
44
+
45
+ latent_dim: int = 10
46
+ hidden_dims: list[int] = field(default_factory=lambda: [128, 64])
47
+ n_genes: int = 2000
48
+ use_batch_correction: bool = False
49
+ likelihood: Literal["poisson", "zinb"] = "poisson"
50
+
51
+ def __post_init__(self) -> None:
52
+ """Set stochastic defaults for VAE sampling and validate."""
53
+ object.__setattr__(self, "stochastic", True)
54
+ if self.stream_name is None:
55
+ object.__setattr__(self, "stream_name", "sample")
56
+ super().__post_init__()
57
+
58
+
59
+ class VAENormalizer(CountReconstructionMixin, CountVAEBackboneMixin, EncoderDecoderOperator):
60
+ """Variational autoencoder for count normalization.
61
+
62
+ This operator learns a low-dimensional latent representation of
63
+ single-cell gene expression data while accounting for technical
64
+ factors like library size.
65
+
66
+ The model:
67
+ - Encoder: counts -> latent (mean, logvar)
68
+ - Reparameterization: z = mean + exp(0.5 * logvar) * epsilon
69
+ - Decoder: z -> gene expression rates (and optionally dispersion/dropout)
70
+
71
+ Supports two likelihood models:
72
+ - Poisson: Simple count model (default)
73
+ - ZINB: Zero-Inflated Negative Binomial for overdispersed data
74
+ with excess zeros, as used in scVI
75
+
76
+ Inherits from EncoderDecoderOperator to get:
77
+
78
+ - reparameterize() for sampling with reparameterization trick
79
+ - kl_divergence() for KL from standard normal
80
+ - elbo_loss() for combining reconstruction and KL losses
81
+
82
+ Args:
83
+ config: VAENormalizerConfig with model parameters.
84
+ rngs: Flax NNX random number generators.
85
+ name: Optional operator name.
86
+
87
+ Example:
88
+ ```python
89
+ config = VAENormalizerConfig(n_genes=2000, latent_dim=10)
90
+ normalizer = VAENormalizer(config, rngs=nnx.Rngs(42))
91
+ data = {"counts": counts, "library_size": lib_size}
92
+ result, state, meta = normalizer.apply(data, {}, None)
93
+ ```
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ config: VAENormalizerConfig,
99
+ *,
100
+ rngs: nnx.Rngs | None = None,
101
+ name: str | None = None,
102
+ ) -> None:
103
+ """Initialize the VAE normalizer.
104
+
105
+ Args:
106
+ config: VAE configuration.
107
+ rngs: Random number generators for initialization and sampling.
108
+ name: Optional operator name.
109
+ """
110
+ super().__init__(config, rngs=rngs, name=name)
111
+ safe_rngs = self._init_count_vae_operator(config=config, rngs=rngs)
112
+
113
+ decoder_out_dim = config.hidden_dims[0] if config.hidden_dims else config.latent_dim
114
+
115
+ # ZINB-specific decoder heads
116
+ if config.likelihood == "zinb":
117
+ self.fc_log_theta = nnx.Linear(
118
+ in_features=decoder_out_dim, out_features=config.n_genes, rngs=safe_rngs
119
+ )
120
+ self.fc_pi_logit = nnx.Linear(
121
+ in_features=decoder_out_dim, out_features=config.n_genes, rngs=safe_rngs
122
+ )
123
+
124
+ def decode(
125
+ self,
126
+ z: Float[Array, "latent_dim"],
127
+ library_size: Float[Array, ""],
128
+ ) -> dict[str, jax.Array]:
129
+ """Decode latent representation to gene expression parameters.
130
+
131
+ Args:
132
+ z: Latent representation.
133
+ library_size: Total counts (library size) for normalization.
134
+
135
+ Returns:
136
+ Dictionary with keys:
137
+ - 'log_rate': Log rates for each gene (always present).
138
+ - 'log_theta': Log dispersion parameter (ZINB only).
139
+ - 'pi_logit': Dropout logit for zero inflation (ZINB only).
140
+ """
141
+ x = self.decode_hidden(z)
142
+
143
+ # Output layer (log rates, normalized by library size)
144
+ log_rate = self.fc_output(x)
145
+
146
+ result: dict[str, jax.Array] = {}
147
+
148
+ # ZINB heads are computed before adding library size
149
+ if self.config.likelihood == "zinb":
150
+ result["log_theta"] = self.fc_log_theta(x)
151
+ result["pi_logit"] = self.fc_pi_logit(x)
152
+
153
+ # Add library size effect (log scale)
154
+ log_rate = log_rate + jnp.log(library_size + 1e-8)
155
+ result["log_rate"] = log_rate
156
+
157
+ return result
158
+
159
+ # kl_divergence() is inherited from EncoderDecoderOperator
160
+
161
+ def compute_elbo_loss(
162
+ self,
163
+ counts: Float[Array, "n_genes"],
164
+ library_size: Float[Array, ""],
165
+ ) -> Float[Array, ""]:
166
+ """Compute negative ELBO loss.
167
+
168
+ Uses gaussian_kl_divergence from artifex for the KL term.
169
+
170
+ Args:
171
+ counts: Gene expression counts.
172
+ library_size: Total counts.
173
+
174
+ Returns:
175
+ Negative ELBO (reconstruction loss + KL divergence).
176
+ """
177
+ # Encode
178
+ mean, logvar = self.encode(counts)
179
+
180
+ # Sample latent using inherited reparameterize (uses self.rngs)
181
+ z = self.reparameterize(mean, logvar)
182
+
183
+ # Decode (returns dict)
184
+ decode_output = self.decode(z, library_size)
185
+
186
+ # Reconstruction loss
187
+ recon_loss = self.reconstruction_loss(counts, decode_output)
188
+
189
+ # KL divergence via artifex (handles unbatched 1-D inputs with sum)
190
+ kl = gaussian_kl_divergence(mean, logvar, reduction="sum")
191
+
192
+ return recon_loss + kl
193
+
194
+ def apply(
195
+ self,
196
+ data: PyTree,
197
+ state: PyTree,
198
+ metadata: dict[str, Any] | None,
199
+ random_params: Any = None,
200
+ stats: dict[str, Any] | None = None,
201
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
202
+ """Apply VAE normalization to count data.
203
+
204
+ This method encodes the counts to latent space, samples a
205
+ latent representation, and decodes to normalized expression.
206
+
207
+ Args:
208
+ data: Dictionary containing:
209
+ - "counts": Gene expression counts (n_genes,)
210
+ - "library_size": Total counts for the cell
211
+ state: Element state (passed through unchanged)
212
+ metadata: Element metadata (passed through unchanged)
213
+ random_params: Optional random parameters (not used)
214
+ stats: Not used
215
+
216
+ Returns:
217
+ Tuple of (transformed_data, state, metadata):
218
+ - transformed_data contains:
219
+
220
+ - "counts": Original counts
221
+ - "normalized": Normalized expression
222
+ - "latent_z": Sampled latent representation
223
+ - "latent_mean": Mean of latent distribution
224
+ - "latent_logvar": Log variance of latent distribution
225
+ - "log_rate": Decoded log rates
226
+ - state is passed through unchanged
227
+ - metadata is passed through unchanged
228
+ """
229
+ counts = data["counts"]
230
+ library_size = data["library_size"]
231
+
232
+ # Encode to latent distribution
233
+ mean, logvar = self.encode(counts)
234
+
235
+ # Sample from latent distribution using inherited reparameterize
236
+ # (uses self.rngs from EncoderDecoderOperator)
237
+ z = self.reparameterize(mean, logvar)
238
+
239
+ # Decode to gene expression rates (returns dict)
240
+ decode_output = self.decode(z, library_size)
241
+ log_rate = decode_output["log_rate"]
242
+
243
+ # Compute normalized expression (rate normalized by library size)
244
+ # This is the "denoised" expression
245
+ normalized = jnp.exp(log_rate - jnp.log(library_size + 1e-8))
246
+
247
+ # Build output data
248
+ transformed_data = {
249
+ "counts": counts,
250
+ "library_size": library_size,
251
+ "normalized": normalized,
252
+ "latent_z": z,
253
+ "latent_mean": mean,
254
+ "latent_logvar": logvar,
255
+ "log_rate": log_rate,
256
+ }
257
+
258
+ return transformed_data, state, metadata
@@ -0,0 +1,17 @@
1
+ """Population genetics operators.
2
+
3
+ This module provides differentiable operators for population genetics analysis
4
+ including ancestry estimation, phasing, and imputation.
5
+ """
6
+
7
+ from diffbio.operators.population.ancestry_estimation import (
8
+ AncestryEstimatorConfig,
9
+ DifferentiableAncestryEstimator,
10
+ create_ancestry_estimator,
11
+ )
12
+
13
+ __all__ = [
14
+ "AncestryEstimatorConfig",
15
+ "DifferentiableAncestryEstimator",
16
+ "create_ancestry_estimator",
17
+ ]