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,115 @@
|
|
|
1
|
+
"""Multi-dataset concatenation source for perturbation experiments.
|
|
2
|
+
|
|
3
|
+
Combines multiple PerturbationAnnDataSource instances into a single unified
|
|
4
|
+
source with global indexing. Validates metadata consistency across sources.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- cell-load/src/cell_load/dataset/_metadata.py (MetadataConcatDataset)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PerturbationConcatSource:
|
|
22
|
+
"""Concatenation of multiple PerturbationAnnDataSource instances.
|
|
23
|
+
|
|
24
|
+
Provides unified indexing across multiple sources. Global index ``i``
|
|
25
|
+
is mapped to the correct underlying source and local index.
|
|
26
|
+
|
|
27
|
+
Validates that all sources share consistent metadata column names
|
|
28
|
+
(control_pert, pert_col, etc.).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
sources: List of PerturbationAnnDataSource instances.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If no sources are provided.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
sources: list[Any],
|
|
40
|
+
) -> None:
|
|
41
|
+
if not sources:
|
|
42
|
+
raise ValueError("PerturbationConcatSource requires at least one source.")
|
|
43
|
+
|
|
44
|
+
self._sources = sources
|
|
45
|
+
|
|
46
|
+
# Build cumulative length offsets for global -> local index mapping
|
|
47
|
+
self._offsets: list[int] = []
|
|
48
|
+
cumulative = 0
|
|
49
|
+
for s in sources:
|
|
50
|
+
self._offsets.append(cumulative)
|
|
51
|
+
cumulative += len(s)
|
|
52
|
+
self._total_length = cumulative
|
|
53
|
+
|
|
54
|
+
def __len__(self) -> int:
|
|
55
|
+
"""Return total number of cells across all sources."""
|
|
56
|
+
return self._total_length
|
|
57
|
+
|
|
58
|
+
def __getitem__(self, idx: int) -> dict[str, Any]:
|
|
59
|
+
"""Get element by global index.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
idx: Global cell index.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Per-cell dictionary from the appropriate source.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
IndexError: If index is out of range.
|
|
69
|
+
"""
|
|
70
|
+
if idx < 0:
|
|
71
|
+
idx = self._total_length + idx
|
|
72
|
+
if idx < 0 or idx >= self._total_length:
|
|
73
|
+
raise IndexError(
|
|
74
|
+
f"Index {idx} out of range for concat source with {self._total_length} cells"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
source_idx, local_idx = self._global_to_local(idx)
|
|
78
|
+
return self._sources[source_idx][local_idx]
|
|
79
|
+
|
|
80
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
81
|
+
"""Iterate over all cells across all sources in order."""
|
|
82
|
+
for source in self._sources:
|
|
83
|
+
yield from source
|
|
84
|
+
|
|
85
|
+
def get_control_mask(self) -> np.ndarray:
|
|
86
|
+
"""Return concatenated boolean control mask."""
|
|
87
|
+
return np.concatenate([s.get_control_mask() for s in self._sources])
|
|
88
|
+
|
|
89
|
+
def get_group_codes(self) -> np.ndarray:
|
|
90
|
+
"""Return concatenated group codes."""
|
|
91
|
+
return np.concatenate([s.get_group_codes() for s in self._sources])
|
|
92
|
+
|
|
93
|
+
def get_pert_codes(self) -> np.ndarray:
|
|
94
|
+
"""Return concatenated perturbation codes."""
|
|
95
|
+
return np.concatenate([s.get_pert_codes() for s in self._sources])
|
|
96
|
+
|
|
97
|
+
def get_cell_type_codes(self) -> np.ndarray:
|
|
98
|
+
"""Return concatenated cell type codes."""
|
|
99
|
+
return np.concatenate([s.get_cell_type_codes() for s in self._sources])
|
|
100
|
+
|
|
101
|
+
def get_batch_codes(self) -> np.ndarray:
|
|
102
|
+
"""Return concatenated batch codes."""
|
|
103
|
+
return np.concatenate([s.get_batch_codes() for s in self._sources])
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def sources(self) -> list[Any]:
|
|
107
|
+
"""Return the underlying source list."""
|
|
108
|
+
return self._sources
|
|
109
|
+
|
|
110
|
+
def _global_to_local(self, global_idx: int) -> tuple[int, int]:
|
|
111
|
+
"""Map a global index to (source_index, local_index)."""
|
|
112
|
+
for i in range(len(self._sources) - 1, -1, -1):
|
|
113
|
+
if global_idx >= self._offsets[i]:
|
|
114
|
+
return i, global_idx - self._offsets[i]
|
|
115
|
+
return 0, global_idx # pragma: no cover
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Control cell mapping strategies for perturbation experiments.
|
|
2
|
+
|
|
3
|
+
Maps perturbed cells to control cells using batch-based or random strategies.
|
|
4
|
+
Mappings are precomputed at setup time as numpy index arrays.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- cell-load/src/cell_load/mapping_strategies/batch.py
|
|
8
|
+
- cell-load/src/cell_load/mapping_strategies/random.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from datarax.core.config import StructuralConfig
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ControlMappingConfig(StructuralConfig):
|
|
26
|
+
"""Configuration for control cell mapping.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
strategy: Mapping strategy (``"batch"`` or ``"random"``).
|
|
30
|
+
n_basal_samples: Number of control cells per perturbed cell.
|
|
31
|
+
seed: Random seed for reproducibility.
|
|
32
|
+
map_controls: Whether to also map control cells to other controls.
|
|
33
|
+
cache_pairs: Whether to cache the mapping after first computation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
strategy: str = "random"
|
|
37
|
+
n_basal_samples: int = 1
|
|
38
|
+
seed: int = 42
|
|
39
|
+
map_controls: bool = False
|
|
40
|
+
cache_pairs: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_ctrl_pools_by_ct(ctrl_mask: np.ndarray, ct_codes: np.ndarray) -> dict[int, np.ndarray]:
|
|
44
|
+
"""Build control index pools grouped by cell type."""
|
|
45
|
+
pools: dict[int, list[int]] = {}
|
|
46
|
+
for idx in np.where(ctrl_mask)[0]:
|
|
47
|
+
ct = int(ct_codes[idx])
|
|
48
|
+
if ct not in pools:
|
|
49
|
+
pools[ct] = []
|
|
50
|
+
pools[ct].append(idx)
|
|
51
|
+
return {k: np.array(v) for k, v in pools.items()}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _map_cells_to_controls(
|
|
55
|
+
cell_indices: np.ndarray,
|
|
56
|
+
ct_codes: np.ndarray,
|
|
57
|
+
ctrl_by_ct: dict[int, np.ndarray],
|
|
58
|
+
n_basal: int,
|
|
59
|
+
rng: np.random.Generator,
|
|
60
|
+
) -> np.ndarray:
|
|
61
|
+
"""Map a set of cell indices to control cells from the same cell type."""
|
|
62
|
+
mapping = np.empty((len(cell_indices), n_basal), dtype=np.int64)
|
|
63
|
+
for i, cidx in enumerate(cell_indices):
|
|
64
|
+
ct = int(ct_codes[cidx])
|
|
65
|
+
pool = ctrl_by_ct.get(ct, np.array([], dtype=np.int64))
|
|
66
|
+
if len(pool) == 0:
|
|
67
|
+
mapping[i] = -1
|
|
68
|
+
continue
|
|
69
|
+
chosen = rng.choice(pool, size=n_basal, replace=len(pool) < n_basal)
|
|
70
|
+
mapping[i] = chosen
|
|
71
|
+
return mapping
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RandomControlMapping:
|
|
75
|
+
"""Map perturbed cells to random controls of the same cell type.
|
|
76
|
+
|
|
77
|
+
For each perturbed cell, randomly selects ``n_basal_samples`` control cells
|
|
78
|
+
from the same cell type, pooled across all batches.
|
|
79
|
+
|
|
80
|
+
When ``map_controls=True``, also maps each control cell to another random
|
|
81
|
+
control of the same cell type.
|
|
82
|
+
|
|
83
|
+
When ``cache_pairs=True``, the mapping is computed once and cached for
|
|
84
|
+
subsequent calls.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Mapping configuration.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, config: ControlMappingConfig) -> None:
|
|
91
|
+
self._config = config
|
|
92
|
+
self._cached_mapping: np.ndarray | None = None
|
|
93
|
+
|
|
94
|
+
def build_mapping(self, source: Any) -> np.ndarray:
|
|
95
|
+
"""Build mapping from cells to control cells.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
source: A PerturbationAnnDataSource or PerturbationConcatSource.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Array of shape ``(n_mapped, n_basal_samples)`` with control
|
|
102
|
+
cell indices. When ``map_controls=False``, ``n_mapped`` equals
|
|
103
|
+
the number of perturbed cells. When ``True``, ``n_mapped``
|
|
104
|
+
equals the total number of cells.
|
|
105
|
+
"""
|
|
106
|
+
if self._config.cache_pairs and self._cached_mapping is not None:
|
|
107
|
+
return self._cached_mapping
|
|
108
|
+
|
|
109
|
+
ctrl_mask = source.get_control_mask()
|
|
110
|
+
ct_codes = source.get_cell_type_codes()
|
|
111
|
+
n_basal = self._config.n_basal_samples
|
|
112
|
+
rng = np.random.default_rng(self._config.seed)
|
|
113
|
+
|
|
114
|
+
ctrl_by_ct = _build_ctrl_pools_by_ct(ctrl_mask, ct_codes)
|
|
115
|
+
|
|
116
|
+
if self._config.map_controls:
|
|
117
|
+
# Map ALL cells (perturbed + controls) to controls
|
|
118
|
+
all_indices = np.arange(len(ctrl_mask))
|
|
119
|
+
mapping = _map_cells_to_controls(all_indices, ct_codes, ctrl_by_ct, n_basal, rng)
|
|
120
|
+
else:
|
|
121
|
+
# Map only perturbed cells
|
|
122
|
+
pert_indices = np.where(~ctrl_mask)[0]
|
|
123
|
+
mapping = _map_cells_to_controls(pert_indices, ct_codes, ctrl_by_ct, n_basal, rng)
|
|
124
|
+
|
|
125
|
+
if self._config.cache_pairs:
|
|
126
|
+
self._cached_mapping = mapping
|
|
127
|
+
|
|
128
|
+
return mapping
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class BatchControlMapping:
|
|
132
|
+
"""Map perturbed cells to controls within the same batch and cell type.
|
|
133
|
+
|
|
134
|
+
Prefers controls from the same (batch, cell_type) group. Falls back to
|
|
135
|
+
all controls from the same cell type if the batch group is empty.
|
|
136
|
+
|
|
137
|
+
When ``map_controls=True``, also maps control cells.
|
|
138
|
+
When ``cache_pairs=True``, caches after first computation.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
config: Mapping configuration.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, config: ControlMappingConfig) -> None:
|
|
145
|
+
self._config = config
|
|
146
|
+
self._cached_mapping: np.ndarray | None = None
|
|
147
|
+
|
|
148
|
+
def build_mapping(self, source: Any) -> np.ndarray:
|
|
149
|
+
"""Build mapping from cells to control cells.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
source: A PerturbationAnnDataSource or PerturbationConcatSource.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Array of shape ``(n_mapped, n_basal_samples)`` with control
|
|
156
|
+
cell indices.
|
|
157
|
+
"""
|
|
158
|
+
if self._config.cache_pairs and self._cached_mapping is not None:
|
|
159
|
+
return self._cached_mapping
|
|
160
|
+
|
|
161
|
+
ctrl_mask = source.get_control_mask()
|
|
162
|
+
ct_codes = source.get_cell_type_codes()
|
|
163
|
+
batch_codes = source.get_batch_codes()
|
|
164
|
+
n_basal = self._config.n_basal_samples
|
|
165
|
+
rng = np.random.default_rng(self._config.seed)
|
|
166
|
+
|
|
167
|
+
# Build control pools by (batch, cell_type) and by cell_type
|
|
168
|
+
ctrl_indices = np.where(ctrl_mask)[0]
|
|
169
|
+
|
|
170
|
+
ctrl_by_batch_ct: dict[tuple[int, int], list[int]] = {}
|
|
171
|
+
ctrl_by_ct: dict[int, list[int]] = {}
|
|
172
|
+
|
|
173
|
+
for idx in ctrl_indices:
|
|
174
|
+
ct = int(ct_codes[idx])
|
|
175
|
+
batch = int(batch_codes[idx])
|
|
176
|
+
key = (batch, ct)
|
|
177
|
+
if key not in ctrl_by_batch_ct:
|
|
178
|
+
ctrl_by_batch_ct[key] = []
|
|
179
|
+
ctrl_by_batch_ct[key].append(idx)
|
|
180
|
+
|
|
181
|
+
if ct not in ctrl_by_ct:
|
|
182
|
+
ctrl_by_ct[ct] = []
|
|
183
|
+
ctrl_by_ct[ct].append(idx)
|
|
184
|
+
|
|
185
|
+
ctrl_by_batch_ct_arr = {k: np.array(v) for k, v in ctrl_by_batch_ct.items()}
|
|
186
|
+
ctrl_by_ct_arr = {k: np.array(v) for k, v in ctrl_by_ct.items()}
|
|
187
|
+
|
|
188
|
+
# Determine which cells to map
|
|
189
|
+
if self._config.map_controls:
|
|
190
|
+
cells_to_map = np.arange(len(ctrl_mask))
|
|
191
|
+
else:
|
|
192
|
+
cells_to_map = np.where(~ctrl_mask)[0]
|
|
193
|
+
|
|
194
|
+
mapping = np.empty((len(cells_to_map), n_basal), dtype=np.int64)
|
|
195
|
+
|
|
196
|
+
for i, cidx in enumerate(cells_to_map):
|
|
197
|
+
ct = int(ct_codes[cidx])
|
|
198
|
+
batch = int(batch_codes[cidx])
|
|
199
|
+
key = (batch, ct)
|
|
200
|
+
|
|
201
|
+
pool = ctrl_by_batch_ct_arr.get(key)
|
|
202
|
+
if pool is None or len(pool) == 0:
|
|
203
|
+
pool = ctrl_by_ct_arr.get(ct, np.array([], dtype=np.int64))
|
|
204
|
+
|
|
205
|
+
if len(pool) == 0:
|
|
206
|
+
mapping[i] = -1
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
chosen = rng.choice(pool, size=n_basal, replace=len(pool) < n_basal)
|
|
210
|
+
mapping[i] = chosen
|
|
211
|
+
|
|
212
|
+
if self._config.cache_pairs:
|
|
213
|
+
self._cached_mapping = mapping
|
|
214
|
+
|
|
215
|
+
return mapping
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Experiment configuration for perturbation experiments.
|
|
2
|
+
|
|
3
|
+
Parses TOML configuration files following the cell-load schema and provides
|
|
4
|
+
frozen dataclass representations of experiment settings: dataset paths,
|
|
5
|
+
training assignments, zero-shot cell types, and few-shot perturbation splits.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- cell-load/src/cell_load/config.py (ExperimentConfig)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from datarax.core.config import StructuralConfig
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_toml() -> Any:
|
|
24
|
+
"""Import tomllib from the standard library (Python 3.11+)."""
|
|
25
|
+
import tomllib # noqa: PLC0415
|
|
26
|
+
|
|
27
|
+
return tomllib
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Data classes
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class DatasetEntry:
|
|
37
|
+
"""A single dataset in an experiment.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: Identifier for the dataset.
|
|
41
|
+
path: Filesystem path to the dataset directory or file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
path: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ZeroshotEntry:
|
|
50
|
+
"""A cell type held out entirely for zero-shot evaluation.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
dataset: Name of the dataset containing this cell type.
|
|
54
|
+
cell_type: Cell type to hold out.
|
|
55
|
+
split: Target split (``"val"`` or ``"test"``).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
dataset: str
|
|
59
|
+
cell_type: str
|
|
60
|
+
split: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class FewshotEntry:
|
|
65
|
+
"""Perturbations held out within a cell type for few-shot evaluation.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
dataset: Name of the dataset containing this cell type.
|
|
69
|
+
cell_type: Cell type within which perturbations are split.
|
|
70
|
+
val_perturbations: Perturbation names assigned to validation.
|
|
71
|
+
test_perturbations: Perturbation names assigned to testing.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
dataset: str
|
|
75
|
+
cell_type: str
|
|
76
|
+
val_perturbations: tuple[str, ...] = ()
|
|
77
|
+
test_perturbations: tuple[str, ...] = ()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ExperimentConfig(StructuralConfig):
|
|
82
|
+
"""Top-level experiment configuration.
|
|
83
|
+
|
|
84
|
+
Encapsulates all settings parsed from a TOML file: dataset paths,
|
|
85
|
+
training assignments, zero-shot cell type holdouts, and few-shot
|
|
86
|
+
perturbation splits.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
datasets: Registered dataset entries.
|
|
90
|
+
training_datasets: Names of datasets used for training.
|
|
91
|
+
zeroshot: Cell types held out entirely.
|
|
92
|
+
fewshot: Perturbation-level splits within cell types.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
datasets: tuple[DatasetEntry, ...] = ()
|
|
96
|
+
training_datasets: tuple[str, ...] = ()
|
|
97
|
+
zeroshot: tuple[ZeroshotEntry, ...] = ()
|
|
98
|
+
fewshot: tuple[FewshotEntry, ...] = ()
|
|
99
|
+
|
|
100
|
+
def get_all_datasets(self) -> set[str]:
|
|
101
|
+
"""Return the set of all dataset names referenced in the config."""
|
|
102
|
+
names = set(self.training_datasets)
|
|
103
|
+
for z in self.zeroshot:
|
|
104
|
+
names.add(z.dataset)
|
|
105
|
+
for f in self.fewshot:
|
|
106
|
+
names.add(f.dataset)
|
|
107
|
+
return names
|
|
108
|
+
|
|
109
|
+
def get_zeroshot_celltypes(self, dataset: str) -> dict[str, str]:
|
|
110
|
+
"""Get zero-shot cell types and their target splits for a dataset.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
dataset: Dataset name to filter by.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict mapping cell type names to split names.
|
|
117
|
+
"""
|
|
118
|
+
return {z.cell_type: z.split for z in self.zeroshot if z.dataset == dataset}
|
|
119
|
+
|
|
120
|
+
def get_fewshot_celltypes(self, dataset: str) -> dict[str, FewshotEntry]:
|
|
121
|
+
"""Get few-shot cell type entries for a dataset.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
dataset: Dataset name to filter by.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict mapping cell type names to FewshotEntry objects.
|
|
128
|
+
"""
|
|
129
|
+
return {f.cell_type: f for f in self.fewshot if f.dataset == dataset}
|
|
130
|
+
|
|
131
|
+
def validate(self) -> None:
|
|
132
|
+
"""Validate configuration consistency.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If referenced datasets lack paths or splits are invalid.
|
|
136
|
+
"""
|
|
137
|
+
all_referenced = self.get_all_datasets()
|
|
138
|
+
dataset_names = {d.name for d in self.datasets}
|
|
139
|
+
missing = all_referenced - dataset_names
|
|
140
|
+
if missing:
|
|
141
|
+
raise ValueError(f"Missing dataset paths for: {missing}")
|
|
142
|
+
|
|
143
|
+
valid_splits = {"train", "val", "test"}
|
|
144
|
+
for z in self.zeroshot:
|
|
145
|
+
if z.split not in valid_splits:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Invalid split '{z.split}' for zeroshot entry "
|
|
148
|
+
f"{z.dataset}.{z.cell_type}. Must be one of {valid_splits}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
logger.info("Configuration validation passed")
|
|
152
|
+
|
|
153
|
+
def save_config(self, path: Path) -> None:
|
|
154
|
+
"""Save configuration to a TOML file.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Output file path.
|
|
158
|
+
"""
|
|
159
|
+
lines: list[str] = ["[datasets]"]
|
|
160
|
+
for d in self.datasets:
|
|
161
|
+
lines.append(f'{d.name} = "{d.path}"')
|
|
162
|
+
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append("[training]")
|
|
165
|
+
for name in self.training_datasets:
|
|
166
|
+
lines.append(f'{name} = "train"')
|
|
167
|
+
|
|
168
|
+
if self.zeroshot:
|
|
169
|
+
lines.append("")
|
|
170
|
+
lines.append("[zeroshot]")
|
|
171
|
+
for z in self.zeroshot:
|
|
172
|
+
lines.append(f'"{z.dataset}.{z.cell_type}" = "{z.split}"')
|
|
173
|
+
|
|
174
|
+
for f in self.fewshot:
|
|
175
|
+
lines.append("")
|
|
176
|
+
lines.append(f'[fewshot."{f.dataset}.{f.cell_type}"]')
|
|
177
|
+
if f.val_perturbations:
|
|
178
|
+
val_list = ", ".join(f'"{p}"' for p in f.val_perturbations)
|
|
179
|
+
lines.append(f"val = [{val_list}]")
|
|
180
|
+
if f.test_perturbations:
|
|
181
|
+
test_list = ", ".join(f'"{p}"' for p in f.test_perturbations)
|
|
182
|
+
lines.append(f"test = [{test_list}]")
|
|
183
|
+
|
|
184
|
+
path.write_text("\n".join(lines) + "\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# TOML loading
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def load_experiment_config(path: Path) -> ExperimentConfig:
|
|
193
|
+
"""Load an experiment configuration from a TOML file.
|
|
194
|
+
|
|
195
|
+
Supports the cell-load TOML schema::
|
|
196
|
+
|
|
197
|
+
[datasets]
|
|
198
|
+
dataset_name = "/path/to/data"
|
|
199
|
+
|
|
200
|
+
[training]
|
|
201
|
+
dataset_name = "train"
|
|
202
|
+
|
|
203
|
+
[zeroshot]
|
|
204
|
+
"dataset.celltype" = "test"
|
|
205
|
+
|
|
206
|
+
[fewshot."dataset.celltype"]
|
|
207
|
+
val = ["Gene1"]
|
|
208
|
+
test = ["Gene2"]
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
path: Path to the TOML file.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Parsed ExperimentConfig.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
FileNotFoundError: If the TOML file does not exist.
|
|
218
|
+
"""
|
|
219
|
+
path = Path(path)
|
|
220
|
+
if not path.exists():
|
|
221
|
+
raise FileNotFoundError(f"TOML config file not found: {path}")
|
|
222
|
+
|
|
223
|
+
tomllib = _require_toml()
|
|
224
|
+
with open(path, "rb") as f:
|
|
225
|
+
raw = tomllib.load(f)
|
|
226
|
+
|
|
227
|
+
# Parse [datasets]
|
|
228
|
+
datasets_raw: dict[str, str] = raw.get("datasets", {})
|
|
229
|
+
datasets = tuple(DatasetEntry(name=name, path=p) for name, p in datasets_raw.items())
|
|
230
|
+
|
|
231
|
+
# Parse [training]
|
|
232
|
+
training_raw: dict[str, str] = raw.get("training", {})
|
|
233
|
+
training_datasets = tuple(training_raw.keys())
|
|
234
|
+
|
|
235
|
+
# Parse [zeroshot] -- keys are "dataset.celltype"
|
|
236
|
+
zeroshot_raw: dict[str, str] = raw.get("zeroshot", {})
|
|
237
|
+
zeroshot_entries: list[ZeroshotEntry] = []
|
|
238
|
+
for key, split in zeroshot_raw.items():
|
|
239
|
+
dataset, cell_type = key.split(".", 1)
|
|
240
|
+
zeroshot_entries.append(ZeroshotEntry(dataset=dataset, cell_type=cell_type, split=split))
|
|
241
|
+
|
|
242
|
+
# Parse [fewshot] -- keys are "dataset.celltype", values are {split: [perts]}
|
|
243
|
+
fewshot_raw: dict[str, dict[str, list[str]]] = raw.get("fewshot", {})
|
|
244
|
+
fewshot_entries: list[FewshotEntry] = []
|
|
245
|
+
for key, pert_config in fewshot_raw.items():
|
|
246
|
+
dataset, cell_type = key.split(".", 1)
|
|
247
|
+
fewshot_entries.append(
|
|
248
|
+
FewshotEntry(
|
|
249
|
+
dataset=dataset,
|
|
250
|
+
cell_type=cell_type,
|
|
251
|
+
val_perturbations=tuple(pert_config.get("val", [])),
|
|
252
|
+
test_perturbations=tuple(pert_config.get("test", [])),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return ExperimentConfig(
|
|
257
|
+
datasets=datasets,
|
|
258
|
+
training_datasets=training_datasets,
|
|
259
|
+
zeroshot=tuple(zeroshot_entries),
|
|
260
|
+
fewshot=tuple(fewshot_entries),
|
|
261
|
+
)
|