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
diffbio/sources/dti.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Deterministic drug-target interaction sources and paired-input helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal, cast
|
|
10
|
+
|
|
11
|
+
import jax.numpy as jnp
|
|
12
|
+
import numpy as np
|
|
13
|
+
from flax import nnx
|
|
14
|
+
|
|
15
|
+
from datarax.core.config import StructuralConfig
|
|
16
|
+
from datarax.core.data_source import DataSourceModule
|
|
17
|
+
from datarax.typing import Element
|
|
18
|
+
|
|
19
|
+
DTI_DATASET_CONTRACT_KEYS = (
|
|
20
|
+
"pair_ids",
|
|
21
|
+
"protein_ids",
|
|
22
|
+
"protein_sequences",
|
|
23
|
+
"drug_ids",
|
|
24
|
+
"drug_smiles",
|
|
25
|
+
"targets",
|
|
26
|
+
"task_type",
|
|
27
|
+
"dataset_provenance",
|
|
28
|
+
)
|
|
29
|
+
_DTI_PROVENANCE_KEYS = (
|
|
30
|
+
"dataset_name",
|
|
31
|
+
"split",
|
|
32
|
+
"source_type",
|
|
33
|
+
"source_path",
|
|
34
|
+
"seed",
|
|
35
|
+
"task_type",
|
|
36
|
+
"n_pairs",
|
|
37
|
+
"promotion_eligible",
|
|
38
|
+
"biological_validation",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, kw_only=True)
|
|
43
|
+
class DTISourceConfig(StructuralConfig):
|
|
44
|
+
"""Configuration for deterministic DTI sources."""
|
|
45
|
+
|
|
46
|
+
dataset_name: Literal["davis", "biosnap"]
|
|
47
|
+
split: Literal["train", "valid", "test"] = "train"
|
|
48
|
+
data_dir: Path | None = None
|
|
49
|
+
seed: int = 42
|
|
50
|
+
use_synthetic_fallback: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_dti_dataset(data: dict[str, Any]) -> None:
|
|
54
|
+
"""Validate the shared DTI paired-input source contract."""
|
|
55
|
+
missing_keys = [key for key in DTI_DATASET_CONTRACT_KEYS if key not in data]
|
|
56
|
+
if missing_keys:
|
|
57
|
+
raise ValueError(f"DTI dataset is missing required keys: {missing_keys}")
|
|
58
|
+
|
|
59
|
+
lengths = [
|
|
60
|
+
len(data["pair_ids"]),
|
|
61
|
+
len(data["protein_ids"]),
|
|
62
|
+
len(data["protein_sequences"]),
|
|
63
|
+
len(data["drug_ids"]),
|
|
64
|
+
len(data["drug_smiles"]),
|
|
65
|
+
int(np.asarray(data["targets"]).shape[0]),
|
|
66
|
+
]
|
|
67
|
+
if len(set(lengths)) != 1:
|
|
68
|
+
raise ValueError("All DTI paired-input fields must have the same length.")
|
|
69
|
+
|
|
70
|
+
task_type = str(data["task_type"])
|
|
71
|
+
if task_type not in {"affinity_regression", "binary_interaction"}:
|
|
72
|
+
raise ValueError(f"Unsupported DTI task_type: {task_type!r}")
|
|
73
|
+
|
|
74
|
+
targets = np.asarray(data["targets"])
|
|
75
|
+
if targets.ndim != 1:
|
|
76
|
+
raise ValueError("DTI targets must be a rank-1 array.")
|
|
77
|
+
if task_type == "binary_interaction":
|
|
78
|
+
unique_values = set(np.asarray(targets, dtype=np.int32).tolist())
|
|
79
|
+
if not unique_values.issubset({0, 1}):
|
|
80
|
+
raise ValueError("Binary DTI targets must contain only 0/1 labels.")
|
|
81
|
+
|
|
82
|
+
provenance = data["dataset_provenance"]
|
|
83
|
+
if not isinstance(provenance, dict):
|
|
84
|
+
raise ValueError("DTI dataset_provenance must be a dict.")
|
|
85
|
+
missing_provenance_keys = [key for key in _DTI_PROVENANCE_KEYS if key not in provenance]
|
|
86
|
+
if missing_provenance_keys:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"DTI dataset_provenance is missing required keys: {missing_provenance_keys}"
|
|
89
|
+
)
|
|
90
|
+
if provenance["task_type"] != task_type:
|
|
91
|
+
raise ValueError("DTI dataset_provenance.task_type must match task_type.")
|
|
92
|
+
if int(provenance["n_pairs"]) != lengths[0]:
|
|
93
|
+
raise ValueError("DTI dataset_provenance.n_pairs must match paired field length.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def deterministic_dti_split(
|
|
97
|
+
n_items: int,
|
|
98
|
+
*,
|
|
99
|
+
seed: int,
|
|
100
|
+
train_frac: float = 2.0 / 3.0,
|
|
101
|
+
valid_frac: float = 1.0 / 6.0,
|
|
102
|
+
) -> dict[str, np.ndarray]:
|
|
103
|
+
"""Build a deterministic train/valid/test split over interaction indices."""
|
|
104
|
+
indices = np.arange(n_items, dtype=np.int32)
|
|
105
|
+
rng = np.random.default_rng(seed)
|
|
106
|
+
rng.shuffle(indices)
|
|
107
|
+
|
|
108
|
+
n_train = int(round(n_items * train_frac))
|
|
109
|
+
n_valid = int(round(n_items * valid_frac))
|
|
110
|
+
n_train = min(max(n_train, 1), n_items - 2)
|
|
111
|
+
n_valid = min(max(n_valid, 1), n_items - n_train - 1)
|
|
112
|
+
n_test = n_items - n_train - n_valid
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"train": np.sort(indices[:n_train]),
|
|
116
|
+
"valid": np.sort(indices[n_train : n_train + n_valid]),
|
|
117
|
+
"test": np.sort(indices[n_train + n_valid : n_train + n_valid + n_test]),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_paired_dti_batch(
|
|
122
|
+
data: dict[str, Any],
|
|
123
|
+
*,
|
|
124
|
+
indices: np.ndarray,
|
|
125
|
+
) -> dict[str, Any]:
|
|
126
|
+
"""Slice a DTI dataset into one aligned paired batch."""
|
|
127
|
+
validate_dti_dataset(data)
|
|
128
|
+
batch_indices = np.asarray(indices, dtype=np.int32)
|
|
129
|
+
provenance = dict(data["dataset_provenance"])
|
|
130
|
+
provenance["source_n_pairs"] = int(provenance["n_pairs"])
|
|
131
|
+
provenance["n_pairs"] = int(batch_indices.shape[0])
|
|
132
|
+
batch = {
|
|
133
|
+
"pair_ids": [data["pair_ids"][index] for index in batch_indices],
|
|
134
|
+
"protein_ids": [data["protein_ids"][index] for index in batch_indices],
|
|
135
|
+
"protein_sequences": [data["protein_sequences"][index] for index in batch_indices],
|
|
136
|
+
"drug_ids": [data["drug_ids"][index] for index in batch_indices],
|
|
137
|
+
"drug_smiles": [data["drug_smiles"][index] for index in batch_indices],
|
|
138
|
+
"targets": jnp.asarray(np.asarray(data["targets"])[batch_indices]),
|
|
139
|
+
"task_type": data["task_type"],
|
|
140
|
+
"dataset_provenance": provenance,
|
|
141
|
+
}
|
|
142
|
+
validate_dti_dataset(batch)
|
|
143
|
+
return batch
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _BaseDTISource(DataSourceModule):
|
|
147
|
+
"""Shared DTI source implementation for dataset-specific wrappers."""
|
|
148
|
+
|
|
149
|
+
_data: list[Element] = nnx.data()
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
config: DTISourceConfig,
|
|
154
|
+
*,
|
|
155
|
+
rngs: nnx.Rngs | None = None,
|
|
156
|
+
name: str | None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
159
|
+
self._data = self._load_dataset()
|
|
160
|
+
self._current_idx = 0
|
|
161
|
+
|
|
162
|
+
def _load_dataset(self) -> list[Element]:
|
|
163
|
+
records = self._load_records()
|
|
164
|
+
split_indices = deterministic_dti_split(len(records), seed=self.config.seed)[
|
|
165
|
+
self.config.split
|
|
166
|
+
]
|
|
167
|
+
return [records[int(index)] for index in split_indices]
|
|
168
|
+
|
|
169
|
+
def _load_records(self) -> list[Element]:
|
|
170
|
+
dataset_path = _resolve_dataset_path(cast(DTISourceConfig, self.config))
|
|
171
|
+
if dataset_path is not None:
|
|
172
|
+
return _load_records_from_table(dataset_path, dataset_name=self.config.dataset_name)
|
|
173
|
+
if not self.config.use_synthetic_fallback:
|
|
174
|
+
raise FileNotFoundError(
|
|
175
|
+
f"No local {self.config.dataset_name} table found under {self.config.data_dir!r}."
|
|
176
|
+
)
|
|
177
|
+
return _build_synthetic_records(dataset_name=self.config.dataset_name)
|
|
178
|
+
|
|
179
|
+
def load(self) -> dict[str, Any]:
|
|
180
|
+
"""Load the paired DTI payload for the configured split."""
|
|
181
|
+
data = {
|
|
182
|
+
"pair_ids": [element.data["pair_id"] for element in self._data],
|
|
183
|
+
"protein_ids": [element.data["protein_id"] for element in self._data],
|
|
184
|
+
"protein_sequences": [element.data["protein_sequence"] for element in self._data],
|
|
185
|
+
"drug_ids": [element.data["drug_id"] for element in self._data],
|
|
186
|
+
"drug_smiles": [element.data["drug_smiles"] for element in self._data],
|
|
187
|
+
"targets": jnp.asarray([element.data["target"] for element in self._data]),
|
|
188
|
+
"task_type": self._data[0].data["task_type"],
|
|
189
|
+
}
|
|
190
|
+
data["dataset_provenance"] = _build_dataset_provenance(
|
|
191
|
+
cast(DTISourceConfig, self.config),
|
|
192
|
+
task_type=str(data["task_type"]),
|
|
193
|
+
n_pairs=len(data["pair_ids"]),
|
|
194
|
+
)
|
|
195
|
+
validate_dti_dataset(data)
|
|
196
|
+
return data
|
|
197
|
+
|
|
198
|
+
def __len__(self) -> int:
|
|
199
|
+
"""Return the number of interactions in the configured split."""
|
|
200
|
+
return len(self._data)
|
|
201
|
+
|
|
202
|
+
def __getitem__(self, index: int) -> Element | None:
|
|
203
|
+
"""Return one interaction element or ``None`` if out of range."""
|
|
204
|
+
if index < 0 or index >= len(self):
|
|
205
|
+
return None
|
|
206
|
+
return self._data[index]
|
|
207
|
+
|
|
208
|
+
def __iter__(self) -> Iterator[Element]:
|
|
209
|
+
"""Iterate over paired DTI elements."""
|
|
210
|
+
return iter(self._data)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class DavisDTISource(_BaseDTISource):
|
|
214
|
+
"""Deterministic affinity-regression source for the Davis DTI task."""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class BioSNAPDTISource(_BaseDTISource):
|
|
218
|
+
"""Deterministic binary-interaction source for the BioSNAP DTI task."""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _resolve_dataset_path(config: DTISourceConfig) -> Path | None:
|
|
222
|
+
"""Return the first matching local dataset table if it exists."""
|
|
223
|
+
if config.data_dir is None:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
candidates = [
|
|
227
|
+
Path(config.data_dir) / f"{config.dataset_name}.csv",
|
|
228
|
+
Path(config.data_dir) / f"{config.dataset_name}.tsv",
|
|
229
|
+
]
|
|
230
|
+
for candidate in candidates:
|
|
231
|
+
if candidate.exists():
|
|
232
|
+
return candidate
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_dataset_provenance(
|
|
237
|
+
config: DTISourceConfig,
|
|
238
|
+
*,
|
|
239
|
+
task_type: str,
|
|
240
|
+
n_pairs: int,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Build one provenance record for the loaded DTI split."""
|
|
243
|
+
dataset_path = _resolve_dataset_path(config)
|
|
244
|
+
source_type = "local_table" if dataset_path is not None else "synthetic_scaffold"
|
|
245
|
+
biological_validation = (
|
|
246
|
+
"local_table_unverified" if dataset_path is not None else "contract_validation_only"
|
|
247
|
+
)
|
|
248
|
+
return {
|
|
249
|
+
"dataset_name": config.dataset_name,
|
|
250
|
+
"split": config.split,
|
|
251
|
+
"source_type": source_type,
|
|
252
|
+
"source_path": None if dataset_path is None else str(dataset_path),
|
|
253
|
+
"seed": config.seed,
|
|
254
|
+
"task_type": task_type,
|
|
255
|
+
"n_pairs": int(n_pairs),
|
|
256
|
+
"promotion_eligible": False,
|
|
257
|
+
"biological_validation": biological_validation,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _load_records_from_table(
|
|
262
|
+
dataset_path: Path,
|
|
263
|
+
*,
|
|
264
|
+
dataset_name: Literal["davis", "biosnap"],
|
|
265
|
+
) -> list[Element]:
|
|
266
|
+
"""Load paired DTI records from a local CSV/TSV table."""
|
|
267
|
+
delimiter = "\t" if dataset_path.suffix == ".tsv" else ","
|
|
268
|
+
task_type = "affinity_regression" if dataset_name == "davis" else "binary_interaction"
|
|
269
|
+
target_columns = ("target", "affinity", "label")
|
|
270
|
+
|
|
271
|
+
with dataset_path.open("r", encoding="utf-8", newline="") as handle:
|
|
272
|
+
reader = csv.DictReader(handle, delimiter=delimiter)
|
|
273
|
+
records: list[Element] = []
|
|
274
|
+
for row_index, row in enumerate(reader):
|
|
275
|
+
target_value = None
|
|
276
|
+
for column_name in target_columns:
|
|
277
|
+
if column_name in row and row[column_name] not in {"", None}:
|
|
278
|
+
target_value = float(row[column_name])
|
|
279
|
+
break
|
|
280
|
+
if target_value is None:
|
|
281
|
+
raise ValueError(f"Missing target column in {dataset_path} row {row_index}.")
|
|
282
|
+
|
|
283
|
+
records.append(
|
|
284
|
+
_build_element(
|
|
285
|
+
pair_id=row.get("pair_id", f"{dataset_name}_{row_index}"),
|
|
286
|
+
protein_id=row["protein_id"],
|
|
287
|
+
protein_sequence=row["protein_sequence"],
|
|
288
|
+
drug_id=row["drug_id"],
|
|
289
|
+
drug_smiles=row["drug_smiles"],
|
|
290
|
+
target=target_value,
|
|
291
|
+
task_type=task_type,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
return records
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _build_synthetic_records(
|
|
298
|
+
*,
|
|
299
|
+
dataset_name: Literal["davis", "biosnap"],
|
|
300
|
+
) -> list[Element]:
|
|
301
|
+
"""Build deterministic fallback paired DTI records."""
|
|
302
|
+
proteins = (
|
|
303
|
+
("P0", "MKTAYI"),
|
|
304
|
+
("P1", "MNNQKLI"),
|
|
305
|
+
("P2", "MPEPTIDER"),
|
|
306
|
+
)
|
|
307
|
+
drugs = (
|
|
308
|
+
("D0", "CCO"),
|
|
309
|
+
("D1", "CCN"),
|
|
310
|
+
("D2", "CCCl"),
|
|
311
|
+
("D3", "c1ccccc1"),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
records: list[Element] = []
|
|
315
|
+
for protein_index, (protein_id, protein_sequence) in enumerate(proteins):
|
|
316
|
+
for drug_index, (drug_id, drug_smiles) in enumerate(drugs):
|
|
317
|
+
if dataset_name == "davis":
|
|
318
|
+
target = 5.5 + 0.45 * protein_index + 0.3 * drug_index
|
|
319
|
+
task_type = "affinity_regression"
|
|
320
|
+
else:
|
|
321
|
+
target = float((protein_index + drug_index) % 2 == 0)
|
|
322
|
+
task_type = "binary_interaction"
|
|
323
|
+
|
|
324
|
+
records.append(
|
|
325
|
+
_build_element(
|
|
326
|
+
pair_id=f"{dataset_name}_{protein_id}_{drug_id}",
|
|
327
|
+
protein_id=protein_id,
|
|
328
|
+
protein_sequence=protein_sequence,
|
|
329
|
+
drug_id=drug_id,
|
|
330
|
+
drug_smiles=drug_smiles,
|
|
331
|
+
target=target,
|
|
332
|
+
task_type=task_type,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
return records
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _build_element(
|
|
339
|
+
*,
|
|
340
|
+
pair_id: str,
|
|
341
|
+
protein_id: str,
|
|
342
|
+
protein_sequence: str,
|
|
343
|
+
drug_id: str,
|
|
344
|
+
drug_smiles: str,
|
|
345
|
+
target: float,
|
|
346
|
+
task_type: str,
|
|
347
|
+
) -> Element:
|
|
348
|
+
"""Build one DTI interaction element."""
|
|
349
|
+
return Element(
|
|
350
|
+
data={
|
|
351
|
+
"pair_id": pair_id,
|
|
352
|
+
"protein_id": protein_id,
|
|
353
|
+
"protein_sequence": protein_sequence,
|
|
354
|
+
"drug_id": drug_id,
|
|
355
|
+
"drug_smiles": drug_smiles,
|
|
356
|
+
"target": target,
|
|
357
|
+
"task_type": task_type,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Embedding-artifact sources built on Datarax source primitives.
|
|
2
|
+
|
|
3
|
+
This module keeps file-format parsing local to DiffBio because the supported
|
|
4
|
+
artifacts are biology-specific, but the actual source abstraction follows the
|
|
5
|
+
same Datarax source model used elsewhere in the repository. The canonical
|
|
6
|
+
runtime substrate is therefore:
|
|
7
|
+
|
|
8
|
+
1. file decoding in one place
|
|
9
|
+
2. source semantics via Datarax ``MemorySource``
|
|
10
|
+
3. biology-specific alignment handled by specialized source subclasses
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, cast
|
|
18
|
+
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
import numpy as np
|
|
21
|
+
from datarax.sources import MemorySource, MemorySourceConfig
|
|
22
|
+
from flax import nnx
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_torch() -> Any:
|
|
26
|
+
"""Import torch, raising a clear error if it is not installed."""
|
|
27
|
+
try:
|
|
28
|
+
import torch # noqa: PLC0415 # pyright: ignore[reportMissingImports]
|
|
29
|
+
|
|
30
|
+
return torch
|
|
31
|
+
except ImportError as err:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"PyTorch is required to load .pt embedding files. "
|
|
34
|
+
"Install with: uv pip install 'diffbio[torch-io]'"
|
|
35
|
+
) from err
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class EmbeddingArtifactPayload:
|
|
40
|
+
"""Canonical embedding matrix plus optional artifact metadata arrays."""
|
|
41
|
+
|
|
42
|
+
embeddings: np.ndarray
|
|
43
|
+
metadata: dict[str, np.ndarray]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _coerce_pt_value(value: Any, *, field_name: str) -> np.ndarray:
|
|
47
|
+
"""Convert a supported PyTorch payload field to a NumPy array."""
|
|
48
|
+
if hasattr(value, "detach") and hasattr(value, "cpu") and hasattr(value, "numpy"):
|
|
49
|
+
return np.asarray(value.detach().cpu().numpy())
|
|
50
|
+
|
|
51
|
+
if isinstance(value, np.ndarray):
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
if isinstance(value, (list, tuple)):
|
|
55
|
+
return np.asarray(value)
|
|
56
|
+
|
|
57
|
+
raise TypeError(
|
|
58
|
+
"Expected PyTorch artifact field "
|
|
59
|
+
f"'{field_name}' to be a tensor, NumPy array, list, or tuple, "
|
|
60
|
+
f"but received {type(value).__name__}."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_npz_payload(path: Path) -> EmbeddingArtifactPayload:
|
|
65
|
+
"""Load the canonical array and metadata arrays from a NumPy archive."""
|
|
66
|
+
with np.load(path) as archive:
|
|
67
|
+
embedding_key = "embeddings" if "embeddings" in archive else next(iter(archive.files), None)
|
|
68
|
+
if embedding_key is None:
|
|
69
|
+
raise ValueError(f"Embedding archive is empty: {path}")
|
|
70
|
+
|
|
71
|
+
metadata = {key: np.asarray(archive[key]) for key in archive.files if key != embedding_key}
|
|
72
|
+
return EmbeddingArtifactPayload(
|
|
73
|
+
embeddings=np.asarray(archive[embedding_key], dtype=np.float32),
|
|
74
|
+
metadata=metadata,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load_pt_payload(path: Path) -> EmbeddingArtifactPayload:
|
|
79
|
+
"""Load a PyTorch embedding artifact plus optional metadata fields."""
|
|
80
|
+
torch = _require_torch()
|
|
81
|
+
payload = torch.load(path, map_location="cpu", weights_only=True)
|
|
82
|
+
|
|
83
|
+
if isinstance(payload, dict):
|
|
84
|
+
if "embeddings" not in payload:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"PyTorch embedding artifacts stored as mappings must include an 'embeddings' entry."
|
|
87
|
+
)
|
|
88
|
+
metadata = {
|
|
89
|
+
str(key): _coerce_pt_value(value, field_name=str(key))
|
|
90
|
+
for key, value in payload.items()
|
|
91
|
+
if key != "embeddings"
|
|
92
|
+
}
|
|
93
|
+
return EmbeddingArtifactPayload(
|
|
94
|
+
embeddings=np.asarray(
|
|
95
|
+
_coerce_pt_value(payload["embeddings"], field_name="embeddings"),
|
|
96
|
+
dtype=np.float32,
|
|
97
|
+
),
|
|
98
|
+
metadata=metadata,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if hasattr(payload, "numpy"):
|
|
102
|
+
return EmbeddingArtifactPayload(
|
|
103
|
+
embeddings=np.asarray(payload.numpy(), dtype=np.float32),
|
|
104
|
+
metadata={},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
raise TypeError(
|
|
108
|
+
"Expected a PyTorch tensor or mapping in the embedding artifact, "
|
|
109
|
+
f"but received {type(payload).__name__}."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_embedding_artifact(path: Path | str) -> EmbeddingArtifactPayload:
|
|
114
|
+
"""Load a canonical embedding artifact plus optional metadata arrays."""
|
|
115
|
+
resolved_path = Path(path)
|
|
116
|
+
if not resolved_path.exists():
|
|
117
|
+
raise FileNotFoundError(f"Embedding file not found: {resolved_path}")
|
|
118
|
+
|
|
119
|
+
suffix = resolved_path.suffix.lower()
|
|
120
|
+
if suffix == ".npy":
|
|
121
|
+
return EmbeddingArtifactPayload(
|
|
122
|
+
embeddings=np.asarray(np.load(resolved_path), dtype=np.float32),
|
|
123
|
+
metadata={},
|
|
124
|
+
)
|
|
125
|
+
if suffix == ".npz":
|
|
126
|
+
return _load_npz_payload(resolved_path)
|
|
127
|
+
if suffix == ".pt":
|
|
128
|
+
return _load_pt_payload(resolved_path)
|
|
129
|
+
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Unsupported embedding file extension '{suffix}'. Use .npy, .npz, or .pt format."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class EmbeddingArtifactSourceConfig(MemorySourceConfig):
|
|
137
|
+
"""Configuration for eager artifact-backed embedding sources."""
|
|
138
|
+
|
|
139
|
+
file_path: str | None = None
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
"""Validate the artifact path and delegate common source validation."""
|
|
143
|
+
super().__post_init__()
|
|
144
|
+
|
|
145
|
+
if self.file_path is None:
|
|
146
|
+
raise ValueError("file_path is required for EmbeddingArtifactSourceConfig")
|
|
147
|
+
|
|
148
|
+
resolved_path = Path(self.file_path)
|
|
149
|
+
if not resolved_path.exists():
|
|
150
|
+
raise FileNotFoundError(f"Embedding file not found: {resolved_path}")
|
|
151
|
+
|
|
152
|
+
if resolved_path.suffix.lower() not in {".npy", ".npz", ".pt"}:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"Embedding artifact sources only support .npy, .npz, or .pt files, "
|
|
155
|
+
f"got '{resolved_path.suffix}'."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class EmbeddingArtifactSource(MemorySource):
|
|
160
|
+
"""Eager Datarax-style source for external embedding artifacts."""
|
|
161
|
+
|
|
162
|
+
config: EmbeddingArtifactSourceConfig # pyright: ignore[reportIncompatibleVariableOverride]
|
|
163
|
+
_artifact_metadata: dict[str, np.ndarray] = nnx.data()
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
config: EmbeddingArtifactSourceConfig,
|
|
168
|
+
*,
|
|
169
|
+
rngs: nnx.Rngs | None = None,
|
|
170
|
+
name: str | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Load an embedding artifact into a Datarax ``MemorySource``."""
|
|
173
|
+
file_path = config.file_path
|
|
174
|
+
if file_path is None:
|
|
175
|
+
raise ValueError("file_path is required for EmbeddingArtifactSource")
|
|
176
|
+
|
|
177
|
+
payload = load_embedding_artifact(file_path)
|
|
178
|
+
source_name = name or f"EmbeddingArtifactSource({file_path})"
|
|
179
|
+
data = {"embeddings": jnp.asarray(payload.embeddings, dtype=jnp.float32)}
|
|
180
|
+
|
|
181
|
+
super().__init__(config, data=data, rngs=rngs, name=source_name)
|
|
182
|
+
|
|
183
|
+
self._artifact_metadata = payload.metadata
|
|
184
|
+
object.__setattr__(self, "_artifact_path", Path(file_path))
|
|
185
|
+
|
|
186
|
+
def load(self) -> dict[str, Any]:
|
|
187
|
+
"""Return the eager in-memory payload exposed by the source."""
|
|
188
|
+
return dict(cast(dict[str, Any], self.data))
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def embeddings(self) -> jnp.ndarray:
|
|
192
|
+
"""Full embedding matrix loaded from the artifact."""
|
|
193
|
+
return cast(jnp.ndarray, cast(dict[str, Any], self.data)["embeddings"])
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def artifact_metadata(self) -> dict[str, np.ndarray]:
|
|
197
|
+
"""Auxiliary metadata arrays loaded from the artifact."""
|
|
198
|
+
return dict(self._artifact_metadata)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def artifact_path(self) -> Path:
|
|
202
|
+
"""Resolved path to the backing embedding artifact."""
|
|
203
|
+
return self._artifact_path
|