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,407 @@
|
|
|
1
|
+
"""End-to-end perturbation experiment data pipeline.
|
|
2
|
+
|
|
3
|
+
Orchestrates the full data setup workflow for single-cell perturbation
|
|
4
|
+
experiments: loading, QC filtering, train/val/test splitting, batch sampling,
|
|
5
|
+
and control cell mapping.
|
|
6
|
+
|
|
7
|
+
This pipeline produces ready-to-train data sources with paired
|
|
8
|
+
(perturbed, control) cell elements. It is a structural (non-differentiable)
|
|
9
|
+
setup pipeline — the differentiable training loop operates on its outputs.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- cell-load PerturbationDataModule
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from flax import nnx
|
|
24
|
+
|
|
25
|
+
from datarax.core.config import StructuralConfig
|
|
26
|
+
from datarax.core.data_source import DataSourceModule
|
|
27
|
+
|
|
28
|
+
from diffbio.operators.singlecell.knockdown_filter import (
|
|
29
|
+
KnockdownFilterConfig,
|
|
30
|
+
OnTargetKnockdownFilter,
|
|
31
|
+
)
|
|
32
|
+
from diffbio.samplers.perturbation_sampler import (
|
|
33
|
+
PerturbationBatchSampler,
|
|
34
|
+
PerturbationSamplerConfig,
|
|
35
|
+
)
|
|
36
|
+
from diffbio.sources.perturbation.concat_source import PerturbationConcatSource
|
|
37
|
+
from diffbio.sources.perturbation.control_mapping import (
|
|
38
|
+
BatchControlMapping,
|
|
39
|
+
ControlMappingConfig,
|
|
40
|
+
RandomControlMapping,
|
|
41
|
+
)
|
|
42
|
+
from diffbio.sources.perturbation.perturbation_source import (
|
|
43
|
+
PerturbationAnnDataSource,
|
|
44
|
+
PerturbationSourceConfig,
|
|
45
|
+
)
|
|
46
|
+
from diffbio.splitters.perturbation import (
|
|
47
|
+
FewShotSplitter,
|
|
48
|
+
FewShotSplitterConfig,
|
|
49
|
+
ZeroShotSplitter,
|
|
50
|
+
ZeroShotSplitterConfig,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class _PipelineSourceConfig:
|
|
58
|
+
"""Perturbation source selection and output-space configuration."""
|
|
59
|
+
|
|
60
|
+
pert_col: str = "perturbation"
|
|
61
|
+
cell_type_col: str = "cell_type"
|
|
62
|
+
batch_col: str = "batch"
|
|
63
|
+
control_pert: str = "non-targeting"
|
|
64
|
+
output_space: str = "all"
|
|
65
|
+
embedding_key: str | None = None
|
|
66
|
+
hvg_col: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class _PipelineBatchingAndSplitConfig:
|
|
71
|
+
"""Control mapping, batching, and split selection configuration."""
|
|
72
|
+
|
|
73
|
+
mapping_strategy: str = "random"
|
|
74
|
+
n_basal_samples: int = 1
|
|
75
|
+
sentence_size: int = 512
|
|
76
|
+
sentences_per_batch: int = 1
|
|
77
|
+
split_mode: str = "random"
|
|
78
|
+
held_out_cell_types: tuple[str, ...] = ()
|
|
79
|
+
held_out_perturbations: tuple[str, ...] = ()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class _PipelineFilterConfig:
|
|
84
|
+
"""Knockdown-filter configuration."""
|
|
85
|
+
|
|
86
|
+
enable_knockdown_filter: bool = False
|
|
87
|
+
residual_expression: float = 0.30
|
|
88
|
+
cell_residual_expression: float = 0.50
|
|
89
|
+
min_cells: int = 30
|
|
90
|
+
var_gene_col: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class _PipelineFractionConfig:
|
|
95
|
+
"""Split fraction and seed configuration."""
|
|
96
|
+
|
|
97
|
+
train_frac: float = 0.8
|
|
98
|
+
valid_frac: float = 0.1
|
|
99
|
+
seed: int = 42
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class PerturbationPipelineConfig(
|
|
104
|
+
_PipelineSourceConfig,
|
|
105
|
+
_PipelineBatchingAndSplitConfig,
|
|
106
|
+
_PipelineFilterConfig,
|
|
107
|
+
_PipelineFractionConfig,
|
|
108
|
+
StructuralConfig,
|
|
109
|
+
):
|
|
110
|
+
"""Configuration for PerturbationPipeline."""
|
|
111
|
+
|
|
112
|
+
def __post_init__(self) -> None:
|
|
113
|
+
"""Validate pipeline configuration."""
|
|
114
|
+
super().__post_init__()
|
|
115
|
+
|
|
116
|
+
if self.output_space == "embedding" and self.embedding_key is None:
|
|
117
|
+
raise ValueError("embedding_key is required when output_space='embedding'")
|
|
118
|
+
|
|
119
|
+
if self.mapping_strategy not in {"batch", "random"}:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"mapping_strategy must be either 'batch' or 'random', "
|
|
122
|
+
f"got {self.mapping_strategy!r}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if self.split_mode not in {"random", "zeroshot", "fewshot"}:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"split_mode must be one of 'random', 'zeroshot', or 'fewshot', "
|
|
128
|
+
f"got {self.split_mode!r}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if self.n_basal_samples <= 0:
|
|
132
|
+
raise ValueError("n_basal_samples must be positive")
|
|
133
|
+
if self.sentence_size <= 0:
|
|
134
|
+
raise ValueError("sentence_size must be positive")
|
|
135
|
+
if self.sentences_per_batch <= 0:
|
|
136
|
+
raise ValueError("sentences_per_batch must be positive")
|
|
137
|
+
if self.min_cells <= 0:
|
|
138
|
+
raise ValueError("min_cells must be positive")
|
|
139
|
+
|
|
140
|
+
if not 0.0 <= self.train_frac <= 1.0:
|
|
141
|
+
raise ValueError("train_frac must be between 0.0 and 1.0")
|
|
142
|
+
if not 0.0 <= self.valid_frac <= 1.0:
|
|
143
|
+
raise ValueError("valid_frac must be between 0.0 and 1.0")
|
|
144
|
+
if self.train_frac + self.valid_frac > 1.0:
|
|
145
|
+
raise ValueError("train_frac + valid_frac must be <= 1.0")
|
|
146
|
+
|
|
147
|
+
if not 0.0 < self.residual_expression <= 1.0:
|
|
148
|
+
raise ValueError("residual_expression must be in (0.0, 1.0]")
|
|
149
|
+
if not 0.0 < self.cell_residual_expression <= 1.0:
|
|
150
|
+
raise ValueError("cell_residual_expression must be in (0.0, 1.0]")
|
|
151
|
+
|
|
152
|
+
if self.split_mode == "random":
|
|
153
|
+
if self.held_out_cell_types or self.held_out_perturbations:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
"held_out_cell_types and held_out_perturbations are only valid for "
|
|
156
|
+
"zeroshot/fewshot split modes"
|
|
157
|
+
)
|
|
158
|
+
elif self.split_mode == "zeroshot":
|
|
159
|
+
if not self.held_out_cell_types:
|
|
160
|
+
raise ValueError("held_out_cell_types is required when split_mode='zeroshot'")
|
|
161
|
+
if self.held_out_perturbations:
|
|
162
|
+
raise ValueError("held_out_perturbations is not used when split_mode='zeroshot'")
|
|
163
|
+
elif not self.held_out_perturbations:
|
|
164
|
+
raise ValueError("held_out_perturbations is required when split_mode='fewshot'")
|
|
165
|
+
elif self.held_out_cell_types:
|
|
166
|
+
raise ValueError("held_out_cell_types is not used when split_mode='fewshot'")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class PerturbationPipeline:
|
|
170
|
+
"""End-to-end data setup pipeline for perturbation experiments.
|
|
171
|
+
|
|
172
|
+
Orchestrates the full workflow from raw H5AD files to ready-to-train
|
|
173
|
+
data sources with paired (perturbed, control) cell output.
|
|
174
|
+
|
|
175
|
+
Workflow:
|
|
176
|
+
1. Load H5AD file(s) into PerturbationAnnDataSource(s)
|
|
177
|
+
2. (Optional) Apply on-target knockdown QC filter
|
|
178
|
+
3. Split into train/val/test via zero-shot, few-shot, or random
|
|
179
|
+
4. Build control cell mapping for each split
|
|
180
|
+
5. Create batch sampler for training
|
|
181
|
+
|
|
182
|
+
Example::
|
|
183
|
+
|
|
184
|
+
config = PerturbationPipelineConfig(
|
|
185
|
+
split_mode="zeroshot",
|
|
186
|
+
held_out_cell_types=("TypeA",),
|
|
187
|
+
mapping_strategy="batch",
|
|
188
|
+
)
|
|
189
|
+
pipeline = PerturbationPipeline(config)
|
|
190
|
+
result = pipeline.setup(["path/to/data.h5ad"])
|
|
191
|
+
|
|
192
|
+
for batch_indices in result.train_sampler:
|
|
193
|
+
elements = [result.train_source[i] for i in batch_indices]
|
|
194
|
+
paired = result.get_paired_batch(batch_indices, split="train")
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, config: PerturbationPipelineConfig) -> None:
|
|
198
|
+
self._config = config
|
|
199
|
+
|
|
200
|
+
def setup(self, file_paths: list[str | Path]) -> PerturbationPipelineResult:
|
|
201
|
+
"""Execute the full data setup workflow.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
file_paths: Paths to H5AD files to load.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
PerturbationPipelineResult with sources, splits, samplers,
|
|
208
|
+
and control mappings.
|
|
209
|
+
"""
|
|
210
|
+
config = self._config
|
|
211
|
+
|
|
212
|
+
# 1. Load sources
|
|
213
|
+
logger.info("Loading %d H5AD file(s)...", len(file_paths))
|
|
214
|
+
sources = []
|
|
215
|
+
for path in file_paths:
|
|
216
|
+
src_config = PerturbationSourceConfig(
|
|
217
|
+
file_path=str(path),
|
|
218
|
+
pert_col=config.pert_col,
|
|
219
|
+
cell_type_col=config.cell_type_col,
|
|
220
|
+
batch_col=config.batch_col,
|
|
221
|
+
control_pert=config.control_pert,
|
|
222
|
+
output_space=config.output_space,
|
|
223
|
+
embedding_key=config.embedding_key,
|
|
224
|
+
hvg_col=config.hvg_col,
|
|
225
|
+
seed=config.seed,
|
|
226
|
+
)
|
|
227
|
+
sources.append(PerturbationAnnDataSource(src_config))
|
|
228
|
+
|
|
229
|
+
source: DataSourceModule
|
|
230
|
+
if len(sources) == 1:
|
|
231
|
+
source = sources[0]
|
|
232
|
+
else:
|
|
233
|
+
source = PerturbationConcatSource(sources=sources)
|
|
234
|
+
|
|
235
|
+
# 2. Knockdown filter (optional)
|
|
236
|
+
filter_mask: np.ndarray | None = None
|
|
237
|
+
if config.enable_knockdown_filter:
|
|
238
|
+
logger.info("Applying knockdown QC filter...")
|
|
239
|
+
filt = OnTargetKnockdownFilter(
|
|
240
|
+
KnockdownFilterConfig(
|
|
241
|
+
pert_col=config.pert_col,
|
|
242
|
+
control_pert=config.control_pert,
|
|
243
|
+
residual_expression=config.residual_expression,
|
|
244
|
+
cell_residual_expression=config.cell_residual_expression,
|
|
245
|
+
min_cells=config.min_cells,
|
|
246
|
+
var_gene_col=config.var_gene_col,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
filter_mask = filt.process(source)
|
|
250
|
+
n_kept = int(filter_mask.sum())
|
|
251
|
+
logger.info(
|
|
252
|
+
"Knockdown filter: %d / %d cells pass",
|
|
253
|
+
n_kept,
|
|
254
|
+
len(filter_mask),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# 3. Split
|
|
258
|
+
logger.info("Splitting data (mode=%s)...", config.split_mode)
|
|
259
|
+
if config.split_mode == "zeroshot":
|
|
260
|
+
splitter = ZeroShotSplitter(
|
|
261
|
+
ZeroShotSplitterConfig(
|
|
262
|
+
held_out_cell_types=config.held_out_cell_types,
|
|
263
|
+
pert_col=config.pert_col,
|
|
264
|
+
cell_type_col=config.cell_type_col,
|
|
265
|
+
train_frac=config.train_frac,
|
|
266
|
+
valid_frac=config.valid_frac,
|
|
267
|
+
test_frac=1.0 - config.train_frac - config.valid_frac,
|
|
268
|
+
seed=config.seed,
|
|
269
|
+
),
|
|
270
|
+
rngs=nnx.Rngs(config.seed),
|
|
271
|
+
)
|
|
272
|
+
elif config.split_mode == "fewshot":
|
|
273
|
+
splitter = FewShotSplitter(
|
|
274
|
+
FewShotSplitterConfig(
|
|
275
|
+
held_out_perturbations=config.held_out_perturbations,
|
|
276
|
+
pert_col=config.pert_col,
|
|
277
|
+
cell_type_col=config.cell_type_col,
|
|
278
|
+
control_pert=config.control_pert,
|
|
279
|
+
train_frac=config.train_frac,
|
|
280
|
+
valid_frac=config.valid_frac,
|
|
281
|
+
test_frac=1.0 - config.train_frac - config.valid_frac,
|
|
282
|
+
seed=config.seed,
|
|
283
|
+
),
|
|
284
|
+
rngs=nnx.Rngs(config.seed),
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
from diffbio.splitters.random import ( # noqa: PLC0415
|
|
288
|
+
RandomSplitter,
|
|
289
|
+
RandomSplitterConfig,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
splitter = RandomSplitter(
|
|
293
|
+
RandomSplitterConfig(
|
|
294
|
+
train_frac=config.train_frac,
|
|
295
|
+
valid_frac=config.valid_frac,
|
|
296
|
+
test_frac=1.0 - config.train_frac - config.valid_frac,
|
|
297
|
+
seed=config.seed,
|
|
298
|
+
),
|
|
299
|
+
rngs=nnx.Rngs(config.seed),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
split_result = splitter.split(source)
|
|
303
|
+
|
|
304
|
+
# Apply filter mask to split indices if knockdown filter was used
|
|
305
|
+
if filter_mask is not None:
|
|
306
|
+
passing = set(np.where(filter_mask)[0])
|
|
307
|
+
train_idx = np.array([i for i in split_result.train_indices if int(i) in passing])
|
|
308
|
+
valid_idx = np.array([i for i in split_result.valid_indices if int(i) in passing])
|
|
309
|
+
test_idx = np.array([i for i in split_result.test_indices if int(i) in passing])
|
|
310
|
+
else:
|
|
311
|
+
train_idx = np.array(split_result.train_indices)
|
|
312
|
+
valid_idx = np.array(split_result.valid_indices)
|
|
313
|
+
test_idx = np.array(split_result.test_indices)
|
|
314
|
+
|
|
315
|
+
logger.info(
|
|
316
|
+
"Split sizes: train=%d, val=%d, test=%d",
|
|
317
|
+
len(train_idx),
|
|
318
|
+
len(valid_idx),
|
|
319
|
+
len(test_idx),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# 4. Control mapping
|
|
323
|
+
logger.info(
|
|
324
|
+
"Building control mapping (strategy=%s)...",
|
|
325
|
+
config.mapping_strategy,
|
|
326
|
+
)
|
|
327
|
+
mapping_config = ControlMappingConfig(
|
|
328
|
+
strategy=config.mapping_strategy,
|
|
329
|
+
n_basal_samples=config.n_basal_samples,
|
|
330
|
+
seed=config.seed,
|
|
331
|
+
)
|
|
332
|
+
if config.mapping_strategy == "batch":
|
|
333
|
+
mapper = BatchControlMapping(mapping_config)
|
|
334
|
+
else:
|
|
335
|
+
mapper = RandomControlMapping(mapping_config)
|
|
336
|
+
|
|
337
|
+
control_mapping = mapper.build_mapping(source)
|
|
338
|
+
|
|
339
|
+
# 5. Train sampler
|
|
340
|
+
group_codes = source.get_group_codes()
|
|
341
|
+
train_group_codes = group_codes[train_idx]
|
|
342
|
+
sampler_config = PerturbationSamplerConfig(
|
|
343
|
+
sentence_size=config.sentence_size,
|
|
344
|
+
sentences_per_batch=config.sentences_per_batch,
|
|
345
|
+
seed=config.seed,
|
|
346
|
+
)
|
|
347
|
+
train_sampler = PerturbationBatchSampler(sampler_config, train_group_codes)
|
|
348
|
+
|
|
349
|
+
return PerturbationPipelineResult(
|
|
350
|
+
source=source,
|
|
351
|
+
train_indices=train_idx,
|
|
352
|
+
valid_indices=valid_idx,
|
|
353
|
+
test_indices=test_idx,
|
|
354
|
+
control_mapping=control_mapping,
|
|
355
|
+
train_sampler=train_sampler,
|
|
356
|
+
filter_mask=filter_mask,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class PerturbationPipelineResult:
|
|
361
|
+
"""Result of PerturbationPipeline.setup().
|
|
362
|
+
|
|
363
|
+
Holds all artifacts needed for training: the data source, split indices,
|
|
364
|
+
control mapping, and train sampler.
|
|
365
|
+
|
|
366
|
+
Attributes:
|
|
367
|
+
source: The underlying data source (single or concatenated).
|
|
368
|
+
train_indices: Cell indices for training.
|
|
369
|
+
valid_indices: Cell indices for validation.
|
|
370
|
+
test_indices: Cell indices for testing.
|
|
371
|
+
control_mapping: Array mapping perturbed cell index to control indices.
|
|
372
|
+
train_sampler: Batch sampler for training iteration.
|
|
373
|
+
filter_mask: Boolean QC mask (None if filtering was disabled).
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
def __init__(
|
|
377
|
+
self,
|
|
378
|
+
source: Any,
|
|
379
|
+
train_indices: np.ndarray,
|
|
380
|
+
valid_indices: np.ndarray,
|
|
381
|
+
test_indices: np.ndarray,
|
|
382
|
+
control_mapping: np.ndarray,
|
|
383
|
+
train_sampler: PerturbationBatchSampler,
|
|
384
|
+
filter_mask: np.ndarray | None = None,
|
|
385
|
+
) -> None:
|
|
386
|
+
self.source = source
|
|
387
|
+
self.train_indices = train_indices
|
|
388
|
+
self.valid_indices = valid_indices
|
|
389
|
+
self.test_indices = test_indices
|
|
390
|
+
self.control_mapping = control_mapping
|
|
391
|
+
self.train_sampler = train_sampler
|
|
392
|
+
self.filter_mask = filter_mask
|
|
393
|
+
|
|
394
|
+
def get_element(self, global_idx: int) -> dict[str, Any]:
|
|
395
|
+
"""Get a single cell element by global index.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
global_idx: Index into the full source.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Per-cell dictionary with counts and metadata.
|
|
402
|
+
"""
|
|
403
|
+
return self.source[global_idx]
|
|
404
|
+
|
|
405
|
+
def get_var_dims(self) -> dict[str, int]:
|
|
406
|
+
"""Return dimensionality info from the source."""
|
|
407
|
+
return self.source.get_var_dims()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""End-to-end differentiable preprocessing pipeline.
|
|
2
|
+
|
|
3
|
+
This module provides a complete read preprocessing pipeline that composes:
|
|
4
|
+
1. Quality filtering - Filter low-quality bases
|
|
5
|
+
2. Adapter removal - Soft trim adapter sequences
|
|
6
|
+
3. Duplicate weighting - Assign probabilistic weights based on uniqueness
|
|
7
|
+
4. Error correction - Neural network-based base correction
|
|
8
|
+
|
|
9
|
+
The pipeline is fully differentiable, enabling gradient-based optimization
|
|
10
|
+
of all preprocessing components jointly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import jax
|
|
18
|
+
import jax.numpy as jnp
|
|
19
|
+
from datarax.core.config import OperatorConfig
|
|
20
|
+
from datarax.core.operator import OperatorModule
|
|
21
|
+
from flax import nnx
|
|
22
|
+
from jaxtyping import Array
|
|
23
|
+
|
|
24
|
+
from diffbio.operators.preprocessing import (
|
|
25
|
+
AdapterRemovalConfig,
|
|
26
|
+
DifferentiableDuplicateWeighting,
|
|
27
|
+
DuplicateWeightingConfig,
|
|
28
|
+
ErrorCorrectionConfig,
|
|
29
|
+
SoftAdapterRemoval,
|
|
30
|
+
SoftErrorCorrection,
|
|
31
|
+
)
|
|
32
|
+
from diffbio.operators.quality_filter import (
|
|
33
|
+
DifferentiableQualityFilter,
|
|
34
|
+
QualityFilterConfig,
|
|
35
|
+
)
|
|
36
|
+
from diffbio.utils.quality import apply_quality_filter
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class PreprocessingPipelineConfig(OperatorConfig):
|
|
43
|
+
# pylint: disable=too-many-instance-attributes
|
|
44
|
+
"""Configuration for the preprocessing pipeline.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
read_length: Expected read length for initialization.
|
|
48
|
+
adapter_sequence: Adapter sequence to remove (Illumina universal default).
|
|
49
|
+
quality_threshold: Initial quality score threshold for filtering.
|
|
50
|
+
adapter_match_threshold: Threshold for adapter matching.
|
|
51
|
+
adapter_temperature: Temperature for soft adapter trimming.
|
|
52
|
+
duplicate_similarity_threshold: Similarity threshold for duplicate detection.
|
|
53
|
+
error_correction_window: Window size for error correction.
|
|
54
|
+
error_correction_hidden_dim: Hidden dimension for error correction network.
|
|
55
|
+
enable_adapter_removal: Whether to enable adapter removal step.
|
|
56
|
+
enable_duplicate_weighting: Whether to enable duplicate weighting step.
|
|
57
|
+
enable_error_correction: Whether to enable error correction step.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
read_length: int = 150
|
|
61
|
+
adapter_sequence: str = "AGATCGGAAGAG"
|
|
62
|
+
quality_threshold: float = 20.0
|
|
63
|
+
adapter_match_threshold: float = 0.8
|
|
64
|
+
adapter_temperature: float = 1.0
|
|
65
|
+
duplicate_similarity_threshold: float = 0.95
|
|
66
|
+
error_correction_window: int = 11
|
|
67
|
+
error_correction_hidden_dim: int = 64
|
|
68
|
+
enable_adapter_removal: bool = True
|
|
69
|
+
enable_duplicate_weighting: bool = True
|
|
70
|
+
enable_error_correction: bool = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PreprocessingPipeline(OperatorModule):
|
|
74
|
+
"""End-to-end differentiable preprocessing pipeline.
|
|
75
|
+
|
|
76
|
+
This pipeline processes sequencing reads through multiple preprocessing steps:
|
|
77
|
+
|
|
78
|
+
Input data structure:
|
|
79
|
+
- reads: Float[Array, "num_reads read_length 4"] - One-hot encoded reads
|
|
80
|
+
- quality: Float[Array, "num_reads read_length"] - Base quality scores
|
|
81
|
+
|
|
82
|
+
Output data structure (adds):
|
|
83
|
+
- preprocessed_reads: Float[Array, "num_reads read_length 4"] - Processed reads
|
|
84
|
+
- preprocessed_quality: Float[Array, "num_reads read_length"] - Processed quality
|
|
85
|
+
- read_weights: Float[Array, "num_reads"] - Read uniqueness weights
|
|
86
|
+
|
|
87
|
+
The pipeline is fully differentiable, supporting gradient-based training
|
|
88
|
+
to optimize all preprocessing components jointly.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
```python
|
|
92
|
+
config = PreprocessingPipelineConfig(read_length=150)
|
|
93
|
+
pipeline = PreprocessingPipeline(config, rngs=nnx.Rngs(42))
|
|
94
|
+
result, state, meta = pipeline.apply(data, {}, None)
|
|
95
|
+
processed = result["preprocessed_reads"]
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
config: PreprocessingPipelineConfig,
|
|
102
|
+
*,
|
|
103
|
+
rngs: nnx.Rngs,
|
|
104
|
+
name: str | None = None,
|
|
105
|
+
):
|
|
106
|
+
"""Initialize the preprocessing pipeline.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
config: Pipeline configuration.
|
|
110
|
+
rngs: Random number generators for parameter initialization.
|
|
111
|
+
name: Optional name for the pipeline.
|
|
112
|
+
"""
|
|
113
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
114
|
+
|
|
115
|
+
# 1. Quality filter (always enabled)
|
|
116
|
+
self.quality_filter = DifferentiableQualityFilter(
|
|
117
|
+
QualityFilterConfig(initial_threshold=config.quality_threshold),
|
|
118
|
+
rngs=rngs,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# 2. Adapter removal (optional)
|
|
122
|
+
self.adapter_removal = (
|
|
123
|
+
SoftAdapterRemoval(
|
|
124
|
+
AdapterRemovalConfig(
|
|
125
|
+
adapter_sequence=config.adapter_sequence,
|
|
126
|
+
match_threshold=config.adapter_match_threshold,
|
|
127
|
+
temperature=config.adapter_temperature,
|
|
128
|
+
),
|
|
129
|
+
rngs=rngs,
|
|
130
|
+
)
|
|
131
|
+
if config.enable_adapter_removal
|
|
132
|
+
else None
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# 3. Duplicate weighting (optional)
|
|
136
|
+
self.duplicate_weighting = (
|
|
137
|
+
DifferentiableDuplicateWeighting(
|
|
138
|
+
DuplicateWeightingConfig(
|
|
139
|
+
similarity_threshold=config.duplicate_similarity_threshold,
|
|
140
|
+
),
|
|
141
|
+
rngs=rngs,
|
|
142
|
+
)
|
|
143
|
+
if config.enable_duplicate_weighting
|
|
144
|
+
else None
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 4. Error correction (optional)
|
|
148
|
+
self.error_correction = (
|
|
149
|
+
SoftErrorCorrection(
|
|
150
|
+
ErrorCorrectionConfig(
|
|
151
|
+
window_size=config.error_correction_window,
|
|
152
|
+
hidden_dim=config.error_correction_hidden_dim,
|
|
153
|
+
),
|
|
154
|
+
rngs=rngs,
|
|
155
|
+
)
|
|
156
|
+
if config.enable_error_correction
|
|
157
|
+
else None
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def apply(
|
|
161
|
+
self,
|
|
162
|
+
data: dict[str, Array],
|
|
163
|
+
state: dict[str, Any],
|
|
164
|
+
metadata: dict[str, Any] | None,
|
|
165
|
+
random_params: Any = None,
|
|
166
|
+
stats: dict[str, Any] | None = None,
|
|
167
|
+
) -> tuple[dict[str, Array], dict[str, Any], dict[str, Any] | None]:
|
|
168
|
+
"""Apply the full preprocessing pipeline to reads.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
data: Input data containing:
|
|
172
|
+
- reads: Float[Array, "num_reads read_length 4"]
|
|
173
|
+
- quality: Float[Array, "num_reads read_length"]
|
|
174
|
+
state: Element state (passed through).
|
|
175
|
+
metadata: Element metadata (passed through).
|
|
176
|
+
random_params: Not used (deterministic pipeline).
|
|
177
|
+
stats: Optional statistics dict.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (output_data, state, metadata) where output_data contains
|
|
181
|
+
all input keys plus preprocessed outputs.
|
|
182
|
+
"""
|
|
183
|
+
reads = data["reads"]
|
|
184
|
+
quality = data["quality"]
|
|
185
|
+
num_reads = reads.shape[0]
|
|
186
|
+
|
|
187
|
+
# Initialize read weights to 1.0 (all reads equally weighted)
|
|
188
|
+
read_weights = jnp.ones((num_reads,))
|
|
189
|
+
|
|
190
|
+
# Step 1: Quality filtering (per-base)
|
|
191
|
+
filtered_reads, filtered_quality = apply_quality_filter(self.quality_filter, reads, quality)
|
|
192
|
+
|
|
193
|
+
# Step 2: Adapter removal (optional) - apply per-read using vmap
|
|
194
|
+
if self.adapter_removal is not None:
|
|
195
|
+
|
|
196
|
+
def apply_adapter_removal(read, quality):
|
|
197
|
+
"""Apply adapter removal to a single read."""
|
|
198
|
+
adapter_data = {"sequence": read, "quality_scores": quality}
|
|
199
|
+
adapter_result, _, _ = self.adapter_removal.apply(adapter_data, {}, None)
|
|
200
|
+
return adapter_result["sequence"], adapter_result["quality_scores"]
|
|
201
|
+
|
|
202
|
+
filtered_reads, filtered_quality = jax.vmap(apply_adapter_removal)(
|
|
203
|
+
filtered_reads, filtered_quality
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Step 3: Duplicate weighting (optional) - operates on full batch for cross-read comparison
|
|
207
|
+
# Use apply_batch which returns weights for all reads (not just first)
|
|
208
|
+
if self.duplicate_weighting is not None:
|
|
209
|
+
raw_weights, _ = self.duplicate_weighting.apply_batch(filtered_reads, filtered_quality)
|
|
210
|
+
# Normalize weights to [0, 1] range for use as probabilities
|
|
211
|
+
read_weights = raw_weights / jnp.max(raw_weights)
|
|
212
|
+
|
|
213
|
+
# Step 4: Error correction (optional) - apply per-read using vmap
|
|
214
|
+
if self.error_correction is not None:
|
|
215
|
+
|
|
216
|
+
def apply_error_correction(read, quality):
|
|
217
|
+
"""Apply error correction to a single read."""
|
|
218
|
+
ec_data = {"sequence": read, "quality_scores": quality}
|
|
219
|
+
ec_result, _, _ = self.error_correction.apply(ec_data, {}, None)
|
|
220
|
+
return ec_result["sequence"]
|
|
221
|
+
|
|
222
|
+
filtered_reads = jax.vmap(apply_error_correction)(filtered_reads, filtered_quality)
|
|
223
|
+
|
|
224
|
+
# Build output preserving input keys
|
|
225
|
+
output_data = {
|
|
226
|
+
**data,
|
|
227
|
+
"preprocessed_reads": filtered_reads,
|
|
228
|
+
"preprocessed_quality": filtered_quality,
|
|
229
|
+
"read_weights": read_weights,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return output_data, state, metadata
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def create_preprocessing_pipeline(
|
|
236
|
+
read_length: int = 150,
|
|
237
|
+
quality_threshold: float = 20.0,
|
|
238
|
+
adapter_sequence: str = "AGATCGGAAGAG",
|
|
239
|
+
enable_adapter_removal: bool = True,
|
|
240
|
+
enable_duplicate_weighting: bool = True,
|
|
241
|
+
enable_error_correction: bool = True,
|
|
242
|
+
seed: int = 42,
|
|
243
|
+
) -> PreprocessingPipeline:
|
|
244
|
+
"""Factory function to create a preprocessing pipeline.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
read_length: Expected read length.
|
|
248
|
+
quality_threshold: Quality score threshold.
|
|
249
|
+
adapter_sequence: Adapter sequence to remove.
|
|
250
|
+
enable_adapter_removal: Whether to enable adapter removal.
|
|
251
|
+
enable_duplicate_weighting: Whether to enable duplicate weighting.
|
|
252
|
+
enable_error_correction: Whether to enable error correction.
|
|
253
|
+
seed: Random seed.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Configured PreprocessingPipeline instance.
|
|
257
|
+
"""
|
|
258
|
+
config = PreprocessingPipelineConfig(
|
|
259
|
+
read_length=read_length,
|
|
260
|
+
quality_threshold=quality_threshold,
|
|
261
|
+
adapter_sequence=adapter_sequence,
|
|
262
|
+
enable_adapter_removal=enable_adapter_removal,
|
|
263
|
+
enable_duplicate_weighting=enable_duplicate_weighting,
|
|
264
|
+
enable_error_correction=enable_error_correction,
|
|
265
|
+
)
|
|
266
|
+
rngs = nnx.Rngs(seed)
|
|
267
|
+
return PreprocessingPipeline(config, rngs=rngs)
|