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,493 @@
1
+ """Differentiable spatial gene detection operators.
2
+
3
+ This module provides Gaussian process-based approaches to spatial gene detection
4
+ inspired by SpatialDE for identifying spatially variable genes in spatial
5
+ transcriptomics data.
6
+
7
+ SpatialDE decomposes expression variability into spatial and non-spatial components
8
+ using GP regression with RBF kernels. The Fraction of Spatial Variance (FSV)
9
+ quantifies how much variance is explained by spatial structure.
10
+
11
+ References:
12
+ Svensson et al. (2018) "SpatialDE: identification of spatially variable genes"
13
+ https://www.nature.com/articles/nmeth.4636
14
+ """
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ import jax
21
+ import jax.numpy as jnp
22
+ from datarax.core.config import OperatorConfig
23
+ from flax import nnx
24
+ from jaxtyping import Array, Float
25
+
26
+ from diffbio.core import soft_ops
27
+ from diffbio.core.base_operators import TemperatureOperator
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class _SpatialKernelConfig:
34
+ """Gaussian-process kernel configuration."""
35
+
36
+ lengthscale: float = 1.0
37
+ variance: float = 1.0
38
+ noise_variance: float = 0.1
39
+ n_inducing_points: int = 100
40
+ learnable_kernel: bool = True
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class _SpatialDetectionConfig:
45
+ """Spatial gene classification configuration."""
46
+
47
+ n_genes: int = 2000
48
+ hidden_dims: tuple[int, ...] | list[int] = (64, 32)
49
+ temperature: float = 1.0
50
+ pvalue_threshold: float = 0.05
51
+ compute_field_ops: bool = False
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class SpatialGeneDetectorConfig(
56
+ _SpatialKernelConfig,
57
+ _SpatialDetectionConfig,
58
+ OperatorConfig,
59
+ ):
60
+ """Configuration for spatial gene detection."""
61
+
62
+ def __post_init__(self) -> None:
63
+ """Validate the spatial gene detector configuration."""
64
+ super().__post_init__()
65
+
66
+ hidden_dims = tuple(self.hidden_dims)
67
+ object.__setattr__(self, "hidden_dims", hidden_dims)
68
+
69
+ if self.n_genes <= 0:
70
+ raise ValueError("n_genes must be positive.")
71
+ if self.lengthscale <= 0.0:
72
+ raise ValueError("lengthscale must be positive.")
73
+ if self.variance <= 0.0:
74
+ raise ValueError("variance must be positive.")
75
+ if self.noise_variance <= 0.0:
76
+ raise ValueError("noise_variance must be positive.")
77
+ if self.n_inducing_points <= 0:
78
+ raise ValueError("n_inducing_points must be positive.")
79
+ if not hidden_dims or any(hidden_dim <= 0 for hidden_dim in hidden_dims):
80
+ raise ValueError("hidden_dims must contain only positive integers.")
81
+ if self.temperature <= 0.0:
82
+ raise ValueError("temperature must be positive.")
83
+ if not 0.0 <= self.pvalue_threshold <= 1.0:
84
+ raise ValueError("pvalue_threshold must be between 0.0 and 1.0.")
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class _FixedKernelState:
89
+ """Static kernel parameters for non-learnable mode."""
90
+
91
+ log_lengthscale: float
92
+ log_variance: float
93
+ log_noise_variance: float
94
+
95
+
96
+ class _LearnableKernelState(nnx.Module):
97
+ """Learnable kernel parameters stored in log-space."""
98
+
99
+ def __init__(self, config: SpatialGeneDetectorConfig) -> None:
100
+ self.log_lengthscale = nnx.Param(jnp.log(jnp.array(config.lengthscale)))
101
+ self.log_variance = nnx.Param(jnp.log(jnp.array(config.variance)))
102
+ self.log_noise_variance = nnx.Param(jnp.log(jnp.array(config.noise_variance)))
103
+
104
+
105
+ class _SpatialSmoothingNetwork(nnx.Module):
106
+ """Neural approximation to the GP posterior mean."""
107
+
108
+ def __init__(
109
+ self,
110
+ *,
111
+ hidden_dims: tuple[int, ...],
112
+ n_genes: int,
113
+ rngs: nnx.Rngs,
114
+ ) -> None:
115
+ smoothing_layers = []
116
+ prev_dim = 2
117
+ for hidden_dim in hidden_dims:
118
+ smoothing_layers.append(nnx.Linear(prev_dim, hidden_dim, rngs=rngs))
119
+ prev_dim = hidden_dim
120
+ self.layers = nnx.List(smoothing_layers)
121
+ self.output = nnx.Linear(prev_dim, n_genes, rngs=rngs)
122
+
123
+ def __call__(self, coords: Float[Array, "n_spots 2"]) -> Float[Array, "n_spots n_genes"]:
124
+ """Predict a spatial deviation field from normalized coordinates."""
125
+ hidden = coords
126
+ for layer in self.layers:
127
+ hidden = nnx.relu(layer(hidden))
128
+ return self.output(hidden)
129
+
130
+
131
+ class DifferentiableSpatialGeneDetector(TemperatureOperator):
132
+ """SpatialDE-style differentiable spatial gene detection.
133
+
134
+ This operator identifies spatially variable genes using a differentiable
135
+ Gaussian process approach. It computes a spatial variance score for each
136
+ gene and provides soft assignments for spatial vs non-spatial genes.
137
+
138
+ The model decomposes gene expression as:
139
+ y = f(x) + epsilon
140
+ where f(x) ~ GP(0, K) is the spatial component and epsilon ~ N(0, sigma^2)
141
+ is the non-spatial noise.
142
+
143
+ The Fraction of Spatial Variance (FSV) is:
144
+ FSV = sigma^2_s / (sigma^2_s + sigma^2_e)
145
+
146
+ Input data structure:
147
+ - spatial_coords: Float[Array, "n_spots 2"] - Spatial coordinates
148
+ - expression: Float[Array, "n_spots n_genes"] - Gene expression
149
+ - total_counts: Float[Array, "n_spots"] - Total counts per spot
150
+
151
+ Output data structure (adds):
152
+ - spatial_variance: Float[Array, "n_genes"] - Spatial variance per gene
153
+ - spatial_pvalues: Float[Array, "n_genes"] - P-values for spatial patterns
154
+ - is_spatial: Float[Array, "n_genes"] - Soft spatial gene indicator
155
+ - smoothed_expression: Float[Array, "n_spots n_genes"] - GP smoothed expression
156
+ - fsv: Float[Array, "n_genes"] - Fraction of Spatial Variance
157
+
158
+ Example:
159
+ ```python
160
+ config = SpatialGeneDetectorConfig(n_genes=2000)
161
+ detector = DifferentiableSpatialGeneDetector(config, rngs=nnx.Rngs(42))
162
+ result, state, meta = detector.apply(data, {}, None)
163
+ spatial_genes = result["is_spatial"] > 0.5
164
+ ```
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ config: SpatialGeneDetectorConfig,
170
+ *,
171
+ rngs: nnx.Rngs,
172
+ name: str | None = None,
173
+ ):
174
+ """Initialize the spatial gene detector.
175
+
176
+ Args:
177
+ config: Detector configuration.
178
+ rngs: Random number generators.
179
+ name: Optional name for the operator.
180
+ """
181
+ super().__init__(config, rngs=rngs, name=name)
182
+
183
+ # Kernel parameters (learnable in log-space for positivity)
184
+ if config.learnable_kernel:
185
+ self.kernel_state = _LearnableKernelState(config)
186
+ else:
187
+ self.kernel_state = nnx.static(
188
+ _FixedKernelState(
189
+ log_lengthscale=float(jnp.log(jnp.array(config.lengthscale))),
190
+ log_variance=float(jnp.log(jnp.array(config.variance))),
191
+ log_noise_variance=float(jnp.log(jnp.array(config.noise_variance))),
192
+ )
193
+ )
194
+
195
+ # Smoothing network for expression (neural approximation to GP mean)
196
+ self.smoothing_network = _SpatialSmoothingNetwork(
197
+ hidden_dims=tuple(config.hidden_dims),
198
+ n_genes=config.n_genes,
199
+ rngs=rngs,
200
+ )
201
+
202
+ @property
203
+ def lengthscale(self) -> Float[Array, ""] | float:
204
+ """Get the characteristic length for RBF kernel."""
205
+ kernel_state = self.kernel_state
206
+ if isinstance(kernel_state, _LearnableKernelState):
207
+ return jnp.exp(kernel_state.log_lengthscale[...])
208
+ return jnp.exp(jnp.asarray(kernel_state.log_lengthscale))
209
+
210
+ @property
211
+ def variance(self) -> Float[Array, ""] | float:
212
+ """Get the signal variance parameter."""
213
+ kernel_state = self.kernel_state
214
+ if isinstance(kernel_state, _LearnableKernelState):
215
+ return jnp.exp(kernel_state.log_variance[...])
216
+ return jnp.exp(jnp.asarray(kernel_state.log_variance))
217
+
218
+ @property
219
+ def noise_variance(self) -> Float[Array, ""] | float:
220
+ """Get current noise variance (sigma^2_e)."""
221
+ kernel_state = self.kernel_state
222
+ if isinstance(kernel_state, _LearnableKernelState):
223
+ return jnp.exp(kernel_state.log_noise_variance[...])
224
+ return jnp.exp(jnp.asarray(kernel_state.log_noise_variance))
225
+
226
+ def compute_kernel(
227
+ self,
228
+ X1: Float[Array, "n1 2"],
229
+ X2: Float[Array, "n2 2"],
230
+ ) -> Float[Array, "n1 n2"]:
231
+ """Compute squared exponential (RBF) kernel matrix.
232
+
233
+ K(x1, x2) = variance * exp(-||x1 - x2||^2 / (2 * lengthscale^2))
234
+
235
+ This is the standard kernel used in SpatialDE for modeling
236
+ spatial covariance.
237
+
238
+ Args:
239
+ X1: First set of spatial coordinates.
240
+ X2: Second set of spatial coordinates.
241
+
242
+ Returns:
243
+ Kernel matrix.
244
+ """
245
+ # Compute squared Euclidean distances
246
+ sq_dist = jnp.sum(
247
+ (X1[:, None, :] - X2[None, :, :]) ** 2,
248
+ axis=-1,
249
+ )
250
+
251
+ # Squared exponential (RBF) kernel
252
+ lengthscale = self.lengthscale
253
+ variance = self.variance
254
+
255
+ K = variance * jnp.exp(-sq_dist / (2 * lengthscale**2))
256
+ return K
257
+
258
+ def compute_spatial_variance(
259
+ self,
260
+ coords: Float[Array, "n_spots 2"],
261
+ expression: Float[Array, "n_spots n_genes"],
262
+ ) -> tuple[Float[Array, "n_genes"], Float[Array, "n_genes"]]:
263
+ """Compute spatial variance and FSV for each gene.
264
+
265
+ Uses neural network approximation to GP posterior mean for efficiency.
266
+ Computes variance decomposition: total = spatial + residual.
267
+
268
+ Args:
269
+ coords: Spatial coordinates.
270
+ expression: Normalized gene expression.
271
+
272
+ Returns:
273
+ Tuple of (spatial_variance, fsv) per gene.
274
+ """
275
+ # Compute smoothed expression (approximate GP mean)
276
+ smoothed = self._smooth_expression(coords, expression)
277
+
278
+ # Center expression
279
+ expression_centered = expression - jnp.mean(expression, axis=0, keepdims=True)
280
+
281
+ # Total variance per gene
282
+ total_var = jnp.var(expression_centered, axis=0)
283
+
284
+ # Residual after spatial smoothing
285
+ residual = expression - smoothed
286
+
287
+ # Residual variance (non-spatial component)
288
+ residual_var = jnp.var(residual, axis=0)
289
+
290
+ # Spatial variance = total - residual (variance explained by space)
291
+ spatial_var = jnp.maximum(total_var - residual_var, 0.0)
292
+
293
+ # Fraction of Spatial Variance (FSV)
294
+ fsv = spatial_var / (total_var + 1e-8)
295
+
296
+ return spatial_var, fsv
297
+
298
+ def _smooth_expression(
299
+ self,
300
+ coords: Float[Array, "n_spots 2"],
301
+ expression: Float[Array, "n_spots n_genes"],
302
+ ) -> Float[Array, "n_spots n_genes"]:
303
+ """Compute smoothed expression using neural network.
304
+
305
+ This provides a differentiable approximation to the GP posterior mean.
306
+
307
+ Args:
308
+ coords: Spatial coordinates.
309
+ expression: Gene expression.
310
+
311
+ Returns:
312
+ Smoothed expression.
313
+ """
314
+ # Normalize coordinates for stable training
315
+ coords_norm = (coords - jnp.mean(coords, axis=0)) / (jnp.std(coords, axis=0) + 1e-6)
316
+
317
+ # Apply smoothing network
318
+ deviation = self.smoothing_network(coords_norm)
319
+
320
+ # Smoothed = mean + learned spatial deviation
321
+ smoothed = jnp.mean(expression, axis=0, keepdims=True) + deviation
322
+
323
+ return smoothed
324
+
325
+ def compute_pvalues(
326
+ self,
327
+ fsv: Float[Array, "n_genes"],
328
+ n_spots: int,
329
+ ) -> Float[Array, "n_genes"]:
330
+ """Compute differentiable pseudo-p-values for spatial patterns.
331
+
332
+ Uses a soft approximation to the likelihood ratio test.
333
+ In SpatialDE, p-values come from comparing the spatial model
334
+ to a null model without spatial structure.
335
+
336
+ Args:
337
+ fsv: Fraction of Spatial Variance per gene.
338
+ n_spots: Number of spatial locations.
339
+
340
+ Returns:
341
+ Soft p-values (lower = more spatially variable).
342
+ """
343
+ # Approximate likelihood ratio statistic
344
+ # Higher FSV -> larger LR statistic -> smaller p-value
345
+ # Scale by n_spots to approximate degrees of freedom effect
346
+ lr_stat = fsv * n_spots
347
+
348
+ # Transform to pseudo-pvalue using sigmoid
349
+ # This gives a differentiable approximation to the chi-squared CDF
350
+ pvalues = nnx.sigmoid(-lr_stat + 2.0)
351
+
352
+ return pvalues
353
+
354
+ def apply(
355
+ self,
356
+ data: dict[str, Array],
357
+ state: dict[str, Any],
358
+ metadata: dict[str, Any] | None,
359
+ random_params: Any = None, # noqa: ARG002
360
+ stats: dict[str, Any] | None = None, # noqa: ARG002
361
+ ) -> tuple[dict[str, Array], dict[str, Any], dict[str, Any] | None]:
362
+ """Apply spatial gene detection.
363
+
364
+ Args:
365
+ data: Input data containing:
366
+ - spatial_coords: Float[Array, "n_spots 2"]
367
+ - expression: Float[Array, "n_spots n_genes"]
368
+ - total_counts: Float[Array, "n_spots"] (optional)
369
+ state: Element state (passed through).
370
+ metadata: Element metadata (passed through).
371
+
372
+ Returns:
373
+ Tuple of (output_data, state, metadata).
374
+ """
375
+ coords = data["spatial_coords"]
376
+ expression = data["expression"]
377
+ n_spots = coords.shape[0]
378
+
379
+ # Normalize expression if total counts provided
380
+ if "total_counts" in data:
381
+ total_counts = data["total_counts"]
382
+ expression_norm = expression / (total_counts[:, None] + 1e-6)
383
+ expression_norm = expression_norm * soft_ops.median(total_counts, softness=0.1)
384
+ else:
385
+ expression_norm = expression
386
+
387
+ # Compute smoothed expression
388
+ smoothed = self._smooth_expression(coords, expression_norm)
389
+
390
+ # Compute spatial variance and FSV
391
+ spatial_variance, fsv = self.compute_spatial_variance(coords, expression_norm)
392
+
393
+ # Compute p-values
394
+ pvalues = self.compute_pvalues(fsv, n_spots)
395
+
396
+ # Soft spatial classification using temperature-controlled sigmoid
397
+ threshold = self.config.pvalue_threshold
398
+ temp = self._temperature
399
+ is_spatial = soft_ops.less(pvalues, threshold, softness=temp)
400
+
401
+ # Build output
402
+ output_data = {
403
+ **data,
404
+ "spatial_variance": spatial_variance,
405
+ "fsv": fsv,
406
+ "spatial_pvalues": pvalues,
407
+ "is_spatial": is_spatial,
408
+ "smoothed_expression": smoothed,
409
+ }
410
+
411
+ # Optionally compute spatial field operations (gradient, laplacian)
412
+ if self.config.compute_field_ops:
413
+ field_ops = _compute_spatial_field_ops(coords, smoothed)
414
+ output_data.update(field_ops)
415
+
416
+ return output_data, state, metadata
417
+
418
+
419
+ def _compute_spatial_field_ops(
420
+ coords: Float[Array, "n_spots 2"],
421
+ smoothed: Float[Array, "n_spots n_genes"],
422
+ ) -> dict[str, Array]:
423
+ """Compute spatial gradient and Laplacian of smoothed expression.
424
+
425
+ Uses opifex's autodiff-based field operations to compute per-gene
426
+ spatial gradients and Laplacians at each spot location. Vectorized
427
+ over genes via ``jax.vmap`` — no Python for-loops.
428
+
429
+ Args:
430
+ coords: Spatial coordinates (n_spots, 2).
431
+ smoothed: Smoothed expression (n_spots, n_genes).
432
+
433
+ Returns:
434
+ Dict with:
435
+ - ``expression_gradient``: Per-gene spatial gradient magnitude (n_genes,).
436
+ - ``expression_laplacian``: Per-gene mean Laplacian (n_genes,).
437
+ """
438
+ from opifex.core.physics import compute_gradient, compute_laplacian # noqa: PLC0415
439
+
440
+ def _gene_field(x: Array, gene_expr: Array) -> Array:
441
+ """RBF-interpolated scalar field for a single gene."""
442
+ dists = jnp.sum((x - coords) ** 2, axis=-1)
443
+ weights = jnp.exp(-dists / 2.0)
444
+ weights = weights / (jnp.sum(weights, axis=-1, keepdims=True) + 1e-8)
445
+ return jnp.sum(weights * gene_expr, axis=-1)
446
+
447
+ def _per_gene_metrics(gene_expr: Array) -> tuple[Array, Array]:
448
+ """Compute gradient magnitude and Laplacian for one gene."""
449
+ field_fn = lambda x: _gene_field(x, gene_expr) # noqa: E731
450
+ grads = compute_gradient(field_fn, coords)
451
+ grad_mag = jnp.mean(jnp.sqrt(jnp.sum(grads**2, axis=-1) + 1e-8))
452
+ lap = compute_laplacian(field_fn, coords)
453
+ lap_mean = jnp.mean(jnp.abs(lap))
454
+ return grad_mag, lap_mean
455
+
456
+ # Vectorize over genes (columns of smoothed)
457
+ gradient_mags, laplacian_means = jax.vmap(_per_gene_metrics)(
458
+ smoothed.T
459
+ ) # smoothed.T is (n_genes, n_spots)
460
+
461
+ return {
462
+ "expression_gradient": gradient_mags,
463
+ "expression_laplacian": laplacian_means,
464
+ }
465
+
466
+
467
+ def create_spatial_gene_detector(
468
+ n_genes: int = 2000,
469
+ n_inducing_points: int = 100,
470
+ lengthscale: float = 1.0,
471
+ variance: float = 1.0,
472
+ seed: int = 42,
473
+ ) -> DifferentiableSpatialGeneDetector:
474
+ """Factory function to create a spatial gene detector.
475
+
476
+ Args:
477
+ n_genes: Number of genes to analyze.
478
+ n_inducing_points: Number of inducing points for sparse GP.
479
+ lengthscale: Initial kernel lengthscale.
480
+ variance: Initial signal variance.
481
+ seed: Random seed.
482
+
483
+ Returns:
484
+ Configured DifferentiableSpatialGeneDetector instance.
485
+ """
486
+ config = SpatialGeneDetectorConfig(
487
+ n_genes=n_genes,
488
+ n_inducing_points=n_inducing_points,
489
+ lengthscale=lengthscale,
490
+ variance=variance,
491
+ )
492
+ rngs = nnx.Rngs(seed)
493
+ return DifferentiableSpatialGeneDetector(config, rngs=rngs)
@@ -0,0 +1,42 @@
1
+ """Differentiable normalization and embedding operators.
2
+
3
+ This module provides operators for:
4
+
5
+ - VAE-based count normalization (scVI-style)
6
+ - Sequence embedding with learned representations
7
+ - Differentiable dimensionality reduction (UMAP, PHATE)
8
+
9
+ All operators maintain gradient flow for end-to-end training.
10
+ """
11
+
12
+ from diffbio.operators.normalization.embedding import (
13
+ SequenceEmbedding,
14
+ SequenceEmbeddingConfig,
15
+ )
16
+ from diffbio.operators.normalization.phate import (
17
+ DifferentiablePHATE,
18
+ PHATEConfig,
19
+ )
20
+ from diffbio.operators.normalization.umap import (
21
+ DifferentiableUMAP,
22
+ UMAPConfig,
23
+ )
24
+ from diffbio.operators.normalization.vae_normalizer import (
25
+ VAENormalizer,
26
+ VAENormalizerConfig,
27
+ )
28
+
29
+ __all__ = [
30
+ # VAE Normalization
31
+ "VAENormalizerConfig",
32
+ "VAENormalizer",
33
+ # Sequence Embedding
34
+ "SequenceEmbeddingConfig",
35
+ "SequenceEmbedding",
36
+ # UMAP Dimensionality Reduction
37
+ "UMAPConfig",
38
+ "DifferentiableUMAP",
39
+ # PHATE Dimensionality Reduction
40
+ "PHATEConfig",
41
+ "DifferentiablePHATE",
42
+ ]