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,491 @@
1
+ """Differentiable chromatin state annotation (ChromHMM-style).
2
+
3
+ This module implements a differentiable HMM-based chromatin state annotator
4
+ that can be used for learning chromatin states from histone modification data.
5
+
6
+ Optionally supports cell-type-conditioned emission probabilities, where each
7
+ chromatin state has per-cell-type Gaussian emission parameters. GMM-style soft
8
+ state assignment (gamma/responsibility) is computed via softmax over
9
+ log-likelihoods, inspired by SCALE.
10
+
11
+ Inherits from TemperatureOperator to get:
12
+
13
+ - _temperature property for temperature-controlled smoothing
14
+ - soft_max() for logsumexp-based smooth maximum
15
+ - soft_argmax() for soft Viterbi decoding
16
+
17
+ Note: This uses a Bernoulli emission model (for histone marks) rather than
18
+ categorical emissions, so it doesn't inherit from HMMOperator which assumes
19
+ categorical emissions. The conditioned mode uses Gaussian emissions.
20
+ """
21
+
22
+ import logging
23
+ import math
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ import flax.nnx as nnx
28
+ import jax
29
+ import jax.numpy as jnp
30
+ from datarax.core.config import OperatorConfig
31
+
32
+ from diffbio.core.base_operators import TemperatureOperator
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ChromatinStateConfig(OperatorConfig):
39
+ """Configuration for differentiable chromatin state annotator.
40
+
41
+ Attributes:
42
+ num_states: Number of chromatin states to learn.
43
+ num_marks: Number of histone marks in input.
44
+ temperature: Temperature for soft operations.
45
+ use_cell_type_conditioning: Whether to condition emission
46
+ probabilities on cell type. When enabled, each state has
47
+ per-cell-type Gaussian emission parameters.
48
+ num_cell_types: Number of cell types for conditioning.
49
+ stream_name: Name of the data stream to process.
50
+ """
51
+
52
+ num_states: int = 15
53
+ num_marks: int = 6
54
+ temperature: float = 1.0
55
+ use_cell_type_conditioning: bool = False
56
+ num_cell_types: int = 1
57
+
58
+
59
+ class ChromatinStateAnnotator(TemperatureOperator):
60
+ """Differentiable chromatin state annotator using HMM.
61
+
62
+ This operator implements a differentiable Hidden Markov Model for
63
+ annotating chromatin states from histone modification data. It uses
64
+ the forward algorithm in log-space for numeric stability and provides
65
+ soft Viterbi decoding for end-to-end differentiability.
66
+
67
+ The HMM has:
68
+ - Learnable transition probabilities between states
69
+ - Learnable emission probabilities for each histone mark per state
70
+ - Learnable initial state distribution
71
+
72
+ When cell-type conditioning is enabled:
73
+ - Each state has per-cell-type Gaussian emission parameters (mean, variance)
74
+ - The cell type vector is used to blend emission parameters
75
+ - GMM-style soft assignment (gamma) is computed via softmax over
76
+ log-likelihoods, per SCALE
77
+
78
+ Inherits from TemperatureOperator to get:
79
+
80
+ - _temperature property for temperature-controlled smoothing
81
+ - soft_max() for logsumexp-based smooth maximum
82
+ - soft_argmax() for soft Viterbi decoding
83
+
84
+ Example:
85
+ ```python
86
+ config = ChromatinStateConfig(
87
+ num_states=15,
88
+ num_marks=6,
89
+ use_cell_type_conditioning=True,
90
+ num_cell_types=5,
91
+ )
92
+ annotator = ChromatinStateAnnotator(config, rngs=rngs)
93
+
94
+ data = {
95
+ "histone_marks": marks, # (length, num_marks)
96
+ "cell_type": cell_type, # (num_cell_types,) soft vector
97
+ }
98
+ result, state, metadata = annotator.apply(data, {}, None)
99
+ state_probs = result["state_probabilities"]
100
+ gamma = result["gamma"] # soft state assignment
101
+ ```
102
+ """
103
+
104
+ def __init__(self, config: ChromatinStateConfig, *, rngs: nnx.Rngs | None = None):
105
+ """Initialize the chromatin state annotator.
106
+
107
+ Args:
108
+ config: Configuration for the annotator.
109
+ rngs: Random number generators for initialization.
110
+ """
111
+ super().__init__(config, rngs=rngs)
112
+ self.config = config
113
+
114
+ if rngs is None:
115
+ rngs = nnx.Rngs(0)
116
+
117
+ num_states = config.num_states
118
+ num_marks = config.num_marks
119
+
120
+ # Initialize transition matrix (log-space)
121
+ # Start with slight preference for self-transitions
122
+ key = rngs.params() if hasattr(rngs, "params") else jax.random.key(0)
123
+ k1, k2, k3, k4, k5 = jax.random.split(key, 5)
124
+
125
+ transition_init = jax.random.normal(k1, (num_states, num_states)) * 0.1
126
+ transition_init = transition_init + jnp.eye(num_states) * 2.0 # Self-loop bias
127
+ self.transition_logits = nnx.Param(transition_init)
128
+
129
+ # Initialize emission parameters (Bernoulli model for default mode)
130
+ emission_init = jax.random.normal(k2, (num_states, num_marks)) * 0.5
131
+ self.emission_logits = nnx.Param(emission_init)
132
+
133
+ # Initial state distribution
134
+ initial_init = jax.random.normal(k3, (num_states,)) * 0.1
135
+ self.initial_logits = nnx.Param(initial_init)
136
+ # Temperature is now managed by TemperatureOperator via self._temperature
137
+
138
+ # Cell-type conditioned emission parameters
139
+ self._use_conditioning = config.use_cell_type_conditioning
140
+ if self._use_conditioning:
141
+ num_cell_types = config.num_cell_types
142
+
143
+ # Per-type emission means: (num_cell_types, num_states, num_marks)
144
+ means_init = jax.random.normal(k4, (num_cell_types, num_states, num_marks)) * 0.5
145
+ self.emission_means = nnx.Param(means_init)
146
+
147
+ # Per-type emission log-variances: (num_cell_types, num_states, num_marks)
148
+ logvar_init = jax.random.normal(k5, (num_cell_types, num_states, num_marks)) * 0.1
149
+ self.emission_logvars = nnx.Param(logvar_init)
150
+
151
+ def _log_transition_matrix(self) -> jax.Array:
152
+ """Get log transition probabilities (row-normalized)."""
153
+ return jax.nn.log_softmax(self.transition_logits[...], axis=-1)
154
+
155
+ def _log_initial_distribution(self) -> jax.Array:
156
+ """Get log initial state probabilities."""
157
+ return jax.nn.log_softmax(self.initial_logits[...])
158
+
159
+ def _log_emission_probs(self, observations: jax.Array) -> jax.Array:
160
+ """Compute log emission probabilities using Bernoulli model.
161
+
162
+ Args:
163
+ observations: Histone mark signals of shape (..., num_marks).
164
+
165
+ Returns:
166
+ Log emission probabilities of shape (..., num_states).
167
+ """
168
+ # Emission model: product of Bernoulli for each mark
169
+ # P(obs | state) = prod_m P(mark_m | state)
170
+ # For continuous input, use sigmoid to get probabilities
171
+
172
+ # emission_logits: (num_states, num_marks)
173
+ # observations: (..., num_marks)
174
+
175
+ # Compute P(mark=1 | state) using sigmoid
176
+ mark_probs = jax.nn.sigmoid(self.emission_logits[...]) # (num_states, num_marks)
177
+
178
+ # Normalize observations to [0, 1] range using sigmoid
179
+ obs_probs = jax.nn.sigmoid(observations) # (..., num_marks)
180
+
181
+ # Log probability of observing the marks given each state
182
+ # Using soft Bernoulli: obs * log(p) + (1-obs) * log(1-p)
183
+ log_mark_probs = jnp.log(mark_probs + 1e-8) # (num_states, num_marks)
184
+ log_not_mark_probs = jnp.log(1 - mark_probs + 1e-8) # (num_states, num_marks)
185
+
186
+ # Broadcast and compute log likelihood
187
+ # obs_probs: (..., num_marks), mark_probs: (num_states, num_marks)
188
+ log_emission = (
189
+ obs_probs[..., None, :] * log_mark_probs
190
+ + (1 - obs_probs[..., None, :]) * log_not_mark_probs
191
+ ) # (..., num_states, num_marks)
192
+
193
+ # Sum over marks
194
+ return log_emission.sum(axis=-1) # (..., num_states)
195
+
196
+ def _log_emission_probs_conditioned(
197
+ self,
198
+ observations: jax.Array,
199
+ cell_type: jax.Array,
200
+ ) -> jax.Array:
201
+ """Compute log emission probabilities conditioned on cell type.
202
+
203
+ Uses Gaussian emission model where parameters are a weighted blend
204
+ of per-cell-type parameters.
205
+
206
+ Args:
207
+ observations: Histone mark signals of shape (..., num_marks).
208
+ cell_type: Cell type vector of shape (num_cell_types,), soft assignment.
209
+
210
+ Returns:
211
+ Log emission probabilities of shape (..., num_states).
212
+ """
213
+ # Blend per-type emission parameters using cell_type weights
214
+ # emission_means: (num_cell_types, num_states, num_marks)
215
+ # cell_type: (num_cell_types,)
216
+ blended_means = jnp.einsum(
217
+ "c,csm->sm", cell_type, self.emission_means[...]
218
+ ) # (num_states, num_marks)
219
+
220
+ blended_logvars = jnp.einsum(
221
+ "c,csm->sm", cell_type, self.emission_logvars[...]
222
+ ) # (num_states, num_marks)
223
+
224
+ # Gaussian log-likelihood: -0.5 * (log(2*pi*var) + (x-mu)^2 / var)
225
+ variance = jnp.exp(blended_logvars) + 1e-8 # (num_states, num_marks)
226
+
227
+ # observations: (..., num_marks) -> (..., 1, num_marks) for broadcasting
228
+ obs_expanded = observations[..., None, :] # (..., 1, num_marks)
229
+
230
+ log_emission = -0.5 * (
231
+ jnp.log(2 * math.pi * variance) + (obs_expanded - blended_means) ** 2 / variance
232
+ ) # (..., num_states, num_marks)
233
+
234
+ # Sum over marks
235
+ return log_emission.sum(axis=-1) # (..., num_states)
236
+
237
+ def _compute_gamma(self, log_emissions: jax.Array) -> jax.Array:
238
+ """Compute GMM-style soft state assignment (responsibility).
239
+
240
+ Gamma is the soft assignment probability of each position to each
241
+ state, computed via softmax over log-likelihoods. This follows the
242
+ SCALE approach where gamma = p(c|z) = p(c)*p(z|c) / p(z).
243
+
244
+ Args:
245
+ log_emissions: Log emission probabilities, shape (length, num_states).
246
+
247
+ Returns:
248
+ Gamma (responsibility) of shape (length, num_states).
249
+ """
250
+ # Use log initial distribution as prior p(c)
251
+ log_prior = self._log_initial_distribution() # (num_states,)
252
+
253
+ # p(c,z) = p(c) * p(z|c) in log space
254
+ log_joint = log_prior + log_emissions # (length, num_states)
255
+
256
+ # Softmax to normalize: gamma = p(c|z) = softmax(log_joint)
257
+ gamma = jax.nn.softmax(log_joint, axis=-1)
258
+
259
+ return gamma
260
+
261
+ def _forward_algorithm(self, log_emissions: jax.Array) -> tuple[jax.Array, jax.Array]:
262
+ """Run forward algorithm in log-space.
263
+
264
+ Args:
265
+ log_emissions: Log emission probabilities of shape (length, num_states).
266
+
267
+ Returns:
268
+ Tuple of (alpha, log_likelihood) where alpha has shape (length, num_states).
269
+ """
270
+ log_trans = self._log_transition_matrix()
271
+ log_init = self._log_initial_distribution()
272
+
273
+ def forward_step(
274
+ log_alpha_prev: jax.Array,
275
+ log_emit_t: jax.Array,
276
+ ) -> tuple[jax.Array, jax.Array]:
277
+ # log_alpha_prev: (num_states,)
278
+ # log_emit_t: (num_states,)
279
+ # log_alpha_t[j] = log_emit_t[j] + logsumexp_i(log_alpha_prev[i] + log_trans[i,j])
280
+
281
+ # Expand for broadcasting: (num_states, 1) + (num_states, num_states)
282
+ log_alpha_trans = log_alpha_prev[:, None] + log_trans
283
+ log_alpha_t = log_emit_t + jax.scipy.special.logsumexp(log_alpha_trans, axis=0)
284
+ return log_alpha_t, log_alpha_t
285
+
286
+ # Initialize with initial distribution
287
+ log_alpha_0 = log_init + log_emissions[0]
288
+
289
+ # Run forward pass
290
+ _, log_alphas = jax.lax.scan(forward_step, log_alpha_0, log_emissions[1:])
291
+
292
+ # Prepend initial alpha
293
+ log_alphas = jnp.concatenate([log_alpha_0[None, :], log_alphas], axis=0)
294
+
295
+ # Compute log likelihood
296
+ log_likelihood = jax.scipy.special.logsumexp(log_alphas[-1])
297
+
298
+ return log_alphas, log_likelihood
299
+
300
+ def _backward_algorithm(self, log_emissions: jax.Array) -> jax.Array:
301
+ """Run backward algorithm in log-space.
302
+
303
+ Args:
304
+ log_emissions: Log emission probabilities of shape (length, num_states).
305
+
306
+ Returns:
307
+ Beta values of shape (length, num_states).
308
+ """
309
+ log_trans = self._log_transition_matrix()
310
+ num_states = self.config.num_states
311
+
312
+ def backward_step(
313
+ log_beta_next: jax.Array,
314
+ log_emit_next: jax.Array,
315
+ ) -> tuple[jax.Array, jax.Array]:
316
+ # log_beta_next: (num_states,)
317
+ # log_emit_next: (num_states,)
318
+ # log_beta_t[i] = logsumexp_j(log_trans[i,j] + log_emit_next[j] + log_beta_next[j])
319
+
320
+ log_beta_t = jax.scipy.special.logsumexp(
321
+ log_trans + log_emit_next[None, :] + log_beta_next[None, :], axis=1
322
+ )
323
+ return log_beta_t, log_beta_t
324
+
325
+ # Initialize with zeros (log(1) = 0)
326
+ log_beta_T = jnp.zeros(num_states)
327
+
328
+ # Run backward pass (reversed)
329
+ _, log_betas_rev = jax.lax.scan(backward_step, log_beta_T, log_emissions[1:][::-1])
330
+
331
+ # Reverse and prepend final beta
332
+ log_betas = jnp.concatenate([log_betas_rev[::-1], log_beta_T[None, :]], axis=0)
333
+
334
+ return log_betas
335
+
336
+ def _compute_posteriors(self, log_alphas: jax.Array, log_betas: jax.Array) -> jax.Array:
337
+ """Compute posterior state probabilities.
338
+
339
+ Args:
340
+ log_alphas: Forward probabilities of shape (length, num_states).
341
+ log_betas: Backward probabilities of shape (length, num_states).
342
+
343
+ Returns:
344
+ Posterior probabilities of shape (length, num_states).
345
+ """
346
+ # P(state_t | observations) = alpha_t * beta_t / P(observations)
347
+ log_posteriors = log_alphas + log_betas
348
+ posteriors = jax.nn.softmax(log_posteriors, axis=-1)
349
+ return posteriors
350
+
351
+ def _soft_viterbi(self, log_emissions: jax.Array) -> jax.Array:
352
+ """Compute soft Viterbi decoding using temperature-scaled max.
353
+
354
+ Args:
355
+ log_emissions: Log emission probabilities of shape (length, num_states).
356
+
357
+ Returns:
358
+ Most likely state at each position (as soft argmax).
359
+ """
360
+ log_trans = self._log_transition_matrix()
361
+ log_init = self._log_initial_distribution()
362
+ # Use inherited _temperature property from TemperatureOperator
363
+ temperature = jnp.abs(self._temperature) + 1e-6
364
+
365
+ def viterbi_step(
366
+ log_delta_prev: jax.Array,
367
+ log_emit_t: jax.Array,
368
+ ) -> tuple[jax.Array, jax.Array]:
369
+ # Soft max over previous states
370
+ log_delta_trans = log_delta_prev[:, None] + log_trans
371
+ # Use logsumexp with temperature scaling for soft max
372
+ log_delta_t = log_emit_t + temperature * jax.scipy.special.logsumexp(
373
+ log_delta_trans / temperature, axis=0
374
+ )
375
+ return log_delta_t, log_delta_t
376
+
377
+ # Initialize
378
+ log_delta_0 = log_init + log_emissions[0]
379
+
380
+ # Forward pass
381
+ _, log_deltas = jax.lax.scan(viterbi_step, log_delta_0, log_emissions[1:])
382
+ log_deltas = jnp.concatenate([log_delta_0[None, :], log_deltas], axis=0)
383
+
384
+ # Soft argmax for most likely state
385
+ state_probs = jax.nn.softmax(log_deltas / temperature, axis=-1)
386
+ most_likely = jnp.sum(state_probs * jnp.arange(self.config.num_states)[None, :], axis=-1)
387
+
388
+ return most_likely
389
+
390
+ def _apply_single(
391
+ self,
392
+ marks: jax.Array,
393
+ cell_type: jax.Array | None = None,
394
+ ) -> dict:
395
+ """Apply HMM to a single sequence.
396
+
397
+ Args:
398
+ marks: Histone mark signals of shape (length, num_marks).
399
+ cell_type: Optional cell type vector of shape (num_cell_types,).
400
+
401
+ Returns:
402
+ Dictionary of outputs.
403
+ """
404
+ # Compute log emissions based on mode
405
+ if self._use_conditioning and cell_type is not None:
406
+ log_emissions = self._log_emission_probs_conditioned(
407
+ marks, cell_type
408
+ ) # (length, num_states)
409
+ else:
410
+ log_emissions = self._log_emission_probs(marks) # (length, num_states)
411
+
412
+ # Forward algorithm
413
+ log_alphas, log_likelihood = self._forward_algorithm(log_emissions)
414
+
415
+ # Backward algorithm
416
+ log_betas = self._backward_algorithm(log_emissions)
417
+
418
+ # Posterior probabilities
419
+ posteriors = self._compute_posteriors(log_alphas, log_betas)
420
+
421
+ # State probabilities (normalized alphas for online prediction)
422
+ state_probs = jax.nn.softmax(log_alphas, axis=-1)
423
+
424
+ # Soft Viterbi path
425
+ viterbi_path = self._soft_viterbi(log_emissions)
426
+
427
+ result = {
428
+ "state_probabilities": state_probs,
429
+ "state_posteriors": posteriors,
430
+ "viterbi_path": viterbi_path,
431
+ "log_likelihood": log_likelihood,
432
+ }
433
+
434
+ # Add gamma (GMM-style soft assignment) for conditioned mode
435
+ if self._use_conditioning and cell_type is not None:
436
+ gamma = self._compute_gamma(log_emissions)
437
+ result["gamma"] = gamma
438
+
439
+ return result
440
+
441
+ def apply(
442
+ self,
443
+ data: dict[str, Any],
444
+ state: dict[str, Any],
445
+ metadata: dict | None,
446
+ random_params: dict | None = None,
447
+ stats: dict | None = None,
448
+ ) -> tuple[dict, dict, dict | None]:
449
+ """Apply chromatin state annotation to histone mark data.
450
+
451
+ Args:
452
+ data: Dictionary containing:
453
+ - 'histone_marks': Signals of shape (length, num_marks) or
454
+ (batch, length, num_marks)
455
+ - 'cell_type': Optional cell type vector of shape
456
+ (num_cell_types,) when conditioning is enabled
457
+ state: Operator state dictionary.
458
+ metadata: Optional metadata dictionary.
459
+ random_params: Optional random parameters (unused).
460
+ stats: Optional statistics dictionary (unused).
461
+
462
+ Returns:
463
+ Tuple of (output_data, state, metadata) where output_data contains:
464
+
465
+ - 'histone_marks': Original histone mark signals
466
+ - 'state_probabilities': State probabilities at each position
467
+ - 'state_posteriors': Posterior state probabilities
468
+ - 'viterbi_path': Soft Viterbi decoding result
469
+ - 'log_likelihood': Log likelihood of the sequence
470
+ - 'gamma': Soft state assignment (only when conditioning enabled)
471
+ """
472
+ del random_params, stats # Unused
473
+
474
+ marks = data["histone_marks"]
475
+ cell_type = data.get("cell_type") if self._use_conditioning else None
476
+
477
+ # Handle single vs batched input
478
+ single_input = marks.ndim == 2
479
+ if single_input:
480
+ result = self._apply_single(marks, cell_type)
481
+ else:
482
+ # Batched input - vmap over batch dimension
483
+ # For conditioned mode, cell_type is shared across batch
484
+ if cell_type is not None:
485
+ result = jax.vmap(lambda m: self._apply_single(m, cell_type))(marks)
486
+ else:
487
+ result = jax.vmap(self._apply_single)(marks)
488
+
489
+ output_data = {**data, **result}
490
+
491
+ return output_data, state, metadata