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,288 @@
1
+ """Contextual epigenomics operator for sequence, TF, and chromatin inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import jax
9
+ import jax.numpy as jnp
10
+ import optax
11
+ from artifex.generative_models.core.layers import TransformerEncoder
12
+ from datarax.core.config import OperatorConfig
13
+ from datarax.core.operator import OperatorModule
14
+ from flax import nnx
15
+
16
+ from diffbio.operators._transformer_validation import TransformerEncoderShapeValidationMixin
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class _ContextualEncoderConfig:
21
+ """Sequence encoder hyperparameters."""
22
+
23
+ hidden_dim: int = 64
24
+ num_layers: int = 2
25
+ num_heads: int = 4
26
+ intermediate_dim: int = 256
27
+ max_length: int = 512
28
+ dropout_rate: float = 0.0
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class _ContextualTaskConfig:
33
+ """Task and conditioning configuration."""
34
+
35
+ num_tf_features: int = 8
36
+ num_outputs: int = 1
37
+ use_tf_context: bool = True
38
+ use_chromatin_guidance: bool = False
39
+ chromatin_guidance_weight: float = 0.1
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ContextualEpigenomicsConfig(
44
+ _ContextualEncoderConfig,
45
+ _ContextualTaskConfig,
46
+ TransformerEncoderShapeValidationMixin,
47
+ OperatorConfig,
48
+ ):
49
+ """Configuration for the contextual epigenomics operator."""
50
+
51
+ def __post_init__(self) -> None:
52
+ """Validate the operator configuration."""
53
+ super().__post_init__()
54
+ if self.num_outputs < 1:
55
+ raise ValueError("num_outputs must be at least 1.")
56
+ if self.num_tf_features < 1:
57
+ raise ValueError("num_tf_features must be at least 1.")
58
+ if self.chromatin_guidance_weight < 0.0:
59
+ raise ValueError("chromatin_guidance_weight must be non-negative.")
60
+
61
+
62
+ class ContextualEpigenomicsOperator(OperatorModule):
63
+ """Single operator path for sequence-only and contextual epigenomics modes."""
64
+
65
+ def __init__(
66
+ self,
67
+ config: ContextualEpigenomicsConfig,
68
+ *,
69
+ rngs: nnx.Rngs | None = None,
70
+ ) -> None:
71
+ super().__init__(config, rngs=rngs)
72
+
73
+ if rngs is None:
74
+ rngs = nnx.Rngs(0)
75
+ if config.dropout_rate > 0 and "dropout" not in rngs:
76
+ rngs = nnx.Rngs(params=rngs.params(), dropout=jax.random.key(1))
77
+
78
+ self.config = config
79
+ self.sequence_projection = nnx.Linear(4, config.hidden_dim, rngs=rngs)
80
+ self.tf_scale = nnx.Linear(config.num_tf_features, config.hidden_dim, rngs=rngs)
81
+ self.tf_shift = nnx.Linear(config.num_tf_features, config.hidden_dim, rngs=rngs)
82
+ self.transformer = TransformerEncoder(
83
+ num_layers=config.num_layers,
84
+ hidden_dim=config.hidden_dim,
85
+ num_heads=config.num_heads,
86
+ mlp_ratio=config.intermediate_dim / config.hidden_dim,
87
+ dropout_rate=config.dropout_rate,
88
+ attention_dropout_rate=0.0,
89
+ max_len=config.max_length,
90
+ pos_encoding_type="sinusoidal",
91
+ rngs=rngs,
92
+ )
93
+ self.output_head = nnx.Linear(config.hidden_dim, config.num_outputs, rngs=rngs)
94
+
95
+ def apply(
96
+ self,
97
+ data: dict[str, Any],
98
+ state: dict[str, Any],
99
+ metadata: dict[str, Any] | None,
100
+ random_params: Any = None,
101
+ stats: dict[str, Any] | None = None,
102
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
103
+ """Apply the contextual epigenomics operator to one batch."""
104
+ del random_params, stats
105
+
106
+ sequence = jnp.asarray(data["sequence"], dtype=jnp.float32)
107
+ tf_context = data.get("tf_context")
108
+ chromatin_contacts = data.get("chromatin_contacts")
109
+ sequence_mask = data.get("sequence_mask")
110
+
111
+ (
112
+ sequence,
113
+ tf_context,
114
+ chromatin_contacts,
115
+ sequence_mask,
116
+ squeeze_batch,
117
+ ) = _canonicalize_contextual_inputs(
118
+ sequence=sequence,
119
+ tf_context=tf_context,
120
+ chromatin_contacts=chromatin_contacts,
121
+ sequence_mask=sequence_mask,
122
+ )
123
+
124
+ hidden = self.sequence_projection(sequence)
125
+ if self.config.use_tf_context:
126
+ if tf_context is None:
127
+ raise ValueError("tf_context is required when use_tf_context=True.")
128
+ hidden = _apply_tf_conditioning(
129
+ hidden=hidden,
130
+ tf_context=jnp.asarray(tf_context, dtype=jnp.float32),
131
+ tf_scale=self.tf_scale,
132
+ tf_shift=self.tf_shift,
133
+ )
134
+
135
+ token_embeddings = self.transformer(
136
+ hidden,
137
+ mask=sequence_mask,
138
+ deterministic=True,
139
+ )
140
+ masked_token_embeddings = token_embeddings * sequence_mask[..., None]
141
+ pooled_embeddings = masked_token_embeddings.sum(axis=1) / jnp.maximum(
142
+ sequence_mask.sum(axis=1, keepdims=True),
143
+ 1.0,
144
+ )
145
+
146
+ logits = self.output_head(token_embeddings)
147
+ if self.config.num_outputs == 1:
148
+ logits = logits.squeeze(-1)
149
+
150
+ chromatin_guidance_loss = jnp.array(0.0, dtype=jnp.float32)
151
+ if self.config.use_chromatin_guidance:
152
+ if chromatin_contacts is None:
153
+ raise ValueError("chromatin_contacts is required when use_chromatin_guidance=True.")
154
+ chromatin_guidance_loss = compute_chromatin_guidance_loss(
155
+ token_embeddings=token_embeddings,
156
+ chromatin_contacts=jnp.asarray(chromatin_contacts, dtype=jnp.float32),
157
+ sequence_mask=sequence_mask,
158
+ )
159
+
160
+ result = {
161
+ **data,
162
+ "embeddings": pooled_embeddings,
163
+ "token_embeddings": token_embeddings,
164
+ "logits": logits,
165
+ "chromatin_guidance_loss": chromatin_guidance_loss,
166
+ }
167
+ if squeeze_batch:
168
+ result["embeddings"] = pooled_embeddings.squeeze(0)
169
+ result["token_embeddings"] = token_embeddings.squeeze(0)
170
+ result["logits"] = logits.squeeze(0)
171
+
172
+ return result, state, metadata
173
+
174
+
175
+ def _canonicalize_contextual_inputs(
176
+ *,
177
+ sequence: jnp.ndarray,
178
+ tf_context: Any,
179
+ chromatin_contacts: Any,
180
+ sequence_mask: Any,
181
+ ) -> tuple[jnp.ndarray, jnp.ndarray | None, jnp.ndarray | None, jnp.ndarray, bool]:
182
+ """Normalize contextual epigenomics inputs to batched tensors."""
183
+ if sequence.ndim not in (2, 3) or sequence.shape[-1] != 4:
184
+ raise ValueError("sequence must have shape (length, 4) or (batch, length, 4).")
185
+
186
+ squeeze_batch = sequence.ndim == 2
187
+ if squeeze_batch:
188
+ sequence = sequence[None, ...]
189
+
190
+ batch_size, sequence_length, _ = sequence.shape
191
+
192
+ tf_tensor: jnp.ndarray | None = None
193
+ if tf_context is not None:
194
+ tf_tensor = jnp.asarray(tf_context, dtype=jnp.float32)
195
+ if tf_tensor.ndim == 1:
196
+ tf_tensor = tf_tensor[None, ...]
197
+ if tf_tensor.ndim != 2 or tf_tensor.shape[0] != batch_size:
198
+ raise ValueError("tf_context must have shape (features,) or (batch, features).")
199
+
200
+ chromatin_tensor: jnp.ndarray | None = None
201
+ if chromatin_contacts is not None:
202
+ chromatin_tensor = jnp.asarray(chromatin_contacts, dtype=jnp.float32)
203
+ if chromatin_tensor.ndim == 2:
204
+ chromatin_tensor = chromatin_tensor[None, ...]
205
+ if chromatin_tensor.ndim != 3 or chromatin_tensor.shape[0] != batch_size:
206
+ raise ValueError(
207
+ "chromatin_contacts must have shape (length, length) or (batch, length, length)."
208
+ )
209
+ if (
210
+ chromatin_tensor.shape[1] != sequence_length
211
+ or chromatin_tensor.shape[2] != sequence_length
212
+ ):
213
+ raise ValueError("chromatin_contacts must align with the sequence length.")
214
+
215
+ if sequence_mask is None:
216
+ mask_tensor = jnp.ones((batch_size, sequence_length), dtype=jnp.float32)
217
+ else:
218
+ mask_tensor = jnp.asarray(sequence_mask, dtype=jnp.float32)
219
+ if mask_tensor.ndim == 1:
220
+ mask_tensor = mask_tensor[None, ...]
221
+ if mask_tensor.shape != (batch_size, sequence_length):
222
+ raise ValueError("sequence_mask must have shape (length,) or (batch, length).")
223
+
224
+ return sequence, tf_tensor, chromatin_tensor, mask_tensor, squeeze_batch
225
+
226
+
227
+ def _apply_tf_conditioning(
228
+ *,
229
+ hidden: jnp.ndarray,
230
+ tf_context: jnp.ndarray,
231
+ tf_scale: nnx.Linear,
232
+ tf_shift: nnx.Linear,
233
+ ) -> jnp.ndarray:
234
+ """Apply FiLM-style TF conditioning to token embeddings."""
235
+ scale = jnp.tanh(tf_scale(tf_context))[:, None, :]
236
+ shift = tf_shift(tf_context)[:, None, :]
237
+ return hidden * (1.0 + scale) + shift
238
+
239
+
240
+ def compute_chromatin_guidance_loss(
241
+ *,
242
+ token_embeddings: jnp.ndarray,
243
+ chromatin_contacts: jnp.ndarray,
244
+ sequence_mask: jnp.ndarray,
245
+ ) -> jnp.ndarray:
246
+ """Compute a chromatin-consistency loss over token embeddings."""
247
+ normalized_embeddings = token_embeddings / jnp.maximum(
248
+ jnp.linalg.norm(token_embeddings, axis=-1, keepdims=True),
249
+ 1e-6,
250
+ )
251
+ similarity = jnp.einsum(
252
+ "bld,bmd->blm",
253
+ normalized_embeddings,
254
+ normalized_embeddings,
255
+ )
256
+ predicted_contacts = jax.nn.sigmoid(similarity)
257
+ pair_mask = sequence_mask[:, :, None] * sequence_mask[:, None, :]
258
+ squared_error = jnp.square(predicted_contacts - chromatin_contacts) * pair_mask
259
+ return squared_error.sum() / jnp.maximum(pair_mask.sum(), 1.0)
260
+
261
+
262
+ def compute_contextual_epigenomics_loss(
263
+ model: ContextualEpigenomicsOperator,
264
+ data: dict[str, Any],
265
+ ) -> dict[str, jnp.ndarray]:
266
+ """Compute supervised plus optional chromatin-guidance losses."""
267
+ result = model.apply(data, {}, None)[0]
268
+ logits = jnp.asarray(result["logits"], dtype=jnp.float32)
269
+ targets = jnp.asarray(data["targets"])
270
+
271
+ if logits.ndim == targets.ndim:
272
+ supervised = optax.sigmoid_binary_cross_entropy(
273
+ logits,
274
+ targets.astype(jnp.float32),
275
+ ).mean()
276
+ else:
277
+ supervised = optax.softmax_cross_entropy_with_integer_labels(
278
+ logits,
279
+ targets.astype(jnp.int32),
280
+ ).mean()
281
+
282
+ chromatin_guidance = jnp.asarray(result["chromatin_guidance_loss"], dtype=jnp.float32)
283
+ total = supervised + model.config.chromatin_guidance_weight * chromatin_guidance
284
+ return {
285
+ "supervised": supervised,
286
+ "chromatin_guidance": chromatin_guidance,
287
+ "total": total,
288
+ }
@@ -0,0 +1,153 @@
1
+ """FNO-based differentiable peak calling for ChIP-seq and ATAC-seq data.
2
+
3
+ Uses a Fourier Neural Operator (FNO) from opifex to learn the mapping
4
+ from coverage signals to peak probabilities. The FNO captures multi-scale
5
+ patterns in the frequency domain, making it well-suited for detecting
6
+ peaks of varying widths without explicit multi-scale CNN kernels.
7
+
8
+ This is an alternative to the CNN-based ``DifferentiablePeakCaller``
9
+ in ``peak_calling.py``. The FNO approach processes the entire signal
10
+ in one pass via spectral convolutions rather than sliding windows.
11
+
12
+ Reference:
13
+ Li et al. (2021) "Fourier Neural Operator for Parametric Partial
14
+ Differential Equations". ICLR 2021.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ import jax.numpy as jnp
24
+ from datarax.core.config import OperatorConfig
25
+ from datarax.core.operator import OperatorModule
26
+ from flax import nnx
27
+
28
+ from diffbio.core import soft_ops
29
+ from jaxtyping import PyTree
30
+ from opifex.neural.operators import FourierNeuralOperator
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class FNOPeakCallerConfig(OperatorConfig):
37
+ """Configuration for FNO-based peak caller.
38
+
39
+ Attributes:
40
+ hidden_channels: Number of hidden channels in FNO layers.
41
+ modes: Number of Fourier modes to retain (controls frequency resolution).
42
+ num_layers: Number of FNO layers.
43
+ threshold: Initial soft threshold for peak classification.
44
+ temperature: Temperature for sigmoid smoothing.
45
+ """
46
+
47
+ hidden_channels: int = 32
48
+ modes: int = 16
49
+ num_layers: int = 4
50
+ threshold: float = 0.5
51
+ temperature: float = 1.0
52
+
53
+
54
+ class FNOPeakCaller(OperatorModule):
55
+ """FNO-based differentiable peak caller.
56
+
57
+ Applies a Fourier Neural Operator to map coverage signals to peak
58
+ probability scores. The FNO learns spectral convolution kernels that
59
+ capture peak patterns at multiple frequency scales simultaneously.
60
+
61
+ Input data:
62
+ - ``"coverage"``: Coverage signal ``(batch, length)`` or ``(length,)``.
63
+
64
+ Output adds:
65
+ - ``"peak_scores"``: Raw FNO output scores ``(batch, length)``.
66
+ - ``"peak_probabilities"``: Sigmoid-transformed scores in [0, 1].
67
+
68
+ Args:
69
+ config: FNOPeakCallerConfig with FNO parameters.
70
+ rngs: Flax NNX random number generators.
71
+ name: Optional operator name.
72
+
73
+ Example:
74
+ >>> config = FNOPeakCallerConfig(hidden_channels=32, modes=16)
75
+ >>> op = FNOPeakCaller(config, rngs=nnx.Rngs(42))
76
+ >>> result, _, _ = op.apply({"coverage": coverage}, {}, None)
77
+ >>> peaks = result["peak_probabilities"] > 0.5
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ config: FNOPeakCallerConfig,
83
+ *,
84
+ rngs: nnx.Rngs | None = None,
85
+ name: str | None = None,
86
+ ) -> None:
87
+ """Initialize FNO peak caller."""
88
+ super().__init__(config, rngs=rngs, name=name)
89
+ self.config: FNOPeakCallerConfig = config
90
+
91
+ if rngs is None:
92
+ rngs = nnx.Rngs(0)
93
+
94
+ # FNO: 1 input channel (coverage) -> 1 output channel (peak score)
95
+ self.fno = FourierNeuralOperator(
96
+ in_channels=1,
97
+ out_channels=1,
98
+ hidden_channels=config.hidden_channels,
99
+ modes=config.modes,
100
+ num_layers=config.num_layers,
101
+ # Opifex now defaults FNOs to 2D spectral layers; this operator is 1D.
102
+ spatial_dims=1,
103
+ rngs=rngs,
104
+ )
105
+
106
+ self.threshold = nnx.Param(jnp.array(config.threshold))
107
+
108
+ def apply(
109
+ self,
110
+ data: PyTree,
111
+ state: PyTree,
112
+ metadata: dict[str, Any] | None,
113
+ random_params: Any = None, # noqa: ARG002
114
+ stats: dict[str, Any] | None = None, # noqa: ARG002
115
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
116
+ """Apply FNO peak detection to coverage signal.
117
+
118
+ Args:
119
+ data: Dict with ``"coverage"`` key — shape ``(batch, length)``
120
+ or ``(length,)`` for unbatched.
121
+ state: Element state (passed through).
122
+ metadata: Element metadata (passed through).
123
+ random_params: Unused.
124
+ stats: Unused.
125
+
126
+ Returns:
127
+ Tuple of (output_data, state, metadata).
128
+ """
129
+ coverage = data["coverage"]
130
+ unbatched = coverage.ndim == 1
131
+ if unbatched:
132
+ coverage = coverage[None, :]
133
+
134
+ # FNO expects (batch, channels, *spatial_dims) — channels-first
135
+ x = coverage[:, None, :] # (batch, 1, length)
136
+ scores = self.fno(x) # (batch, 1, length)
137
+ scores = scores[:, 0, :] # (batch, length)
138
+
139
+ # Soft peak classification
140
+ temp = self.config.temperature
141
+ probs = soft_ops.greater(scores, self.threshold[...], softness=temp)
142
+
143
+ if unbatched:
144
+ scores = scores.squeeze(0)
145
+ probs = probs.squeeze(0)
146
+
147
+ output_data = {
148
+ **data,
149
+ "peak_scores": scores,
150
+ "peak_probabilities": probs,
151
+ }
152
+
153
+ return output_data, state, metadata