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,279 @@
1
+ """Shared multi-omics provenance and embedding-artifact sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Mapping
9
+
10
+ import jax.numpy as jnp
11
+ from flax import nnx
12
+
13
+ from diffbio.sources.indexed_embeddings import IndexedEmbeddingSource, IndexedEmbeddingSourceConfig
14
+
15
+ MULTIOMICS_DATASET_PROVENANCE_KEYS = (
16
+ "dataset_name",
17
+ "source_type",
18
+ "modalities",
19
+ "curation_status",
20
+ "biological_validation",
21
+ "promotion_eligible",
22
+ "source_path",
23
+ )
24
+ MULTIOMICS_ARTIFACT_METADATA_KEYS = (
25
+ "artifact_id",
26
+ "artifact_type",
27
+ "modalities",
28
+ "embedding_source",
29
+ "foundation_source_name",
30
+ "promotion_eligible",
31
+ )
32
+ _SUPPORTED_MULTIOMICS_MODALITIES = frozenset(
33
+ {
34
+ "rna",
35
+ "atac",
36
+ "protein",
37
+ "spatial",
38
+ "metabolomics",
39
+ "mass_spectrometry",
40
+ }
41
+ )
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class MultiOmicsEmbeddingSourceConfig(IndexedEmbeddingSourceConfig):
46
+ """Configuration for sample-indexed multi-omics embedding artifacts."""
47
+
48
+ row_id_key: str = "sample_ids"
49
+
50
+
51
+ class MultiOmicsEmbeddingSource(IndexedEmbeddingSource):
52
+ """Indexed embedding source specialized for multi-omics sample artifacts."""
53
+
54
+ config: MultiOmicsEmbeddingSourceConfig # pyright: ignore[reportIncompatibleVariableOverride]
55
+
56
+ @classmethod
57
+ def from_path(
58
+ cls,
59
+ path: Path | str,
60
+ *,
61
+ rngs: nnx.Rngs | None = None,
62
+ ) -> MultiOmicsEmbeddingSource:
63
+ """Build the canonical sample-indexed source for a multi-omics artifact."""
64
+ return cls(MultiOmicsEmbeddingSourceConfig(file_path=str(path)), rngs=rngs)
65
+
66
+ @property
67
+ def sample_ids(self) -> tuple[str, ...] | None:
68
+ """Tuple of persisted sample identifiers, if present."""
69
+ return self.row_ids
70
+
71
+ def align_to_reference_sample_ids(
72
+ self,
73
+ *,
74
+ reference_sample_ids: Sequence[str],
75
+ require_sample_ids: bool = True,
76
+ ) -> jnp.ndarray:
77
+ """Align imported embeddings to a reference multi-omics sample order."""
78
+ return self.align_to_reference_ids(
79
+ reference_ids=tuple(reference_sample_ids),
80
+ require_row_ids=require_sample_ids,
81
+ artifact_label="Multi-omics",
82
+ id_display_name="Sample ID",
83
+ )
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class MetabolomicsEmbeddingSourceConfig(IndexedEmbeddingSourceConfig):
88
+ """Configuration for spectrum-indexed metabolomics embedding artifacts."""
89
+
90
+ row_id_key: str = "spectrum_ids"
91
+
92
+
93
+ class MetabolomicsEmbeddingSource(IndexedEmbeddingSource):
94
+ """Indexed embedding source specialized for metabolomics spectrum artifacts."""
95
+
96
+ config: MetabolomicsEmbeddingSourceConfig # pyright: ignore[reportIncompatibleVariableOverride]
97
+
98
+ @classmethod
99
+ def from_path(
100
+ cls,
101
+ path: Path | str,
102
+ *,
103
+ rngs: nnx.Rngs | None = None,
104
+ ) -> MetabolomicsEmbeddingSource:
105
+ """Build the canonical spectrum-indexed source for a metabolomics artifact."""
106
+ return cls(MetabolomicsEmbeddingSourceConfig(file_path=str(path)), rngs=rngs)
107
+
108
+ @property
109
+ def spectrum_ids(self) -> tuple[str, ...] | None:
110
+ """Tuple of persisted spectrum identifiers, if present."""
111
+ return self.row_ids
112
+
113
+ def align_to_reference_spectrum_ids(
114
+ self,
115
+ *,
116
+ reference_spectrum_ids: Sequence[str],
117
+ require_spectrum_ids: bool = True,
118
+ ) -> jnp.ndarray:
119
+ """Align imported embeddings to a reference spectrum order."""
120
+ return self.align_to_reference_ids(
121
+ reference_ids=tuple(reference_spectrum_ids),
122
+ require_row_ids=require_spectrum_ids,
123
+ artifact_label="Metabolomics",
124
+ id_display_name="Spectrum ID",
125
+ )
126
+
127
+
128
+ def build_multiomics_dataset_provenance(
129
+ *,
130
+ dataset_name: str,
131
+ source_type: str,
132
+ modalities: Sequence[str],
133
+ curation_status: str,
134
+ biological_validation: str,
135
+ promotion_eligible: bool,
136
+ source_path: str | None = None,
137
+ ) -> dict[str, Any]:
138
+ """Build canonical provenance for benchmarked multi-omics datasets."""
139
+ provenance: dict[str, Any] = {
140
+ "dataset_name": dataset_name,
141
+ "source_type": source_type,
142
+ "modalities": list(modalities),
143
+ "curation_status": curation_status,
144
+ "biological_validation": biological_validation,
145
+ "promotion_eligible": promotion_eligible,
146
+ "source_path": source_path,
147
+ }
148
+ return validate_multiomics_dataset_provenance(provenance)
149
+
150
+
151
+ def validate_multiomics_dataset_provenance(
152
+ provenance: Mapping[str, Any],
153
+ ) -> dict[str, Any]:
154
+ """Validate and normalize a multi-omics dataset provenance payload."""
155
+ missing = [key for key in MULTIOMICS_DATASET_PROVENANCE_KEYS if key not in provenance]
156
+ if missing:
157
+ raise ValueError(f"multi-omics dataset_provenance is missing required keys: {missing}")
158
+
159
+ normalized = {key: provenance[key] for key in MULTIOMICS_DATASET_PROVENANCE_KEYS}
160
+ for key in (
161
+ "dataset_name",
162
+ "source_type",
163
+ "curation_status",
164
+ "biological_validation",
165
+ ):
166
+ _require_non_empty_string(normalized[key], field_name=f"dataset_provenance.{key}")
167
+
168
+ normalized["modalities"] = _normalize_modalities(normalized["modalities"])
169
+ if not isinstance(normalized["promotion_eligible"], bool):
170
+ raise TypeError("multi-omics dataset_provenance.promotion_eligible must be a bool.")
171
+ if str(normalized["source_type"]).startswith("synthetic") and normalized["promotion_eligible"]:
172
+ raise ValueError("Synthetic multi-omics provenance cannot be promotion_eligible.")
173
+ if normalized["source_path"] is not None:
174
+ _require_non_empty_string(
175
+ normalized["source_path"],
176
+ field_name="dataset_provenance.source_path",
177
+ )
178
+ return normalized
179
+
180
+
181
+ def build_multiomics_artifact_metadata(
182
+ *,
183
+ artifact_id: str,
184
+ artifact_type: str,
185
+ modalities: Sequence[str],
186
+ embedding_source: str,
187
+ foundation_source_name: str,
188
+ promotion_eligible: bool,
189
+ ) -> dict[str, Any]:
190
+ """Build canonical metadata for imported or benchmark-produced omics artifacts."""
191
+ metadata: dict[str, Any] = {
192
+ "artifact_id": artifact_id,
193
+ "artifact_type": artifact_type,
194
+ "modalities": list(modalities),
195
+ "embedding_source": embedding_source,
196
+ "foundation_source_name": foundation_source_name,
197
+ "promotion_eligible": promotion_eligible,
198
+ }
199
+ return validate_multiomics_artifact_metadata(metadata)
200
+
201
+
202
+ def validate_multiomics_artifact_metadata(
203
+ metadata: Mapping[str, Any],
204
+ ) -> dict[str, Any]:
205
+ """Validate and normalize multi-omics artifact metadata."""
206
+ missing = [key for key in MULTIOMICS_ARTIFACT_METADATA_KEYS if key not in metadata]
207
+ if missing:
208
+ raise ValueError(f"multi-omics artifact metadata is missing required keys: {missing}")
209
+
210
+ normalized = {key: metadata[key] for key in MULTIOMICS_ARTIFACT_METADATA_KEYS}
211
+ for key in ("artifact_id", "artifact_type", "embedding_source", "foundation_source_name"):
212
+ _require_non_empty_string(normalized[key], field_name=f"artifact_metadata.{key}")
213
+ normalized["modalities"] = _normalize_modalities(normalized["modalities"])
214
+ if not isinstance(normalized["promotion_eligible"], bool):
215
+ raise TypeError("multi-omics artifact metadata promotion_eligible must be a bool.")
216
+ return normalized
217
+
218
+
219
+ def load_multiomics_embedding_source(
220
+ path: Path | str,
221
+ *,
222
+ rngs: nnx.Rngs | None = None,
223
+ ) -> MultiOmicsEmbeddingSource:
224
+ """Build the canonical sample-indexed multi-omics embedding source."""
225
+ return MultiOmicsEmbeddingSource.from_path(path, rngs=rngs)
226
+
227
+
228
+ def align_multiomics_embeddings(
229
+ *,
230
+ reference_sample_ids: Sequence[str],
231
+ artifact_path: Path | str,
232
+ require_sample_ids: bool = True,
233
+ ) -> jnp.ndarray:
234
+ """Align imported multi-omics embeddings to a reference sample order."""
235
+ return load_multiomics_embedding_source(artifact_path).align_to_reference_sample_ids(
236
+ reference_sample_ids=reference_sample_ids,
237
+ require_sample_ids=require_sample_ids,
238
+ )
239
+
240
+
241
+ def load_metabolomics_embedding_source(
242
+ path: Path | str,
243
+ *,
244
+ rngs: nnx.Rngs | None = None,
245
+ ) -> MetabolomicsEmbeddingSource:
246
+ """Build the canonical spectrum-indexed metabolomics embedding source."""
247
+ return MetabolomicsEmbeddingSource.from_path(path, rngs=rngs)
248
+
249
+
250
+ def align_metabolomics_embeddings(
251
+ *,
252
+ reference_spectrum_ids: Sequence[str],
253
+ artifact_path: Path | str,
254
+ require_spectrum_ids: bool = True,
255
+ ) -> jnp.ndarray:
256
+ """Align imported metabolomics embeddings to a reference spectrum order."""
257
+ return load_metabolomics_embedding_source(artifact_path).align_to_reference_spectrum_ids(
258
+ reference_spectrum_ids=reference_spectrum_ids,
259
+ require_spectrum_ids=require_spectrum_ids,
260
+ )
261
+
262
+
263
+ def _normalize_modalities(raw_modalities: Any) -> list[str]:
264
+ """Normalize and validate modality identifiers."""
265
+ if not isinstance(raw_modalities, Sequence) or isinstance(raw_modalities, str):
266
+ raise TypeError("modalities must be a non-empty sequence of strings.")
267
+ modalities = [str(modality) for modality in raw_modalities]
268
+ if not modalities:
269
+ raise ValueError("modalities must contain at least one modality.")
270
+ invalid = sorted(set(modalities) - _SUPPORTED_MULTIOMICS_MODALITIES)
271
+ if invalid:
272
+ raise ValueError(f"Unsupported multi-omics modalities: {invalid}")
273
+ return modalities
274
+
275
+
276
+ def _require_non_empty_string(value: Any, *, field_name: str) -> None:
277
+ """Require one non-empty string field."""
278
+ if not isinstance(value, str) or not value:
279
+ raise TypeError(f"{field_name} must be a non-empty string.")
@@ -0,0 +1,108 @@
1
+ """Pancreas endocrinogenesis DataSource for trajectory benchmarks.
2
+
3
+ Loads the scVelo pancreas dataset (Bastidas-Ponce et al. 2019,
4
+ Bergen et al. 2020) with spliced/unspliced layers for RNA velocity
5
+ and precomputed PCA embeddings for pseudotime inference.
6
+
7
+ Dataset: 3,696 cells, 27,998 genes, 8 cell types, 5 coarse types.
8
+ Source: scVelo tutorial dataset.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import jax.numpy as jnp
19
+ import numpy as np
20
+ from flax import nnx
21
+
22
+ from datarax.core.config import StructuralConfig
23
+
24
+ from diffbio.sources._benchmark_source import (
25
+ BenchmarkDataSource,
26
+ encode_label_column,
27
+ )
28
+ from diffbio.sources._utils import to_dense_float32 as _to_dense
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _FILENAME = "endocrinogenesis_day15.h5ad"
33
+
34
+
35
+ @dataclass(frozen=True, kw_only=True)
36
+ class PancreasConfig(StructuralConfig):
37
+ """Configuration for PancreasSource.
38
+
39
+ Attributes:
40
+ data_dir: Directory containing the downloaded h5ad file.
41
+ subsample: If set, randomly subsample this many cells.
42
+ cluster_key: Column in obs for cell type labels.
43
+ """
44
+
45
+ data_dir: str = "/media/mahdi/ssd23/Data/scvelo"
46
+ subsample: int | None = None
47
+ cluster_key: str = "clusters"
48
+
49
+ def __post_init__(self) -> None:
50
+ """Validate configuration."""
51
+ super().__post_init__()
52
+ path = Path(self.data_dir) / _FILENAME
53
+ if not path.exists():
54
+ raise FileNotFoundError(
55
+ f"Dataset not found: {path}. Download the scVelo "
56
+ f"pancreas dataset to {self.data_dir}/"
57
+ )
58
+
59
+
60
+ class PancreasSource(BenchmarkDataSource):
61
+ """DataSource for the scVelo pancreas endocrinogenesis dataset.
62
+
63
+ Provides counts, spliced/unspliced layers, PCA embeddings,
64
+ and cell type labels for trajectory and velocity benchmarks.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ config: PancreasConfig,
70
+ *,
71
+ rngs: nnx.Rngs | None = None,
72
+ name: str | None = None,
73
+ ) -> None:
74
+ """Load the pancreas dataset."""
75
+ super().__init__(config, rngs=rngs, name=name or "PancreasSource")
76
+ self.data = self._load(config)
77
+ self._log_loaded_summary(logger, "pancreas", ("n_cells", "n_genes", "n_types"))
78
+
79
+ def _load(self, config: PancreasConfig) -> dict[str, Any]:
80
+ """Load and preprocess the h5ad file."""
81
+ adata, counts = self._load_benchmark_counts(config, _FILENAME, _to_dense)
82
+
83
+ # Spliced/unspliced for velocity
84
+ spliced = jnp.array(_to_dense(adata.layers["spliced"]))
85
+ unspliced = jnp.array(_to_dense(adata.layers["unspliced"]))
86
+
87
+ # Cell type labels
88
+ labels = encode_label_column(adata.obs[config.cluster_key])
89
+
90
+ # Embeddings
91
+ embeddings = jnp.array(
92
+ np.asarray(
93
+ adata.obsm.get("X_pca", np.zeros((adata.n_obs, 50))),
94
+ dtype=np.float32,
95
+ )
96
+ )
97
+
98
+ return {
99
+ "counts": counts,
100
+ "spliced": spliced,
101
+ "unspliced": unspliced,
102
+ "cell_type_labels": labels,
103
+ "embeddings": embeddings,
104
+ "gene_names": list(adata.var_names),
105
+ "n_cells": adata.n_obs,
106
+ "n_genes": adata.n_vars,
107
+ "n_types": int(len(np.unique(labels))),
108
+ }
@@ -0,0 +1,69 @@
1
+ """Perturbation-aware data loading for single-cell experiments.
2
+
3
+ This sub-package provides data sources, configuration, and utilities for
4
+ loading single-cell perturbation experiment data (e.g., CRISPR screens),
5
+ porting and adapting features from the cell-load library to the
6
+ JAX/datarax ecosystem.
7
+
8
+ Sources:
9
+ PerturbationAnnDataSource: Single-file perturbation-aware AnnData source
10
+ PerturbationConcatSource: Multi-dataset concatenation source
11
+
12
+ Configuration:
13
+ ExperimentConfig: TOML-based experiment configuration
14
+ PerturbationSourceConfig: Source configuration
15
+
16
+ Control Mapping:
17
+ RandomControlMapping: Random control cell mapping within cell type
18
+ BatchControlMapping: Batch-aware control cell mapping
19
+
20
+ Utilities:
21
+ H5MetadataCache: Singleton cache for H5 metadata
22
+ GlobalH5MetadataCache: Process-global cache manager
23
+ """
24
+
25
+ from diffbio.sources.perturbation._types import MappingStrategy, OutputSpaceMode
26
+ from diffbio.sources.perturbation.concat_source import PerturbationConcatSource
27
+ from diffbio.sources.perturbation.control_mapping import (
28
+ BatchControlMapping,
29
+ ControlMappingConfig,
30
+ RandomControlMapping,
31
+ )
32
+ from diffbio.sources.perturbation.experiment_config import (
33
+ DatasetEntry,
34
+ ExperimentConfig,
35
+ FewshotEntry,
36
+ ZeroshotEntry,
37
+ load_experiment_config,
38
+ )
39
+ from diffbio.sources.perturbation.h5_metadata_cache import (
40
+ GlobalH5MetadataCache,
41
+ H5MetadataCache,
42
+ )
43
+ from diffbio.sources.perturbation.perturbation_source import (
44
+ PerturbationAnnDataSource,
45
+ PerturbationSourceConfig,
46
+ )
47
+
48
+ __all__ = [
49
+ # Types
50
+ "MappingStrategy",
51
+ "OutputSpaceMode",
52
+ # Sources
53
+ "PerturbationAnnDataSource",
54
+ "PerturbationSourceConfig",
55
+ "PerturbationConcatSource",
56
+ # Configuration
57
+ "ExperimentConfig",
58
+ "DatasetEntry",
59
+ "ZeroshotEntry",
60
+ "FewshotEntry",
61
+ "load_experiment_config",
62
+ # Control Mapping
63
+ "ControlMappingConfig",
64
+ "RandomControlMapping",
65
+ "BatchControlMapping",
66
+ # Cache
67
+ "H5MetadataCache",
68
+ "GlobalH5MetadataCache",
69
+ ]
@@ -0,0 +1,51 @@
1
+ """Shared type aliases and enums for perturbation data loading.
2
+
3
+ Provides canonical string enums and type aliases used across the perturbation
4
+ sub-package: output space modes, mapping strategies, and cell/perturbation
5
+ label types.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+ from typing import TypeAlias
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Type aliases
15
+ # ---------------------------------------------------------------------------
16
+
17
+ CellIndex: TypeAlias = int
18
+ PerturbationLabel: TypeAlias = str
19
+ CellTypeLabel: TypeAlias = str
20
+ BatchLabel: TypeAlias = str
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Enums
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ class OutputSpaceMode(StrEnum):
29
+ """Output representation mode for perturbation data.
30
+
31
+ Attributes:
32
+ GENE: Highly variable gene (HVG) subset of the count matrix.
33
+ ALL: Full gene expression matrix.
34
+ EMBEDDING: Pre-computed embedding only (no raw counts).
35
+ """
36
+
37
+ GENE = "gene"
38
+ ALL = "all"
39
+ EMBEDDING = "embedding"
40
+
41
+
42
+ class MappingStrategy(StrEnum):
43
+ """Strategy for mapping perturbed cells to control cells.
44
+
45
+ Attributes:
46
+ BATCH: Map within same batch and cell type.
47
+ RANDOM: Map to random control of same cell type.
48
+ """
49
+
50
+ BATCH = "batch"
51
+ RANDOM = "random"
@@ -0,0 +1,125 @@
1
+ """Shared utility functions for the perturbation sub-package.
2
+
3
+ Ports and adapts utility functions from cell-load's data_utils module to work
4
+ with JAX arrays and numpy instead of PyTorch tensors.
5
+
6
+ References:
7
+ - cell-load/src/cell_load/utils/data_utils.py
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable
13
+ from typing import Any
14
+
15
+ import jax.numpy as jnp
16
+ import numpy as np
17
+
18
+
19
+ def safe_decode_array(arr: Any) -> np.ndarray:
20
+ """Decode byte-string arrays to UTF-8 and cast all entries to Python str.
21
+
22
+ Args:
23
+ arr: Array-like of bytes or other objects.
24
+
25
+ Returns:
26
+ Numpy string array with all elements decoded to str.
27
+ """
28
+ decoded: list[str] = []
29
+ for x in arr:
30
+ if isinstance(x, (bytes, bytearray)):
31
+ decoded.append(x.decode("utf-8", errors="ignore"))
32
+ else:
33
+ decoded.append(str(x))
34
+ return np.array(decoded, dtype=str)
35
+
36
+
37
+ def generate_onehot_map(keys: Iterable[str]) -> dict[str, jnp.ndarray]:
38
+ """Build a map from each unique key to a one-hot JAX array.
39
+
40
+ Keys are sorted to ensure deterministic ordering across runs.
41
+
42
+ Args:
43
+ keys: Iterable of hashable string items.
44
+
45
+ Returns:
46
+ Dict mapping each unique key to a float32 one-hot JAX array
47
+ of length equal to the number of unique keys.
48
+ """
49
+ unique_keys = sorted(set(keys))
50
+ n = len(unique_keys)
51
+ identity = jnp.eye(n, dtype=jnp.float32)
52
+ return {k: identity[i] for i, k in enumerate(unique_keys)}
53
+
54
+
55
+ def is_discrete_counts(x: jnp.ndarray, n_cells: int = 100) -> bool:
56
+ """Detect if data appears to be raw integer counts.
57
+
58
+ Checks whether the row sums of the first ``n_cells`` rows are integers
59
+ (fractional part approximately zero).
60
+
61
+ Args:
62
+ x: Array of shape ``(n_cells, n_genes)``.
63
+ n_cells: Number of cells to sample for detection.
64
+
65
+ Returns:
66
+ True if data appears to be discrete/raw counts.
67
+ """
68
+ top_n = min(x.shape[0], n_cells)
69
+ row_sums = x[:top_n].sum(axis=1)
70
+ frac_part = row_sums - jnp.floor(row_sums)
71
+ return bool(jnp.all(jnp.abs(frac_part) < 1e-7))
72
+
73
+
74
+ def is_log_transformed(x: jnp.ndarray) -> bool:
75
+ """Detect if data is log-transformed by checking the global maximum.
76
+
77
+ Log1p-transformed data typically has a maximum below 15.
78
+
79
+ Args:
80
+ x: Array of expression values.
81
+
82
+ Returns:
83
+ True if data appears to be log-transformed.
84
+ """
85
+ return bool(x.max() < 15.0)
86
+
87
+
88
+ def split_perturbations_by_cell_fraction(
89
+ pert_groups: dict[str, np.ndarray],
90
+ val_fraction: float,
91
+ rng: np.random.Generator,
92
+ ) -> tuple[list[str], list[str]]:
93
+ """Partition perturbations so the val subset approximates a target cell fraction.
94
+
95
+ Uses a greedy algorithm: shuffles perturbations, then greedily assigns each
96
+ to the val subset if doing so brings the val cell count closer to the target.
97
+
98
+ Args:
99
+ pert_groups: Dict mapping perturbation names to arrays of cell indices.
100
+ val_fraction: Target fraction of total cells to assign to validation.
101
+ rng: Numpy random generator for shuffling.
102
+
103
+ Returns:
104
+ Tuple of (train_perturbation_names, val_perturbation_names).
105
+ """
106
+ total_cells = sum(len(indices) for indices in pert_groups.values())
107
+ target_val_cells = val_fraction * total_cells
108
+
109
+ pert_size_list = [(p, len(pert_groups[p])) for p in pert_groups]
110
+ rng.shuffle(pert_size_list)
111
+
112
+ val_perts: list[str] = []
113
+ current_val_cells = 0
114
+
115
+ for pert, size in pert_size_list:
116
+ new_val_cells = current_val_cells + size
117
+ diff_if_add = abs(new_val_cells - target_val_cells)
118
+ diff_if_skip = abs(current_val_cells - target_val_cells)
119
+
120
+ if diff_if_add < diff_if_skip:
121
+ val_perts.append(pert)
122
+ current_val_cells = new_val_cells
123
+
124
+ train_perts = [p for p, _ in pert_size_list if p not in set(val_perts)]
125
+ return train_perts, val_perts