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,330 @@
|
|
|
1
|
+
"""Molecular splitters for drug discovery applications.
|
|
2
|
+
|
|
3
|
+
This module provides molecular-aware splitting utilities:
|
|
4
|
+
- ScaffoldSplitter: Split by Bemis-Murcko scaffold for drug discovery
|
|
5
|
+
- TanimotoClusterSplitter: Split by fingerprint similarity clustering
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Sequence
|
|
11
|
+
|
|
12
|
+
import jax.numpy as jnp
|
|
13
|
+
from flax import nnx
|
|
14
|
+
|
|
15
|
+
from datarax.core.data_source import DataSourceModule
|
|
16
|
+
|
|
17
|
+
from diffbio.splitters.base import SplitResult, SplitterConfig, SplitterModule
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ScaffoldSplitterConfig(SplitterConfig):
|
|
24
|
+
"""Configuration for scaffold splitter.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
smiles_key: Key in data element containing SMILES string (default: "smiles")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
smiles_key: str = "smiles"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ScaffoldSplitter(SplitterModule):
|
|
34
|
+
"""Split molecules by Bemis-Murcko scaffold.
|
|
35
|
+
|
|
36
|
+
Inherits from SplitterModule (StructuralModule) because:
|
|
37
|
+
|
|
38
|
+
- Non-parametric: scaffold extraction is deterministic
|
|
39
|
+
- Frozen config: splitting strategy doesn't change
|
|
40
|
+
- Domain-specific: requires RDKit and molecular knowledge
|
|
41
|
+
|
|
42
|
+
Ensures train/test sets have different molecular scaffolds,
|
|
43
|
+
preventing data leakage from structurally similar molecules.
|
|
44
|
+
This is the industry standard for drug discovery benchmarks.
|
|
45
|
+
|
|
46
|
+
Requires RDKit installation.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
config = ScaffoldSplitterConfig(smiles_key="mol_smiles")
|
|
51
|
+
splitter = ScaffoldSplitter(config)
|
|
52
|
+
result = splitter.split(molecule_source)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
References:
|
|
56
|
+
Bemis, Guy W., and Mark A. Murcko. "The properties of known drugs.
|
|
57
|
+
1. Molecular frameworks." Journal of medicinal chemistry 39.15 (1996): 2887-2893.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
config: ScaffoldSplitterConfig,
|
|
63
|
+
*,
|
|
64
|
+
rngs: nnx.Rngs | None = None,
|
|
65
|
+
name: str | None = None,
|
|
66
|
+
):
|
|
67
|
+
"""Initialize ScaffoldSplitter.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Scaffold splitter configuration
|
|
71
|
+
rngs: Random number generators (unused for scaffold splitting)
|
|
72
|
+
name: Optional module name
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ImportError: If RDKit is not installed
|
|
76
|
+
"""
|
|
77
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
78
|
+
try:
|
|
79
|
+
from rdkit import Chem
|
|
80
|
+
from rdkit.Chem.Scaffolds import MurckoScaffold
|
|
81
|
+
|
|
82
|
+
self._Chem = Chem
|
|
83
|
+
self._MurckoScaffold = MurckoScaffold
|
|
84
|
+
except ImportError as e:
|
|
85
|
+
raise ImportError("ScaffoldSplitter requires RDKit: pip install rdkit") from e
|
|
86
|
+
|
|
87
|
+
def _generate_scaffolds(self, smiles_list: Sequence[str]) -> dict[str, list[int]]:
|
|
88
|
+
"""Generate Bemis-Murcko scaffolds for molecules.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
smiles_list: List of SMILES strings
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary mapping scaffold SMILES to list of molecule indices
|
|
95
|
+
"""
|
|
96
|
+
scaffolds: dict[str, list[int]] = {}
|
|
97
|
+
for idx, smiles in enumerate(smiles_list):
|
|
98
|
+
mol = self._Chem.MolFromSmiles(smiles)
|
|
99
|
+
if mol is None:
|
|
100
|
+
# Invalid SMILES - assign to empty scaffold group
|
|
101
|
+
scaffold = ""
|
|
102
|
+
else:
|
|
103
|
+
try:
|
|
104
|
+
scaffold = self._MurckoScaffold.MurckoScaffoldSmiles(mol=mol)
|
|
105
|
+
except (ValueError, RuntimeError):
|
|
106
|
+
# Some molecules may not have a valid scaffold
|
|
107
|
+
scaffold = ""
|
|
108
|
+
|
|
109
|
+
if scaffold not in scaffolds:
|
|
110
|
+
scaffolds[scaffold] = []
|
|
111
|
+
scaffolds[scaffold].append(idx)
|
|
112
|
+
|
|
113
|
+
return scaffolds
|
|
114
|
+
|
|
115
|
+
def split(self, data_source: DataSourceModule) -> SplitResult:
|
|
116
|
+
"""Split by scaffold, largest scaffolds go to train first.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
data_source: Datarax DataSourceModule to split
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
SplitResult with scaffold-based train/valid/test indices
|
|
123
|
+
"""
|
|
124
|
+
# Extract SMILES from data source
|
|
125
|
+
smiles_list = [data_source[i].data[self.config.smiles_key] for i in range(len(data_source))]
|
|
126
|
+
|
|
127
|
+
scaffolds = self._generate_scaffolds(smiles_list)
|
|
128
|
+
|
|
129
|
+
# Sort scaffold groups by size (largest first)
|
|
130
|
+
scaffold_sets = sorted(scaffolds.values(), key=len, reverse=True)
|
|
131
|
+
return self.assign_groups_to_splits(scaffold_sets, len(data_source))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class TanimotoClusterSplitterConfig(SplitterConfig):
|
|
136
|
+
"""Configuration for Tanimoto cluster splitter.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
smiles_key: Key in data element containing SMILES string (default: "smiles")
|
|
140
|
+
fingerprint_type: Type of fingerprint ("morgan", "rdkit", "maccs")
|
|
141
|
+
fingerprint_radius: Radius for Morgan fingerprints (default: 2)
|
|
142
|
+
fingerprint_bits: Number of bits for fingerprints (default: 2048)
|
|
143
|
+
similarity_cutoff: Tanimoto similarity cutoff for clustering (default: 0.6)
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
smiles_key: str = "smiles"
|
|
147
|
+
fingerprint_type: str = "morgan"
|
|
148
|
+
fingerprint_radius: int = 2
|
|
149
|
+
fingerprint_bits: int = 2048
|
|
150
|
+
similarity_cutoff: float = 0.6
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TanimotoClusterSplitter(SplitterModule):
|
|
154
|
+
"""Split by Tanimoto similarity clustering (Butina algorithm).
|
|
155
|
+
|
|
156
|
+
Groups similar molecules together using fingerprint similarity,
|
|
157
|
+
then assigns clusters to train/valid/test to ensure structural
|
|
158
|
+
diversity between splits.
|
|
159
|
+
|
|
160
|
+
Inherits from SplitterModule (StructuralModule) because:
|
|
161
|
+
|
|
162
|
+
- Non-parametric: clustering is deterministic given fingerprints
|
|
163
|
+
- Frozen config: splitting strategy doesn't change
|
|
164
|
+
- Domain-specific: requires RDKit fingerprints
|
|
165
|
+
|
|
166
|
+
Requires RDKit installation.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
```python
|
|
170
|
+
config = TanimotoClusterSplitterConfig(similarity_cutoff=0.6)
|
|
171
|
+
splitter = TanimotoClusterSplitter(config)
|
|
172
|
+
result = splitter.split(molecule_source)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
References:
|
|
176
|
+
Butina, Darko. "Unsupervised data base clustering based on daylight's
|
|
177
|
+
fingerprint and Tanimoto similarity." JCICS 39.4 (1999): 747-750.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
config: TanimotoClusterSplitterConfig,
|
|
183
|
+
*,
|
|
184
|
+
rngs: nnx.Rngs | None = None,
|
|
185
|
+
name: str | None = None,
|
|
186
|
+
):
|
|
187
|
+
"""Initialize TanimotoClusterSplitter.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
config: Tanimoto cluster splitter configuration
|
|
191
|
+
rngs: Random number generators (unused)
|
|
192
|
+
name: Optional module name
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
ImportError: If RDKit is not installed
|
|
196
|
+
"""
|
|
197
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
198
|
+
try:
|
|
199
|
+
from rdkit import Chem, DataStructs
|
|
200
|
+
from rdkit.Chem import AllChem, MACCSkeys
|
|
201
|
+
from rdkit.ML.Cluster import Butina
|
|
202
|
+
|
|
203
|
+
self._Chem = Chem
|
|
204
|
+
self._DataStructs = DataStructs
|
|
205
|
+
self._AllChem = AllChem
|
|
206
|
+
self._MACCSkeys = MACCSkeys
|
|
207
|
+
self._Butina = Butina
|
|
208
|
+
except ImportError as e:
|
|
209
|
+
raise ImportError("TanimotoClusterSplitter requires RDKit: pip install rdkit") from e
|
|
210
|
+
|
|
211
|
+
def _compute_fingerprint(self, mol: Any) -> Any | None:
|
|
212
|
+
"""Compute fingerprint for a molecule.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
mol: RDKit molecule object
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Fingerprint object or None if computation fails
|
|
219
|
+
"""
|
|
220
|
+
if mol is None:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
if self.config.fingerprint_type == "morgan":
|
|
225
|
+
return self._AllChem.GetMorganFingerprintAsBitVect(
|
|
226
|
+
mol,
|
|
227
|
+
self.config.fingerprint_radius,
|
|
228
|
+
nBits=self.config.fingerprint_bits,
|
|
229
|
+
)
|
|
230
|
+
elif self.config.fingerprint_type == "rdkit":
|
|
231
|
+
return self._Chem.RDKFingerprint(mol, fpSize=self.config.fingerprint_bits)
|
|
232
|
+
elif self.config.fingerprint_type == "maccs":
|
|
233
|
+
return self._MACCSkeys.GenMACCSKeys(mol)
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError(f"Unknown fingerprint type: {self.config.fingerprint_type}")
|
|
236
|
+
except (ValueError, RuntimeError):
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def _compute_fingerprints(self, smiles_list: Sequence[str]) -> list[tuple[int, Any]]:
|
|
240
|
+
"""Compute fingerprints for all molecules.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
smiles_list: List of SMILES strings
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of (index, fingerprint) tuples for valid molecules
|
|
247
|
+
"""
|
|
248
|
+
valid_fps: list[tuple[int, Any]] = []
|
|
249
|
+
for idx, smiles in enumerate(smiles_list):
|
|
250
|
+
mol = self._Chem.MolFromSmiles(smiles)
|
|
251
|
+
fp = self._compute_fingerprint(mol)
|
|
252
|
+
if fp is not None:
|
|
253
|
+
valid_fps.append((idx, fp))
|
|
254
|
+
return valid_fps
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _all_train_result(size: int) -> SplitResult:
|
|
258
|
+
"""Create a split result with all items assigned to train."""
|
|
259
|
+
return SplitResult(
|
|
260
|
+
train_indices=jnp.array(list(range(size)), dtype=jnp.int32),
|
|
261
|
+
valid_indices=jnp.array([], dtype=jnp.int32),
|
|
262
|
+
test_indices=jnp.array([], dtype=jnp.int32),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _cluster_fingerprints(self, fp_list: list[Any]) -> list[tuple[int, ...]]:
|
|
266
|
+
"""Cluster fingerprints using Butina on condensed Tanimoto distances."""
|
|
267
|
+
n_valid = len(fp_list)
|
|
268
|
+
dists = []
|
|
269
|
+
for i in range(1, n_valid):
|
|
270
|
+
sims = self._DataStructs.BulkTanimotoSimilarity(fp_list[i], fp_list[:i])
|
|
271
|
+
dists.extend([1 - s for s in sims])
|
|
272
|
+
|
|
273
|
+
dist_threshold = 1 - self.config.similarity_cutoff
|
|
274
|
+
clusters = self._Butina.ClusterData(dists, n_valid, dist_threshold, isDistData=True)
|
|
275
|
+
return sorted(clusters, key=len, reverse=True)
|
|
276
|
+
|
|
277
|
+
def _assign_clusters_to_splits(
|
|
278
|
+
self,
|
|
279
|
+
sorted_clusters: list[tuple[int, ...]],
|
|
280
|
+
indices: list[int],
|
|
281
|
+
total_size: int,
|
|
282
|
+
) -> tuple[list[int], list[int], list[int]]:
|
|
283
|
+
"""Assign clusters to train/valid/test while preserving cluster membership."""
|
|
284
|
+
remapped_clusters = ([indices[i] for i in cluster] for cluster in sorted_clusters)
|
|
285
|
+
split_result = self.assign_groups_to_splits(remapped_clusters, total_size)
|
|
286
|
+
return (
|
|
287
|
+
split_result.train_indices.tolist(),
|
|
288
|
+
split_result.valid_indices.tolist(),
|
|
289
|
+
split_result.test_indices.tolist(),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def split(self, data_source: DataSourceModule) -> SplitResult:
|
|
293
|
+
"""Cluster by Tanimoto similarity and split.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
data_source: Datarax DataSourceModule to split
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
SplitResult with cluster-based train/valid/test indices
|
|
300
|
+
"""
|
|
301
|
+
total_size = len(data_source)
|
|
302
|
+
smiles_list = [data_source[i].data[self.config.smiles_key] for i in range(total_size)]
|
|
303
|
+
|
|
304
|
+
# Compute fingerprints
|
|
305
|
+
valid_fps = self._compute_fingerprints(smiles_list)
|
|
306
|
+
|
|
307
|
+
# Track invalid molecules (those without valid fingerprints)
|
|
308
|
+
valid_indices = {idx for idx, _ in valid_fps}
|
|
309
|
+
invalid_indices = [i for i in range(total_size) if i not in valid_indices]
|
|
310
|
+
|
|
311
|
+
if len(valid_fps) == 0:
|
|
312
|
+
return self._all_train_result(total_size)
|
|
313
|
+
|
|
314
|
+
# Unpack indices and fingerprints
|
|
315
|
+
indices, fp_list = zip(*valid_fps)
|
|
316
|
+
indices = list(indices)
|
|
317
|
+
fp_list = list(fp_list)
|
|
318
|
+
sorted_clusters = self._cluster_fingerprints(fp_list)
|
|
319
|
+
train_inds, valid_inds, test_inds = self._assign_clusters_to_splits(
|
|
320
|
+
sorted_clusters, indices, total_size
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Add invalid molecules to train (they couldn't be clustered)
|
|
324
|
+
train_inds.extend(invalid_indices)
|
|
325
|
+
|
|
326
|
+
return SplitResult(
|
|
327
|
+
train_indices=jnp.array(train_inds, dtype=jnp.int32),
|
|
328
|
+
valid_indices=jnp.array(valid_inds, dtype=jnp.int32),
|
|
329
|
+
test_indices=jnp.array(test_inds, dtype=jnp.int32),
|
|
330
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Perturbation-aware splitters for zero-shot and few-shot evaluation.
|
|
2
|
+
|
|
3
|
+
Provides specialized splitting strategies for single-cell perturbation
|
|
4
|
+
experiments: holding out entire cell types (zero-shot) or specific
|
|
5
|
+
perturbations within cell types (few-shot).
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- cell-load/src/cell_load/config.py (zeroshot/fewshot split logic)
|
|
9
|
+
- cell-load/src/cell_load/utils/data_utils.py (split_perturbations_by_cell_fraction)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import jax.numpy as jnp
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from diffbio.splitters.base import SplitResult, SplitterConfig, SplitterModule
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ZeroShotSplitterConfig(SplitterConfig):
|
|
28
|
+
"""Configuration for ZeroShotSplitter.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
held_out_cell_types: Cell types held out entirely for test.
|
|
32
|
+
pert_col: Obs column name for perturbation identity.
|
|
33
|
+
cell_type_col: Obs column name for cell type.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
held_out_cell_types: tuple[str, ...] = ()
|
|
37
|
+
pert_col: str = "perturbation"
|
|
38
|
+
cell_type_col: str = "cell_type"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ZeroShotSplitter(SplitterModule):
|
|
42
|
+
"""Hold out entire cell types for zero-shot evaluation.
|
|
43
|
+
|
|
44
|
+
All cells of specified cell types go to the test set. Remaining
|
|
45
|
+
cells are split into train and validation by the configured fractions.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Splitter configuration.
|
|
49
|
+
rngs: Optional RNG state.
|
|
50
|
+
name: Optional module name.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def split(self, data_source: Any) -> SplitResult:
|
|
54
|
+
"""Split data source by cell type holdout.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
data_source: A PerturbationAnnDataSource or similar source
|
|
58
|
+
providing element dicts with perturbation metadata.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
SplitResult with train/valid/test indices.
|
|
62
|
+
"""
|
|
63
|
+
n = len(data_source)
|
|
64
|
+
held_out = set(self.config.held_out_cell_types)
|
|
65
|
+
|
|
66
|
+
test_indices: list[int] = []
|
|
67
|
+
remaining_indices: list[int] = []
|
|
68
|
+
|
|
69
|
+
for i in range(n):
|
|
70
|
+
elem = data_source[i]
|
|
71
|
+
ct = elem.get(
|
|
72
|
+
"cell_type_name",
|
|
73
|
+
str(elem.get("obs", {}).get(self.config.cell_type_col, "")),
|
|
74
|
+
)
|
|
75
|
+
if ct in held_out:
|
|
76
|
+
test_indices.append(i)
|
|
77
|
+
else:
|
|
78
|
+
remaining_indices.append(i)
|
|
79
|
+
|
|
80
|
+
# Split remaining into train/val
|
|
81
|
+
rng = np.random.default_rng(self.config.seed)
|
|
82
|
+
remaining = np.array(remaining_indices)
|
|
83
|
+
rng.shuffle(remaining)
|
|
84
|
+
|
|
85
|
+
# Adjust fractions for the remaining subset
|
|
86
|
+
total_remaining = len(remaining)
|
|
87
|
+
train_frac = self.config.train_frac
|
|
88
|
+
val_frac = self.config.valid_frac
|
|
89
|
+
total_frac = train_frac + val_frac
|
|
90
|
+
if total_frac > 0:
|
|
91
|
+
adjusted_train = train_frac / total_frac
|
|
92
|
+
else:
|
|
93
|
+
adjusted_train = 0.5
|
|
94
|
+
|
|
95
|
+
n_train = int(total_remaining * adjusted_train)
|
|
96
|
+
train = remaining[:n_train].tolist()
|
|
97
|
+
valid = remaining[n_train:].tolist()
|
|
98
|
+
|
|
99
|
+
return SplitResult(
|
|
100
|
+
train_indices=jnp.array(train, dtype=jnp.int32),
|
|
101
|
+
valid_indices=jnp.array(valid, dtype=jnp.int32),
|
|
102
|
+
test_indices=jnp.array(test_indices, dtype=jnp.int32),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class FewShotSplitterConfig(SplitterConfig):
|
|
108
|
+
"""Configuration for FewShotSplitter.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
held_out_perturbations: Perturbation names assigned to test.
|
|
112
|
+
pert_col: Obs column name for perturbation identity.
|
|
113
|
+
cell_type_col: Obs column name for cell type.
|
|
114
|
+
control_pert: Label identifying control cells (always in train).
|
|
115
|
+
val_subsample_fraction: Fraction of validation data to keep.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
held_out_perturbations: tuple[str, ...] = ()
|
|
119
|
+
pert_col: str = "perturbation"
|
|
120
|
+
cell_type_col: str = "cell_type"
|
|
121
|
+
control_pert: str = "non-targeting"
|
|
122
|
+
val_subsample_fraction: float | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class FewShotSplitter(SplitterModule):
|
|
126
|
+
"""Hold out specific perturbations for few-shot evaluation.
|
|
127
|
+
|
|
128
|
+
Cells with held-out perturbations go to test. Control cells always
|
|
129
|
+
go to train. Remaining perturbed cells are split between train and
|
|
130
|
+
validation.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: Splitter configuration.
|
|
134
|
+
rngs: Optional RNG state.
|
|
135
|
+
name: Optional module name.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def split(self, data_source: Any) -> SplitResult:
|
|
139
|
+
"""Split data source by perturbation holdout.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
data_source: A PerturbationAnnDataSource or similar source.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
SplitResult with train/valid/test indices.
|
|
146
|
+
"""
|
|
147
|
+
n = len(data_source)
|
|
148
|
+
held_out = set(self.config.held_out_perturbations)
|
|
149
|
+
control = self.config.control_pert
|
|
150
|
+
|
|
151
|
+
test_indices: list[int] = []
|
|
152
|
+
control_indices: list[int] = []
|
|
153
|
+
remaining_indices: list[int] = []
|
|
154
|
+
|
|
155
|
+
for i in range(n):
|
|
156
|
+
elem = data_source[i]
|
|
157
|
+
pert = elem.get(
|
|
158
|
+
"pert_name",
|
|
159
|
+
str(elem.get("obs", {}).get(self.config.pert_col, "")),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if pert in held_out:
|
|
163
|
+
test_indices.append(i)
|
|
164
|
+
elif pert == control:
|
|
165
|
+
control_indices.append(i)
|
|
166
|
+
else:
|
|
167
|
+
remaining_indices.append(i)
|
|
168
|
+
|
|
169
|
+
# Split remaining non-control, non-test cells into train/val
|
|
170
|
+
rng = np.random.default_rng(self.config.seed)
|
|
171
|
+
remaining = np.array(remaining_indices)
|
|
172
|
+
rng.shuffle(remaining)
|
|
173
|
+
|
|
174
|
+
train_frac = self.config.train_frac
|
|
175
|
+
val_frac = self.config.valid_frac
|
|
176
|
+
total_frac = train_frac + val_frac
|
|
177
|
+
if total_frac > 0:
|
|
178
|
+
adjusted_train = train_frac / total_frac
|
|
179
|
+
else:
|
|
180
|
+
adjusted_train = 0.5
|
|
181
|
+
|
|
182
|
+
n_train = int(len(remaining) * adjusted_train)
|
|
183
|
+
train_from_remaining = remaining[:n_train].tolist()
|
|
184
|
+
valid_from_remaining = remaining[n_train:].tolist()
|
|
185
|
+
|
|
186
|
+
# Controls always go to train
|
|
187
|
+
train = control_indices + train_from_remaining
|
|
188
|
+
|
|
189
|
+
# Apply validation subsample
|
|
190
|
+
valid = valid_from_remaining
|
|
191
|
+
if self.config.val_subsample_fraction is not None and len(valid) > 0:
|
|
192
|
+
n_keep = max(1, int(len(valid) * self.config.val_subsample_fraction))
|
|
193
|
+
valid = valid[:n_keep]
|
|
194
|
+
|
|
195
|
+
return SplitResult(
|
|
196
|
+
train_indices=jnp.array(train, dtype=jnp.int32),
|
|
197
|
+
valid_indices=jnp.array(valid, dtype=jnp.int32),
|
|
198
|
+
test_indices=jnp.array(test_indices, dtype=jnp.int32),
|
|
199
|
+
)
|