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,45 @@
1
+ """Shared utilities for DiffBio data sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+
10
+ def _require_anndata() -> Any:
11
+ """Import anndata, raising a clear error if not installed.
12
+
13
+ Returns:
14
+ The anndata module.
15
+
16
+ Raises:
17
+ ImportError: If anndata is not installed.
18
+ """
19
+ try:
20
+ import anndata # noqa: PLC0415
21
+
22
+ return anndata
23
+ except ImportError as err:
24
+ raise ImportError(
25
+ "anndata is required for this source. Install it with: uv pip install anndata"
26
+ ) from err
27
+
28
+
29
+ def to_dense_float32(matrix: Any) -> np.ndarray:
30
+ """Convert a sparse or dense matrix to a dense float32 numpy array.
31
+
32
+ Handles scipy sparse matrices, numpy arrays, and other array-like
33
+ inputs. Always returns a contiguous float32 numpy array.
34
+
35
+ Args:
36
+ matrix: Input matrix (sparse or dense).
37
+
38
+ Returns:
39
+ Dense numpy array with dtype float32.
40
+ """
41
+ import scipy.sparse # noqa: PLC0415
42
+
43
+ if scipy.sparse.issparse(matrix):
44
+ return np.asarray(matrix.toarray(), dtype=np.float32)
45
+ return np.asarray(matrix, dtype=np.float32)
@@ -0,0 +1,387 @@
1
+ """AnnData interop layer for DiffBio data dictionaries.
2
+
3
+ Provides bidirectional conversion between DiffBio's standard data dict format
4
+ (with keys ``counts``, ``obs``, ``var``, ``obsm``) and AnnData objects.
5
+
6
+ This enables integration with the broader single-cell ecosystem (scanpy,
7
+ scvi-tools, etc.) while keeping DiffBio's internal representation as
8
+ JAX-native dictionaries suitable for differentiable pipelines.
9
+
10
+ Also provides utilities for benchmark evaluation:
11
+
12
+ - ``from_anndata_to_operator_input``: Convert AnnData to operator-specific dicts.
13
+ - ``to_grader_answer``: Convert operator output dicts to grader-expected formats.
14
+
15
+ Both ``anndata`` and ``pandas`` are optional dependencies. Functions raise
16
+ ``ImportError`` with installation instructions if they are not available.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import jax.numpy as jnp
25
+ import numpy as np
26
+
27
+ from diffbio.sources._anndata_shared import (
28
+ build_anndata_data,
29
+ extract_anndata_annotations,
30
+ to_dense_array,
31
+ )
32
+ from diffbio.sources._utils import _require_anndata
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ if TYPE_CHECKING:
37
+ import anndata
38
+
39
+
40
+ def _require_pandas() -> Any:
41
+ """Import pandas, raising a clear error if not installed.
42
+
43
+ Returns:
44
+ The pandas module.
45
+
46
+ Raises:
47
+ ImportError: If pandas is not installed.
48
+ """
49
+ try:
50
+ import pandas as pd # noqa: PLC0415
51
+
52
+ return pd
53
+ except ImportError as err:
54
+ raise ImportError(
55
+ "pandas is required for AnnData interop. Install with: uv pip install pandas"
56
+ ) from err
57
+
58
+
59
+ def to_anndata(data_dict: dict[str, Any]) -> anndata.AnnData:
60
+ """Convert a DiffBio data dict to an AnnData object.
61
+
62
+ Translates the standard DiffBio dictionary format (as produced by
63
+ ``AnnDataSource.load()``) into an ``anndata.AnnData`` object for use
64
+ with scanpy, scvi-tools, and other AnnData-based tools.
65
+
66
+ JAX arrays in ``counts`` and ``obsm`` are converted to numpy via
67
+ ``np.asarray()``. The ``obs`` and ``var`` dicts become pandas
68
+ DataFrames.
69
+
70
+ Args:
71
+ data_dict: Dictionary with keys:
72
+ - ``counts``: JAX or numpy array of shape (n_cells, n_genes).
73
+ - ``obs``: Dict mapping column names to per-cell arrays.
74
+ - ``var``: Dict mapping column names to per-gene arrays.
75
+ - ``obsm`` (optional): Dict mapping embedding names to arrays.
76
+
77
+ Returns:
78
+ AnnData object with ``.X``, ``.obs``, ``.var``, and ``.obsm``
79
+ populated from the input dictionary.
80
+
81
+ Raises:
82
+ ImportError: If anndata or pandas is not installed.
83
+ """
84
+ ad = _require_anndata()
85
+ pd = _require_pandas()
86
+
87
+ counts_np = to_dense_array(data_dict["counts"])
88
+
89
+ n_obs, n_vars = counts_np.shape
90
+ obs_df = pd.DataFrame(
91
+ data_dict.get("obs", {}),
92
+ index=[str(i) for i in range(n_obs)],
93
+ )
94
+ var_df = pd.DataFrame(
95
+ data_dict.get("var", {}),
96
+ index=[str(i) for i in range(n_vars)],
97
+ )
98
+
99
+ adata = ad.AnnData(X=counts_np, obs=obs_df, var=var_df)
100
+
101
+ obsm = data_dict.get("obsm", {})
102
+ for key, value in obsm.items():
103
+ adata.obsm[key] = np.asarray(value, dtype=np.float32)
104
+
105
+ return adata
106
+
107
+
108
+ def from_anndata(adata: anndata.AnnData) -> dict[str, Any]:
109
+ """Convert an AnnData object to a DiffBio data dict.
110
+
111
+ Translates an ``anndata.AnnData`` object into the standard DiffBio
112
+ dictionary format compatible with ``AnnDataSource.load()`` output.
113
+
114
+ Sparse ``.X`` matrices are converted to dense before wrapping in a
115
+ JAX array. ``.obs`` and ``.var`` DataFrames become plain dicts of
116
+ numpy arrays. ``.obsm`` entries become JAX arrays.
117
+
118
+ Args:
119
+ adata: AnnData object to convert.
120
+
121
+ Returns:
122
+ Dictionary with keys:
123
+ - ``counts``: Dense JAX array of shape (n_cells, n_genes).
124
+ - ``obs``: Dict mapping column names to numpy arrays.
125
+ - ``var``: Dict mapping column names to numpy arrays.
126
+ - ``obsm``: Dict mapping embedding names to JAX arrays.
127
+ """
128
+ counts = jnp.array(to_dense_array(adata.X))
129
+ obs, var, obsm = extract_anndata_annotations(adata)
130
+
131
+ return build_anndata_data(counts=counts, obs=obs, var=var, obsm=obsm)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Benchmark evaluation utilities
136
+ # ---------------------------------------------------------------------------
137
+
138
+ # Mapping from task_type to the conversion strategy for building operator inputs.
139
+ _TASK_TYPE_BUILDERS: dict[str, str] = {
140
+ "qc_filtering": "counts",
141
+ "clustering": "embeddings",
142
+ "batch_correction": "embeddings_with_batch",
143
+ "differential_expression": "counts_with_design",
144
+ "trajectory": "embeddings",
145
+ "normalization": "counts_with_library",
146
+ "spatial_analysis": "counts_with_spatial",
147
+ "cell_annotation": "embeddings_with_batch",
148
+ }
149
+
150
+
151
+ def from_anndata_to_operator_input(
152
+ adata: anndata.AnnData,
153
+ task_type: str,
154
+ ) -> dict[str, Any]:
155
+ """Convert an AnnData object to a DiffBio operator input dict.
156
+
157
+ Produces the specific data dict keys expected by the operator
158
+ associated with ``task_type``. For example, clustering operators
159
+ expect an ``"embeddings"`` key from PCA, while batch correction
160
+ additionally requires ``"batch_labels"``.
161
+
162
+ Args:
163
+ adata: AnnData object with counts, obs metadata, and embeddings.
164
+ task_type: Category of the benchmark task. Supported values:
165
+ ``"qc_filtering"``, ``"clustering"``, ``"batch_correction"``,
166
+ ``"differential_expression"``, ``"trajectory"``,
167
+ ``"normalization"``, ``"spatial_analysis"``,
168
+ ``"cell_annotation"``.
169
+
170
+ Returns:
171
+ Dictionary with keys appropriate for the target operator.
172
+
173
+ Raises:
174
+ ValueError: If task_type is not recognised.
175
+ KeyError: If required data is missing from adata.
176
+ """
177
+ strategy = _TASK_TYPE_BUILDERS.get(task_type)
178
+ if strategy is None:
179
+ raise ValueError(
180
+ f"Unknown task_type {task_type!r}. Supported: {sorted(_TASK_TYPE_BUILDERS)}"
181
+ )
182
+
183
+ counts = jnp.array(to_dense_array(adata.X))
184
+
185
+ if strategy == "counts":
186
+ library_size = jnp.sum(counts, axis=1, keepdims=True)
187
+ return {"counts": counts, "library_size": library_size}
188
+
189
+ if strategy == "counts_with_design":
190
+ n_cells = adata.n_obs
191
+ if "batch" in adata.obs.columns:
192
+ batch_arr = np.asarray(adata.obs["batch"])
193
+ unique_batches = np.unique(batch_arr)
194
+ design = np.zeros((n_cells, len(unique_batches)), dtype=np.float32)
195
+ for i, b in enumerate(unique_batches):
196
+ design[batch_arr == b, i] = 1.0
197
+ else:
198
+ design = np.ones((n_cells, 1), dtype=np.float32)
199
+ return {"counts": counts, "design": jnp.array(design)}
200
+
201
+ if strategy == "counts_with_library":
202
+ library_size = jnp.sum(counts, axis=1, keepdims=True)
203
+ return {"counts": counts, "library_size": library_size}
204
+
205
+ if strategy == "counts_with_spatial":
206
+ if "spatial" not in (adata.obsm or {}):
207
+ raise KeyError("AnnData missing obsm['spatial'] required for spatial_analysis tasks")
208
+ spatial = jnp.array(np.asarray(adata.obsm["spatial"], dtype=np.float32))
209
+ return {"counts": counts, "spatial_coords": spatial}
210
+
211
+ # embeddings-based strategies
212
+ embedding_key = _pick_embedding_key(adata)
213
+ embeddings = jnp.array(np.asarray(adata.obsm[embedding_key], dtype=np.float32))
214
+
215
+ if strategy == "embeddings":
216
+ return {"embeddings": embeddings}
217
+
218
+ # embeddings_with_batch
219
+ if "batch" in adata.obs.columns:
220
+ batch_arr = np.asarray(adata.obs["batch"])
221
+ unique_batches = np.unique(batch_arr)
222
+ batch_indices = np.searchsorted(unique_batches, batch_arr)
223
+ batch_labels = jnp.array(batch_indices, dtype=jnp.int32)
224
+ else:
225
+ batch_labels = jnp.zeros(adata.n_obs, dtype=jnp.int32)
226
+
227
+ return {"embeddings": embeddings, "batch_labels": batch_labels}
228
+
229
+
230
+ def _pick_embedding_key(adata: anndata.AnnData) -> str:
231
+ """Select the best available embedding key from adata.obsm.
232
+
233
+ Prefers ``X_pca`` > ``X_scvi`` > first available key.
234
+
235
+ Args:
236
+ adata: AnnData object.
237
+
238
+ Returns:
239
+ Key name from adata.obsm.
240
+
241
+ Raises:
242
+ KeyError: If no embeddings are available.
243
+ """
244
+ if adata.obsm is None or len(adata.obsm) == 0:
245
+ raise KeyError("AnnData has no obsm embeddings for operator input")
246
+
247
+ for preferred in ("X_pca", "X_scvi", "X_umap"):
248
+ if preferred in adata.obsm:
249
+ return preferred
250
+
251
+ return next(iter(adata.obsm.keys()))
252
+
253
+
254
+ def to_grader_answer(
255
+ operator_output: dict[str, Any],
256
+ task_type: str,
257
+ ) -> Any:
258
+ """Convert DiffBio operator output to grader-expected answer format.
259
+
260
+ Extracts the relevant result from an operator's output dict and
261
+ converts it to the primitive type expected by the benchmark grader
262
+ (float, str, list, dict, or set).
263
+
264
+ Args:
265
+ operator_output: Dictionary returned by an operator's ``apply()``
266
+ method (the data dict component).
267
+ task_type: Category of the benchmark task, determining which
268
+ output key to extract and how to format it.
269
+
270
+ Returns:
271
+ The answer in the format expected by the corresponding grader:
272
+ - ``"qc_filtering"`` -> ``float`` (number of cells passing)
273
+ - ``"clustering"`` -> ``dict[str, float]`` (cluster distribution)
274
+ or ``set[str]`` (cluster label set)
275
+ - ``"differential_expression"`` -> ``list[str]`` (top DE genes)
276
+ - ``"batch_correction"`` -> ``float`` (batch mixing metric)
277
+ - ``"normalization"`` -> ``float`` (reconstruction metric)
278
+ - ``"trajectory"`` -> ``float`` (pseudotime correlation)
279
+ - ``"spatial_analysis"`` -> ``set[str]`` (domain labels)
280
+ - ``"cell_annotation"`` -> ``str`` (predicted cell type)
281
+
282
+ Raises:
283
+ ValueError: If task_type is not recognised.
284
+ """
285
+ converters: dict[str, Any] = {
286
+ "qc_filtering": _answer_qc_filtering,
287
+ "clustering": _answer_clustering,
288
+ "differential_expression": _answer_de,
289
+ "batch_correction": _answer_batch_correction,
290
+ "normalization": _answer_normalization,
291
+ "trajectory": _answer_trajectory,
292
+ "spatial_analysis": _answer_spatial,
293
+ "cell_annotation": _answer_cell_annotation,
294
+ }
295
+ converter = converters.get(task_type)
296
+ if converter is None:
297
+ raise ValueError(f"Unknown task_type {task_type!r}. Supported: {sorted(converters)}")
298
+ return converter(operator_output)
299
+
300
+
301
+ def _answer_qc_filtering(output: dict[str, Any]) -> float:
302
+ """Extract cell count after quality filtering."""
303
+ if "retention_weights" in output:
304
+ weights = np.asarray(output["retention_weights"])
305
+ return float(np.sum(weights > 0.5))
306
+ if "counts" in output:
307
+ return float(np.asarray(output["counts"]).shape[0])
308
+ return 0.0
309
+
310
+
311
+ def _answer_clustering(output: dict[str, Any]) -> dict[str, float]:
312
+ """Extract cluster label distribution from clustering output."""
313
+ if "cluster_labels" in output:
314
+ labels = np.asarray(output["cluster_labels"])
315
+ elif "cluster_assignments" in output:
316
+ labels = np.asarray(output["cluster_assignments"])
317
+ if labels.ndim == 2:
318
+ labels = np.argmax(labels, axis=-1)
319
+ else:
320
+ return {}
321
+
322
+ unique, counts = np.unique(labels, return_counts=True)
323
+ total = float(counts.sum())
324
+ return {str(label): float(count / total) for label, count in zip(unique, counts)}
325
+
326
+
327
+ def _answer_de(output: dict[str, Any]) -> list[str]:
328
+ """Extract top differentially expressed gene names."""
329
+ if "top_genes" in output:
330
+ return [str(g) for g in output["top_genes"]]
331
+ if "significant" in output and "gene_names" in output:
332
+ sig = np.asarray(output["significant"])
333
+ genes = np.asarray(output["gene_names"])
334
+ return [str(g) for g in genes[sig > 0.5]]
335
+ if "log_fold_change" in output and "gene_names" in output:
336
+ lfc = np.abs(np.asarray(output["log_fold_change"]))
337
+ genes = np.asarray(output["gene_names"])
338
+ top_idx = np.argsort(lfc)[::-1][:20]
339
+ return [str(genes[i]) for i in top_idx]
340
+ return []
341
+
342
+
343
+ def _answer_batch_correction(output: dict[str, Any]) -> float:
344
+ """Extract batch mixing metric from correction output."""
345
+ if "batch_mixing_score" in output:
346
+ return float(output["batch_mixing_score"])
347
+ if "corrected_embeddings" in output and "batch_labels" in output:
348
+ corrected = np.asarray(output["corrected_embeddings"])
349
+ return float(np.std(corrected))
350
+ return 0.0
351
+
352
+
353
+ def _answer_normalization(output: dict[str, Any]) -> float:
354
+ """Extract reconstruction quality from normalizer output."""
355
+ if "reconstruction_loss" in output:
356
+ return float(output["reconstruction_loss"])
357
+ if "normalized" in output:
358
+ return float(np.mean(np.asarray(output["normalized"])))
359
+ return 0.0
360
+
361
+
362
+ def _answer_trajectory(output: dict[str, Any]) -> float:
363
+ """Extract pseudotime summary from trajectory output."""
364
+ if "pseudotime" in output:
365
+ return float(np.max(np.asarray(output["pseudotime"])))
366
+ return 0.0
367
+
368
+
369
+ def _answer_spatial(output: dict[str, Any]) -> set[str]:
370
+ """Extract spatial domain label set."""
371
+ if "domain_assignments" in output:
372
+ assignments = np.asarray(output["domain_assignments"])
373
+ if assignments.ndim == 2:
374
+ assignments = np.argmax(assignments, axis=-1)
375
+ return {str(d) for d in np.unique(assignments)}
376
+ return set()
377
+
378
+
379
+ def _answer_cell_annotation(output: dict[str, Any]) -> str:
380
+ """Extract dominant cell type annotation."""
381
+ if "cell_type" in output:
382
+ return str(output["cell_type"])
383
+ if "cluster_labels" in output:
384
+ labels = np.asarray(output["cluster_labels"])
385
+ unique, counts = np.unique(labels, return_counts=True)
386
+ return str(unique[np.argmax(counts)])
387
+ return ""