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,184 @@
|
|
|
1
|
+
"""Sigmoidal switch differential expression operator.
|
|
2
|
+
|
|
3
|
+
This module provides a differentiable model of gene expression as a
|
|
4
|
+
sigmoidal function of pseudotime. Each gene has a learnable switch time,
|
|
5
|
+
amplitude, and baseline, enabling gradient-based identification of
|
|
6
|
+
dynamically regulated genes along a trajectory.
|
|
7
|
+
|
|
8
|
+
Key technique: Models expression as ``a * sigmoid((t - t_switch) / T) + b``
|
|
9
|
+
where the sigmoid temperature controls smoothness of the transition.
|
|
10
|
+
|
|
11
|
+
Applications: Identifying switch-like gene regulation events in
|
|
12
|
+
single-cell pseudotime trajectories.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import jax
|
|
20
|
+
import jax.numpy as jnp
|
|
21
|
+
from datarax.core.config import OperatorConfig
|
|
22
|
+
from flax import nnx
|
|
23
|
+
from jaxtyping import Array, Float, PyTree
|
|
24
|
+
|
|
25
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SwitchDEConfig(OperatorConfig):
|
|
32
|
+
"""Configuration for sigmoidal switch differential expression.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
n_genes: Number of genes to model.
|
|
36
|
+
temperature: Temperature controlling sigmoid smoothness.
|
|
37
|
+
Lower values produce sharper switch transitions.
|
|
38
|
+
learnable_temperature: Whether temperature is a learnable parameter.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
n_genes: int = 2000
|
|
42
|
+
temperature: float = 1.0
|
|
43
|
+
learnable_temperature: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DifferentiableSwitchDE(TemperatureOperator):
|
|
47
|
+
"""Differentiable sigmoidal switch model for differential expression.
|
|
48
|
+
|
|
49
|
+
Models gene expression as a sigmoidal function of pseudotime:
|
|
50
|
+
``g(t) = amplitude * sigmoid((t - t_switch) / temperature) + baseline``
|
|
51
|
+
|
|
52
|
+
Each gene has learnable parameters for switch time, amplitude, and
|
|
53
|
+
baseline expression level. The switch score quantifies how strongly
|
|
54
|
+
a gene switches, computed as the maximum sigmoid derivative scaled
|
|
55
|
+
by amplitude.
|
|
56
|
+
|
|
57
|
+
Inherits from TemperatureOperator to get:
|
|
58
|
+
|
|
59
|
+
- _temperature property for temperature-controlled smoothing
|
|
60
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
61
|
+
- soft_argmax() for soft position selection
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
config: SwitchDEConfig with model parameters.
|
|
65
|
+
rngs: Flax NNX random number generators.
|
|
66
|
+
name: Optional operator name.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
```python
|
|
70
|
+
config = SwitchDEConfig(n_genes=2000, temperature=1.0)
|
|
71
|
+
op = DifferentiableSwitchDE(config, rngs=nnx.Rngs(42))
|
|
72
|
+
data = {"counts": counts, "pseudotime": pseudotime}
|
|
73
|
+
result, state, meta = op.apply(data, {}, None)
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
config: SwitchDEConfig,
|
|
80
|
+
*,
|
|
81
|
+
rngs: nnx.Rngs | None = None,
|
|
82
|
+
name: str | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Initialize the sigmoidal switch DE operator.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Switch DE configuration.
|
|
88
|
+
rngs: Random number generators for initialization.
|
|
89
|
+
name: Optional operator name.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
92
|
+
|
|
93
|
+
n_genes = config.n_genes
|
|
94
|
+
|
|
95
|
+
# Switch time per gene (init to 0.5 = midpoint of pseudotime)
|
|
96
|
+
self.t_switch = nnx.Param(jnp.full((n_genes,), 0.5))
|
|
97
|
+
|
|
98
|
+
# Sigmoid amplitude per gene (init to 1.0)
|
|
99
|
+
self.amplitude = nnx.Param(jnp.ones((n_genes,)))
|
|
100
|
+
|
|
101
|
+
# Baseline expression per gene (init to 0.0)
|
|
102
|
+
self.baseline = nnx.Param(jnp.zeros((n_genes,)))
|
|
103
|
+
|
|
104
|
+
def _compute_predicted_expression(
|
|
105
|
+
self,
|
|
106
|
+
pseudotime: Float[Array, "n_cells"],
|
|
107
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
108
|
+
"""Compute predicted expression from the sigmoidal model.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
pseudotime: Pseudotime values per cell.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Predicted expression matrix (n_cells, n_genes).
|
|
115
|
+
"""
|
|
116
|
+
temp = self._temperature
|
|
117
|
+
t_switch = self.t_switch[...]
|
|
118
|
+
amplitude = self.amplitude[...]
|
|
119
|
+
baseline = self.baseline[...]
|
|
120
|
+
|
|
121
|
+
# Sigmoid argument: (t - t_switch) / temperature
|
|
122
|
+
# pseudotime: (n_cells,) -> (n_cells, 1), t_switch: (n_genes,) -> (1, n_genes)
|
|
123
|
+
sigmoid_arg = (pseudotime[:, None] - t_switch[None, :]) / temp
|
|
124
|
+
predicted = amplitude[None, :] * jax.nn.sigmoid(sigmoid_arg) + baseline[None, :]
|
|
125
|
+
|
|
126
|
+
return predicted
|
|
127
|
+
|
|
128
|
+
def _compute_switch_scores(self) -> Float[Array, "n_genes"]:
|
|
129
|
+
"""Compute switch score per gene.
|
|
130
|
+
|
|
131
|
+
The switch score is the maximum sigmoid derivative scaled by
|
|
132
|
+
amplitude: ``amplitude * (1 / (4 * temperature))``.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Switch scores per gene.
|
|
136
|
+
"""
|
|
137
|
+
temp = self._temperature
|
|
138
|
+
amplitude = self.amplitude[...]
|
|
139
|
+
return amplitude * (1.0 / (4.0 * temp))
|
|
140
|
+
|
|
141
|
+
def apply(
|
|
142
|
+
self,
|
|
143
|
+
data: PyTree,
|
|
144
|
+
state: PyTree,
|
|
145
|
+
metadata: dict[str, Any] | None,
|
|
146
|
+
random_params: Any = None,
|
|
147
|
+
stats: dict[str, Any] | None = None,
|
|
148
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
149
|
+
"""Apply sigmoidal switch DE model to single-cell data.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
data: Dictionary containing:
|
|
153
|
+
- "counts": Gene expression counts (n_cells, n_genes)
|
|
154
|
+
- "pseudotime": Pseudotime values per cell (n_cells,)
|
|
155
|
+
state: Element state (passed through unchanged).
|
|
156
|
+
metadata: Element metadata (passed through unchanged).
|
|
157
|
+
random_params: Not used.
|
|
158
|
+
stats: Not used.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (transformed_data, state, metadata):
|
|
162
|
+
- transformed_data contains:
|
|
163
|
+
|
|
164
|
+
- "counts": Original expression counts
|
|
165
|
+
- "pseudotime": Original pseudotime
|
|
166
|
+
- "switch_times": Learned switch time per gene
|
|
167
|
+
- "switch_scores": Switch score per gene
|
|
168
|
+
- "predicted_expression": Predicted expression from model
|
|
169
|
+
- state is passed through unchanged
|
|
170
|
+
- metadata is passed through unchanged
|
|
171
|
+
"""
|
|
172
|
+
pseudotime = data["pseudotime"]
|
|
173
|
+
|
|
174
|
+
predicted = self._compute_predicted_expression(pseudotime)
|
|
175
|
+
scores = self._compute_switch_scores()
|
|
176
|
+
|
|
177
|
+
transformed_data = {
|
|
178
|
+
**data,
|
|
179
|
+
"switch_times": self.t_switch[...],
|
|
180
|
+
"switch_scores": scores,
|
|
181
|
+
"predicted_expression": predicted,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""Differentiable trajectory inference for single-cell analysis.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable pseudotime computation and fate probability
|
|
4
|
+
estimation, enabling gradient-based optimization of trajectory inference in
|
|
5
|
+
single-cell RNA-seq data.
|
|
6
|
+
|
|
7
|
+
Key techniques:
|
|
8
|
+
- Diffusion maps via subspace iteration with QR orthogonalization for
|
|
9
|
+
pseudotime ordering. This replaces eigendecomposition (whose backward pass
|
|
10
|
+
produces NaN when eigenvalues are near-degenerate — JAX issue #669) with
|
|
11
|
+
repeated matmul + QR, which has well-conditioned gradients.
|
|
12
|
+
- Absorption probabilities via linear solve on the fundamental matrix for
|
|
13
|
+
fate probability estimation.
|
|
14
|
+
|
|
15
|
+
Both operations are end-to-end differentiable, allowing backpropagation
|
|
16
|
+
into upstream embeddings.
|
|
17
|
+
|
|
18
|
+
Applications: Developmental trajectory ordering, lineage fate commitment
|
|
19
|
+
analysis, and cell-state transition characterization.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
from datarax.core.config import OperatorConfig
|
|
28
|
+
from datarax.core.operator import OperatorModule
|
|
29
|
+
from flax import nnx
|
|
30
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
31
|
+
|
|
32
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL
|
|
33
|
+
|
|
34
|
+
from diffbio.core.graph_utils import (
|
|
35
|
+
compute_fuzzy_membership,
|
|
36
|
+
compute_pairwise_distances,
|
|
37
|
+
symmetrize_graph,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"PseudotimeConfig",
|
|
44
|
+
"DifferentiablePseudotime",
|
|
45
|
+
"FateProbabilityConfig",
|
|
46
|
+
"DifferentiableFateProbability",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Configurations
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class PseudotimeConfig(OperatorConfig):
|
|
57
|
+
"""Configuration for pseudotime computation.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
n_neighbors: Number of neighbors for k-NN graph construction.
|
|
61
|
+
n_diffusion_components: Number of diffusion map components to retain.
|
|
62
|
+
root_cell_index: Index of the root cell (pseudotime origin).
|
|
63
|
+
metric: Distance metric, ``"euclidean"`` or ``"cosine"``.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
n_neighbors: int = 15
|
|
67
|
+
n_diffusion_components: int = 10
|
|
68
|
+
root_cell_index: int = 0
|
|
69
|
+
metric: str = "euclidean"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class FateProbabilityConfig(OperatorConfig):
|
|
74
|
+
"""Configuration for fate probability computation.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
n_macrostates: Number of macrostates (terminal fates).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
n_macrostates: int = 2
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# DifferentiablePseudotime
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DifferentiablePseudotime(OperatorModule):
|
|
89
|
+
"""Differentiable pseudotime computation via diffusion maps.
|
|
90
|
+
|
|
91
|
+
Constructs a k-NN affinity graph, builds a Markov transition matrix, and
|
|
92
|
+
computes diffusion components through subspace iteration with QR
|
|
93
|
+
orthogonalization. Pseudotime is defined as the Euclidean distance in
|
|
94
|
+
diffusion-component space from the designated root cell.
|
|
95
|
+
|
|
96
|
+
Algorithm:
|
|
97
|
+
1. Compute pairwise distances between cells.
|
|
98
|
+
2. Compute fuzzy membership with local bandwidth (k-th neighbor).
|
|
99
|
+
3. Symmetrize the graph via fuzzy set union.
|
|
100
|
+
4. Row-normalize to obtain a Markov transition matrix.
|
|
101
|
+
5. Extract the top ``n_diffusion_components`` eigenvectors of the
|
|
102
|
+
symmetrized transition matrix via subspace iteration (repeated
|
|
103
|
+
matmul + QR), excluding the trivial eigenvalue 1.
|
|
104
|
+
6. Weight eigenvectors by their Rayleigh-quotient eigenvalues to
|
|
105
|
+
form diffusion components.
|
|
106
|
+
7. Pseudotime = L2 distance from root cell in diffusion-component
|
|
107
|
+
space.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config: PseudotimeConfig with operator parameters.
|
|
111
|
+
rngs: Flax NNX random number generators (unused, kept for API).
|
|
112
|
+
name: Optional operator name.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> config = PseudotimeConfig(n_neighbors=15, n_diffusion_components=10)
|
|
116
|
+
>>> op = DifferentiablePseudotime(config)
|
|
117
|
+
>>> data = {"embeddings": jnp.ones((50, 20))}
|
|
118
|
+
>>> result, state, meta = op.apply(data, {}, None)
|
|
119
|
+
>>> result["pseudotime"].shape
|
|
120
|
+
(50,)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
config: PseudotimeConfig,
|
|
126
|
+
*,
|
|
127
|
+
rngs: nnx.Rngs | None = None,
|
|
128
|
+
name: str | None = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Initialize the pseudotime operator.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: Pseudotime configuration.
|
|
134
|
+
rngs: Random number generators (unused, present for API consistency).
|
|
135
|
+
name: Optional operator name.
|
|
136
|
+
"""
|
|
137
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
138
|
+
|
|
139
|
+
def _build_transition_matrix(
|
|
140
|
+
self,
|
|
141
|
+
embeddings: Float[Array, "n_cells n_features"],
|
|
142
|
+
) -> Float[Array, "n_cells n_cells"]:
|
|
143
|
+
"""Build a row-stochastic transition matrix from cell embeddings.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
embeddings: Cell embedding matrix.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Row-stochastic Markov transition matrix.
|
|
150
|
+
"""
|
|
151
|
+
n_cells = embeddings.shape[0]
|
|
152
|
+
|
|
153
|
+
# Pairwise distances
|
|
154
|
+
distances = compute_pairwise_distances(embeddings, metric=self.config.metric)
|
|
155
|
+
|
|
156
|
+
# Mask diagonal with large sentinel
|
|
157
|
+
distances = distances + jnp.eye(n_cells) * DISTANCE_MASK_SENTINEL
|
|
158
|
+
|
|
159
|
+
# Fuzzy membership with local bandwidth
|
|
160
|
+
membership = compute_fuzzy_membership(distances, k=self.config.n_neighbors)
|
|
161
|
+
|
|
162
|
+
# Symmetrize via fuzzy set union
|
|
163
|
+
symmetric = symmetrize_graph(membership)
|
|
164
|
+
|
|
165
|
+
# Row-normalize to Markov transition matrix
|
|
166
|
+
row_sums = jnp.sum(symmetric, axis=1, keepdims=True)
|
|
167
|
+
transition = symmetric / (row_sums + 1e-10)
|
|
168
|
+
|
|
169
|
+
return transition
|
|
170
|
+
|
|
171
|
+
def _compute_diffusion_embedding(
|
|
172
|
+
self,
|
|
173
|
+
transition: Float[Array, "n_cells n_cells"],
|
|
174
|
+
n_components: int,
|
|
175
|
+
root_index: int,
|
|
176
|
+
) -> tuple[
|
|
177
|
+
Float[Array, "n_cells"],
|
|
178
|
+
Float[Array, "n_cells n_comp"],
|
|
179
|
+
]:
|
|
180
|
+
"""Compute pseudotime and diffusion components from Markov powers.
|
|
181
|
+
|
|
182
|
+
Instead of eigendecomposing the transition matrix (whose backward
|
|
183
|
+
pass produces NaN when eigenvalues are near-degenerate — JAX
|
|
184
|
+
issue #669), this method accumulates
|
|
185
|
+
``M_sum = sum_{t=1}^{T} M^t`` via repeated matrix multiplication.
|
|
186
|
+
The rows of ``M_sum`` form a diffusion embedding: the DPT
|
|
187
|
+
distance between cells *i* and *j* equals the L2 distance
|
|
188
|
+
between rows *i* and *j* of ``M_sum`` (Haghverdi et al. 2016).
|
|
189
|
+
|
|
190
|
+
Because the computation uses only matrix multiplication and
|
|
191
|
+
addition, the backward pass is free of the degenerate-eigenvalue
|
|
192
|
+
singularity and produces well-conditioned finite gradients.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
transition: Row-stochastic transition matrix.
|
|
196
|
+
n_components: Number of diffusion components to retain in
|
|
197
|
+
the output embedding.
|
|
198
|
+
root_index: Index of the root cell for pseudotime origin.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Tuple of (pseudotime, diffusion_components) where pseudotime
|
|
202
|
+
has shape ``(n_cells,)`` and diffusion_components has shape
|
|
203
|
+
``(n_cells, n_components)``.
|
|
204
|
+
"""
|
|
205
|
+
# Symmetrize (should be nearly symmetric already)
|
|
206
|
+
sym = (transition + transition.T) / 2.0
|
|
207
|
+
|
|
208
|
+
n_cells = transition.shape[0]
|
|
209
|
+
# Use Python min() so the result is a static int (required for JIT).
|
|
210
|
+
n_comp = min(n_components, n_cells - 1)
|
|
211
|
+
|
|
212
|
+
# Accumulate M_sum = sum_{t=1}^{T} sym^t.
|
|
213
|
+
# This approximates (I - sym)^{-1} - I (the DPT kernel).
|
|
214
|
+
# Enough terms for the geometric series to converge.
|
|
215
|
+
n_powers = max(n_comp * 2, 10)
|
|
216
|
+
m_power = sym
|
|
217
|
+
m_sum = jnp.zeros_like(sym)
|
|
218
|
+
for _ in range(n_powers):
|
|
219
|
+
m_sum = m_sum + m_power
|
|
220
|
+
m_power = m_power @ sym
|
|
221
|
+
|
|
222
|
+
# Pseudotime = L2 distance from root cell in M_sum row space.
|
|
223
|
+
root_row = m_sum[root_index]
|
|
224
|
+
diff = m_sum - root_row[None, :]
|
|
225
|
+
pseudotime = jnp.sqrt(jnp.sum(diff**2, axis=-1) + 1e-10)
|
|
226
|
+
pseudotime = pseudotime - pseudotime[root_index]
|
|
227
|
+
|
|
228
|
+
# Extract n_comp diffusion components for the output embedding.
|
|
229
|
+
# Center rows to remove the trivial (constant) component, then
|
|
230
|
+
# take the first n_comp columns as coordinates.
|
|
231
|
+
row_mean = jnp.mean(m_sum, axis=0, keepdims=True)
|
|
232
|
+
diffusion_components = (m_sum - row_mean)[:, :n_comp]
|
|
233
|
+
|
|
234
|
+
return pseudotime, diffusion_components
|
|
235
|
+
|
|
236
|
+
def apply(
|
|
237
|
+
self,
|
|
238
|
+
data: PyTree,
|
|
239
|
+
state: PyTree,
|
|
240
|
+
metadata: dict[str, Any] | None,
|
|
241
|
+
random_params: Any = None,
|
|
242
|
+
stats: dict[str, Any] | None = None,
|
|
243
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
244
|
+
"""Apply pseudotime computation to cell embeddings.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
data: Dictionary containing:
|
|
248
|
+
- ``"embeddings"``: Cell embeddings ``(n_cells, n_features)``
|
|
249
|
+
state: Element state (passed through unchanged).
|
|
250
|
+
metadata: Element metadata (passed through unchanged).
|
|
251
|
+
random_params: Not used (deterministic operator).
|
|
252
|
+
stats: Not used.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple of (transformed_data, state, metadata):
|
|
256
|
+
- transformed_data contains:
|
|
257
|
+
|
|
258
|
+
- ``"pseudotime"``: Pseudotime values ``(n_cells,)``
|
|
259
|
+
- ``"diffusion_components"``: Diffusion map coordinates
|
|
260
|
+
``(n_cells, n_diffusion_components)``
|
|
261
|
+
- ``"transition_matrix"``: Markov transition matrix
|
|
262
|
+
``(n_cells, n_cells)``
|
|
263
|
+
- All original data keys are preserved
|
|
264
|
+
- state is passed through unchanged
|
|
265
|
+
- metadata is passed through unchanged
|
|
266
|
+
"""
|
|
267
|
+
embeddings = data["embeddings"]
|
|
268
|
+
|
|
269
|
+
# Build transition matrix
|
|
270
|
+
transition = self._build_transition_matrix(embeddings)
|
|
271
|
+
|
|
272
|
+
# Pseudotime and diffusion components from Markov powers
|
|
273
|
+
pseudotime, dc = self._compute_diffusion_embedding(
|
|
274
|
+
transition,
|
|
275
|
+
self.config.n_diffusion_components,
|
|
276
|
+
self.config.root_cell_index,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
transformed_data = {
|
|
280
|
+
**data,
|
|
281
|
+
"pseudotime": pseudotime,
|
|
282
|
+
"diffusion_components": dc,
|
|
283
|
+
"transition_matrix": transition,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return transformed_data, state, metadata
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# DifferentiableFateProbability
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class DifferentiableFateProbability(OperatorModule):
|
|
295
|
+
"""Differentiable fate probability estimation via absorption probabilities.
|
|
296
|
+
|
|
297
|
+
Given a Markov transition matrix and a set of terminal (absorbing) state
|
|
298
|
+
indices, partitions cells into transient and absorbing sets and computes
|
|
299
|
+
the probability that each transient cell will eventually reach each
|
|
300
|
+
absorbing state.
|
|
301
|
+
|
|
302
|
+
Algorithm:
|
|
303
|
+
1. Partition states into transient (T) and absorbing (A).
|
|
304
|
+
2. Extract sub-matrices Q = transition[T, T] and R = transition[T, A].
|
|
305
|
+
3. Solve ``(I - Q) @ B = R`` for B (absorption probabilities).
|
|
306
|
+
4. Assign probability 1 to each absorbing state for itself.
|
|
307
|
+
|
|
308
|
+
The linear solve ``jnp.linalg.solve`` is fully differentiable.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
config: FateProbabilityConfig with operator parameters.
|
|
312
|
+
rngs: Flax NNX random number generators (unused, kept for API).
|
|
313
|
+
name: Optional operator name.
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
>>> config = FateProbabilityConfig(n_macrostates=2)
|
|
317
|
+
>>> op = DifferentiableFateProbability(config)
|
|
318
|
+
>>> data = {"transition_matrix": T, "terminal_states": jnp.array([18, 19])}
|
|
319
|
+
>>> result, state, meta = op.apply(data, {}, None)
|
|
320
|
+
>>> result["fate_probabilities"].shape
|
|
321
|
+
(20, 2)
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
config: FateProbabilityConfig,
|
|
327
|
+
*,
|
|
328
|
+
rngs: nnx.Rngs | None = None,
|
|
329
|
+
name: str | None = None,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Initialize the fate probability operator.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
config: Fate probability configuration.
|
|
335
|
+
rngs: Random number generators (unused, present for API consistency).
|
|
336
|
+
name: Optional operator name.
|
|
337
|
+
"""
|
|
338
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
339
|
+
|
|
340
|
+
def _compute_absorption_probabilities(
|
|
341
|
+
self,
|
|
342
|
+
transition_matrix: Float[Array, "n n"],
|
|
343
|
+
terminal_states: Int[Array, "n_terminal"],
|
|
344
|
+
) -> tuple[Float[Array, "n n_terminal"], Int[Array, "n"]]:
|
|
345
|
+
"""Compute absorption probabilities for all cells.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
transition_matrix: Row-stochastic Markov transition matrix.
|
|
349
|
+
terminal_states: Indices of terminal (absorbing) states.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Tuple of (fate_probabilities, macrostates) where
|
|
353
|
+
fate_probabilities has shape ``(n_cells, n_terminal)`` and
|
|
354
|
+
macrostates has shape ``(n_cells,)`` (argmax assignment).
|
|
355
|
+
"""
|
|
356
|
+
n_cells = transition_matrix.shape[0]
|
|
357
|
+
n_terminal = terminal_states.shape[0]
|
|
358
|
+
|
|
359
|
+
# Build boolean mask for transient states
|
|
360
|
+
is_terminal = jnp.zeros(n_cells, dtype=jnp.bool_)
|
|
361
|
+
is_terminal = is_terminal.at[terminal_states].set(True)
|
|
362
|
+
is_transient = ~is_terminal
|
|
363
|
+
|
|
364
|
+
# Index arrays for transient and absorbing states
|
|
365
|
+
transient_indices = jnp.where(is_transient, size=n_cells - n_terminal)[0]
|
|
366
|
+
|
|
367
|
+
# Extract sub-matrices
|
|
368
|
+
# Q = transition[transient, transient]
|
|
369
|
+
q_matrix = transition_matrix[jnp.ix_(transient_indices, transient_indices)]
|
|
370
|
+
|
|
371
|
+
# R = transition[transient, absorbing]
|
|
372
|
+
r_matrix = transition_matrix[jnp.ix_(transient_indices, terminal_states)]
|
|
373
|
+
|
|
374
|
+
# Solve (I - Q) @ B = R => B = (I - Q)^{-1} @ R
|
|
375
|
+
n_transient = transient_indices.shape[0]
|
|
376
|
+
identity = jnp.eye(n_transient)
|
|
377
|
+
# Adding small regularization for numerical stability
|
|
378
|
+
lhs = identity - q_matrix + jnp.eye(n_transient) * 1e-8
|
|
379
|
+
absorption = jnp.linalg.solve(lhs, r_matrix)
|
|
380
|
+
|
|
381
|
+
# Clamp to valid probability range
|
|
382
|
+
absorption = jnp.clip(absorption, 0.0, 1.0)
|
|
383
|
+
|
|
384
|
+
# Normalize rows to sum to 1
|
|
385
|
+
row_sums = jnp.sum(absorption, axis=1, keepdims=True)
|
|
386
|
+
absorption = absorption / (row_sums + 1e-10)
|
|
387
|
+
|
|
388
|
+
# Build full fate probability matrix
|
|
389
|
+
fate = jnp.zeros((n_cells, n_terminal))
|
|
390
|
+
|
|
391
|
+
# Set transient-cell probabilities
|
|
392
|
+
fate = fate.at[transient_indices].set(absorption)
|
|
393
|
+
|
|
394
|
+
# Set absorbing-cell probabilities: 1 for self, 0 for others
|
|
395
|
+
fate = fate.at[terminal_states, jnp.arange(n_terminal)].set(1.0)
|
|
396
|
+
|
|
397
|
+
# Macrostate assignment = argmax of fate probabilities
|
|
398
|
+
macrostates = jnp.argmax(fate, axis=1)
|
|
399
|
+
|
|
400
|
+
return fate, macrostates
|
|
401
|
+
|
|
402
|
+
def apply(
|
|
403
|
+
self,
|
|
404
|
+
data: PyTree,
|
|
405
|
+
state: PyTree,
|
|
406
|
+
metadata: dict[str, Any] | None,
|
|
407
|
+
random_params: Any = None,
|
|
408
|
+
stats: dict[str, Any] | None = None,
|
|
409
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
410
|
+
"""Apply fate probability estimation.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
data: Dictionary containing:
|
|
414
|
+
- ``"transition_matrix"``: Markov transition matrix
|
|
415
|
+
``(n_cells, n_cells)``
|
|
416
|
+
- ``"terminal_states"``: Indices of terminal states
|
|
417
|
+
``(n_terminal,)``
|
|
418
|
+
state: Element state (passed through unchanged).
|
|
419
|
+
metadata: Element metadata (passed through unchanged).
|
|
420
|
+
random_params: Not used (deterministic operator).
|
|
421
|
+
stats: Not used.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Tuple of (transformed_data, state, metadata):
|
|
425
|
+
- transformed_data contains:
|
|
426
|
+
|
|
427
|
+
- ``"fate_probabilities"``: Absorption probabilities
|
|
428
|
+
``(n_cells, n_terminal)``
|
|
429
|
+
- ``"macrostates"``: Argmax fate assignment ``(n_cells,)``
|
|
430
|
+
- All original data keys are preserved
|
|
431
|
+
- state is passed through unchanged
|
|
432
|
+
- metadata is passed through unchanged
|
|
433
|
+
"""
|
|
434
|
+
transition_matrix = data["transition_matrix"]
|
|
435
|
+
terminal_states = data["terminal_states"]
|
|
436
|
+
|
|
437
|
+
fate, macrostates = self._compute_absorption_probabilities(
|
|
438
|
+
transition_matrix, terminal_states
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
transformed_data = {
|
|
442
|
+
**data,
|
|
443
|
+
"fate_probabilities": fate,
|
|
444
|
+
"macrostates": macrostates,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return transformed_data, state, metadata
|