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,678 @@
1
+ """Differentiable CNV Segmentation operator.
2
+
3
+ This module provides differentiable implementations of copy number
4
+ variation segmentation:
5
+
6
+ - ``DifferentiableCNVSegmentation``: Attention-based soft changepoint detection.
7
+ - ``EnhancedCNVSegmentation``: Multi-signal fusion (log-ratio + BAF + SNP
8
+ density), pyramidal smoothing (infercnvpy-style), STDDEV-based dynamic
9
+ thresholding, and HMM state-to-copy-number mapping.
10
+
11
+ Key techniques:
12
+ - Attention mechanism identifies segment boundaries softly.
13
+ - Pyramidal (triangular) convolution for robust spatial smoothing.
14
+ - Dynamic threshold = scale * std(smoothed_signal) filters noise.
15
+ - Learnable linear fusion of heterogeneous signals.
16
+ - Soft copy-number state posteriors via learned emission model.
17
+
18
+ Applications: CNV analysis, coverage depth segmentation, breakpoint detection.
19
+ """
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ import jax
25
+ import jax.numpy as jnp
26
+ from datarax.core.config import OperatorConfig
27
+ from flax import nnx
28
+ from jaxtyping import Array, Float, PyTree
29
+
30
+ from diffbio.core import soft_ops
31
+ from diffbio.core.base_operators import TemperatureOperator
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CNVSegmentationConfig(OperatorConfig):
36
+ """Configuration for DifferentiableCNVSegmentation.
37
+
38
+ Attributes:
39
+ max_segments: Maximum number of segments to detect.
40
+ hidden_dim: Hidden dimension for attention layers.
41
+ attention_heads: Number of attention heads.
42
+ temperature: Temperature for softmax operations.
43
+ """
44
+
45
+ max_segments: int = 100
46
+ hidden_dim: int = 64
47
+ attention_heads: int = 4
48
+ temperature: float = 1.0
49
+
50
+ def __post_init__(self) -> None:
51
+ """Validate segmentation hyperparameters."""
52
+ super().__post_init__()
53
+
54
+ if self.max_segments <= 0:
55
+ raise ValueError(f"max_segments must be positive, got {self.max_segments}")
56
+ if self.hidden_dim <= 0:
57
+ raise ValueError(f"hidden_dim must be positive, got {self.hidden_dim}")
58
+ if self.attention_heads <= 0:
59
+ raise ValueError(f"attention_heads must be positive, got {self.attention_heads}")
60
+ if self.temperature <= 0.0:
61
+ raise ValueError(f"temperature must be positive, got {self.temperature}")
62
+ if self.hidden_dim % self.attention_heads != 0:
63
+ raise ValueError(
64
+ "hidden_dim must be divisible by attention_heads, got "
65
+ f"hidden_dim={self.hidden_dim}, attention_heads={self.attention_heads}"
66
+ )
67
+
68
+
69
+ class DifferentiableCNVSegmentation(TemperatureOperator):
70
+ """Soft CNV segmentation using attention-based changepoint detection.
71
+
72
+ This operator identifies segment boundaries in coverage data using
73
+ attention mechanisms, replacing hard Circular Binary Segmentation
74
+ with differentiable soft assignments.
75
+
76
+ Algorithm:
77
+ 1. Project coverage signal into hidden space
78
+ 2. Use self-attention to identify changepoint positions
79
+ 3. Compute soft segment assignments via attention
80
+ 4. Compute segment means as weighted averages
81
+
82
+ Inherits from TemperatureOperator to get:
83
+
84
+ - _temperature property for temperature-controlled smoothing
85
+ - soft_max() for logsumexp-based smooth maximum
86
+ - soft_argmax() for soft position selection
87
+
88
+ Args:
89
+ config: CNVSegmentationConfig with model parameters.
90
+ rngs: Flax NNX random number generators.
91
+ name: Optional operator name.
92
+
93
+ Example:
94
+ ```python
95
+ config = CNVSegmentationConfig(max_segments=50)
96
+ segmenter = DifferentiableCNVSegmentation(config, rngs=nnx.Rngs(42))
97
+ data = {"coverage": coverage_signal} # (n_positions,)
98
+ result, state, meta = segmenter.apply(data, {}, None)
99
+ ```
100
+ """
101
+
102
+ config: CNVSegmentationConfig # pyright: ignore[reportIncompatibleVariableOverride]
103
+
104
+ def __init__(
105
+ self,
106
+ config: CNVSegmentationConfig,
107
+ *,
108
+ rngs: nnx.Rngs | None = None,
109
+ name: str | None = None,
110
+ ) -> None:
111
+ """Initialize the CNV segmentation operator.
112
+
113
+ Args:
114
+ config: Segmentation configuration.
115
+ rngs: Random number generators for initialization.
116
+ name: Optional operator name.
117
+ """
118
+ super().__init__(config, rngs=rngs, name=name)
119
+
120
+ if rngs is None:
121
+ rngs = nnx.Rngs(0)
122
+
123
+ # Input projection: coverage value -> hidden
124
+ self.input_proj = nnx.Linear(1, config.hidden_dim, rngs=rngs)
125
+
126
+ # Positional encoding projection
127
+ self.pos_proj = nnx.Linear(1, config.hidden_dim, rngs=rngs)
128
+
129
+ # Attention projections (for boundary detection)
130
+ self.query_proj = nnx.Linear(config.hidden_dim, config.hidden_dim, rngs=rngs)
131
+ self.key_proj = nnx.Linear(config.hidden_dim, config.hidden_dim, rngs=rngs)
132
+ self.value_proj = nnx.Linear(config.hidden_dim, config.hidden_dim, rngs=rngs)
133
+
134
+ # Boundary detection head
135
+ self.boundary_head = nnx.Linear(config.hidden_dim, 1, rngs=rngs)
136
+
137
+ # Segment centroids (learnable)
138
+ key = rngs.params()
139
+ init_centroids = jax.random.normal(key, (config.max_segments, config.hidden_dim)) * 0.1
140
+ self.segment_centroids = nnx.Param(init_centroids)
141
+
142
+ def compute_embeddings(
143
+ self,
144
+ coverage: Float[Array, "n_positions"],
145
+ ) -> Float[Array, "n_positions hidden_dim"]:
146
+ """Compute position embeddings from coverage signal.
147
+
148
+ Args:
149
+ coverage: Coverage values at each position.
150
+
151
+ Returns:
152
+ Embedded representation of each position.
153
+ """
154
+ n_positions = coverage.shape[0]
155
+
156
+ # Project coverage values
157
+ coverage_emb = self.input_proj(coverage[:, None]) # (n_positions, hidden_dim)
158
+
159
+ # Add positional encoding
160
+ positions = jnp.arange(n_positions, dtype=jnp.float32) / n_positions
161
+ pos_emb = self.pos_proj(positions[:, None]) # (n_positions, hidden_dim)
162
+
163
+ embeddings = coverage_emb + pos_emb
164
+
165
+ return embeddings
166
+
167
+ def compute_boundary_probs(
168
+ self,
169
+ embeddings: Float[Array, "n_positions hidden_dim"],
170
+ ) -> Float[Array, "n_positions"]:
171
+ """Compute soft boundary probabilities using self-attention.
172
+
173
+ Args:
174
+ embeddings: Position embeddings.
175
+
176
+ Returns:
177
+ Probability of being a segment boundary at each position.
178
+ """
179
+ n_positions = embeddings.shape[0]
180
+
181
+ # Self-attention to detect changepoints
182
+ Q = self.query_proj(embeddings) # (n_positions, hidden_dim)
183
+ K = self.key_proj(embeddings) # (n_positions, hidden_dim)
184
+ V = self.value_proj(embeddings) # (n_positions, hidden_dim)
185
+
186
+ # Compute attention scores
187
+ head_dim = self.config.hidden_dim // self.config.attention_heads
188
+ scale = jnp.sqrt(head_dim).astype(embeddings.dtype)
189
+
190
+ # Reshape for multi-head attention
191
+ Q = Q.reshape(n_positions, self.config.attention_heads, head_dim)
192
+ K = K.reshape(n_positions, self.config.attention_heads, head_dim)
193
+ V = V.reshape(n_positions, self.config.attention_heads, head_dim)
194
+
195
+ # Attention: (n_positions, n_heads, n_positions)
196
+ attn_scores = jnp.einsum("nhd,mhd->nhm", Q, K) / scale
197
+
198
+ # Soft attention weights
199
+ attn_weights = jax.nn.softmax(attn_scores / self._temperature, axis=-1)
200
+
201
+ # Attend to values
202
+ attended = jnp.einsum("nhm,mhd->nhd", attn_weights, V)
203
+ attended = attended.reshape(n_positions, self.config.hidden_dim)
204
+
205
+ # Compute boundary probability from attended features
206
+ # Look at how much attention pattern changes
207
+ boundary_logits = self.boundary_head(attended).squeeze(-1) # (n_positions,)
208
+
209
+ # Boundaries at positions where signal changes
210
+ boundary_probs = jax.nn.sigmoid(boundary_logits)
211
+
212
+ return boundary_probs
213
+
214
+ def compute_segment_assignments(
215
+ self,
216
+ embeddings: Float[Array, "n_positions hidden_dim"],
217
+ ) -> Float[Array, "n_positions max_segments"]:
218
+ """Compute soft segment assignments via attention to centroids.
219
+
220
+ Args:
221
+ embeddings: Position embeddings.
222
+
223
+ Returns:
224
+ Soft assignment probability to each segment.
225
+ """
226
+ centroids = self.segment_centroids[...] # (max_segments, hidden_dim)
227
+
228
+ # Compute similarity to segment centroids
229
+ # (n_positions, hidden_dim) x (hidden_dim, max_segments) -> (n_positions, max_segments)
230
+ similarities = jnp.einsum("nh,sh->ns", embeddings, centroids)
231
+
232
+ # Soft assignments via softmax
233
+ assignments = jax.nn.softmax(similarities / self._temperature, axis=-1)
234
+
235
+ return assignments
236
+
237
+ def compute_segment_means(
238
+ self,
239
+ coverage: Float[Array, "n_positions"],
240
+ assignments: Float[Array, "n_positions max_segments"],
241
+ ) -> Float[Array, "max_segments"]:
242
+ """Compute segment mean values.
243
+
244
+ Args:
245
+ coverage: Coverage values.
246
+ assignments: Soft segment assignments.
247
+
248
+ Returns:
249
+ Mean coverage for each segment.
250
+ """
251
+ # Weighted sum of coverage / sum of weights
252
+ weighted_sum = jnp.einsum("n,ns->s", coverage, assignments) # (max_segments,)
253
+ weight_sum = jnp.sum(assignments, axis=0) + 1e-10 # (max_segments,)
254
+
255
+ segment_means = weighted_sum / weight_sum
256
+
257
+ return segment_means
258
+
259
+ def compute_smoothed_coverage(
260
+ self,
261
+ coverage: Float[Array, "n_positions"],
262
+ assignments: Float[Array, "n_positions max_segments"],
263
+ segment_means: Float[Array, "max_segments"],
264
+ ) -> Float[Array, "n_positions"]:
265
+ """Compute smoothed coverage from segment assignments.
266
+
267
+ Args:
268
+ coverage: Original coverage values.
269
+ assignments: Soft segment assignments.
270
+ segment_means: Mean value for each segment.
271
+
272
+ Returns:
273
+ Smoothed coverage (soft segmentation result).
274
+ """
275
+ # Weighted combination of segment means
276
+ smoothed = jnp.einsum("ns,s->n", assignments, segment_means)
277
+
278
+ return smoothed
279
+
280
+ def apply(
281
+ self,
282
+ data: PyTree,
283
+ state: PyTree,
284
+ metadata: dict[str, Any] | None,
285
+ random_params: Any = None,
286
+ stats: dict[str, Any] | None = None,
287
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
288
+ """Apply CNV segmentation to coverage data.
289
+
290
+ Args:
291
+ data: Dictionary containing:
292
+ - "coverage": Coverage signal (n_positions,)
293
+ state: Element state (passed through unchanged)
294
+ metadata: Element metadata (passed through unchanged)
295
+ random_params: Not used
296
+ stats: Not used
297
+
298
+ Returns:
299
+ Tuple of (transformed_data, state, metadata):
300
+ - transformed_data contains:
301
+
302
+ - "coverage": Original coverage
303
+ - "boundary_probs": Soft boundary probabilities
304
+ - "segment_assignments": Soft segment memberships
305
+ - "segment_means": Mean value per segment
306
+ - "smoothed_coverage": Segmented/smoothed signal
307
+ - state is passed through unchanged
308
+ - metadata is passed through unchanged
309
+ """
310
+ coverage = data["coverage"]
311
+
312
+ # Compute embeddings
313
+ embeddings = self.compute_embeddings(coverage)
314
+
315
+ # Detect boundaries
316
+ boundary_probs = self.compute_boundary_probs(embeddings)
317
+
318
+ # Soft segment assignments
319
+ segment_assignments = self.compute_segment_assignments(embeddings)
320
+
321
+ # Segment statistics
322
+ segment_means = self.compute_segment_means(coverage, segment_assignments)
323
+
324
+ # Smoothed signal
325
+ smoothed_coverage = self.compute_smoothed_coverage(
326
+ coverage, segment_assignments, segment_means
327
+ )
328
+
329
+ # Build output data
330
+ transformed_data = {
331
+ "coverage": coverage,
332
+ "boundary_probs": boundary_probs,
333
+ "segment_assignments": segment_assignments,
334
+ "segment_means": segment_means,
335
+ "smoothed_coverage": smoothed_coverage,
336
+ }
337
+
338
+ return transformed_data, state, metadata
339
+
340
+
341
+ # =========================================================================
342
+ # Enhanced CNV Segmentation
343
+ # =========================================================================
344
+
345
+
346
+ @dataclass(frozen=True)
347
+ class EnhancedCNVSegmentationConfig(CNVSegmentationConfig):
348
+ """Configuration for EnhancedCNVSegmentation.
349
+
350
+ Extends the base CNV segmentation with multi-signal fusion, pyramidal
351
+ smoothing, dynamic thresholding, and HMM copy-number state mapping.
352
+
353
+ Attributes:
354
+ max_segments: Maximum number of segments to detect.
355
+ hidden_dim: Hidden dimension for attention layers.
356
+ attention_heads: Number of attention heads.
357
+ temperature: Temperature for softmax operations.
358
+ use_baf: Whether to incorporate B-allele frequency signal.
359
+ baf_weight: Initial weight for BAF signal in fusion.
360
+ smoothing_window: Window size for pyramidal smoothing convolution.
361
+ threshold_scale: Multiplier for STDDEV-based dynamic threshold.
362
+ n_copy_states: Number of discrete copy-number states (0-somy to N-somy).
363
+ """
364
+
365
+ use_baf: bool = False
366
+ baf_weight: float = 0.3
367
+ smoothing_window: int = 100
368
+ threshold_scale: float = 1.5
369
+ n_copy_states: int = 5
370
+
371
+ def __post_init__(self) -> None:
372
+ """Validate enhanced segmentation hyperparameters."""
373
+ super().__post_init__()
374
+
375
+ if not 0.0 <= self.baf_weight <= 1.0:
376
+ raise ValueError(f"baf_weight must be in [0, 1], got {self.baf_weight}")
377
+ if self.smoothing_window <= 0:
378
+ raise ValueError(f"smoothing_window must be positive, got {self.smoothing_window}")
379
+ if self.threshold_scale <= 0.0:
380
+ raise ValueError(f"threshold_scale must be positive, got {self.threshold_scale}")
381
+ if self.n_copy_states <= 1:
382
+ raise ValueError(f"n_copy_states must be greater than 1, got {self.n_copy_states}")
383
+
384
+
385
+ def _build_pyramidal_kernel(window_size: int) -> Float[Array, "window_size"]:
386
+ """Build a normalized pyramidal (triangular) smoothing kernel.
387
+
388
+ Mirrors the infercnvpy approach: ``min(r, r[::-1])`` creates a
389
+ triangle that peaks at the centre and tapers linearly to the edges.
390
+
391
+ Args:
392
+ window_size: Length of the kernel (must be >= 1).
393
+
394
+ Returns:
395
+ Normalised 1-D pyramidal kernel that sums to 1.
396
+ """
397
+ r = jnp.arange(1, window_size + 1, dtype=jnp.float32)
398
+ pyramid = jnp.minimum(r, r[::-1])
399
+ return pyramid / jnp.sum(pyramid)
400
+
401
+
402
+ class EnhancedCNVSegmentation(DifferentiableCNVSegmentation):
403
+ """Enhanced CNV segmentation with multi-signal fusion and pyramidal smoothing.
404
+
405
+ Inherits from DifferentiableCNVSegmentation and adds:
406
+
407
+ 1. **Multi-signal fusion** -- learnable linear combination of log-ratio
408
+ coverage, BAF, and SNP density signals.
409
+ 2. **Pyramidal smoothing** -- infercnvpy-style triangular convolution
410
+ for spatial noise reduction.
411
+ 3. **Dynamic thresholding** -- ``threshold_scale * std(smoothed)``
412
+ filters low-amplitude noise.
413
+ 4. **HMM state mapping** -- soft copy-number posteriors (0-somy to
414
+ 4-somy by default) via learned emission model.
415
+
416
+ Args:
417
+ config: EnhancedCNVSegmentationConfig with model parameters.
418
+ rngs: Flax NNX random number generators.
419
+ name: Optional operator name.
420
+
421
+ Example:
422
+ ```python
423
+ config = EnhancedCNVSegmentationConfig(
424
+ max_segments=50, use_baf=True, smoothing_window=100,
425
+ )
426
+ op = EnhancedCNVSegmentation(config, rngs=nnx.Rngs(0))
427
+ data = {"coverage": cov, "baf_signal": baf, "snp_density": snp}
428
+ result, state, meta = op.apply(data, {}, None)
429
+ ```
430
+ """
431
+
432
+ config: EnhancedCNVSegmentationConfig # pyright: ignore[reportIncompatibleVariableOverride]
433
+
434
+ def __init__(
435
+ self,
436
+ config: EnhancedCNVSegmentationConfig,
437
+ *,
438
+ rngs: nnx.Rngs | None = None,
439
+ name: str | None = None,
440
+ ) -> None:
441
+ """Initialize the enhanced CNV segmentation operator.
442
+
443
+ Args:
444
+ config: Enhanced segmentation configuration.
445
+ rngs: Random number generators for initialization.
446
+ name: Optional operator name.
447
+ """
448
+ super().__init__(config, rngs=rngs, name=name)
449
+
450
+ if rngs is None:
451
+ rngs = nnx.Rngs(0)
452
+
453
+ # --- Signal fusion ---
454
+ # Number of input channels: coverage is always present.
455
+ # BAF and SNP density are optional extras.
456
+ n_signals = 3 if config.use_baf else 1
457
+ self.signal_fusion = nnx.Linear(n_signals, 1, rngs=rngs)
458
+
459
+ # --- Copy-number state mapping head ---
460
+ self.copy_number_head = nnx.Linear(
461
+ config.hidden_dim,
462
+ config.n_copy_states,
463
+ rngs=rngs,
464
+ )
465
+
466
+ # -----------------------------------------------------------------
467
+ # Multi-signal fusion
468
+ # -----------------------------------------------------------------
469
+
470
+ def fuse_signals(
471
+ self,
472
+ data: dict[str, Float[Array, "n_positions"]],
473
+ ) -> Float[Array, "n_positions"]:
474
+ """Fuse multiple genomic signals via learned linear combination.
475
+
476
+ When ``use_baf`` is enabled, the operator expects ``baf_signal``
477
+ and ``snp_density`` alongside ``coverage`` in the data dict.
478
+ A learnable ``nnx.Linear(n_signals, 1)`` combines them.
479
+
480
+ Args:
481
+ data: Dictionary with at least ``"coverage"`` key.
482
+
483
+ Returns:
484
+ Fused 1-D signal of shape ``(n_positions,)``.
485
+ """
486
+ coverage = data["coverage"]
487
+
488
+ if self.config.use_baf:
489
+ baf = data.get("baf_signal", jnp.zeros_like(coverage))
490
+ snp = data.get("snp_density", jnp.zeros_like(coverage))
491
+ baf_scaled = baf * self.config.baf_weight
492
+ # Stack into (n_positions, 3)
493
+ stacked = jnp.stack([coverage, baf_scaled, snp], axis=-1)
494
+ else:
495
+ stacked = coverage[:, None] # (n_positions, 1)
496
+
497
+ # Linear fusion -> (n_positions, 1) -> squeeze
498
+ fused = self.signal_fusion(stacked).squeeze(-1)
499
+ return fused
500
+
501
+ # -----------------------------------------------------------------
502
+ # Pyramidal smoothing (infercnvpy-style)
503
+ # -----------------------------------------------------------------
504
+
505
+ def pyramidal_smooth(
506
+ self,
507
+ signal: Float[Array, "n_positions"],
508
+ ) -> Float[Array, "n_positions"]:
509
+ """Apply pyramidal (triangular) convolution smoothing.
510
+
511
+ Mirrors the infercnvpy ``_running_mean`` approach: a triangular
512
+ kernel ``min(r, r[::-1])`` is convolved with the signal using
513
+ ``'same'`` mode so the output length matches the input.
514
+
515
+ The convolution is implemented via ``jax.numpy.convolve`` which
516
+ is fully differentiable and JIT-compatible.
517
+
518
+ Args:
519
+ signal: 1-D signal to smooth.
520
+
521
+ Returns:
522
+ Smoothed signal of the same length.
523
+ """
524
+ kernel = _build_pyramidal_kernel(self.config.smoothing_window)
525
+ # 'same' mode preserves signal length
526
+ smoothed = jnp.convolve(signal, kernel, mode="same")
527
+ return smoothed
528
+
529
+ # -----------------------------------------------------------------
530
+ # Dynamic thresholding (infercnvpy-style)
531
+ # -----------------------------------------------------------------
532
+
533
+ def dynamic_threshold_filter(
534
+ self,
535
+ signal: Float[Array, "n_positions"],
536
+ ) -> tuple[Float[Array, "n_positions"], Float[Array, ""]]:
537
+ """Apply STDDEV-based dynamic noise filtering.
538
+
539
+ Following infercnvpy Step 5: ``threshold = scale * std(signal)``.
540
+ Values with absolute magnitude below the threshold are pushed
541
+ toward zero using a soft sigmoid gate for differentiability.
542
+
543
+ Args:
544
+ signal: Smoothed signal to filter.
545
+
546
+ Returns:
547
+ Tuple of (filtered_signal, threshold_value).
548
+ """
549
+ noise_threshold = self.config.threshold_scale * jnp.std(signal)
550
+ # Soft gate: sigmoid((|x| - threshold) / temperature)
551
+ # Outputs near 0 when |x| < threshold, near 1 when |x| > threshold
552
+ gate = soft_ops.greater(
553
+ soft_ops.abs(signal, softness=self._temperature),
554
+ noise_threshold,
555
+ softness=self._temperature,
556
+ )
557
+ filtered = signal * gate
558
+ return filtered, noise_threshold
559
+
560
+ # -----------------------------------------------------------------
561
+ # HMM state mapping
562
+ # -----------------------------------------------------------------
563
+
564
+ def compute_copy_number_posteriors(
565
+ self,
566
+ embeddings: Float[Array, "n_positions hidden_dim"],
567
+ ) -> Float[Array, "n_positions n_copy_states"]:
568
+ """Map embeddings to soft copy-number state posteriors.
569
+
570
+ Uses a learned linear head followed by temperature-scaled softmax
571
+ to produce per-position posterior probabilities over discrete
572
+ copy-number states (0-somy through (n_copy_states-1)-somy).
573
+
574
+ Args:
575
+ embeddings: Position embeddings.
576
+
577
+ Returns:
578
+ Copy-number state posteriors, shape ``(n_positions, n_copy_states)``.
579
+ """
580
+ logits = self.copy_number_head(embeddings)
581
+ return jax.nn.softmax(logits / self._temperature, axis=-1)
582
+
583
+ def compute_expected_copy_number(
584
+ self,
585
+ posteriors: Float[Array, "n_positions n_copy_states"],
586
+ ) -> Float[Array, "n_positions"]:
587
+ """Compute expected copy number as posterior-weighted state values.
588
+
589
+ E[CN] = sum_k( k * P(state=k) ) for k in 0..n_copy_states-1.
590
+
591
+ Args:
592
+ posteriors: Copy-number state posteriors.
593
+
594
+ Returns:
595
+ Expected copy number at each position.
596
+ """
597
+ state_values = jnp.arange(self.config.n_copy_states, dtype=jnp.float32)
598
+ return jnp.einsum("ns,s->n", posteriors, state_values)
599
+
600
+ # -----------------------------------------------------------------
601
+ # Main apply
602
+ # -----------------------------------------------------------------
603
+
604
+ def apply(
605
+ self,
606
+ data: PyTree,
607
+ state: PyTree,
608
+ metadata: dict[str, Any] | None,
609
+ random_params: Any = None,
610
+ stats: dict[str, Any] | None = None,
611
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
612
+ """Apply enhanced CNV segmentation to genomic signal data.
613
+
614
+ Args:
615
+ data: Dictionary containing:
616
+ - ``"coverage"``: Log-ratio coverage signal ``(n_positions,)``
617
+ - ``"baf_signal"`` (optional): B-allele frequency ``(n_positions,)``
618
+ - ``"snp_density"`` (optional): SNP density ``(n_positions,)``
619
+ state: Element state (passed through unchanged).
620
+ metadata: Element metadata (passed through unchanged).
621
+ random_params: Not used.
622
+ stats: Not used.
623
+
624
+ Returns:
625
+ Tuple of ``(transformed_data, state, metadata)`` where
626
+ ``transformed_data`` contains:
627
+
628
+ - ``"coverage"``: Original coverage
629
+ - ``"fused_signal"``: Fused multi-signal output
630
+ - ``"pyramidal_smoothed"``: After pyramidal convolution
631
+ - ``"thresholded_signal"``: After dynamic noise filtering
632
+ - ``"dynamic_threshold"``: Scalar threshold value
633
+ - ``"boundary_probs"``: Soft boundary probabilities
634
+ - ``"segment_assignments"``: Soft segment memberships
635
+ - ``"segment_means"``: Mean value per segment
636
+ - ``"smoothed_coverage"``: Final segmented/smoothed signal
637
+ - ``"copy_number_posteriors"``: Per-position CN state posteriors
638
+ - ``"expected_copy_number"``: Expected copy number per position
639
+ """
640
+ coverage = data["coverage"]
641
+
642
+ # 1. Multi-signal fusion
643
+ fused = self.fuse_signals(data)
644
+
645
+ # 2. Pyramidal smoothing
646
+ smoothed = self.pyramidal_smooth(fused)
647
+
648
+ # 3. Dynamic thresholding
649
+ thresholded, threshold_val = self.dynamic_threshold_filter(smoothed)
650
+
651
+ # 4. Attention-based segmentation on thresholded signal (inherited methods)
652
+ embeddings = self.compute_embeddings(thresholded)
653
+ boundary_probs = self.compute_boundary_probs(embeddings)
654
+ segment_assignments = self.compute_segment_assignments(embeddings)
655
+ segment_means = self.compute_segment_means(thresholded, segment_assignments)
656
+ smoothed_coverage = self.compute_smoothed_coverage(
657
+ thresholded, segment_assignments, segment_means
658
+ )
659
+
660
+ # 5. HMM state mapping
661
+ cn_posteriors = self.compute_copy_number_posteriors(embeddings)
662
+ expected_cn = self.compute_expected_copy_number(cn_posteriors)
663
+
664
+ transformed_data = {
665
+ "coverage": coverage,
666
+ "fused_signal": fused,
667
+ "pyramidal_smoothed": smoothed,
668
+ "thresholded_signal": thresholded,
669
+ "dynamic_threshold": threshold_val,
670
+ "boundary_probs": boundary_probs,
671
+ "segment_assignments": segment_assignments,
672
+ "segment_means": segment_means,
673
+ "smoothed_coverage": smoothed_coverage,
674
+ "copy_number_posteriors": cn_posteriors,
675
+ "expected_copy_number": expected_cn,
676
+ }
677
+
678
+ return transformed_data, state, metadata