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,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
+ }