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,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