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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- 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
|