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,333 @@
1
+ """Differentiable ambient RNA removal operator.
2
+
3
+ This module provides CellBender-style ambient RNA removal using a VAE
4
+ architecture that learns to separate cell-intrinsic from ambient signal.
5
+
6
+ Key technique: Uses variational autoencoder to model cell-specific expression
7
+ and ambient contamination fraction, enabling decontamination with uncertainty
8
+ quantification.
9
+
10
+ Applications: Removing ambient RNA contamination from single-cell RNA-seq data,
11
+ improving cell type identification and differential expression analysis.
12
+
13
+ Inherits from EncoderDecoderOperator to get:
14
+
15
+ - reparameterize() for sampling with reparameterization trick
16
+ - kl_divergence() for KL from standard normal
17
+ - elbo_loss() for combining reconstruction and KL losses
18
+ """
19
+
20
+ import logging
21
+ from dataclasses import dataclass, field
22
+ from typing import Any
23
+
24
+ import jax
25
+ import jax.numpy as jnp
26
+ from artifex.generative_models.core.base import MLP
27
+ from datarax.core.config import OperatorConfig
28
+ from flax import nnx
29
+ from jaxtyping import Array, Float, PyTree
30
+
31
+ from diffbio.configs import apply_stochastic_sampling_defaults
32
+ from diffbio.core.base_operators import EncoderDecoderOperator
33
+ from diffbio.utils.nn_utils import ARTIFEX_GELU_MLP_KWARGS
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class AmbientRemovalConfig(OperatorConfig):
40
+ """Configuration for DifferentiableAmbientRemoval.
41
+
42
+ Attributes:
43
+ n_genes: Number of genes in expression profiles.
44
+ latent_dim: Dimension of latent space.
45
+ hidden_dims: Hidden layer dimensions for encoder/decoder.
46
+ ambient_prior: Prior probability of ambient contamination.
47
+ temperature: Temperature for softmax operations.
48
+ """
49
+
50
+ n_genes: int = 2000
51
+ latent_dim: int = 64
52
+ hidden_dims: list[int] = field(default_factory=lambda: [256, 128])
53
+ ambient_prior: float = 0.01
54
+ temperature: float = 1.0
55
+
56
+ def __post_init__(self) -> None:
57
+ """Set stochastic defaults and validate."""
58
+ apply_stochastic_sampling_defaults(self)
59
+ super().__post_init__()
60
+ if not self.hidden_dims:
61
+ raise ValueError(
62
+ "AmbientRemovalConfig.hidden_dims must contain at least one hidden dimension."
63
+ )
64
+
65
+
66
+ class AmbientEncoder(nnx.Module):
67
+ """Encoder network for ambient removal VAE."""
68
+
69
+ def __init__(
70
+ self,
71
+ n_genes: int,
72
+ hidden_dims: list[int],
73
+ latent_dim: int,
74
+ *,
75
+ rngs: nnx.Rngs,
76
+ ):
77
+ """Initialize the encoder.
78
+
79
+ Args:
80
+ n_genes: Number of input genes.
81
+ hidden_dims: Hidden layer dimensions.
82
+ latent_dim: Latent space dimension.
83
+ rngs: Random number generators.
84
+ """
85
+ super().__init__()
86
+ self.backbone = MLP(
87
+ hidden_dims=hidden_dims,
88
+ in_features=n_genes,
89
+ rngs=rngs,
90
+ **ARTIFEX_GELU_MLP_KWARGS,
91
+ )
92
+
93
+ # Latent projections
94
+ self.mean_proj = nnx.Linear(in_features=hidden_dims[-1], out_features=latent_dim, rngs=rngs)
95
+ self.logvar_proj = nnx.Linear(
96
+ in_features=hidden_dims[-1], out_features=latent_dim, rngs=rngs
97
+ )
98
+
99
+ # Contamination fraction projection
100
+ self.contamination_proj = nnx.Linear(in_features=hidden_dims[-1], out_features=1, rngs=rngs)
101
+
102
+ def __call__(
103
+ self,
104
+ counts: Float[Array, "n_cells n_genes"],
105
+ ) -> tuple[
106
+ Float[Array, "n_cells latent_dim"],
107
+ Float[Array, "n_cells latent_dim"],
108
+ Float[Array, "n_cells"],
109
+ ]:
110
+ """Encode counts to latent space.
111
+
112
+ Args:
113
+ counts: Input count matrix.
114
+
115
+ Returns:
116
+ Tuple of (mean, logvar, contamination_fraction).
117
+ """
118
+ # Log-normalize for encoder input
119
+ backbone_output = self.backbone(jnp.log1p(counts))
120
+ if isinstance(backbone_output, tuple):
121
+ raise TypeError("AmbientEncoder backbone must return a single tensor output.")
122
+
123
+ # Latent parameters
124
+ mean = self.mean_proj(backbone_output)
125
+ logvar = self.logvar_proj(backbone_output)
126
+
127
+ # Contamination fraction (bounded 0-1)
128
+ contamination = jax.nn.sigmoid(self.contamination_proj(backbone_output)).squeeze(-1)
129
+
130
+ return mean, logvar, contamination
131
+
132
+
133
+ class AmbientDecoder(nnx.Module):
134
+ """Decoder network for ambient removal VAE."""
135
+
136
+ def __init__(
137
+ self,
138
+ latent_dim: int,
139
+ hidden_dims: list[int],
140
+ n_genes: int,
141
+ *,
142
+ rngs: nnx.Rngs,
143
+ ):
144
+ """Initialize the decoder.
145
+
146
+ Args:
147
+ latent_dim: Latent space dimension.
148
+ hidden_dims: Hidden layer dimensions (reversed from encoder).
149
+ n_genes: Number of output genes.
150
+ rngs: Random number generators.
151
+ """
152
+ super().__init__()
153
+ decoder_hidden_dims = list(reversed(hidden_dims))
154
+ self.backbone = MLP(
155
+ hidden_dims=decoder_hidden_dims,
156
+ in_features=latent_dim,
157
+ rngs=rngs,
158
+ **ARTIFEX_GELU_MLP_KWARGS,
159
+ )
160
+
161
+ # Output projection (log-rate for Poisson/NB)
162
+ self.output_proj = nnx.Linear(
163
+ in_features=decoder_hidden_dims[-1], out_features=n_genes, rngs=rngs
164
+ )
165
+
166
+ def __call__(
167
+ self,
168
+ z: Float[Array, "n_cells latent_dim"],
169
+ ) -> Float[Array, "n_cells n_genes"]:
170
+ """Decode latent to gene expression rates.
171
+
172
+ Args:
173
+ z: Latent representation.
174
+
175
+ Returns:
176
+ Log-rate parameters for gene expression.
177
+ """
178
+ backbone_output = self.backbone(z)
179
+ if isinstance(backbone_output, tuple):
180
+ raise TypeError("AmbientDecoder backbone must return a single tensor output.")
181
+
182
+ # Output log-rates
183
+ log_rate = self.output_proj(backbone_output)
184
+
185
+ return log_rate
186
+
187
+
188
+ class DifferentiableAmbientRemoval(EncoderDecoderOperator):
189
+ """Differentiable ambient RNA removal using VAE.
190
+
191
+ This operator removes ambient RNA contamination from single-cell
192
+ count data using a variational autoencoder that models both
193
+ cell-intrinsic expression and ambient contamination.
194
+
195
+ Algorithm:
196
+ 1. Encode counts to latent space + contamination fraction
197
+ 2. Sample latent (reparameterization trick)
198
+ 3. Decode to cell-intrinsic expression rate
199
+ 4. Compute decontaminated counts by subtracting ambient contribution
200
+
201
+ Inherits from EncoderDecoderOperator to get:
202
+
203
+ - reparameterize() for sampling with reparameterization trick
204
+ - kl_divergence() for KL from standard normal
205
+ - elbo_loss() for combining reconstruction and KL losses
206
+
207
+ Args:
208
+ config: AmbientRemovalConfig with model parameters.
209
+ rngs: Flax NNX random number generators.
210
+ name: Optional operator name.
211
+
212
+ Example:
213
+ ```python
214
+ config = AmbientRemovalConfig(n_genes=2000)
215
+ remover = DifferentiableAmbientRemoval(config, rngs=nnx.Rngs(42))
216
+ data = {"counts": counts, "ambient_profile": ambient}
217
+ result, state, meta = remover.apply(data, {}, None)
218
+ ```
219
+ """
220
+
221
+ def __init__(
222
+ self,
223
+ config: AmbientRemovalConfig,
224
+ *,
225
+ rngs: nnx.Rngs | None = None,
226
+ name: str | None = None,
227
+ ):
228
+ """Initialize the ambient removal operator.
229
+
230
+ Args:
231
+ config: Ambient removal configuration.
232
+ rngs: Random number generators for initialization.
233
+ name: Optional operator name.
234
+ """
235
+ super().__init__(config, rngs=rngs, name=name)
236
+
237
+ if rngs is None:
238
+ rngs = nnx.Rngs(0)
239
+
240
+ self.ambient_prior = config.ambient_prior
241
+ self.stochastic = nnx.static(config.stochastic)
242
+
243
+ # Encoder
244
+ self.encoder = AmbientEncoder(
245
+ n_genes=config.n_genes,
246
+ hidden_dims=config.hidden_dims,
247
+ latent_dim=config.latent_dim,
248
+ rngs=rngs,
249
+ )
250
+
251
+ # Decoder
252
+ self.decoder = AmbientDecoder(
253
+ latent_dim=config.latent_dim,
254
+ hidden_dims=config.hidden_dims,
255
+ n_genes=config.n_genes,
256
+ rngs=rngs,
257
+ )
258
+
259
+ # reparameterize() is inherited from EncoderDecoderOperator
260
+ # kl_divergence() is inherited from EncoderDecoderOperator
261
+ # elbo_loss() is inherited from EncoderDecoderOperator
262
+
263
+ def apply(
264
+ self,
265
+ data: PyTree,
266
+ state: PyTree,
267
+ metadata: dict[str, Any] | None,
268
+ random_params: Any = None,
269
+ stats: dict[str, Any] | None = None,
270
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
271
+ """Apply ambient RNA removal.
272
+
273
+ Args:
274
+ data: Dictionary containing:
275
+ - "counts": Raw count matrix (n_cells, n_genes)
276
+ - "ambient_profile": Ambient expression profile (n_genes,)
277
+ state: Element state (passed through unchanged)
278
+ metadata: Element metadata (passed through unchanged)
279
+ random_params: Random key for stochastic sampling
280
+ stats: Not used
281
+
282
+ Returns:
283
+ Tuple of (transformed_data, state, metadata):
284
+ - transformed_data contains:
285
+
286
+ - "counts": Original counts
287
+ - "ambient_profile": Original ambient profile
288
+ - "decontaminated_counts": Decontaminated counts
289
+ - "contamination_fraction": Estimated contamination per cell
290
+ - "latent": Latent representation
291
+ - "latent_mean": Mean of latent distribution
292
+ - "latent_logvar": Log variance of latent distribution
293
+ - "reconstructed": Reconstructed expression
294
+ - state is passed through unchanged
295
+ - metadata is passed through unchanged
296
+ """
297
+ counts = data["counts"]
298
+ ambient_profile = data["ambient_profile"]
299
+
300
+ # Encode
301
+ mean, logvar, contamination = self.encoder(counts)
302
+
303
+ # Sample latent using inherited reparameterize (uses self.rngs)
304
+ z = self.reparameterize(mean, logvar)
305
+
306
+ # Decode to cell-intrinsic expression rate
307
+ log_rate = self.decoder(z)
308
+ cell_rate = jax.nn.softplus(log_rate) # Non-negative
309
+
310
+ # Compute total counts per cell for scaling
311
+ total_counts = jnp.sum(counts, axis=-1, keepdims=True)
312
+
313
+ # Model: observed = (1 - contamination) * cell + contamination * ambient * total
314
+ # Decontaminated = observed - contamination * ambient * total
315
+ ambient_contribution = contamination[:, None] * ambient_profile[None, :] * total_counts
316
+ decontaminated = jnp.maximum(counts - ambient_contribution, 0.0)
317
+
318
+ # Reconstructed expression (for loss computation)
319
+ cell_contribution = (1.0 - contamination[:, None]) * cell_rate
320
+ reconstructed = cell_contribution + ambient_contribution
321
+
322
+ transformed_data = {
323
+ "counts": counts,
324
+ "ambient_profile": ambient_profile,
325
+ "decontaminated_counts": decontaminated,
326
+ "contamination_fraction": contamination,
327
+ "latent": z,
328
+ "latent_mean": mean,
329
+ "latent_logvar": logvar,
330
+ "reconstructed": reconstructed,
331
+ }
332
+
333
+ return transformed_data, state, metadata
@@ -0,0 +1,191 @@
1
+ """Differentiable archetypal analysis for single-cell data.
2
+
3
+ Implements PCHA (Principal Convex Hull Analysis, Morup & Hansen 2012) as a
4
+ differentiable autoencoder with softmax bottleneck. Each cell is represented
5
+ as a temperature-controlled convex combination of learnable archetype
6
+ prototypes.
7
+
8
+ Algorithm:
9
+ 1. Encode cells to archetype weight space via MLP.
10
+ 2. Apply temperature-scaled softmax to enforce simplex constraints.
11
+ 3. Reconstruct cells as the convex combination ``weights @ archetypes``.
12
+
13
+ Inherits from ``TemperatureOperator`` to get temperature-controlled smoothing.
14
+ """
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ import jax
21
+ from artifex.generative_models.core.base import MLP
22
+ from datarax.core.config import OperatorConfig
23
+ from flax import nnx
24
+ from jaxtyping import Array, Float, PyTree
25
+
26
+ from diffbio.core.base_operators import TemperatureOperator
27
+ from diffbio.utils.nn_utils import ensure_rngs, get_rng_key
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ __all__ = [
32
+ "ArchetypalAnalysisConfig",
33
+ "DifferentiableArchetypalAnalysis",
34
+ ]
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ArchetypalAnalysisConfig(OperatorConfig):
39
+ """Configuration for DifferentiableArchetypalAnalysis.
40
+
41
+ Attributes:
42
+ n_genes: Number of input genes (features per cell).
43
+ n_archetypes: Number of archetype prototypes to learn.
44
+ hidden_dim: Hidden dimension for the encoder MLP.
45
+ temperature: Softmax temperature (lower = sharper assignments).
46
+ learnable_temperature: Whether temperature is a learnable parameter.
47
+ """
48
+
49
+ n_genes: int = 2000
50
+ n_archetypes: int = 5
51
+ hidden_dim: int = 64
52
+ temperature: float = 1.0
53
+ learnable_temperature: bool = False
54
+
55
+
56
+ class DifferentiableArchetypalAnalysis(TemperatureOperator):
57
+ """Differentiable archetypal analysis with softmax simplex constraints.
58
+
59
+ Each cell is encoded into archetype weight space via an MLP, then
60
+ temperature-controlled softmax produces simplex weights. The
61
+ reconstruction is the convex combination of learnable archetype
62
+ prototypes, enabling end-to-end gradient-based optimisation.
63
+
64
+ Inherits from ``TemperatureOperator`` to get:
65
+
66
+ - ``_temperature`` property for temperature-controlled smoothing
67
+ - ``soft_max()`` for logsumexp-based smooth maximum
68
+
69
+ Args:
70
+ config: ArchetypalAnalysisConfig with model parameters.
71
+ rngs: Flax NNX random number generators.
72
+ name: Optional operator name.
73
+
74
+ Example:
75
+ ```python
76
+ import jax.numpy as jnp
77
+ config = ArchetypalAnalysisConfig(n_genes=2000, n_archetypes=5)
78
+ op = DifferentiableArchetypalAnalysis(config, rngs=nnx.Rngs(0))
79
+ data = {"counts": jnp.ones((100, 2000))}
80
+ result, state, meta = op.apply(data, {}, None)
81
+ ```
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ config: ArchetypalAnalysisConfig,
87
+ *,
88
+ rngs: nnx.Rngs | None = None,
89
+ name: str | None = None,
90
+ ) -> None:
91
+ """Initialize the archetypal analysis operator.
92
+
93
+ Args:
94
+ config: Archetypal analysis configuration.
95
+ rngs: Random number generators for weight initialisation.
96
+ name: Optional operator name.
97
+ """
98
+ super().__init__(config, rngs=rngs, name=name)
99
+
100
+ rngs = ensure_rngs(rngs)
101
+
102
+ self.encoder_layers = MLP(
103
+ hidden_dims=[config.hidden_dim],
104
+ in_features=config.n_genes,
105
+ activation="relu",
106
+ output_activation="relu",
107
+ use_batch_norm=False,
108
+ rngs=rngs,
109
+ )
110
+ self.projection = nnx.Linear(
111
+ in_features=config.hidden_dim,
112
+ out_features=config.n_archetypes,
113
+ rngs=rngs,
114
+ )
115
+
116
+ # Learnable archetype prototypes (n_archetypes, n_genes)
117
+ key = get_rng_key(rngs, "params", fallback_seed=1)
118
+ init_archetypes = jax.random.normal(key, (config.n_archetypes, config.n_genes)) * 0.1
119
+ self.archetypes = nnx.Param(init_archetypes)
120
+
121
+ def encode(
122
+ self,
123
+ counts: Float[Array, "n_cells n_genes"],
124
+ ) -> Float[Array, "n_cells n_archetypes"]:
125
+ """Encode cells to simplex weights over archetypes.
126
+
127
+ Args:
128
+ counts: Cell-by-gene count matrix.
129
+
130
+ Returns:
131
+ Simplex weights of shape ``(n_cells, n_archetypes)``.
132
+ """
133
+ hidden: jax.Array = self.encoder_layers(counts)
134
+ logits = self.projection(hidden)
135
+ weights = jax.nn.softmax(logits / self._temperature, axis=-1)
136
+ return weights
137
+
138
+ def reconstruct(
139
+ self,
140
+ weights: Float[Array, "n_cells n_archetypes"],
141
+ ) -> Float[Array, "n_cells n_genes"]:
142
+ """Reconstruct cells as convex combinations of archetypes.
143
+
144
+ Args:
145
+ weights: Simplex weights per cell.
146
+
147
+ Returns:
148
+ Reconstructed cell-by-gene matrix.
149
+ """
150
+ return weights @ self.archetypes[...]
151
+
152
+ def apply(
153
+ self,
154
+ data: PyTree,
155
+ state: PyTree,
156
+ metadata: dict[str, Any] | None,
157
+ random_params: Any = None,
158
+ stats: dict[str, Any] | None = None,
159
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
160
+ """Apply archetypal analysis to a cell-by-gene count matrix.
161
+
162
+ Args:
163
+ data: Dictionary containing:
164
+ - ``"counts"``: Cell-by-gene matrix ``(n_cells, n_genes)``
165
+ state: Element state (passed through unchanged).
166
+ metadata: Element metadata (passed through unchanged).
167
+ random_params: Not used.
168
+ stats: Not used.
169
+
170
+ Returns:
171
+ Tuple of ``(transformed_data, state, metadata)`` where
172
+ ``transformed_data`` contains:
173
+
174
+ - ``"counts"``: Original count matrix
175
+ - ``"archetype_weights"``: Simplex weights ``(n_cells, n_archetypes)``
176
+ - ``"archetypes"``: Archetype prototypes ``(n_archetypes, n_genes)``
177
+ - ``"reconstructed"``: Reconstructed counts ``(n_cells, n_genes)``
178
+ """
179
+ counts = data["counts"]
180
+
181
+ weights = self.encode(counts)
182
+ reconstructed = self.reconstruct(weights)
183
+
184
+ transformed_data = {
185
+ **data,
186
+ "archetype_weights": weights,
187
+ "archetypes": self.archetypes[...],
188
+ "reconstructed": reconstructed,
189
+ }
190
+
191
+ return transformed_data, state, metadata