diffbio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
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