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,429 @@
|
|
|
1
|
+
"""Differentiable imputation operators for single-cell data.
|
|
2
|
+
|
|
3
|
+
This module provides two complementary imputation strategies:
|
|
4
|
+
|
|
5
|
+
1. **DifferentiableDiffusionImputer**: MAGIC-style diffusion imputation that
|
|
6
|
+
constructs a cell-cell affinity graph using an alpha-decaying kernel,
|
|
7
|
+
builds a row-stochastic Markov matrix ``M = D^{-1} A``, and computes
|
|
8
|
+
``M^t`` via repeated matrix multiplication for diffusion-based imputation.
|
|
9
|
+
|
|
10
|
+
2. **DifferentiableTransformerDenoiser**: Transformer-based gene denoiser that
|
|
11
|
+
treats genes as tokens, randomly masks a fraction of them, and predicts
|
|
12
|
+
the masked gene expression values from the unmasked context using a
|
|
13
|
+
transformer encoder. Reuses ``TransformerSequenceEncoder`` from the
|
|
14
|
+
foundation models module (DRY).
|
|
15
|
+
|
|
16
|
+
Applications: Denoising dropout events in scRNA-seq count matrices, recovering
|
|
17
|
+
gene-gene relationships masked by technical noise.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from datarax.core.config import OperatorConfig
|
|
27
|
+
from datarax.core.operator import OperatorModule
|
|
28
|
+
from flax import nnx
|
|
29
|
+
from jaxtyping import Array, Float, PyTree
|
|
30
|
+
|
|
31
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL
|
|
32
|
+
from diffbio.core import soft_ops
|
|
33
|
+
from diffbio.core.graph_utils import (
|
|
34
|
+
compute_pairwise_distances,
|
|
35
|
+
symmetrize_graph,
|
|
36
|
+
)
|
|
37
|
+
from diffbio.operators._masked_gene_transformer import (
|
|
38
|
+
MaskedGeneTransformerConfigBase,
|
|
39
|
+
MaskedGeneTransformerOperatorMixin,
|
|
40
|
+
build_masked_gene_transformer_encoder,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class DiffusionImputerConfig(OperatorConfig):
|
|
48
|
+
"""Configuration for MAGIC-style diffusion imputation.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
n_neighbors: Number of neighbors for local bandwidth estimation.
|
|
52
|
+
diffusion_t: Number of diffusion time steps (matrix power).
|
|
53
|
+
n_pca_components: Number of PCA components (reserved for future use).
|
|
54
|
+
decay: Exponent for the alpha-decaying kernel (MAGIC default is 1).
|
|
55
|
+
metric: Distance metric, either ``"euclidean"`` or ``"cosine"``.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
n_neighbors: int = 5
|
|
59
|
+
diffusion_t: int = 3
|
|
60
|
+
n_pca_components: int = 100
|
|
61
|
+
decay: float = 1.0
|
|
62
|
+
metric: str = "euclidean"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DifferentiableDiffusionImputer(OperatorModule):
|
|
66
|
+
"""Differentiable MAGIC-style diffusion imputation.
|
|
67
|
+
|
|
68
|
+
Constructs a cell-cell affinity graph using an alpha-decaying kernel,
|
|
69
|
+
symmetrizes it, builds a row-stochastic Markov matrix ``M = D^{-1} A``,
|
|
70
|
+
and computes ``M^t`` via repeated matrix multiplication for imputation.
|
|
71
|
+
This avoids eigendecomposition (whose backward pass produces NaN when
|
|
72
|
+
eigenvalues are near-degenerate) while remaining fully differentiable.
|
|
73
|
+
|
|
74
|
+
Algorithm:
|
|
75
|
+
1. Compute pairwise distances between cells
|
|
76
|
+
2. Build alpha-decay affinity: ``K(i,j) = exp(-(d/sigma_i)^decay)``
|
|
77
|
+
3. Symmetrize the affinity via fuzzy set union
|
|
78
|
+
4. Row-normalize to Markov matrix ``M = D^{-1} A``
|
|
79
|
+
5. Compute ``M^t`` via repeated matrix multiplication (t iterations)
|
|
80
|
+
6. Impute: ``imputed = M^t @ counts``
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: DiffusionImputerConfig with operator parameters.
|
|
84
|
+
rngs: Flax NNX random number generators (not used, kept for API).
|
|
85
|
+
name: Optional operator name.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> config = DiffusionImputerConfig(n_neighbors=5, diffusion_t=3)
|
|
89
|
+
>>> imputer = DifferentiableDiffusionImputer(config, rngs=nnx.Rngs(0))
|
|
90
|
+
>>> data = {"counts": jnp.ones((100, 2000))}
|
|
91
|
+
>>> result, state, meta = imputer.apply(data, {}, None)
|
|
92
|
+
>>> result["imputed_counts"].shape
|
|
93
|
+
(100, 2000)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
config: DiffusionImputerConfig,
|
|
99
|
+
*,
|
|
100
|
+
rngs: nnx.Rngs | None = None,
|
|
101
|
+
name: str | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize the diffusion imputer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Imputation configuration.
|
|
107
|
+
rngs: Random number generators (unused, present for API consistency).
|
|
108
|
+
name: Optional operator name.
|
|
109
|
+
"""
|
|
110
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
111
|
+
|
|
112
|
+
def _build_alpha_decay_affinity(
|
|
113
|
+
self,
|
|
114
|
+
distances: Float[Array, "n n"],
|
|
115
|
+
k: int,
|
|
116
|
+
decay: float,
|
|
117
|
+
) -> Float[Array, "n n"]:
|
|
118
|
+
"""Build alpha-decaying kernel following MAGIC.
|
|
119
|
+
|
|
120
|
+
``K(i,j) = exp(-(d(i,j) / sigma_i)^decay)`` where sigma_i is the
|
|
121
|
+
distance to the k-th nearest neighbor.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
distances: Pairwise distance matrix with diagonal masked to DISTANCE_MASK_SENTINEL.
|
|
125
|
+
k: Number of neighbors for local bandwidth estimation.
|
|
126
|
+
decay: Exponent for the alpha-decaying kernel.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Affinity matrix of shape ``(n, n)`` with zero diagonal.
|
|
130
|
+
"""
|
|
131
|
+
n = distances.shape[0]
|
|
132
|
+
k_eff = min(k, n - 2)
|
|
133
|
+
|
|
134
|
+
# Local bandwidth: k-th nearest neighbor distance
|
|
135
|
+
sorted_dists = soft_ops.sort(distances, axis=-1, softness=0.1)
|
|
136
|
+
sigma = jnp.maximum(sorted_dists[:, k_eff], 1e-8)
|
|
137
|
+
|
|
138
|
+
# Alpha-decay kernel
|
|
139
|
+
affinity = jnp.exp(-((distances / sigma[:, None]) ** decay))
|
|
140
|
+
|
|
141
|
+
# Zero diagonal
|
|
142
|
+
affinity = affinity * (1.0 - jnp.eye(n))
|
|
143
|
+
|
|
144
|
+
return affinity
|
|
145
|
+
|
|
146
|
+
def _build_symmetric_affinity(
|
|
147
|
+
self,
|
|
148
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
149
|
+
) -> Float[Array, "n_cells n_cells"]:
|
|
150
|
+
"""Build the symmetric affinity matrix from counts.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
counts: Gene expression matrix of shape ``(n_cells, n_genes)``.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Symmetric affinity matrix of shape ``(n_cells, n_cells)``.
|
|
157
|
+
"""
|
|
158
|
+
n_cells = counts.shape[0]
|
|
159
|
+
|
|
160
|
+
# Pairwise distances
|
|
161
|
+
distances = compute_pairwise_distances(counts, metric=self.config.metric)
|
|
162
|
+
|
|
163
|
+
# Mask diagonal
|
|
164
|
+
distances = distances + jnp.eye(n_cells) * DISTANCE_MASK_SENTINEL
|
|
165
|
+
|
|
166
|
+
# Alpha-decay kernel
|
|
167
|
+
affinity = self._build_alpha_decay_affinity(
|
|
168
|
+
distances, self.config.n_neighbors, self.config.decay
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Symmetrize via fuzzy set union
|
|
172
|
+
return symmetrize_graph(affinity)
|
|
173
|
+
|
|
174
|
+
def _diffuse(
|
|
175
|
+
self,
|
|
176
|
+
affinity_sym: Float[Array, "n_cells n_cells"],
|
|
177
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
178
|
+
t: int,
|
|
179
|
+
) -> tuple[Float[Array, "n_cells n_genes"], Float[Array, "n_cells n_cells"]]:
|
|
180
|
+
"""Compute M^t via repeated matrix multiplication of the Markov matrix.
|
|
181
|
+
|
|
182
|
+
The Markov matrix is ``M = D^{-1} A`` where ``A`` is the symmetric
|
|
183
|
+
affinity and ``D`` is the diagonal degree matrix. We compute ``M^t``
|
|
184
|
+
by repeatedly multiplying ``M`` by itself ``t`` times. This avoids
|
|
185
|
+
eigendecomposition (whose backward pass produces NaN gradients when
|
|
186
|
+
eigenvalues are near-degenerate) while remaining fully differentiable.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
affinity_sym: Symmetric affinity matrix.
|
|
190
|
+
counts: Original gene expression counts.
|
|
191
|
+
t: Diffusion time (exponent).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Tuple of (imputed counts, diffusion operator M^t).
|
|
195
|
+
"""
|
|
196
|
+
n_cells = affinity_sym.shape[0]
|
|
197
|
+
|
|
198
|
+
if t == 0:
|
|
199
|
+
identity = jnp.eye(n_cells)
|
|
200
|
+
return counts, identity
|
|
201
|
+
|
|
202
|
+
# Build row-stochastic Markov matrix M = D^{-1} A
|
|
203
|
+
degree = jnp.sum(affinity_sym, axis=1, keepdims=True)
|
|
204
|
+
markov = affinity_sym / jnp.maximum(degree, 1e-10)
|
|
205
|
+
|
|
206
|
+
# Compute M^t via repeated matrix multiplication
|
|
207
|
+
diffusion_op = markov
|
|
208
|
+
for _ in range(t - 1):
|
|
209
|
+
diffusion_op = diffusion_op @ markov
|
|
210
|
+
|
|
211
|
+
# Ensure row-stochasticity after powering (numerical correction)
|
|
212
|
+
row_sums = jnp.sum(diffusion_op, axis=1, keepdims=True)
|
|
213
|
+
diffusion_op = diffusion_op / jnp.maximum(row_sums, 1e-10)
|
|
214
|
+
|
|
215
|
+
# Impute
|
|
216
|
+
imputed = diffusion_op @ counts
|
|
217
|
+
|
|
218
|
+
return imputed, diffusion_op
|
|
219
|
+
|
|
220
|
+
def apply(
|
|
221
|
+
self,
|
|
222
|
+
data: PyTree,
|
|
223
|
+
state: PyTree,
|
|
224
|
+
metadata: dict[str, Any] | None,
|
|
225
|
+
random_params: Any = None,
|
|
226
|
+
stats: dict[str, Any] | None = None,
|
|
227
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
228
|
+
"""Apply diffusion imputation to single-cell count data.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
data: Dictionary containing:
|
|
232
|
+
- ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
233
|
+
state: Element state (passed through unchanged).
|
|
234
|
+
metadata: Element metadata (passed through unchanged).
|
|
235
|
+
random_params: Not used (deterministic operator).
|
|
236
|
+
stats: Not used.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Tuple of (transformed_data, state, metadata):
|
|
240
|
+
- transformed_data contains:
|
|
241
|
+
|
|
242
|
+
- ``"counts"``: Original counts
|
|
243
|
+
- ``"imputed_counts"``: Diffusion-imputed counts
|
|
244
|
+
- ``"diffusion_operator"``: The M^t matrix
|
|
245
|
+
- state is passed through unchanged
|
|
246
|
+
- metadata is passed through unchanged
|
|
247
|
+
"""
|
|
248
|
+
counts = data["counts"]
|
|
249
|
+
|
|
250
|
+
# Build symmetric affinity matrix
|
|
251
|
+
affinity_sym = self._build_symmetric_affinity(counts)
|
|
252
|
+
|
|
253
|
+
# Diffuse via repeated matrix multiplication of Markov matrix
|
|
254
|
+
imputed, diffusion_op = self._diffuse(affinity_sym, counts, self.config.diffusion_t)
|
|
255
|
+
|
|
256
|
+
transformed_data = {
|
|
257
|
+
**data,
|
|
258
|
+
"imputed_counts": imputed,
|
|
259
|
+
"diffusion_operator": diffusion_op,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return transformed_data, state, metadata
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(frozen=True)
|
|
266
|
+
class TransformerDenoiserConfig(MaskedGeneTransformerConfigBase):
|
|
267
|
+
"""Configuration for transformer-based gene denoising.
|
|
268
|
+
|
|
269
|
+
The denoiser treats genes as tokens: each gene has an expression value and
|
|
270
|
+
a gene ID. A random fraction of genes is masked (expression zeroed) and the
|
|
271
|
+
transformer predicts the original expression from the unmasked context.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class DifferentiableTransformerDenoiser(
|
|
276
|
+
MaskedGeneTransformerOperatorMixin,
|
|
277
|
+
OperatorModule,
|
|
278
|
+
):
|
|
279
|
+
"""Transformer-based gene denoiser for single-cell expression data.
|
|
280
|
+
|
|
281
|
+
Genes are treated as tokens in a sequence. For each cell the operator:
|
|
282
|
+
|
|
283
|
+
1. Randomly masks ``mask_ratio`` fraction of genes (sets expression to 0).
|
|
284
|
+
2. Projects gene IDs into embeddings via ``TransformerSequenceEncoder``
|
|
285
|
+
(token_embedding mode) and adds a learned projection of the expression
|
|
286
|
+
value so the transformer receives both identity and magnitude.
|
|
287
|
+
3. Passes the sequence through a transformer encoder to obtain
|
|
288
|
+
contextualised gene representations.
|
|
289
|
+
4. Predicts masked gene expression from context via a linear output head.
|
|
290
|
+
5. Returns imputed counts where masked positions are replaced with
|
|
291
|
+
predictions and unmasked positions are kept from the original input.
|
|
292
|
+
|
|
293
|
+
Each cell is processed independently via ``jax.vmap`` over the cell
|
|
294
|
+
dimension.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
config: TransformerDenoiserConfig with operator parameters.
|
|
298
|
+
rngs: Flax NNX random number generators.
|
|
299
|
+
name: Optional operator name.
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
>>> config = TransformerDenoiserConfig(n_genes=2000, hidden_dim=128)
|
|
303
|
+
>>> denoiser = DifferentiableTransformerDenoiser(
|
|
304
|
+
... config, rngs=nnx.Rngs(params=0, sample=1, dropout=2))
|
|
305
|
+
>>> rp = denoiser.generate_random_params(
|
|
306
|
+
... jax.random.key(0), {"counts": (100, 2000)})
|
|
307
|
+
>>> data = {"counts": counts, "gene_ids": jnp.arange(2000)}
|
|
308
|
+
>>> result, state, meta = denoiser.apply(data, {}, None, random_params=rp)
|
|
309
|
+
>>> result["imputed_counts"].shape
|
|
310
|
+
(100, 2000)
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
config: TransformerDenoiserConfig,
|
|
316
|
+
*,
|
|
317
|
+
rngs: nnx.Rngs | None = None,
|
|
318
|
+
name: str | None = None,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Initialize the transformer denoiser.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
config: Denoiser configuration.
|
|
324
|
+
rngs: Random number generators for parameter initialisation.
|
|
325
|
+
name: Optional operator name.
|
|
326
|
+
"""
|
|
327
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
328
|
+
|
|
329
|
+
if rngs is None:
|
|
330
|
+
rngs = nnx.Rngs(params=0, sample=1, dropout=2)
|
|
331
|
+
|
|
332
|
+
# Reuse the shared masked-gene token encoder contract.
|
|
333
|
+
self.encoder = build_masked_gene_transformer_encoder(config, rngs=rngs)
|
|
334
|
+
|
|
335
|
+
# Learned projection from scalar expression value to hidden_dim
|
|
336
|
+
self.expression_projection = nnx.Linear(1, config.hidden_dim, rngs=rngs)
|
|
337
|
+
|
|
338
|
+
# Output head: hidden_dim -> 1 (predict scalar expression per gene)
|
|
339
|
+
self.output_head = nnx.Linear(config.hidden_dim, 1, rngs=rngs)
|
|
340
|
+
|
|
341
|
+
def _impute_single_cell(
|
|
342
|
+
self,
|
|
343
|
+
expression: Float[Array, "n_genes"],
|
|
344
|
+
gene_ids: Float[Array, "n_genes"],
|
|
345
|
+
mask: Float[Array, "n_genes"],
|
|
346
|
+
) -> Float[Array, "n_genes"]:
|
|
347
|
+
"""Impute expression for a single cell.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
expression: Gene expression values for one cell.
|
|
351
|
+
gene_ids: Integer gene IDs.
|
|
352
|
+
mask: Binary mask (1 = masked / to-predict, 0 = observed).
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Imputed expression values for all genes.
|
|
356
|
+
"""
|
|
357
|
+
# Zero out masked gene expression (the transformer must predict these)
|
|
358
|
+
masked_expression = expression * (1.0 - mask)
|
|
359
|
+
|
|
360
|
+
# Embed gene IDs via the encoder's input projection (nnx.Embed)
|
|
361
|
+
gene_embeddings = self.encoder.input_projection(gene_ids)
|
|
362
|
+
|
|
363
|
+
# Add expression information: project scalar expression to hidden_dim
|
|
364
|
+
expr_projected = self.expression_projection(
|
|
365
|
+
masked_expression[:, None]
|
|
366
|
+
) # (n_genes, hidden_dim)
|
|
367
|
+
hidden = gene_embeddings + expr_projected # (n_genes, hidden_dim)
|
|
368
|
+
|
|
369
|
+
# Add batch dimension for transformer (expects [batch, seq, hidden])
|
|
370
|
+
hidden = hidden[None, :, :] # (1, n_genes, hidden_dim)
|
|
371
|
+
|
|
372
|
+
# Apply transformer encoder
|
|
373
|
+
hidden = self.encoder.transformer(hidden, mask=None, deterministic=True)
|
|
374
|
+
|
|
375
|
+
# Remove batch dimension
|
|
376
|
+
hidden = hidden[0] # (n_genes, hidden_dim)
|
|
377
|
+
|
|
378
|
+
# Predict expression from contextualised representations
|
|
379
|
+
predictions = self.output_head(hidden).squeeze(-1) # (n_genes,)
|
|
380
|
+
|
|
381
|
+
# Replace masked positions with predictions, keep originals for unmasked
|
|
382
|
+
imputed = jnp.where(mask > 0.5, predictions, expression)
|
|
383
|
+
|
|
384
|
+
return imputed
|
|
385
|
+
|
|
386
|
+
def apply(
|
|
387
|
+
self,
|
|
388
|
+
data: PyTree,
|
|
389
|
+
state: PyTree,
|
|
390
|
+
metadata: dict[str, Any] | None,
|
|
391
|
+
random_params: Any = None,
|
|
392
|
+
stats: dict[str, Any] | None = None,
|
|
393
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
394
|
+
"""Apply transformer denoising to single-cell count data.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
data: Dictionary containing:
|
|
398
|
+
- ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
399
|
+
- ``"gene_ids"``: Integer gene IDs ``(n_genes,)``
|
|
400
|
+
state: Element state (passed through unchanged).
|
|
401
|
+
metadata: Element metadata (passed through unchanged).
|
|
402
|
+
random_params: JAX random key for mask generation.
|
|
403
|
+
stats: Not used.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Tuple of (transformed_data, state, metadata):
|
|
407
|
+
- transformed_data contains:
|
|
408
|
+
|
|
409
|
+
- All original keys from data
|
|
410
|
+
- ``"imputed_counts"``: Denoised expression ``(n_cells, n_genes)``
|
|
411
|
+
- ``"mask"``: Binary mask used ``(n_genes,)``
|
|
412
|
+
- state is passed through unchanged
|
|
413
|
+
- metadata is passed through unchanged
|
|
414
|
+
"""
|
|
415
|
+
counts, gene_ids_int, mask = self.prepare_masked_gene_batch(data, random_params)
|
|
416
|
+
|
|
417
|
+
# Process each cell independently via vmap
|
|
418
|
+
imputed = jax.vmap(
|
|
419
|
+
self._impute_single_cell,
|
|
420
|
+
in_axes=(0, None, None),
|
|
421
|
+
)(counts, gene_ids_int, mask)
|
|
422
|
+
|
|
423
|
+
transformed_data = {
|
|
424
|
+
**data,
|
|
425
|
+
"imputed_counts": imputed,
|
|
426
|
+
"mask": mask,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Three-stage on-target knockdown quality filter.
|
|
2
|
+
|
|
3
|
+
Ports cell-load's three-stage filtering for perturbation experiments:
|
|
4
|
+
1. Perturbation-level: filter perturbations by average knockdown.
|
|
5
|
+
2. Cell-level: filter individual cells by residual expression.
|
|
6
|
+
3. Minimum count: remove perturbations with too few remaining cells.
|
|
7
|
+
|
|
8
|
+
Controls are always preserved.
|
|
9
|
+
|
|
10
|
+
References:
|
|
11
|
+
- cell-load/src/cell_load/utils/data_utils.py (filter_on_target_knockdown)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from datarax.core.config import StructuralConfig
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_on_target_knockdown(
|
|
28
|
+
gene_expression: np.ndarray,
|
|
29
|
+
pert_mask: np.ndarray,
|
|
30
|
+
ctrl_mask: np.ndarray,
|
|
31
|
+
residual_expression: float = 0.30,
|
|
32
|
+
) -> bool:
|
|
33
|
+
"""Check if a perturbation shows on-target knockdown for a single gene.
|
|
34
|
+
|
|
35
|
+
Returns True if the average expression in perturbed cells is below
|
|
36
|
+
``residual_expression`` times the control mean.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
gene_expression: 1D array of expression values for the target gene.
|
|
40
|
+
pert_mask: Boolean mask for perturbed cells.
|
|
41
|
+
ctrl_mask: Boolean mask for control cells.
|
|
42
|
+
residual_expression: Maximum allowed ratio of perturbed to control mean.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if knockdown is on-target (expression sufficiently reduced).
|
|
46
|
+
"""
|
|
47
|
+
ctrl_mean = float(gene_expression[ctrl_mask].mean())
|
|
48
|
+
if np.isclose(ctrl_mean, 0.0):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
pert_mean = float(gene_expression[pert_mask].mean())
|
|
52
|
+
return (pert_mean / ctrl_mean) < residual_expression
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class KnockdownFilterConfig(StructuralConfig):
|
|
57
|
+
"""Configuration for OnTargetKnockdownFilter.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
pert_col: Obs column name for perturbation identity.
|
|
61
|
+
control_pert: Label identifying control cells.
|
|
62
|
+
residual_expression: Stage 1 threshold (perturbation-level).
|
|
63
|
+
cell_residual_expression: Stage 2 threshold (per-cell).
|
|
64
|
+
min_cells: Stage 3 minimum cells per perturbation.
|
|
65
|
+
var_gene_col: Column in var for gene names. If None, uses var index.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
pert_col: str = "perturbation"
|
|
69
|
+
control_pert: str = "non-targeting"
|
|
70
|
+
residual_expression: float = 0.30
|
|
71
|
+
cell_residual_expression: float = 0.50
|
|
72
|
+
min_cells: int = 30
|
|
73
|
+
var_gene_col: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OnTargetKnockdownFilter:
|
|
77
|
+
"""Three-stage quality control filter for perturbation experiments.
|
|
78
|
+
|
|
79
|
+
**Stage 1** (perturbation-level): Filters perturbations where average
|
|
80
|
+
target gene expression in perturbed cells is >= ``residual_expression``
|
|
81
|
+
times the control mean.
|
|
82
|
+
|
|
83
|
+
**Stage 2** (cell-level): Filters individual cells where target gene
|
|
84
|
+
expression remains too high relative to the control mean.
|
|
85
|
+
|
|
86
|
+
**Stage 3** (count threshold): Removes perturbations with fewer than
|
|
87
|
+
``min_cells`` remaining after stages 1 and 2.
|
|
88
|
+
|
|
89
|
+
Controls are always preserved in all stages.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config: Filter configuration.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, config: KnockdownFilterConfig) -> None:
|
|
96
|
+
self._config = config
|
|
97
|
+
|
|
98
|
+
def process(self, source: Any) -> np.ndarray:
|
|
99
|
+
"""Apply three-stage filtering and return a boolean cell mask.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
source: A PerturbationAnnDataSource with ``load()``,
|
|
103
|
+
``get_control_mask()``, and perturbation metadata.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Boolean array of shape ``(n_cells,)`` — True for cells passing
|
|
107
|
+
all three filter stages.
|
|
108
|
+
"""
|
|
109
|
+
data = source.load()
|
|
110
|
+
counts = np.asarray(data["counts"])
|
|
111
|
+
obs = data["obs"]
|
|
112
|
+
var = data["var"]
|
|
113
|
+
|
|
114
|
+
pert_labels = np.asarray(obs[self._config.pert_col])
|
|
115
|
+
ctrl_mask = source.get_control_mask()
|
|
116
|
+
n_cells = len(pert_labels)
|
|
117
|
+
|
|
118
|
+
# Build gene name -> index mapping
|
|
119
|
+
if self._config.var_gene_col is not None and self._config.var_gene_col in var:
|
|
120
|
+
gene_names = np.asarray(var[self._config.var_gene_col])
|
|
121
|
+
else:
|
|
122
|
+
gene_names = np.asarray(list(var.keys()))
|
|
123
|
+
|
|
124
|
+
gene_to_idx: dict[str, int] = {}
|
|
125
|
+
for i, name in enumerate(gene_names):
|
|
126
|
+
name_str = str(name)
|
|
127
|
+
if name_str not in gene_to_idx:
|
|
128
|
+
gene_to_idx[name_str] = i
|
|
129
|
+
|
|
130
|
+
unique_perts = set(pert_labels) - {self._config.control_pert}
|
|
131
|
+
|
|
132
|
+
# --- Stage 1: Perturbation-level filter ---
|
|
133
|
+
perts_passing_stage1: set[str] = set()
|
|
134
|
+
for pert in unique_perts:
|
|
135
|
+
if pert not in gene_to_idx:
|
|
136
|
+
continue
|
|
137
|
+
gene_idx = gene_to_idx[pert]
|
|
138
|
+
gene_expr = counts[:, gene_idx]
|
|
139
|
+
pert_mask = pert_labels == pert
|
|
140
|
+
|
|
141
|
+
if is_on_target_knockdown(
|
|
142
|
+
gene_expr, pert_mask, ctrl_mask, self._config.residual_expression
|
|
143
|
+
):
|
|
144
|
+
perts_passing_stage1.add(pert)
|
|
145
|
+
|
|
146
|
+
# --- Stage 2: Cell-level filter ---
|
|
147
|
+
keep_mask = np.zeros(n_cells, dtype=bool)
|
|
148
|
+
keep_mask[ctrl_mask] = True # Always keep controls
|
|
149
|
+
|
|
150
|
+
ctrl_mean_cache: dict[str, float] = {}
|
|
151
|
+
|
|
152
|
+
for pert in perts_passing_stage1:
|
|
153
|
+
if pert not in gene_to_idx:
|
|
154
|
+
continue
|
|
155
|
+
gene_idx = gene_to_idx[pert]
|
|
156
|
+
|
|
157
|
+
if pert not in ctrl_mean_cache:
|
|
158
|
+
ctrl_mean_cache[pert] = float(counts[ctrl_mask, gene_idx].mean())
|
|
159
|
+
ctrl_mean = ctrl_mean_cache[pert]
|
|
160
|
+
|
|
161
|
+
if np.isclose(ctrl_mean, 0.0):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
pert_cell_mask = pert_labels == pert
|
|
165
|
+
ratios = counts[pert_cell_mask, gene_idx] / ctrl_mean
|
|
166
|
+
passing = ratios < self._config.cell_residual_expression
|
|
167
|
+
pert_indices = np.where(pert_cell_mask)[0]
|
|
168
|
+
keep_mask[pert_indices[passing]] = True
|
|
169
|
+
|
|
170
|
+
# --- Stage 3: Minimum cell filter ---
|
|
171
|
+
for pert in unique_perts:
|
|
172
|
+
pert_cell_mask = (pert_labels == pert) & keep_mask
|
|
173
|
+
if pert_cell_mask.sum() < self._config.min_cells:
|
|
174
|
+
keep_mask[pert_cell_mask] = False
|
|
175
|
+
|
|
176
|
+
return keep_mask
|