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,166 @@
1
+ """Differentiable read-depth downsampling for count data.
2
+
3
+ Provides a differentiable approximation to binomial read downsampling,
4
+ enabling gradient flow through the downsampling operation via a
5
+ straight-through estimator.
6
+
7
+ References:
8
+ - cell-load/src/cell_load/dataset/_perturbation.py (downsampling logic)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from typing import Any
16
+
17
+ import jax
18
+ import jax.numpy as jnp
19
+ from flax import nnx
20
+
21
+ from datarax.core.config import OperatorConfig
22
+ from datarax.core.operator import OperatorModule
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class DownsamplingConfig(OperatorConfig):
29
+ """Configuration for ReadDownsampler.
30
+
31
+ Attributes:
32
+ mode: Downsampling mode. ``"fraction"`` scales by a fixed fraction.
33
+ ``"target_depth"`` computes per-cell fraction from target read depth.
34
+ fraction: Fraction of reads to keep when ``mode="fraction"``.
35
+ target_depth: Target total reads per cell when ``mode="target_depth"``.
36
+ apply_log1p: Whether to apply log1p to the downsampled counts.
37
+ is_log1p_input: Whether the input counts are already log1p-transformed.
38
+ """
39
+
40
+ mode: str = "fraction"
41
+ fraction: float = 1.0
42
+ target_depth: int | None = None
43
+ apply_log1p: bool = True
44
+ is_log1p_input: bool = True
45
+
46
+ def __post_init__(self) -> None:
47
+ """Validate configuration."""
48
+ # Downsampling is stochastic
49
+ object.__setattr__(self, "stochastic", True)
50
+ if self.stream_name is None:
51
+ object.__setattr__(self, "stream_name", "downsample")
52
+ super().__post_init__()
53
+
54
+
55
+ class ReadDownsampler(OperatorModule):
56
+ """Differentiable read-depth downsampler.
57
+
58
+ Approximates binomial downsampling using a continuous relaxation that
59
+ allows gradient flow via the straight-through estimator:
60
+
61
+ - **Forward:** ``downsampled = floor(counts * fraction)`` plus a
62
+ stochastic rounding of the remainder.
63
+ - **Backward:** Gradients flow through ``counts * fraction`` (the
64
+ expected value).
65
+
66
+ Handles log1p domain: if input is log1p-transformed, applies expm1
67
+ before downsampling and log1p after.
68
+
69
+ Args:
70
+ config: Downsampling configuration.
71
+ rngs: RNG state for stochastic sampling.
72
+ name: Optional module name.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ config: DownsamplingConfig,
78
+ *,
79
+ rngs: nnx.Rngs | None = None,
80
+ name: str | None = None,
81
+ ) -> None:
82
+ super().__init__(config, rngs=rngs, name=name)
83
+
84
+ def apply(
85
+ self,
86
+ data: dict[str, Any],
87
+ state: dict[str, Any],
88
+ metadata: Any,
89
+ random_params: Any = None, # noqa: ARG002
90
+ stats: Any = None, # noqa: ARG002
91
+ ) -> tuple[dict[str, Any], dict[str, Any], Any]:
92
+ """Apply differentiable downsampling to count data.
93
+
94
+ Args:
95
+ data: Dict with ``"counts"`` key containing expression matrix.
96
+ state: Pipeline state (passed through).
97
+ metadata: Pipeline metadata (passed through).
98
+ random_params: Unused (RNG handled internally).
99
+ stats: Unused.
100
+
101
+ Returns:
102
+ Tuple of (updated_data, state, metadata).
103
+ """
104
+ counts = data["counts"]
105
+ config: DownsamplingConfig = self.config # type: ignore[assignment]
106
+
107
+ # Invert log1p if needed
108
+ if config.is_log1p_input:
109
+ counts = jnp.expm1(counts)
110
+
111
+ # Compute per-cell fraction
112
+ fraction: float | jnp.ndarray
113
+ if config.mode == "target_depth" and config.target_depth is not None:
114
+ cell_totals = jnp.sum(counts, axis=-1, keepdims=True)
115
+ cell_totals = jnp.maximum(cell_totals, 1.0) # avoid div by zero
116
+ fraction = jnp.minimum(config.target_depth / cell_totals, 1.0)
117
+ else:
118
+ fraction = config.fraction
119
+
120
+ # Straight-through differentiable downsampling
121
+ downsampled = _straight_through_downsample(counts, fraction, self.rngs)
122
+
123
+ # Ensure non-negative
124
+ downsampled = jnp.maximum(downsampled, 0.0)
125
+
126
+ # Re-apply log1p if configured
127
+ if config.apply_log1p:
128
+ downsampled = jnp.log1p(downsampled)
129
+
130
+ return {**data, "counts": downsampled}, state, metadata
131
+
132
+
133
+ def _straight_through_downsample(
134
+ counts: jnp.ndarray,
135
+ fraction: float | jnp.ndarray,
136
+ rngs: nnx.Rngs | None,
137
+ ) -> jnp.ndarray:
138
+ """Downsample counts with straight-through gradient estimator.
139
+
140
+ Forward: ``floor(counts * f) + Bernoulli(counts * f - floor(counts * f))``
141
+ Backward: gradient flows through ``counts * f`` (expected value).
142
+
143
+ Args:
144
+ counts: Count matrix.
145
+ fraction: Downsampling fraction (scalar or per-cell array).
146
+ rngs: RNG state for Bernoulli sampling.
147
+
148
+ Returns:
149
+ Downsampled count matrix.
150
+ """
151
+ expected = counts * fraction
152
+ floored = jnp.floor(expected)
153
+ remainder = expected - floored
154
+
155
+ # Stochastic rounding of remainder
156
+ if rngs is not None and "downsample" in rngs:
157
+ key = rngs.downsample()
158
+ else:
159
+ key = jax.random.key(0)
160
+
161
+ uniform = jax.random.uniform(key, shape=remainder.shape)
162
+ rounded = jnp.where(uniform < remainder, 1.0, 0.0)
163
+
164
+ # Forward: discrete value. Backward: gradient through expected value.
165
+ discrete = floored + rounded
166
+ return expected + jax.lax.stop_gradient(discrete - expected)
@@ -0,0 +1,519 @@
1
+ """Enhanced differentiable batch correction operators using MMD and WGAN.
2
+
3
+ This module provides two neural-network-based batch correction strategies:
4
+
5
+ - **DifferentiableMMDBatchCorrection**: Autoencoder with Maximum Mean Discrepancy
6
+ (MMD) regularisation that penalises distributional differences between batches
7
+ in latent space.
8
+ - **DifferentiableWGANBatchCorrection**: Adversarial autoencoder with a Wasserstein
9
+ GAN discriminator that learns batch-invariant latent representations through
10
+ gradient reversal.
11
+
12
+ Design references:
13
+ - scGPT gradient reversal for adversarial batch correction.
14
+ - scVI batch encoding as one-hot or embedding.
15
+ - scGPT domain-specific batch normalisation.
16
+
17
+ Both operators inherit from ``OperatorModule`` and follow the standard
18
+ ``apply(data, state, metadata)`` interface for pipeline composability.
19
+ """
20
+
21
+ import logging
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ import jax
26
+ import jax.numpy as jnp
27
+ from artifex.generative_models.core.base import MLP
28
+ from artifex.generative_models.core.losses.adversarial import (
29
+ wasserstein_discriminator_loss,
30
+ wasserstein_generator_loss,
31
+ )
32
+ from artifex.generative_models.core.losses.base import reduce_loss
33
+ from artifex.generative_models.core.losses.divergence import maximum_mean_discrepancy
34
+ from datarax.core.config import OperatorConfig
35
+ from datarax.core.operator import OperatorModule
36
+ from flax import nnx
37
+ from jaxtyping import Array, Float, Int, PyTree
38
+
39
+ from diffbio.operators._loss_balancing import LossBalancingMixin
40
+ from diffbio.utils.nn_utils import ensure_rngs
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ __all__ = [
45
+ "DifferentiableMMDBatchCorrection",
46
+ "DifferentiableWGANBatchCorrection",
47
+ "MMDBatchCorrectionConfig",
48
+ "WGANBatchCorrectionConfig",
49
+ ]
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Gradient reversal primitive (JAX custom_vjp)
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ @jax.custom_vjp
58
+ def _gradient_reversal(x: jax.Array, scale: float) -> jax.Array:
59
+ """Pass-through forward, negate gradients backward."""
60
+ return x
61
+
62
+
63
+ def _gradient_reversal_fwd(x: jax.Array, scale: float) -> tuple[jax.Array, float]:
64
+ """Forward pass for gradient reversal."""
65
+ return x, scale
66
+
67
+
68
+ def _gradient_reversal_bwd(scale: float, grad: jax.Array) -> tuple[jax.Array, None]:
69
+ """Backward pass: negate and scale the gradient."""
70
+ return (-scale * grad, None)
71
+
72
+
73
+ _gradient_reversal.defvjp(_gradient_reversal_fwd, _gradient_reversal_bwd)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Configs
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class MMDBatchCorrectionConfig(OperatorConfig):
83
+ """Configuration for MMD-based batch correction.
84
+
85
+ Attributes:
86
+ n_genes: Number of input genes (features).
87
+ hidden_dim: Width of hidden layers in the autoencoder.
88
+ latent_dim: Dimensionality of the latent space.
89
+ kernel_bandwidth: Bandwidth for the RBF kernel in the MMD loss.
90
+ use_gradnorm: Whether to use GradNormBalancer for multi-task loss
91
+ balancing between reconstruction and MMD losses.
92
+ """
93
+
94
+ n_genes: int = 2000
95
+ hidden_dim: int = 128
96
+ latent_dim: int = 64
97
+ kernel_bandwidth: float = 1.0
98
+ use_gradnorm: bool = False
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class WGANBatchCorrectionConfig(OperatorConfig):
103
+ """Configuration for WGAN-based batch correction.
104
+
105
+ Attributes:
106
+ n_genes: Number of input genes (features).
107
+ hidden_dim: Width of hidden layers in the generator autoencoder.
108
+ latent_dim: Dimensionality of the latent space.
109
+ discriminator_hidden_dim: Width of hidden layers in the discriminator.
110
+ use_gradnorm: Whether to use GradNormBalancer for multi-task loss
111
+ balancing between generator and discriminator losses.
112
+ """
113
+
114
+ n_genes: int = 2000
115
+ hidden_dim: int = 128
116
+ latent_dim: int = 64
117
+ discriminator_hidden_dim: int = 64
118
+ use_gradnorm: bool = False
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # MMD batch correction
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ class DifferentiableMMDBatchCorrection(LossBalancingMixin, OperatorModule):
127
+ """Autoencoder batch correction with MMD regularisation.
128
+
129
+ Architecture:
130
+ Encoder MLP maps gene expression to a latent representation, and a
131
+ decoder MLP reconstructs the expression from that latent. The MMD
132
+ loss penalises distributional mismatch between batches in latent
133
+ space so the learned representation becomes batch-invariant.
134
+
135
+ Loss:
136
+ ``reconstruction_mse + mmd(latent_batch_0, latent_batch_1, ...)``
137
+
138
+ Args:
139
+ config: MMDBatchCorrectionConfig with model hyper-parameters.
140
+ rngs: Flax NNX random number generators.
141
+ name: Optional operator name.
142
+
143
+ Example:
144
+ >>> config = MMDBatchCorrectionConfig(n_genes=2000)
145
+ >>> op = DifferentiableMMDBatchCorrection(config, rngs=nnx.Rngs(0))
146
+ >>> result, _, _ = op.apply(data, {}, None)
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ config: MMDBatchCorrectionConfig,
152
+ *,
153
+ rngs: nnx.Rngs | None = None,
154
+ name: str | None = None,
155
+ ) -> None:
156
+ """Initialise encoder and decoder MLPs.
157
+
158
+ Args:
159
+ config: Operator configuration.
160
+ rngs: Random number generators for weight initialisation.
161
+ name: Optional operator name.
162
+ """
163
+ super().__init__(config, rngs=rngs, name=name)
164
+
165
+ rngs = ensure_rngs(rngs)
166
+
167
+ self.encoder = MLP(
168
+ hidden_dims=[config.hidden_dim, config.hidden_dim, config.latent_dim],
169
+ in_features=config.n_genes,
170
+ activation="relu",
171
+ use_batch_norm=False,
172
+ rngs=rngs,
173
+ )
174
+
175
+ self.decoder = MLP(
176
+ hidden_dims=[config.hidden_dim, config.hidden_dim, config.n_genes],
177
+ in_features=config.latent_dim,
178
+ activation="relu",
179
+ use_batch_norm=False,
180
+ rngs=rngs,
181
+ )
182
+
183
+ # -- helpers --------------------------------------------------------------
184
+
185
+ def _encode(
186
+ self, expression: Float[Array, "n_cells n_genes"]
187
+ ) -> Float[Array, "n_cells latent_dim"]:
188
+ """Map expression to latent space.
189
+
190
+ Args:
191
+ expression: Input gene expression matrix.
192
+
193
+ Returns:
194
+ Latent representation.
195
+ """
196
+ latent: jax.Array = self.encoder(expression)
197
+ return latent
198
+
199
+ def _decode(
200
+ self, latent: Float[Array, "n_cells latent_dim"]
201
+ ) -> Float[Array, "n_cells n_genes"]:
202
+ """Reconstruct expression from latent space.
203
+
204
+ Args:
205
+ latent: Latent representation.
206
+
207
+ Returns:
208
+ Reconstructed gene expression.
209
+ """
210
+ reconstruction: jax.Array = self.decoder(latent)
211
+ return reconstruction
212
+
213
+ def _compute_pairwise_mmd(
214
+ self,
215
+ latent: Float[Array, "n_cells latent_dim"],
216
+ batch_labels: Int[Array, "n_cells"],
217
+ ) -> Float[Array, ""]:
218
+ """Compute MMD between batch-0 and non-batch-0 cells in latent space.
219
+
220
+ Uses masked mean embeddings to remain fully JIT-compatible (no boolean
221
+ indexing). For multi-batch data the comparison is batch-0 vs the rest,
222
+ which encourages all batches to align in latent space. When only one
223
+ batch is present the two groups are identical and MMD is near zero.
224
+
225
+ Args:
226
+ latent: Latent representations for all cells.
227
+ batch_labels: Integer batch assignments per cell.
228
+
229
+ Returns:
230
+ Scalar MMD loss.
231
+ """
232
+ n_cells = latent.shape[0]
233
+
234
+ # Soft masks (JIT-safe: no boolean indexing)
235
+ is_batch0 = (batch_labels == 0).astype(jnp.float32) # (n_cells,)
236
+ is_other = 1.0 - is_batch0
237
+
238
+ n_batch0 = jnp.maximum(is_batch0.sum(), 1.0)
239
+ n_other = jnp.maximum(is_other.sum(), 1.0)
240
+
241
+ # Weighted latent: zero out cells not in the group, then stack as
242
+ # (1, n_cells, latent_dim) for the MMD function which expects
243
+ # [batch, samples, features].
244
+ latent_batch0 = latent * is_batch0[:, None] # (n_cells, d)
245
+ latent_other = latent * is_other[:, None]
246
+
247
+ # Normalize so the masked-out zeros don't bias the kernel.
248
+ # Scale the active entries up by n_cells / n_active so that the
249
+ # effective sample still fills the (n_cells, d) array.
250
+ latent_batch0 = latent_batch0 * (n_cells / n_batch0)
251
+ latent_other = latent_other * (n_cells / n_other)
252
+
253
+ return maximum_mean_discrepancy(
254
+ latent_batch0[None, ...], # (1, n_cells, d)
255
+ latent_other[None, ...], # (1, n_cells, d)
256
+ kernel_type="rbf",
257
+ kernel_bandwidth=self.config.kernel_bandwidth,
258
+ reduction="mean",
259
+ )
260
+
261
+ # -- apply ----------------------------------------------------------------
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
+ """Encode, decode, and compute MMD + reconstruction losses.
272
+
273
+ Args:
274
+ data: Dictionary containing:
275
+ - ``"expression"``: Gene expression matrix ``(n_cells, n_genes)``
276
+ - ``"batch_labels"``: Integer batch assignments ``(n_cells,)``
277
+ state: Pipeline state (passed through unchanged).
278
+ metadata: Pipeline metadata (passed through unchanged).
279
+ random_params: Unused.
280
+ stats: Unused.
281
+
282
+ Returns:
283
+ Tuple of ``(result, state, metadata)`` where *result* contains:
284
+ - ``"expression"``: Original expression
285
+ - ``"batch_labels"``: Original batch labels
286
+ - ``"corrected_expression"``: Decoded (corrected) expression
287
+ - ``"latent"``: Latent representation
288
+ - ``"mmd_loss"``: Scalar MMD loss between batches
289
+ - ``"reconstruction_loss"``: Scalar MSE reconstruction loss
290
+ """
291
+ expression = data["expression"]
292
+ batch_labels = data["batch_labels"]
293
+
294
+ # Forward pass
295
+ latent = self._encode(expression)
296
+ reconstructed = self._decode(latent)
297
+
298
+ # Losses
299
+ reconstruction_loss = reduce_loss(
300
+ (reconstructed - expression) ** 2,
301
+ reduction="mean",
302
+ )
303
+ mmd_loss = self._compute_pairwise_mmd(latent, batch_labels)
304
+
305
+ result = {
306
+ **data,
307
+ "corrected_expression": reconstructed,
308
+ "latent": latent,
309
+ "mmd_loss": mmd_loss,
310
+ "reconstruction_loss": reconstruction_loss,
311
+ }
312
+ return result, state, metadata
313
+
314
+
315
+ # ---------------------------------------------------------------------------
316
+ # WGAN batch correction
317
+ # ---------------------------------------------------------------------------
318
+
319
+
320
+ class DifferentiableWGANBatchCorrection(LossBalancingMixin, OperatorModule):
321
+ """Adversarial autoencoder batch correction with Wasserstein GAN loss.
322
+
323
+ Architecture:
324
+ An encoder (generator) maps gene expression to a batch-invariant
325
+ latent space and a decoder reconstructs the expression. A separate
326
+ discriminator tries to predict the batch label from the latent
327
+ representation. Gradient reversal (a la scGPT) ensures the encoder
328
+ learns to *fool* the discriminator, yielding batch-invariant latents.
329
+
330
+ Losses:
331
+ - ``generator_loss``: Wasserstein generator loss (encoder wants to
332
+ fool the discriminator) plus reconstruction MSE.
333
+ - ``discriminator_loss``: Wasserstein discriminator/critic loss.
334
+
335
+ Args:
336
+ config: WGANBatchCorrectionConfig with model hyper-parameters.
337
+ rngs: Flax NNX random number generators.
338
+ name: Optional operator name.
339
+
340
+ Example:
341
+ >>> config = WGANBatchCorrectionConfig(n_genes=2000)
342
+ >>> op = DifferentiableWGANBatchCorrection(config, rngs=nnx.Rngs(0))
343
+ >>> result, _, _ = op.apply(data, {}, None)
344
+ """
345
+
346
+ def __init__(
347
+ self,
348
+ config: WGANBatchCorrectionConfig,
349
+ *,
350
+ rngs: nnx.Rngs | None = None,
351
+ name: str | None = None,
352
+ ) -> None:
353
+ """Initialise encoder, decoder, and discriminator MLPs.
354
+
355
+ Args:
356
+ config: Operator configuration.
357
+ rngs: Random number generators for weight initialisation.
358
+ name: Optional operator name.
359
+ """
360
+ super().__init__(config, rngs=rngs, name=name)
361
+
362
+ rngs = ensure_rngs(rngs)
363
+
364
+ self.encoder = MLP(
365
+ hidden_dims=[config.hidden_dim, config.hidden_dim, config.latent_dim],
366
+ in_features=config.n_genes,
367
+ activation="relu",
368
+ use_batch_norm=False,
369
+ rngs=rngs,
370
+ )
371
+
372
+ self.decoder = MLP(
373
+ hidden_dims=[config.hidden_dim, config.hidden_dim, config.n_genes],
374
+ in_features=config.latent_dim,
375
+ activation="relu",
376
+ use_batch_norm=False,
377
+ rngs=rngs,
378
+ )
379
+
380
+ self.discriminator = MLP(
381
+ hidden_dims=[
382
+ config.discriminator_hidden_dim,
383
+ config.discriminator_hidden_dim,
384
+ 1,
385
+ ],
386
+ in_features=config.latent_dim,
387
+ activation="relu",
388
+ use_batch_norm=False,
389
+ rngs=rngs,
390
+ )
391
+
392
+ # -- helpers --------------------------------------------------------------
393
+
394
+ def _encode(
395
+ self, expression: Float[Array, "n_cells n_genes"]
396
+ ) -> Float[Array, "n_cells latent_dim"]:
397
+ """Map expression to latent space.
398
+
399
+ Args:
400
+ expression: Input gene expression matrix.
401
+
402
+ Returns:
403
+ Latent representation.
404
+ """
405
+ latent: jax.Array = self.encoder(expression)
406
+ return latent
407
+
408
+ def _decode(
409
+ self, latent: Float[Array, "n_cells latent_dim"]
410
+ ) -> Float[Array, "n_cells n_genes"]:
411
+ """Reconstruct expression from latent space.
412
+
413
+ Args:
414
+ latent: Latent representation.
415
+
416
+ Returns:
417
+ Reconstructed gene expression.
418
+ """
419
+ reconstruction: jax.Array = self.decoder(latent)
420
+ return reconstruction
421
+
422
+ def _discriminate(self, latent: Float[Array, "n_cells latent_dim"]) -> Float[Array, "n_cells"]:
423
+ """Compute discriminator (critic) scores from latent representations.
424
+
425
+ Args:
426
+ latent: Latent representation.
427
+
428
+ Returns:
429
+ Per-cell scalar critic scores.
430
+ """
431
+ scores: jax.Array = self.discriminator(latent)
432
+ return scores.squeeze(-1) # (n_cells,)
433
+
434
+ # -- apply ----------------------------------------------------------------
435
+
436
+ def apply(
437
+ self,
438
+ data: PyTree,
439
+ state: PyTree,
440
+ metadata: dict[str, Any] | None,
441
+ random_params: Any = None,
442
+ stats: dict[str, Any] | None = None,
443
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
444
+ """Encode, decode, and compute adversarial + reconstruction losses.
445
+
446
+ The discriminator receives latent codes through a gradient reversal
447
+ layer so that encoder gradients push toward batch invariance while
448
+ discriminator gradients push toward better batch classification.
449
+
450
+ Args:
451
+ data: Dictionary containing:
452
+ - ``"expression"``: Gene expression matrix ``(n_cells, n_genes)``
453
+ - ``"batch_labels"``: Integer batch assignments ``(n_cells,)``
454
+ state: Pipeline state (passed through unchanged).
455
+ metadata: Pipeline metadata (passed through unchanged).
456
+ random_params: Unused.
457
+ stats: Unused.
458
+
459
+ Returns:
460
+ Tuple of ``(result, state, metadata)`` where *result* contains:
461
+ - ``"expression"``: Original expression
462
+ - ``"batch_labels"``: Original batch labels
463
+ - ``"corrected_expression"``: Decoded (corrected) expression
464
+ - ``"latent"``: Latent representation
465
+ - ``"discriminator_scores"``: Per-cell critic scores
466
+ - ``"generator_loss"``: Scalar Wasserstein generator loss
467
+ - ``"discriminator_loss"``: Scalar Wasserstein discriminator loss
468
+ """
469
+ expression = data["expression"]
470
+ batch_labels = data["batch_labels"]
471
+
472
+ # Encode -> decode
473
+ latent = self._encode(expression)
474
+ reconstructed = self._decode(latent)
475
+
476
+ # Discriminator path: gradient reversal so encoder learns to fool it
477
+ latent_reversed = _gradient_reversal(latent, 1.0)
478
+ disc_scores = self._discriminate(latent_reversed)
479
+
480
+ # Reconstruction loss
481
+ reconstruction_loss = reduce_loss(
482
+ (reconstructed - expression) ** 2,
483
+ reduction="mean",
484
+ )
485
+
486
+ # Identify "real" (batch 0) and "fake" (batch != 0) for WGAN framing.
487
+ # The discriminator tries to distinguish batch 0 from the rest.
488
+ # We compute masked means manually to stay JIT-compatible (no boolean
489
+ # indexing), then delegate to the library Wasserstein loss functions.
490
+ is_batch0 = (batch_labels == 0).astype(jnp.float32)
491
+ is_other = 1.0 - is_batch0
492
+ n_real = jnp.maximum(is_batch0.sum(), 1.0)
493
+ n_fake = jnp.maximum(is_other.sum(), 1.0)
494
+
495
+ # Masked scores: one scalar per group (batch-mean critic output)
496
+ real_mean_score = (disc_scores * is_batch0).sum() / n_real
497
+ fake_mean_score = (disc_scores * is_other).sum() / n_fake
498
+
499
+ # Use library Wasserstein losses (each expects 1-D scores)
500
+ generator_loss = wasserstein_generator_loss(fake_mean_score[None]) + reconstruction_loss
501
+ discriminator_loss = wasserstein_discriminator_loss(
502
+ real_mean_score[None],
503
+ fake_mean_score[None],
504
+ )
505
+
506
+ result = {
507
+ **data,
508
+ "corrected_expression": reconstructed,
509
+ "latent": latent,
510
+ "discriminator_scores": disc_scores,
511
+ "generator_loss": generator_loss,
512
+ "discriminator_loss": discriminator_loss,
513
+ }
514
+ return result, state, metadata
515
+
516
+
517
+ # ---------------------------------------------------------------------------
518
+ # Internal MLP container (nnx.Module so it appears in the grad tree)
519
+ # ---------------------------------------------------------------------------