diffbio 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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 ""
|