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