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,555 @@
1
+ """Differentiable peak calling for ChIP-seq and ATAC-seq data.
2
+
3
+ This module implements a CNN-based differentiable peak caller that can be
4
+ used for ChIP-seq and ATAC-seq analysis with end-to-end gradient flow.
5
+
6
+ Optionally includes a VAE-based denoising stage (inspired by SCALE) that
7
+ encodes the coverage signal into a latent space and decodes it using a
8
+ Poisson decoder before peak detection.
9
+
10
+ Inherits from TemperatureOperator to get:
11
+
12
+ - _temperature property for temperature-controlled smoothing
13
+ - soft_max() for logsumexp-based smooth maximum
14
+ - soft_argmax() for soft position selection
15
+ """
16
+
17
+ import logging
18
+ from dataclasses import dataclass
19
+ from typing import Any
20
+
21
+ import flax.nnx as nnx
22
+ import jax
23
+ import jax.numpy as jnp
24
+ from artifex.generative_models.core.losses.divergence import gaussian_kl_divergence
25
+ from datarax.core.config import OperatorConfig
26
+
27
+ from diffbio.core import soft_ops
28
+ from diffbio.core.base_operators import TemperatureOperator
29
+ from diffbio.utils.nn_utils import ensure_rngs, get_rng_key
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class _PeakDetectionConfig:
36
+ """Peak detection hyperparameters."""
37
+
38
+ window_size: int = 200
39
+ num_filters: int = 32
40
+ kernel_sizes: tuple[int, ...] = (5, 11, 21)
41
+ threshold: float = 0.5
42
+ temperature: float = 1.0
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class _PeakDenoisingConfig:
47
+ """Optional VAE denoising hyperparameters."""
48
+
49
+ learnable_temperature: bool = True
50
+ min_peak_width: int = 50
51
+ use_vae_denoising: bool = False
52
+ vae_latent_dim: int = 16
53
+ vae_hidden_dim: int = 64
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class PeakCallerConfig(_PeakDetectionConfig, _PeakDenoisingConfig, OperatorConfig):
58
+ """Configuration for differentiable peak caller."""
59
+
60
+ def __post_init__(self) -> None:
61
+ """Validate the peak caller configuration."""
62
+ super().__post_init__()
63
+
64
+ if self.window_size <= 0:
65
+ raise ValueError("window_size must be positive.")
66
+ if self.num_filters <= 0:
67
+ raise ValueError("num_filters must be positive.")
68
+ if not self.kernel_sizes or any(kernel_size <= 0 for kernel_size in self.kernel_sizes):
69
+ raise ValueError("kernel_sizes must contain only positive integers.")
70
+ if not 0.0 <= self.threshold <= 1.0:
71
+ raise ValueError("threshold must be between 0.0 and 1.0.")
72
+ if self.temperature <= 0.0:
73
+ raise ValueError("temperature must be positive.")
74
+ if self.min_peak_width <= 0:
75
+ raise ValueError("min_peak_width must be positive.")
76
+ if self.vae_latent_dim <= 0:
77
+ raise ValueError("vae_latent_dim must be positive.")
78
+ if self.vae_hidden_dim <= 0:
79
+ raise ValueError("vae_hidden_dim must be positive.")
80
+
81
+
82
+ class SignalVAE(nnx.Module):
83
+ """VAE for denoising coverage signals with Poisson decoder.
84
+
85
+ Inspired by SCALE (Single-Cell ATAC-seq Analysis via Latent feature
86
+ Extraction), this module uses a VAE with a Poisson decoder to denoise
87
+ coverage signals. The Poisson distribution naturally models count data
88
+ (read coverage) and the latent space captures the underlying signal.
89
+
90
+ Architecture:
91
+ Encoder: coverage -> hidden -> (mean, logvar)
92
+ Reparameterize: z = mean + exp(0.5 * logvar) * epsilon
93
+ Decoder: z -> hidden -> log_rate (Poisson parameter)
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ signal_dim: int,
99
+ latent_dim: int = 16,
100
+ hidden_dim: int = 64,
101
+ *,
102
+ rngs: nnx.Rngs,
103
+ ) -> None:
104
+ """Initialize the signal VAE.
105
+
106
+ Args:
107
+ signal_dim: Dimension of the input signal (length).
108
+ latent_dim: Dimension of the latent space.
109
+ hidden_dim: Dimension of hidden layers.
110
+ rngs: Random number generators for initialization.
111
+ """
112
+ super().__init__()
113
+ self.signal_dim = signal_dim
114
+ self.latent_dim = latent_dim
115
+
116
+ # Encoder
117
+ self.enc_hidden = nnx.Linear(signal_dim, hidden_dim, rngs=rngs)
118
+ self.enc_mean = nnx.Linear(hidden_dim, latent_dim, rngs=rngs)
119
+ self.enc_logvar = nnx.Linear(hidden_dim, latent_dim, rngs=rngs)
120
+
121
+ # Decoder (Poisson: outputs log-rate)
122
+ self.dec_hidden = nnx.Linear(latent_dim, hidden_dim, rngs=rngs)
123
+ self.dec_output = nnx.Linear(hidden_dim, signal_dim, rngs=rngs)
124
+
125
+ def encode(self, x: jax.Array) -> tuple[jax.Array, jax.Array]:
126
+ """Encode signal to latent distribution parameters.
127
+
128
+ Args:
129
+ x: Input signal of shape (..., signal_dim).
130
+
131
+ Returns:
132
+ Tuple of (mean, logvar) each of shape (..., latent_dim).
133
+ """
134
+ h = nnx.relu(self.enc_hidden(x))
135
+ mean = self.enc_mean(h)
136
+ logvar = jnp.clip(self.enc_logvar(h), -10.0, 10.0)
137
+ return mean, logvar
138
+
139
+ def decode(self, z: jax.Array) -> jax.Array:
140
+ """Decode latent representation to Poisson log-rate.
141
+
142
+ Args:
143
+ z: Latent representation of shape (..., latent_dim).
144
+
145
+ Returns:
146
+ Log-rate for Poisson decoder of shape (..., signal_dim).
147
+ """
148
+ h = nnx.relu(self.dec_hidden(z))
149
+ log_rate = self.dec_output(h)
150
+ return log_rate
151
+
152
+ def __call__(
153
+ self,
154
+ x: jax.Array,
155
+ rng_key: jax.Array,
156
+ ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
157
+ """Forward pass: encode, sample, decode.
158
+
159
+ Args:
160
+ x: Input signal of shape (..., signal_dim).
161
+ rng_key: JAX PRNG key for reparameterization sampling.
162
+
163
+ Returns:
164
+ Tuple of (denoised, mean, logvar, log_rate):
165
+ - denoised: Denoised signal (exp(log_rate)), shape (..., signal_dim).
166
+ - mean: Latent mean, shape (..., latent_dim).
167
+ - logvar: Latent log-variance, shape (..., latent_dim).
168
+ - log_rate: Raw Poisson log-rate, shape (..., signal_dim).
169
+ """
170
+ mean, logvar = self.encode(x)
171
+
172
+ # Reparameterization trick
173
+ std = jnp.exp(0.5 * logvar)
174
+ epsilon = jax.random.normal(rng_key, mean.shape)
175
+ z = mean + std * epsilon
176
+
177
+ log_rate = self.decode(z)
178
+ # Poisson rate = exp(log_rate), clamp for stability
179
+ denoised = jnp.exp(jnp.clip(log_rate, -10.0, 10.0))
180
+
181
+ return denoised, mean, logvar, log_rate
182
+
183
+
184
+ class PeakDetectionCNN(nnx.Module):
185
+ """CNN module for detecting peak patterns in coverage signals.
186
+
187
+ Uses multi-scale convolutions to capture peaks of varying widths.
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ num_filters: int = 32,
193
+ kernel_sizes: tuple[int, ...] = (5, 11, 21),
194
+ *,
195
+ rngs: nnx.Rngs,
196
+ ):
197
+ """Initialize the peak detection CNN.
198
+
199
+ Args:
200
+ num_filters: Number of filters per convolution layer.
201
+ kernel_sizes: Kernel sizes for multi-scale detection.
202
+ rngs: Random number generators for initialization.
203
+ """
204
+ super().__init__()
205
+ self.num_filters = num_filters
206
+ self.kernel_sizes = kernel_sizes
207
+
208
+ # Multi-scale convolution layers - use nnx.List for proper parameter tracking
209
+ self.conv_layers = nnx.List(
210
+ [
211
+ nnx.Conv(
212
+ in_features=1,
213
+ out_features=num_filters,
214
+ kernel_size=(kernel_size,),
215
+ padding="SAME",
216
+ rngs=rngs,
217
+ )
218
+ for kernel_size in kernel_sizes
219
+ ]
220
+ )
221
+
222
+ # Combine multi-scale features
223
+ total_features = num_filters * len(kernel_sizes)
224
+ self.combine_conv = nnx.Conv(
225
+ in_features=total_features,
226
+ out_features=num_filters,
227
+ kernel_size=(3,),
228
+ padding="SAME",
229
+ rngs=rngs,
230
+ )
231
+
232
+ # Final prediction layer
233
+ self.output_conv = nnx.Conv(
234
+ in_features=num_filters,
235
+ out_features=1,
236
+ kernel_size=(1,),
237
+ padding="SAME",
238
+ rngs=rngs,
239
+ )
240
+
241
+ def __call__(self, x: jax.Array) -> jax.Array:
242
+ """Forward pass for peak detection.
243
+
244
+ Args:
245
+ x: Input coverage signal of shape (batch, length, 1) or (batch, length).
246
+
247
+ Returns:
248
+ Peak scores of shape (batch, length).
249
+ """
250
+ # Ensure input has channel dimension
251
+ if x.ndim == 2:
252
+ x = x[..., None]
253
+
254
+ # Multi-scale convolutions
255
+ features = []
256
+ for conv in self.conv_layers:
257
+ feat = nnx.relu(conv(x))
258
+ features.append(feat)
259
+
260
+ # Concatenate multi-scale features
261
+ combined = jnp.concatenate(features, axis=-1)
262
+
263
+ # Combine and predict
264
+ x = nnx.relu(self.combine_conv(combined))
265
+ scores = self.output_conv(x)
266
+
267
+ return scores.squeeze(-1)
268
+
269
+
270
+ class DifferentiablePeakCaller(TemperatureOperator):
271
+ """Differentiable peak caller for ChIP-seq and ATAC-seq data.
272
+
273
+ This operator uses a CNN-based approach to detect peaks in coverage
274
+ signals, with soft thresholding for end-to-end differentiability.
275
+
276
+ Optionally applies VAE-based denoising before peak detection, using a
277
+ Poisson decoder (per SCALE) to model count data. When VAE denoising
278
+ is enabled, the pipeline is:
279
+ coverage -> VAE encoder -> latent -> Poisson decoder -> denoised -> CNN -> peaks
280
+
281
+ The operator processes coverage data and outputs:
282
+ - Peak probabilities at each position
283
+ - Peak boundaries (soft)
284
+ - Peak summits
285
+ - Denoised coverage (when VAE is enabled)
286
+
287
+ Example:
288
+ ```python
289
+ config = PeakCallerConfig(
290
+ window_size=200,
291
+ num_filters=32,
292
+ threshold=0.5,
293
+ use_vae_denoising=True,
294
+ )
295
+ peak_caller = DifferentiablePeakCaller(config, rngs=rngs)
296
+
297
+ data = {"coverage": coverage_signal}
298
+ result, state, metadata = peak_caller.apply(data, {}, None)
299
+ peak_probs = result["peak_probabilities"]
300
+ ```
301
+ """
302
+
303
+ def __init__(self, config: PeakCallerConfig, *, rngs: nnx.Rngs | None = None):
304
+ """Initialize the differentiable peak caller.
305
+
306
+ Args:
307
+ config: Configuration for the peak caller.
308
+ rngs: Random number generators for initialization.
309
+ """
310
+ super().__init__(config, rngs=rngs)
311
+ self.config = config
312
+
313
+ # Initialize RNGs if not provided
314
+ if rngs is None:
315
+ rngs = nnx.Rngs(0)
316
+
317
+ self._rngs = ensure_rngs(rngs)
318
+
319
+ # Learnable threshold parameter
320
+ self.threshold = nnx.Param(jnp.array(config.threshold))
321
+
322
+ # Temperature is managed by TemperatureOperator via self._temperature
323
+
324
+ # Peak detection CNN
325
+ self.peak_cnn = PeakDetectionCNN(
326
+ num_filters=config.num_filters,
327
+ kernel_sizes=config.kernel_sizes,
328
+ rngs=rngs,
329
+ )
330
+
331
+ # Local maximum detection kernel (for summit finding)
332
+ self.summit_kernel_size = config.min_peak_width
333
+
334
+ # Optional VAE denoiser
335
+ self._use_vae = config.use_vae_denoising
336
+ if self._use_vae:
337
+ # Signal dim is set per-call since it depends on input length,
338
+ # but we pre-initialize the VAE with a fixed config dim.
339
+ # For position-independent denoising, we use a 1D VAE per position
340
+ # applied across the signal length. This uses a fixed-dim VAE.
341
+ self.vae_encoder = SignalVAE(
342
+ signal_dim=config.window_size,
343
+ latent_dim=config.vae_latent_dim,
344
+ hidden_dim=config.vae_hidden_dim,
345
+ rngs=rngs,
346
+ )
347
+
348
+ def _vae_denoise(self, coverage: jax.Array) -> tuple[jax.Array, jax.Array]:
349
+ """Apply VAE-based denoising to coverage signal.
350
+
351
+ Processes each sample in the batch independently through the VAE.
352
+ The signal is reshaped into windows of size window_size, denoised
353
+ per-window, and reassembled.
354
+
355
+ Args:
356
+ coverage: Coverage signal of shape (batch, length).
357
+
358
+ Returns:
359
+ Tuple of (denoised_coverage, kl_loss):
360
+ - denoised_coverage: Denoised signal of shape (batch, length).
361
+ - kl_loss: Scalar KL divergence loss.
362
+ """
363
+ batch_size, length = coverage.shape
364
+ window_size = self.config.window_size
365
+
366
+ # Pad signal to be divisible by window_size
367
+ remainder = length % window_size
368
+ if remainder != 0:
369
+ pad_amount = window_size - remainder
370
+ coverage_padded = jnp.pad(coverage, ((0, 0), (0, pad_amount)), mode="edge")
371
+ else:
372
+ pad_amount = 0
373
+ coverage_padded = coverage
374
+
375
+ padded_length = coverage_padded.shape[1]
376
+ num_windows = padded_length // window_size
377
+
378
+ # Reshape to (batch * num_windows, window_size)
379
+ windows = coverage_padded.reshape(batch_size * num_windows, window_size)
380
+
381
+ # Log-transform for VAE input (coverage is count-like data)
382
+ vae_input = jnp.log1p(jnp.abs(windows))
383
+
384
+ # Get RNG key for sampling
385
+ rng_key = get_rng_key(self._rngs, "sample", fallback_seed=0)
386
+
387
+ # Run VAE forward pass
388
+ denoised_windows, mean, logvar, _ = self.vae_encoder(vae_input, rng_key)
389
+
390
+ # KL divergence via artifex
391
+ kl_loss = gaussian_kl_divergence(mean, logvar, reduction="sum")
392
+
393
+ # Reassemble signal
394
+ denoised_padded = denoised_windows.reshape(batch_size, padded_length)
395
+
396
+ # Remove padding
397
+ denoised = denoised_padded[:, :length]
398
+
399
+ return denoised, kl_loss
400
+
401
+ def _soft_local_max(
402
+ self, scores: jax.Array, window_size: int, temperature: jax.Array | float
403
+ ) -> jax.Array:
404
+ """Compute soft local maximum indicator.
405
+
406
+ Args:
407
+ scores: Peak scores of shape (batch, length).
408
+ window_size: Window size for local maximum detection.
409
+ temperature: Temperature for softmax.
410
+
411
+ Returns:
412
+ Soft local maximum indicators of shape (batch, length).
413
+ """
414
+ _, length = scores.shape
415
+ half_window = window_size // 2
416
+
417
+ # Pad scores for windowed comparison
418
+ padded = jnp.pad(
419
+ scores, ((0, 0), (half_window, half_window)), mode="constant", constant_values=-jnp.inf
420
+ )
421
+
422
+ # Extract windows around each position
423
+ def extract_windows(padded_row: jax.Array) -> jax.Array:
424
+ indices = jnp.arange(length)
425
+ windows = jax.vmap(lambda i: jax.lax.dynamic_slice(padded_row, (i,), (window_size,)))(
426
+ indices
427
+ )
428
+ return windows
429
+
430
+ windows = jax.vmap(extract_windows)(padded) # (batch, length, window_size)
431
+
432
+ # Compute softmax over window to find local maximum
433
+ # The center position should have high weight if it's the maximum
434
+ center_idx = half_window
435
+ window_softmax = jax.nn.softmax(windows / temperature, axis=-1)
436
+
437
+ # Extract probability of center being the maximum
438
+ local_max_prob = window_softmax[:, :, center_idx]
439
+
440
+ return local_max_prob
441
+
442
+ def _compute_peak_boundaries(self, peak_probs: jax.Array) -> tuple[jax.Array, jax.Array]:
443
+ """Compute soft peak boundaries.
444
+
445
+ Args:
446
+ peak_probs: Peak probabilities of shape (batch, length).
447
+
448
+ Returns:
449
+ Tuple of (peak_starts, peak_ends) as soft indicators.
450
+ """
451
+ # Compute gradient of peak probabilities
452
+ # Rising edge indicates start, falling edge indicates end
453
+ grad = jnp.diff(peak_probs, axis=-1, prepend=peak_probs[:, :1])
454
+
455
+ # Soft peak starts (positive gradient)
456
+ peak_starts = soft_ops.greater(grad, 0.0, softness=0.1)
457
+
458
+ # Soft peak ends (negative gradient)
459
+ peak_ends = soft_ops.less(grad, 0.0, softness=0.1)
460
+
461
+ return peak_starts, peak_ends
462
+
463
+ def apply(
464
+ self,
465
+ data: dict[str, Any],
466
+ state: dict[str, Any],
467
+ metadata: dict | None,
468
+ random_params: dict | None = None,
469
+ stats: dict | None = None,
470
+ ) -> tuple[dict, dict, dict | None]:
471
+ """Apply peak calling to coverage data.
472
+
473
+ When VAE denoising is enabled, the coverage signal is first denoised
474
+ through a VAE with Poisson decoder before peak detection.
475
+
476
+ Args:
477
+ data: Dictionary containing:
478
+ - 'coverage': Coverage signal of shape (batch, length) or (length,)
479
+ state: Operator state dictionary.
480
+ metadata: Optional metadata dictionary.
481
+ random_params: Optional random parameters.
482
+ stats: Optional statistics dictionary.
483
+
484
+ Returns:
485
+ Tuple of (output_data, state, metadata) where output_data contains:
486
+
487
+ - 'coverage': Original coverage signal
488
+ - 'peak_scores': Raw peak detection scores
489
+ - 'peak_probabilities': Soft peak probabilities
490
+ - 'peak_summits': Soft summit indicators
491
+ - 'peak_starts': Soft peak start indicators
492
+ - 'peak_ends': Soft peak end indicators
493
+ - 'denoised_coverage': Denoised signal (only when VAE enabled)
494
+ - 'vae_kl_loss': KL divergence loss (only when VAE enabled)
495
+ """
496
+ del random_params, stats # Unused
497
+
498
+ coverage = data["coverage"]
499
+
500
+ # Handle single sequence input
501
+ single_input = coverage.ndim == 1
502
+ if single_input:
503
+ coverage = coverage[None, :]
504
+
505
+ # Optional VAE denoising
506
+ vae_extras: dict[str, Any] = {}
507
+ if self._use_vae:
508
+ denoised, kl_loss = self._vae_denoise(coverage)
509
+ vae_extras["denoised_coverage"] = denoised
510
+ vae_extras["vae_kl_loss"] = kl_loss
511
+ # Use denoised signal for peak detection
512
+ cnn_input = denoised
513
+ else:
514
+ cnn_input = coverage
515
+
516
+ # Get peak scores from CNN
517
+ peak_scores = self.peak_cnn(cnn_input)
518
+
519
+ # Apply soft threshold
520
+ temperature = jnp.abs(self._temperature) + 1e-6
521
+ peak_probs = soft_ops.greater(peak_scores, self.threshold[...], softness=temperature)
522
+
523
+ # Find soft summits (local maxima)
524
+ summit_probs = self._soft_local_max(
525
+ peak_scores * peak_probs, # Weight by peak probability
526
+ self.summit_kernel_size,
527
+ temperature,
528
+ )
529
+
530
+ # Compute soft peak boundaries
531
+ peak_starts, peak_ends = self._compute_peak_boundaries(peak_probs)
532
+
533
+ # Remove batch dimension if input was single
534
+ if single_input:
535
+ peak_scores = peak_scores[0]
536
+ peak_probs = peak_probs[0]
537
+ summit_probs = summit_probs[0]
538
+ peak_starts = peak_starts[0]
539
+ peak_ends = peak_ends[0]
540
+ coverage = coverage[0]
541
+ for key in list(vae_extras.keys()):
542
+ if key == "denoised_coverage":
543
+ vae_extras[key] = vae_extras[key][0]
544
+
545
+ output_data = {
546
+ **data,
547
+ "peak_scores": peak_scores,
548
+ "peak_probabilities": peak_probs,
549
+ "peak_summits": summit_probs,
550
+ "peak_starts": peak_starts,
551
+ "peak_ends": peak_ends,
552
+ **vae_extras,
553
+ }
554
+
555
+ return output_data, state, metadata
@@ -0,0 +1,119 @@
1
+ """Foundation model operators for DiffBio.
2
+
3
+ This module provides transformer-based sequence encoders and single-cell
4
+ foundation model infrastructure plus shared adapter contracts.
5
+
6
+ Operators:
7
+ TransformerSequenceEncoder: BERT-style transformer for sequence embedding
8
+ DifferentiableFoundationModel: Geneformer/scGPT-style foundation model
9
+ GeneTokenizer: Rank-value gene tokenization via soft sorting
10
+
11
+ Factory Functions:
12
+ create_dna_encoder: Create encoder for DNA sequences
13
+ create_rna_encoder: Create encoder for RNA sequences
14
+ create_foundation_model: Create registered foundation-model operator
15
+ """
16
+
17
+ from diffbio.operators.foundation_models.contracts import (
18
+ AdapterMode,
19
+ FOUNDATION_BENCHMARK_COMPARISON_AXES,
20
+ FoundationArtifactSpec,
21
+ FoundationEmbeddingMixin,
22
+ FoundationEmbeddingOperatorConfig,
23
+ FoundationModelKind,
24
+ PoolingStrategy,
25
+ build_foundation_benchmark_metadata,
26
+ build_foundation_model_metadata,
27
+ create_foundation_model,
28
+ decode_foundation_model_metadata,
29
+ decode_foundation_text,
30
+ encode_foundation_text,
31
+ get_foundation_model_cls,
32
+ register_foundation_model,
33
+ )
34
+ from diffbio.operators.foundation_models.adapters import (
35
+ FoundationBenchmarkAdapter,
36
+ SequenceFoundationAdapter,
37
+ create_foundation_adapter,
38
+ get_foundation_adapter_cls,
39
+ register_foundation_adapter,
40
+ )
41
+ from diffbio.operators.foundation_models.embedding_probe import (
42
+ EmbeddingProbeConfig,
43
+ LinearEmbeddingProbe,
44
+ )
45
+ from diffbio.operators.foundation_models.experimental import (
46
+ EXPERIMENTAL_FOUNDATION_MODEL_NAMESPACE,
47
+ FOUNDATION_EXPERIMENTAL_CAPABILITIES,
48
+ FOUNDATION_EXPERIMENTAL_PROMOTION_CRITERIA,
49
+ ExperimentalFoundationCapability,
50
+ get_experimental_foundation_capability,
51
+ is_experimental_foundation_capability,
52
+ )
53
+ from diffbio.operators.foundation_models.foundation_model import (
54
+ DifferentiableFoundationModel,
55
+ FoundationModelConfig,
56
+ GeneTokenizer,
57
+ )
58
+ from diffbio.operators.foundation_models.precomputed import (
59
+ DNABERT2PrecomputedAdapter,
60
+ GeneformerPrecomputedAdapter,
61
+ NucleotideTransformerPrecomputedAdapter,
62
+ ProteinLMPrecomputedAdapter,
63
+ ScGPTPrecomputedAdapter,
64
+ SequencePrecomputedAdapter,
65
+ SingleCellPrecomputedAdapter,
66
+ )
67
+ from diffbio.operators.foundation_models.frozen import FrozenSequenceEncoderAdapter
68
+ from diffbio.operators.foundation_models.transformer_encoder import (
69
+ TransformerSequenceEncoder,
70
+ TransformerSequenceEncoderConfig,
71
+ create_dna_encoder,
72
+ create_rna_encoder,
73
+ )
74
+
75
+ __all__ = [
76
+ "DifferentiableFoundationModel",
77
+ "DNABERT2PrecomputedAdapter",
78
+ "EmbeddingProbeConfig",
79
+ "EXPERIMENTAL_FOUNDATION_MODEL_NAMESPACE",
80
+ "FOUNDATION_EXPERIMENTAL_CAPABILITIES",
81
+ "FOUNDATION_EXPERIMENTAL_PROMOTION_CRITERIA",
82
+ "FoundationBenchmarkAdapter",
83
+ "ExperimentalFoundationCapability",
84
+ "FoundationModelConfig",
85
+ "GeneTokenizer",
86
+ "GeneformerPrecomputedAdapter",
87
+ "FrozenSequenceEncoderAdapter",
88
+ "LinearEmbeddingProbe",
89
+ "NucleotideTransformerPrecomputedAdapter",
90
+ "ProteinLMPrecomputedAdapter",
91
+ "ScGPTPrecomputedAdapter",
92
+ "SequenceFoundationAdapter",
93
+ "SequencePrecomputedAdapter",
94
+ "AdapterMode",
95
+ "FOUNDATION_BENCHMARK_COMPARISON_AXES",
96
+ "FoundationArtifactSpec",
97
+ "FoundationEmbeddingMixin",
98
+ "FoundationEmbeddingOperatorConfig",
99
+ "FoundationModelKind",
100
+ "PoolingStrategy",
101
+ "SingleCellPrecomputedAdapter",
102
+ "TransformerSequenceEncoder",
103
+ "TransformerSequenceEncoderConfig",
104
+ "build_foundation_benchmark_metadata",
105
+ "build_foundation_model_metadata",
106
+ "create_foundation_adapter",
107
+ "create_foundation_model",
108
+ "create_dna_encoder",
109
+ "decode_foundation_model_metadata",
110
+ "decode_foundation_text",
111
+ "encode_foundation_text",
112
+ "get_foundation_adapter_cls",
113
+ "get_foundation_model_cls",
114
+ "create_rna_encoder",
115
+ "get_experimental_foundation_capability",
116
+ "register_foundation_adapter",
117
+ "register_foundation_model",
118
+ "is_experimental_foundation_capability",
119
+ ]