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,513 @@
|
|
|
1
|
+
"""Perturbation-aware AnnData source for single-cell experiments.
|
|
2
|
+
|
|
3
|
+
Extends AnnDataSource with perturbation metadata extraction, integer encoding,
|
|
4
|
+
control cell identification, output space selection, and one-hot perturbation
|
|
5
|
+
maps. Follows the eager-loading pattern from datarax.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- cell-load/src/cell_load/dataset/_perturbation.py (PerturbationDataset)
|
|
9
|
+
- diffbio/sources/anndata_source.py (AnnDataSource)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import jax
|
|
21
|
+
import jax.numpy as jnp
|
|
22
|
+
import numpy as np
|
|
23
|
+
from flax import nnx
|
|
24
|
+
|
|
25
|
+
from diffbio.sources._anndata_shared import (
|
|
26
|
+
build_anndata_data,
|
|
27
|
+
extract_anndata_annotations,
|
|
28
|
+
read_h5ad,
|
|
29
|
+
to_dense_array,
|
|
30
|
+
)
|
|
31
|
+
from diffbio.sources.anndata_source import AnnDataSource, AnnDataSourceConfig
|
|
32
|
+
from diffbio.sources.perturbation._types import OutputSpaceMode
|
|
33
|
+
from diffbio.sources.perturbation.output_space import select_output_counts
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class _EncodedObsColumn:
|
|
40
|
+
"""Categorical obs column with codes, labels, and one-hot lookup."""
|
|
41
|
+
|
|
42
|
+
codes: np.ndarray
|
|
43
|
+
categories: np.ndarray
|
|
44
|
+
labels: np.ndarray
|
|
45
|
+
onehot_matrix: np.ndarray
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class _CategoricalMetadataState:
|
|
50
|
+
"""Categorical perturbation metadata and lookup tables."""
|
|
51
|
+
|
|
52
|
+
perturbation: _EncodedObsColumn
|
|
53
|
+
cell_type: _EncodedObsColumn
|
|
54
|
+
batch: _EncodedObsColumn
|
|
55
|
+
control_mask: np.ndarray
|
|
56
|
+
group_codes: np.ndarray
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class _FeatureViewState:
|
|
61
|
+
"""Feature-view metadata used to build source elements."""
|
|
62
|
+
|
|
63
|
+
perturbation_embeddings_matrix: np.ndarray | None
|
|
64
|
+
barcodes: np.ndarray | None
|
|
65
|
+
additional_obs: tuple[str, ...]
|
|
66
|
+
hvg_indices: np.ndarray | None
|
|
67
|
+
gene_names: tuple[str, ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _encode_obs_column(labels: np.ndarray) -> _EncodedObsColumn:
|
|
71
|
+
"""Encode one categorical obs column into reusable lookup tables."""
|
|
72
|
+
categories = np.array(sorted(set(labels)))
|
|
73
|
+
label_to_code = {label: idx for idx, label in enumerate(categories)}
|
|
74
|
+
codes = np.array([label_to_code[label] for label in labels], dtype=np.int32)
|
|
75
|
+
onehot_matrix = np.eye(len(categories), dtype=np.float32)
|
|
76
|
+
return _EncodedObsColumn(
|
|
77
|
+
codes=codes,
|
|
78
|
+
categories=categories,
|
|
79
|
+
labels=labels,
|
|
80
|
+
onehot_matrix=onehot_matrix,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class _PerturbationMetadataConfig:
|
|
86
|
+
"""Perturbation metadata column and passthrough configuration."""
|
|
87
|
+
|
|
88
|
+
pert_col: str = "perturbation"
|
|
89
|
+
cell_type_col: str = "cell_type"
|
|
90
|
+
batch_col: str = "batch"
|
|
91
|
+
control_pert: str = "non-targeting"
|
|
92
|
+
include_barcodes: bool = False
|
|
93
|
+
additional_obs: tuple[str, ...] = ()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class _PerturbationViewConfig:
|
|
98
|
+
"""Perturbation output-view and feature configuration."""
|
|
99
|
+
|
|
100
|
+
output_space: str = "gene"
|
|
101
|
+
embedding_key: str | None = None
|
|
102
|
+
hvg_col: str | None = None
|
|
103
|
+
should_yield_controls: bool = True
|
|
104
|
+
perturbation_features_file: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class PerturbationSourceConfig(
|
|
109
|
+
_PerturbationMetadataConfig,
|
|
110
|
+
_PerturbationViewConfig,
|
|
111
|
+
AnnDataSourceConfig,
|
|
112
|
+
):
|
|
113
|
+
"""Configuration for PerturbationAnnDataSource."""
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
"""Validate perturbation-specific configuration."""
|
|
117
|
+
super().__post_init__()
|
|
118
|
+
|
|
119
|
+
OutputSpaceMode(self.output_space)
|
|
120
|
+
if self.output_space == OutputSpaceMode.EMBEDDING.value and self.embedding_key is None:
|
|
121
|
+
raise ValueError("embedding_key is required when output_space='embedding'")
|
|
122
|
+
|
|
123
|
+
if len(set(self.additional_obs)) != len(self.additional_obs):
|
|
124
|
+
raise ValueError("additional_obs must not contain duplicates")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PerturbationAnnDataSource(AnnDataSource):
|
|
128
|
+
"""Perturbation-aware eager-loading AnnData source.
|
|
129
|
+
|
|
130
|
+
Extends :class:`AnnDataSource` with:
|
|
131
|
+
|
|
132
|
+
- Perturbation / cell type / batch metadata extraction and integer encoding
|
|
133
|
+
- Control cell identification via a boolean mask
|
|
134
|
+
- HVG subsetting and output space selection
|
|
135
|
+
- One-hot perturbation maps (or external embeddings from file)
|
|
136
|
+
- Cell barcode tracking
|
|
137
|
+
- Additional obs column passthrough
|
|
138
|
+
|
|
139
|
+
Output dictionary keys (in addition to AnnDataSource keys):
|
|
140
|
+
|
|
141
|
+
- ``pert_code``: Integer-encoded perturbation.
|
|
142
|
+
- ``cell_type_code``: Integer-encoded cell type.
|
|
143
|
+
- ``batch_code``: Integer-encoded batch.
|
|
144
|
+
- ``is_control``: Boolean flag.
|
|
145
|
+
- ``pert_emb``: One-hot or external perturbation embedding.
|
|
146
|
+
- ``cell_type_onehot``: Cell type one-hot vector.
|
|
147
|
+
- ``batch_onehot``: Batch one-hot vector.
|
|
148
|
+
- ``pert_name``: Perturbation label string.
|
|
149
|
+
- ``cell_type_name``: Cell type label string.
|
|
150
|
+
- ``batch_name``: Batch label string.
|
|
151
|
+
- ``barcode``: Cell barcode (if ``include_barcodes=True``).
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
config: PerturbationSourceConfig,
|
|
157
|
+
*,
|
|
158
|
+
rngs: nnx.Rngs | None = None,
|
|
159
|
+
name: str | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Initialize PerturbationAnnDataSource.
|
|
162
|
+
|
|
163
|
+
Loads data eagerly and extracts perturbation metadata.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
config: Source configuration.
|
|
167
|
+
rngs: Optional RNG state for shuffling.
|
|
168
|
+
name: Optional module name.
|
|
169
|
+
"""
|
|
170
|
+
# Skip AnnDataSource.__init__ — we need to customize loading
|
|
171
|
+
# Call DataSourceModule.__init__ directly
|
|
172
|
+
if name is None:
|
|
173
|
+
name = f"PerturbationAnnDataSource({config.file_path})"
|
|
174
|
+
|
|
175
|
+
from datarax.core.data_source import DataSourceModule # noqa: PLC0415
|
|
176
|
+
|
|
177
|
+
DataSourceModule.__init__(self, config, rngs=rngs, name=name)
|
|
178
|
+
|
|
179
|
+
adata = read_h5ad(config)
|
|
180
|
+
|
|
181
|
+
# -- Count matrix --
|
|
182
|
+
full_counts = jnp.array(to_dense_array(adata.X))
|
|
183
|
+
|
|
184
|
+
# -- HVG indices --
|
|
185
|
+
hvg_indices: np.ndarray | None = None
|
|
186
|
+
if config.hvg_col is not None and config.hvg_col in adata.var.columns:
|
|
187
|
+
hvg_indices = np.where(np.asarray(adata.var[config.hvg_col]))[0]
|
|
188
|
+
|
|
189
|
+
# -- Apply output space selection --
|
|
190
|
+
counts = select_output_counts(
|
|
191
|
+
full_counts, hvg_indices, OutputSpaceMode(config.output_space)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
obs, var, obsm = extract_anndata_annotations(adata)
|
|
195
|
+
|
|
196
|
+
required_obs_cols = (
|
|
197
|
+
config.pert_col,
|
|
198
|
+
config.cell_type_col,
|
|
199
|
+
config.batch_col,
|
|
200
|
+
*config.additional_obs,
|
|
201
|
+
)
|
|
202
|
+
missing_obs_cols = tuple(column for column in required_obs_cols if column not in obs)
|
|
203
|
+
if missing_obs_cols:
|
|
204
|
+
missing = ", ".join(missing_obs_cols)
|
|
205
|
+
raise ValueError(f"additional_obs/required obs columns missing from AnnData: {missing}")
|
|
206
|
+
|
|
207
|
+
if config.embedding_key is not None:
|
|
208
|
+
if config.embedding_key not in obsm:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"embedding_key '{config.embedding_key}' not found in AnnData.obsm"
|
|
211
|
+
)
|
|
212
|
+
obsm = {config.embedding_key: obsm[config.embedding_key]}
|
|
213
|
+
|
|
214
|
+
# -- Perturbation metadata --
|
|
215
|
+
perturbation = _encode_obs_column(np.asarray(obs[config.pert_col]))
|
|
216
|
+
cell_type = _encode_obs_column(np.asarray(obs[config.cell_type_col]))
|
|
217
|
+
batch = _encode_obs_column(np.asarray(obs[config.batch_col]))
|
|
218
|
+
|
|
219
|
+
# Control mask
|
|
220
|
+
control_mask = perturbation.labels == config.control_pert
|
|
221
|
+
|
|
222
|
+
# Group codes: ravel_multi_index for fast (celltype, pert) grouping
|
|
223
|
+
group_codes = np.ravel_multi_index(
|
|
224
|
+
(cell_type.codes, perturbation.codes),
|
|
225
|
+
(len(cell_type.categories), len(perturbation.categories)),
|
|
226
|
+
).astype(np.int32)
|
|
227
|
+
|
|
228
|
+
# External perturbation embeddings (overrides one-hot)
|
|
229
|
+
pert_embeddings_matrix: np.ndarray | None = None
|
|
230
|
+
if config.perturbation_features_file is not None:
|
|
231
|
+
from diffbio.sources.embeddings import ( # noqa: PLC0415
|
|
232
|
+
EmbeddingArtifactSource,
|
|
233
|
+
EmbeddingArtifactSourceConfig,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
ext_emb = EmbeddingArtifactSource(
|
|
237
|
+
EmbeddingArtifactSourceConfig(
|
|
238
|
+
file_path=str(Path(config.perturbation_features_file))
|
|
239
|
+
)
|
|
240
|
+
).embeddings
|
|
241
|
+
pert_embeddings_matrix = np.asarray(ext_emb)
|
|
242
|
+
if pert_embeddings_matrix.shape[0] != len(perturbation.categories):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"perturbation_features_file must have one row per perturbation category; "
|
|
245
|
+
f"expected {len(perturbation.categories)}, "
|
|
246
|
+
f"got {pert_embeddings_matrix.shape[0]}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Barcodes
|
|
250
|
+
barcodes: np.ndarray | None = None
|
|
251
|
+
if config.include_barcodes:
|
|
252
|
+
if "barcode" not in obs:
|
|
253
|
+
raise ValueError("barcode column is required when include_barcodes=True")
|
|
254
|
+
barcodes = np.asarray(obs["barcode"])
|
|
255
|
+
|
|
256
|
+
# -- Visible index mapping for should_yield_controls --
|
|
257
|
+
if config.should_yield_controls:
|
|
258
|
+
visible_indices = np.arange(adata.n_obs, dtype=np.int64)
|
|
259
|
+
else:
|
|
260
|
+
visible_indices = np.where(~control_mask)[0].astype(np.int64)
|
|
261
|
+
|
|
262
|
+
# -- Store all data --
|
|
263
|
+
self._initialize_loaded_source(
|
|
264
|
+
config=config,
|
|
265
|
+
adata=adata,
|
|
266
|
+
data=build_anndata_data(counts=counts, obs=obs, var=var, obsm=obsm),
|
|
267
|
+
length=len(visible_indices),
|
|
268
|
+
)
|
|
269
|
+
self._visible_indices = visible_indices
|
|
270
|
+
|
|
271
|
+
self._categorical_state = nnx.static(
|
|
272
|
+
_CategoricalMetadataState(
|
|
273
|
+
perturbation=perturbation,
|
|
274
|
+
cell_type=cell_type,
|
|
275
|
+
batch=batch,
|
|
276
|
+
control_mask=control_mask,
|
|
277
|
+
group_codes=group_codes,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
self._feature_state = nnx.static(
|
|
281
|
+
_FeatureViewState(
|
|
282
|
+
perturbation_embeddings_matrix=pert_embeddings_matrix,
|
|
283
|
+
barcodes=barcodes,
|
|
284
|
+
additional_obs=config.additional_obs,
|
|
285
|
+
hvg_indices=hvg_indices,
|
|
286
|
+
gene_names=tuple(adata.var_names),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# =================================================================
|
|
291
|
+
# DataSourceModule protocol overrides
|
|
292
|
+
# =================================================================
|
|
293
|
+
|
|
294
|
+
def __getitem__(self, idx: int) -> dict[str, Any]:
|
|
295
|
+
"""Get data for a single cell by index, including perturbation metadata.
|
|
296
|
+
|
|
297
|
+
When ``should_yield_controls=False``, indices are remapped to skip
|
|
298
|
+
control cells transparently.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
idx: Cell index (supports negative indexing).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dict with counts, perturbation metadata, and embeddings.
|
|
305
|
+
"""
|
|
306
|
+
if idx < 0:
|
|
307
|
+
idx = self.length + idx
|
|
308
|
+
if idx < 0 or idx >= self.length:
|
|
309
|
+
raise IndexError(f"Cell index {idx} out of range for dataset with {self.length} cells")
|
|
310
|
+
internal_idx = int(self._visible_indices[idx])
|
|
311
|
+
return self._build_pert_element(internal_idx)
|
|
312
|
+
|
|
313
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
314
|
+
"""Iterate over visible cells with optional shuffling."""
|
|
315
|
+
return self._iter_with_builder(self._build_visible_element)
|
|
316
|
+
|
|
317
|
+
def get_batch(self, batch_size: int, key: jax.Array | None = None) -> dict[str, Any]:
|
|
318
|
+
"""Get a batch of cells with perturbation metadata.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
batch_size: Number of cells.
|
|
322
|
+
key: Optional RNG key for stateless random sampling.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Batched dictionary.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
def _gather(data: dict[str, Any], indices: jax.Array) -> dict[str, Any]:
|
|
329
|
+
np_indices = np.array(indices)
|
|
330
|
+
return self._build_batch_element(np_indices)
|
|
331
|
+
|
|
332
|
+
return self._get_batch_with_gather(batch_size, key, _gather)
|
|
333
|
+
|
|
334
|
+
# =================================================================
|
|
335
|
+
# Perturbation-specific public API
|
|
336
|
+
# =================================================================
|
|
337
|
+
|
|
338
|
+
def get_control_mask(self) -> np.ndarray:
|
|
339
|
+
"""Return boolean mask where True indicates a control cell."""
|
|
340
|
+
return self._categorical_state.control_mask
|
|
341
|
+
|
|
342
|
+
def get_pert_codes(self) -> np.ndarray:
|
|
343
|
+
"""Return per-cell integer perturbation codes."""
|
|
344
|
+
return self._categorical_state.perturbation.codes
|
|
345
|
+
|
|
346
|
+
def get_cell_type_codes(self) -> np.ndarray:
|
|
347
|
+
"""Return per-cell integer cell type codes."""
|
|
348
|
+
return self._categorical_state.cell_type.codes
|
|
349
|
+
|
|
350
|
+
def get_batch_codes(self) -> np.ndarray:
|
|
351
|
+
"""Return per-cell integer batch codes."""
|
|
352
|
+
return self._categorical_state.batch.codes
|
|
353
|
+
|
|
354
|
+
def get_pert_categories(self) -> np.ndarray:
|
|
355
|
+
"""Return sorted array of unique perturbation labels."""
|
|
356
|
+
return np.array(self._categorical_state.perturbation.categories)
|
|
357
|
+
|
|
358
|
+
def get_cell_type_categories(self) -> np.ndarray:
|
|
359
|
+
"""Return sorted array of unique cell type labels."""
|
|
360
|
+
return np.array(self._categorical_state.cell_type.categories)
|
|
361
|
+
|
|
362
|
+
def get_batch_categories(self) -> np.ndarray:
|
|
363
|
+
"""Return sorted array of unique batch labels."""
|
|
364
|
+
return np.array(self._categorical_state.batch.categories)
|
|
365
|
+
|
|
366
|
+
def get_group_codes(self) -> np.ndarray:
|
|
367
|
+
"""Return per-cell group codes for (cell_type, perturbation) grouping."""
|
|
368
|
+
return self._categorical_state.group_codes
|
|
369
|
+
|
|
370
|
+
def get_onehot_map(self) -> dict[str, jnp.ndarray]:
|
|
371
|
+
"""Return perturbation one-hot encoding map (JAX arrays)."""
|
|
372
|
+
categories = self._categorical_state.perturbation.categories
|
|
373
|
+
matrix = self._categorical_state.perturbation.onehot_matrix
|
|
374
|
+
return {str(category): jnp.array(matrix[i]) for i, category in enumerate(categories)}
|
|
375
|
+
|
|
376
|
+
def get_gene_names(self, output_space: str = "all") -> list[str]:
|
|
377
|
+
"""Return gene names, optionally filtered by output space.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
output_space: ``"all"`` for all genes, ``"gene"`` for HVG subset.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
List of gene name strings.
|
|
384
|
+
"""
|
|
385
|
+
if output_space == "gene" and self._feature_state.hvg_indices is not None:
|
|
386
|
+
return [self._feature_state.gene_names[i] for i in self._feature_state.hvg_indices]
|
|
387
|
+
return list(self._feature_state.gene_names)
|
|
388
|
+
|
|
389
|
+
def get_n_genes(self) -> int:
|
|
390
|
+
"""Return total number of genes."""
|
|
391
|
+
return len(self._feature_state.gene_names)
|
|
392
|
+
|
|
393
|
+
def get_var_dims(self) -> dict[str, int]:
|
|
394
|
+
"""Return dimensionality info for the dataset.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dict with ``n_genes``, ``n_cells``, ``n_perts``,
|
|
398
|
+
``n_cell_types``, ``n_batches``.
|
|
399
|
+
"""
|
|
400
|
+
return {
|
|
401
|
+
"n_genes": len(self._feature_state.gene_names),
|
|
402
|
+
"n_cells": self.length,
|
|
403
|
+
"n_perts": len(self._categorical_state.perturbation.categories),
|
|
404
|
+
"n_cell_types": len(self._categorical_state.cell_type.categories),
|
|
405
|
+
"n_batches": len(self._categorical_state.batch.categories),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# =================================================================
|
|
409
|
+
# Internal helpers
|
|
410
|
+
# =================================================================
|
|
411
|
+
|
|
412
|
+
def _build_pert_element(self, idx: int) -> dict[str, Any]:
|
|
413
|
+
"""Build a per-cell dictionary with perturbation metadata."""
|
|
414
|
+
cell_counts = self.data["counts"][idx]
|
|
415
|
+
cell_obs = {col: arr[idx] for col, arr in self.data["obs"].items()}
|
|
416
|
+
cell_obsm: dict[str, jnp.ndarray] = {}
|
|
417
|
+
for emb_name, emb_arr in self.data["obsm"].items():
|
|
418
|
+
cell_obsm[emb_name] = emb_arr[idx]
|
|
419
|
+
|
|
420
|
+
# Perturbation embedding (indexed by code, converted to JAX)
|
|
421
|
+
categorical_state = self._categorical_state
|
|
422
|
+
feature_state = self._feature_state
|
|
423
|
+
pc = int(categorical_state.perturbation.codes[idx])
|
|
424
|
+
cc = int(categorical_state.cell_type.codes[idx])
|
|
425
|
+
bc = int(categorical_state.batch.codes[idx])
|
|
426
|
+
|
|
427
|
+
if feature_state.perturbation_embeddings_matrix is not None:
|
|
428
|
+
pert_emb = jnp.array(feature_state.perturbation_embeddings_matrix[pc])
|
|
429
|
+
else:
|
|
430
|
+
pert_emb = jnp.array(categorical_state.perturbation.onehot_matrix[pc])
|
|
431
|
+
|
|
432
|
+
pert_name = str(categorical_state.perturbation.labels[idx])
|
|
433
|
+
ct_name = str(categorical_state.cell_type.labels[idx])
|
|
434
|
+
batch_name = str(categorical_state.batch.labels[idx])
|
|
435
|
+
|
|
436
|
+
element: dict[str, Any] = {
|
|
437
|
+
"counts": cell_counts,
|
|
438
|
+
"obs": cell_obs,
|
|
439
|
+
"obsm": cell_obsm,
|
|
440
|
+
"pert_code": pc,
|
|
441
|
+
"cell_type_code": cc,
|
|
442
|
+
"batch_code": bc,
|
|
443
|
+
"is_control": bool(categorical_state.control_mask[idx]),
|
|
444
|
+
"pert_emb": pert_emb,
|
|
445
|
+
"cell_type_onehot": jnp.array(categorical_state.cell_type.onehot_matrix[cc]),
|
|
446
|
+
"batch_onehot": jnp.array(categorical_state.batch.onehot_matrix[bc]),
|
|
447
|
+
"pert_name": pert_name,
|
|
448
|
+
"cell_type_name": ct_name,
|
|
449
|
+
"batch_name": batch_name,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
for column in feature_state.additional_obs:
|
|
453
|
+
element[column] = cell_obs[column]
|
|
454
|
+
|
|
455
|
+
if feature_state.barcodes is not None:
|
|
456
|
+
element["barcode"] = str(feature_state.barcodes[idx])
|
|
457
|
+
|
|
458
|
+
return element
|
|
459
|
+
|
|
460
|
+
def _build_pert_element_from_data(self, data: dict[str, Any], idx: int) -> dict[str, Any]:
|
|
461
|
+
"""Build element from data dict (used by eager_iter callback)."""
|
|
462
|
+
return self._build_pert_element(idx)
|
|
463
|
+
|
|
464
|
+
def _build_visible_element(self, data: dict[str, Any], idx: int) -> dict[str, Any]:
|
|
465
|
+
"""Build element with visible index remapping (used by __iter__)."""
|
|
466
|
+
internal_idx = int(self._visible_indices[idx])
|
|
467
|
+
return self._build_pert_element(internal_idx)
|
|
468
|
+
|
|
469
|
+
def _build_batch_element(self, indices: np.ndarray) -> dict[str, Any]:
|
|
470
|
+
"""Build a batched dictionary for multiple cells."""
|
|
471
|
+
counts = self.data["counts"][indices]
|
|
472
|
+
obs = {col: np.asarray(arr)[indices] for col, arr in self.data["obs"].items()}
|
|
473
|
+
obsm: dict[str, jnp.ndarray] = {}
|
|
474
|
+
for emb_name, emb_arr in self.data["obsm"].items():
|
|
475
|
+
obsm[emb_name] = emb_arr[indices]
|
|
476
|
+
|
|
477
|
+
categorical_state = self._categorical_state
|
|
478
|
+
feature_state = self._feature_state
|
|
479
|
+
pert_codes = categorical_state.perturbation.codes[indices]
|
|
480
|
+
ct_codes = categorical_state.cell_type.codes[indices]
|
|
481
|
+
batch_codes = categorical_state.batch.codes[indices]
|
|
482
|
+
|
|
483
|
+
# Build batched embeddings (indexed by codes, converted to JAX)
|
|
484
|
+
if feature_state.perturbation_embeddings_matrix is not None:
|
|
485
|
+
pert_embs = jnp.array(feature_state.perturbation_embeddings_matrix[pert_codes])
|
|
486
|
+
else:
|
|
487
|
+
pert_embs = jnp.array(categorical_state.perturbation.onehot_matrix[pert_codes])
|
|
488
|
+
|
|
489
|
+
transformed_data = {
|
|
490
|
+
"counts": counts,
|
|
491
|
+
"obs": obs,
|
|
492
|
+
"obsm": obsm,
|
|
493
|
+
"pert_code": jnp.array(pert_codes),
|
|
494
|
+
"cell_type_code": jnp.array(ct_codes),
|
|
495
|
+
"batch_code": jnp.array(batch_codes),
|
|
496
|
+
"is_control": jnp.array(categorical_state.control_mask[indices]),
|
|
497
|
+
"pert_emb": pert_embs,
|
|
498
|
+
"cell_type_onehot": jnp.array(categorical_state.cell_type.onehot_matrix[ct_codes]),
|
|
499
|
+
"batch_onehot": jnp.array(categorical_state.batch.onehot_matrix[batch_codes]),
|
|
500
|
+
"pert_name": [str(categorical_state.perturbation.labels[i]) for i in indices],
|
|
501
|
+
"cell_type_name": [str(categorical_state.cell_type.labels[i]) for i in indices],
|
|
502
|
+
"batch_name": [str(categorical_state.batch.labels[i]) for i in indices],
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
for column in feature_state.additional_obs:
|
|
506
|
+
transformed_data[column] = obs[column]
|
|
507
|
+
|
|
508
|
+
if feature_state.barcodes is not None:
|
|
509
|
+
transformed_data["barcode"] = (
|
|
510
|
+
np.asarray(feature_state.barcodes)[indices].astype(str).tolist()
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return transformed_data
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""SeqFISH cortex spatial transcriptomics DataSource.
|
|
2
|
+
|
|
3
|
+
Loads the seqFISH mouse cortex dataset (Lohoff et al., Nature
|
|
4
|
+
Biotechnology 2022) from a pre-downloaded h5ad file. This dataset
|
|
5
|
+
provides spatially resolved gene expression for 19,416 cells with
|
|
6
|
+
351 genes and 22 cell types.
|
|
7
|
+
|
|
8
|
+
The dataset must be pre-downloaded (e.g. via squidpy) to a local
|
|
9
|
+
directory. See ``benchmarks/README.md`` for download instructions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
import numpy as np
|
|
21
|
+
from datarax.core.config import StructuralConfig
|
|
22
|
+
from flax import nnx
|
|
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
|
+
from diffbio.sources.multiomics import build_multiomics_dataset_provenance
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_FILENAME = "seqfish_cortex.h5ad"
|
|
34
|
+
_SEQFISH_MODALITIES = ("rna", "spatial")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, kw_only=True)
|
|
38
|
+
class SeqFISHConfig(StructuralConfig):
|
|
39
|
+
"""Configuration for SeqFISHSource.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
data_dir: Directory containing the seqfish_cortex.h5ad file.
|
|
43
|
+
subsample: If set, randomly subsample this many cells.
|
|
44
|
+
Use for quick/CI benchmark runs.
|
|
45
|
+
label_key: Column name in obs for cell type labels.
|
|
46
|
+
spatial_key: Key in obsm for spatial coordinates.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
data_dir: str = "/media/mahdi/ssd23/Data/spatial"
|
|
50
|
+
subsample: int | None = None
|
|
51
|
+
label_key: str = "celltype_mapped_refined"
|
|
52
|
+
spatial_key: str = "spatial"
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
"""Validate configuration."""
|
|
56
|
+
super().__post_init__()
|
|
57
|
+
path = Path(self.data_dir) / _FILENAME
|
|
58
|
+
if not path.exists():
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
f"Dataset not found: {path}. Download via squidpy: squidpy.datasets.seqfish()"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SeqFISHSource(BenchmarkDataSource):
|
|
65
|
+
"""DataSource for the seqFISH mouse cortex dataset.
|
|
66
|
+
|
|
67
|
+
Loads spatially resolved gene expression (19,416 cells, 351
|
|
68
|
+
genes, 22 cell types) from a pre-downloaded h5ad file.
|
|
69
|
+
|
|
70
|
+
Follows the datarax DataSourceModule pattern: eager loading
|
|
71
|
+
at init, dict-based access via ``load()``, length via
|
|
72
|
+
``__len__``.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```python
|
|
76
|
+
config = SeqFISHConfig(data_dir="/path/to/data")
|
|
77
|
+
source = SeqFISHSource(config)
|
|
78
|
+
data = source.load()
|
|
79
|
+
print(data["counts"].shape) # (19416, 351)
|
|
80
|
+
```
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
iter_static_keys = ("gene_names", "cell_type_names")
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
config: SeqFISHConfig,
|
|
88
|
+
*,
|
|
89
|
+
rngs: nnx.Rngs | None = None,
|
|
90
|
+
name: str | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Load the seqFISH cortex dataset.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
config: Configuration with data directory and options.
|
|
96
|
+
rngs: Optional RNG state (unused, for interface compat).
|
|
97
|
+
name: Optional module name.
|
|
98
|
+
"""
|
|
99
|
+
super().__init__(config, rngs=rngs, name=name or "SeqFISHSource")
|
|
100
|
+
self.data = self._load(config)
|
|
101
|
+
self._log_loaded_summary(logger, "seqfish_cortex", ("n_cells", "n_genes", "n_types"))
|
|
102
|
+
|
|
103
|
+
def _load(self, config: SeqFISHConfig) -> dict[str, Any]:
|
|
104
|
+
"""Load and preprocess the h5ad file."""
|
|
105
|
+
adata, counts = self._load_benchmark_counts(config, _FILENAME, _to_dense)
|
|
106
|
+
|
|
107
|
+
# Encode cell type labels as integer codes
|
|
108
|
+
cell_type_labels, cell_type_names = encode_label_column(
|
|
109
|
+
adata.obs[config.label_key],
|
|
110
|
+
include_names=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Spatial coordinates
|
|
114
|
+
spatial_coords = jnp.array(np.asarray(adata.obsm[config.spatial_key], dtype=np.float32))
|
|
115
|
+
|
|
116
|
+
gene_names = list(adata.var_names)
|
|
117
|
+
dataset_path = Path(config.data_dir) / _FILENAME
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"counts": counts,
|
|
121
|
+
"cell_ids": tuple(str(cell_id) for cell_id in adata.obs_names),
|
|
122
|
+
"cell_type_labels": cell_type_labels,
|
|
123
|
+
"cell_type_names": cell_type_names,
|
|
124
|
+
"spatial_coords": spatial_coords,
|
|
125
|
+
"gene_names": gene_names,
|
|
126
|
+
"n_cells": adata.n_obs,
|
|
127
|
+
"n_genes": adata.n_vars,
|
|
128
|
+
"n_types": int(len(np.unique(cell_type_labels))),
|
|
129
|
+
"dataset_provenance": build_multiomics_dataset_provenance(
|
|
130
|
+
dataset_name="seqfish_cortex",
|
|
131
|
+
source_type="curated_spatial_transcriptomics",
|
|
132
|
+
modalities=_SEQFISH_MODALITIES,
|
|
133
|
+
curation_status="download_required_local_h5ad",
|
|
134
|
+
biological_validation="published_benchmark_dataset",
|
|
135
|
+
promotion_eligible=True,
|
|
136
|
+
source_path=str(dataset_path),
|
|
137
|
+
),
|
|
138
|
+
"modality_contract": {
|
|
139
|
+
"modalities": list(_SEQFISH_MODALITIES),
|
|
140
|
+
"primary_modality": "rna",
|
|
141
|
+
"spatial_key": config.spatial_key,
|
|
142
|
+
"label_key": config.label_key,
|
|
143
|
+
"count_key": "X",
|
|
144
|
+
},
|
|
145
|
+
}
|