diffbio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,493 @@
1
+ """MolNet benchmark data source for drug discovery.
2
+
3
+ This module provides MolNetSource for loading MoleculeNet benchmark datasets:
4
+ - BBBP (Blood-Brain Barrier Penetration)
5
+ - Tox21 (Toxicity)
6
+ - ESOL (Solubility)
7
+ - FreeSolv (Solvation Energy)
8
+ - Lipophilicity
9
+ - And more...
10
+
11
+ Reference:
12
+ Wu et al. "MoleculeNet: A Benchmark for Molecular Machine Learning"
13
+ Chemical Science, 2018.
14
+ """
15
+
16
+ import csv
17
+ import gzip
18
+ import logging
19
+ import shutil
20
+ import urllib.error
21
+ import urllib.request
22
+ import warnings
23
+ from collections.abc import Iterator
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+ from typing import Literal
27
+
28
+ import jax.numpy as jnp
29
+ from flax import nnx
30
+
31
+ from datarax.core.config import StructuralConfig
32
+ from datarax.core.data_source import DataSourceModule
33
+ from datarax.typing import Element
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # MolNet dataset catalog with download URLs and metadata
38
+ # URLs point to DeepChem's hosted data files
39
+ MOLNET_DATASETS: dict[str, dict] = {
40
+ # ADMET datasets
41
+ "bbbp": {
42
+ "task_type": "classification",
43
+ "n_tasks": 1,
44
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv",
45
+ "smiles_col": "smiles",
46
+ "label_cols": ["p_np"],
47
+ },
48
+ "tox21": {
49
+ "task_type": "classification",
50
+ "n_tasks": 12,
51
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/tox21.csv.gz",
52
+ "smiles_col": "smiles",
53
+ "label_cols": [
54
+ "NR-AR",
55
+ "NR-AR-LBD",
56
+ "NR-AhR",
57
+ "NR-Aromatase",
58
+ "NR-ER",
59
+ "NR-ER-LBD",
60
+ "NR-PPAR-gamma",
61
+ "SR-ARE",
62
+ "SR-ATAD5",
63
+ "SR-HSE",
64
+ "SR-MMP",
65
+ "SR-p53",
66
+ ],
67
+ },
68
+ # Physiology datasets
69
+ "esol": {
70
+ "task_type": "regression",
71
+ "n_tasks": 1,
72
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv",
73
+ "smiles_col": "smiles",
74
+ "label_cols": ["measured log solubility in mols per litre"],
75
+ },
76
+ "freesolv": {
77
+ "task_type": "regression",
78
+ "n_tasks": 1,
79
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv",
80
+ "smiles_col": "smiles",
81
+ "label_cols": ["expt"],
82
+ },
83
+ "lipophilicity": {
84
+ "task_type": "regression",
85
+ "n_tasks": 1,
86
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv",
87
+ "smiles_col": "smiles",
88
+ "label_cols": ["exp"],
89
+ },
90
+ # HIV dataset
91
+ "hiv": {
92
+ "task_type": "classification",
93
+ "n_tasks": 1,
94
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv",
95
+ "smiles_col": "smiles",
96
+ "label_cols": ["HIV_active"],
97
+ },
98
+ # BACE dataset
99
+ "bace": {
100
+ "task_type": "classification",
101
+ "n_tasks": 1,
102
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv",
103
+ "smiles_col": "mol",
104
+ "label_cols": ["Class"],
105
+ },
106
+ # ClinTox dataset
107
+ "clintox": {
108
+ "task_type": "classification",
109
+ "n_tasks": 2,
110
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/clintox.csv.gz",
111
+ "smiles_col": "smiles",
112
+ "label_cols": ["FDA_APPROVED", "CT_TOX"],
113
+ },
114
+ # SIDER dataset
115
+ "sider": {
116
+ "task_type": "classification",
117
+ "n_tasks": 27,
118
+ "url": "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/sider.csv.gz",
119
+ "smiles_col": "smiles",
120
+ "label_cols": None, # All columns except smiles are labels
121
+ },
122
+ }
123
+
124
+ # Compact synthetic fallback used when network is unavailable and no cache exists.
125
+ _FALLBACK_MOLNET_SMILES: tuple[str, ...] = (
126
+ "CCO",
127
+ "CCN",
128
+ "CCC",
129
+ "CCCl",
130
+ "CCBr",
131
+ "CC(C)O",
132
+ "CC(C)N",
133
+ "c1ccccc1",
134
+ "c1ccncc1",
135
+ "CCOC(=O)C",
136
+ "CC(=O)O",
137
+ "CC(=O)N",
138
+ "CCS",
139
+ "CCP",
140
+ "COC",
141
+ "CN(C)C",
142
+ "CC(C)C",
143
+ "CC(C)(C)O",
144
+ "O=C(O)C",
145
+ "NCCO",
146
+ )
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class MolNetSourceConfig(StructuralConfig):
151
+ """Configuration for MolNet benchmark data source.
152
+
153
+ Attributes:
154
+ dataset_name: Name of the MolNet dataset (e.g., "bbbp", "tox21", "esol")
155
+ split: Which split to load ("train", "valid", or "test")
156
+ data_dir: Directory to store downloaded data (default: ~/.diffbio/molnet)
157
+ download: Whether to download if data not found (default: True)
158
+ """
159
+
160
+ dataset_name: str = ""
161
+ split: Literal["train", "valid", "test"] = "train"
162
+ data_dir: Path | None = None
163
+ download: bool = True
164
+
165
+ def __post_init__(self) -> None:
166
+ """Validate configuration after initialization."""
167
+ super().__post_init__()
168
+ if not self.dataset_name:
169
+ raise ValueError("dataset_name is required")
170
+
171
+
172
+ class MolNetSource(DataSourceModule):
173
+ """MolNet benchmark data source extending Datarax DataSourceModule.
174
+
175
+ Provides standardized access to MoleculeNet benchmark datasets with proper
176
+ train/valid/test splits. Supports automatic downloading and caching.
177
+
178
+ Inherits from DataSourceModule (StructuralModule) because:
179
+
180
+ - Non-parametric: data loading is deterministic
181
+ - Frozen config: dataset parameters don't change
182
+ - Domain-specific: requires molecular data handling
183
+
184
+ Example:
185
+ ```python
186
+ config = MolNetSourceConfig(dataset_name="bbbp", split="train")
187
+ source = MolNetSource(config)
188
+ for element in source:
189
+ print(element.data["smiles"], element.data["y"])
190
+ ```
191
+
192
+ References:
193
+ Wu et al. "MoleculeNet: A Benchmark for Molecular Machine Learning"
194
+ Chemical Science, 2018.
195
+ """
196
+
197
+ # Annotate data storage for Flax NNX
198
+ _data: list = nnx.data()
199
+
200
+ def __init__(
201
+ self,
202
+ config: MolNetSourceConfig,
203
+ *,
204
+ rngs: nnx.Rngs | None = None,
205
+ name: str | None = None,
206
+ ):
207
+ """Initialize MolNetSource.
208
+
209
+ Args:
210
+ config: MolNet source configuration
211
+ rngs: Random number generators (unused for data loading)
212
+ name: Optional module name
213
+
214
+ Raises:
215
+ ValueError: If dataset_name is unknown
216
+ FileNotFoundError: If data not found and download=False
217
+ """
218
+ super().__init__(config, rngs=rngs, name=name)
219
+
220
+ # Validate dataset name
221
+ if config.dataset_name not in MOLNET_DATASETS:
222
+ available = ", ".join(sorted(MOLNET_DATASETS.keys()))
223
+ raise ValueError(
224
+ f"Unknown dataset: '{config.dataset_name}'. Available datasets: {available}"
225
+ )
226
+
227
+ # Set up data directory
228
+ if config.data_dir is None:
229
+ self._data_dir = Path.home() / ".diffbio" / "molnet"
230
+ else:
231
+ self._data_dir = Path(config.data_dir)
232
+
233
+ # Load the dataset
234
+ self._data = self._load_dataset()
235
+ self._current_idx = 0
236
+
237
+ def _load_dataset(self) -> list[Element]:
238
+ """Load the MolNet dataset.
239
+
240
+ Returns:
241
+ List of Element objects containing SMILES and labels
242
+ """
243
+ dataset_info = MOLNET_DATASETS[self.config.dataset_name]
244
+ data_path = self._get_data_path()
245
+
246
+ self._ensure_dataset_file(data_path, dataset_info)
247
+
248
+ # Parse CSV file
249
+ return self._parse_csv(data_path, dataset_info)
250
+
251
+ def _ensure_dataset_file(self, data_path: Path, dataset_info: dict) -> None:
252
+ """Ensure the dataset file exists locally."""
253
+ if data_path.exists():
254
+ return
255
+
256
+ if not self.config.download:
257
+ raise FileNotFoundError(
258
+ f"Dataset file not found: {data_path}. Set download=True to download automatically."
259
+ )
260
+
261
+ if self._copy_from_default_cache(data_path):
262
+ return
263
+
264
+ try:
265
+ self._download_dataset(dataset_info["url"], data_path)
266
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
267
+ self._write_builtin_fallback_dataset(data_path, dataset_info)
268
+ warnings.warn(
269
+ "Unable to download MolNet dataset "
270
+ f"'{self.config.dataset_name}' ({exc!r}); using a built-in fallback sample.",
271
+ RuntimeWarning,
272
+ stacklevel=2,
273
+ )
274
+
275
+ def _get_data_path(self) -> Path:
276
+ """Get the path to the dataset file."""
277
+ url = MOLNET_DATASETS[self.config.dataset_name]["url"]
278
+ filename = url.split("/")[-1]
279
+ return self._data_dir / self.config.dataset_name / filename
280
+
281
+ def _copy_from_default_cache(self, data_path: Path) -> bool:
282
+ """Copy from shared ~/.diffbio cache when using a custom data_dir."""
283
+ default_cache_path = (
284
+ Path.home() / ".diffbio" / "molnet" / self.config.dataset_name / data_path.name
285
+ )
286
+ if default_cache_path == data_path or not default_cache_path.exists():
287
+ return False
288
+
289
+ data_path.parent.mkdir(parents=True, exist_ok=True)
290
+ shutil.copy2(default_cache_path, data_path)
291
+ return True
292
+
293
+ def _download_dataset(self, url: str, data_path: Path) -> None:
294
+ """Download dataset from URL.
295
+
296
+ Args:
297
+ url: URL to download from
298
+ data_path: Local path to save to
299
+ """
300
+ from urllib.parse import urlparse
301
+
302
+ # Validate URL scheme for security
303
+ parsed = urlparse(url)
304
+ if parsed.scheme not in ("http", "https"):
305
+ raise ValueError(f"Invalid URL scheme: {parsed.scheme}. Only http/https allowed.")
306
+
307
+ # Create directory
308
+ data_path.parent.mkdir(parents=True, exist_ok=True)
309
+
310
+ # Download file
311
+ urllib.request.urlretrieve(url, data_path) # nosec B310
312
+
313
+ @staticmethod
314
+ def _fallback_label_values(row_idx: int, n_labels: int, task_type: str) -> list[str]:
315
+ """Generate deterministic fallback labels by task type."""
316
+ if task_type == "classification":
317
+ return [str((row_idx + col_idx) % 2) for col_idx in range(n_labels)]
318
+
319
+ base = -2.0 + (0.15 * row_idx)
320
+ return [f"{base + (0.01 * col_idx):.3f}" for col_idx in range(n_labels)]
321
+
322
+ def _write_builtin_fallback_dataset(self, data_path: Path, dataset_info: dict) -> None:
323
+ """Write a small synthetic dataset to support offline execution."""
324
+ label_cols = dataset_info["label_cols"]
325
+ resolved_label_cols = (
326
+ [f"task_{idx}" for idx in range(dataset_info["n_tasks"])]
327
+ if label_cols is None
328
+ else list(label_cols)
329
+ )
330
+ header = [dataset_info["smiles_col"], *resolved_label_cols]
331
+
332
+ rows: list[list[str]] = []
333
+ for row_idx, smiles in enumerate(_FALLBACK_MOLNET_SMILES):
334
+ labels = self._fallback_label_values(
335
+ row_idx, len(resolved_label_cols), dataset_info["task_type"]
336
+ )
337
+ rows.append([smiles, *labels])
338
+
339
+ data_path.parent.mkdir(parents=True, exist_ok=True)
340
+ if str(data_path).endswith(".gz"):
341
+ with gzip.open(data_path, "wt", newline="", encoding="utf-8") as file_handle:
342
+ writer = csv.writer(file_handle)
343
+ writer.writerow(header)
344
+ writer.writerows(rows)
345
+ else:
346
+ with open(data_path, "w", newline="", encoding="utf-8") as file_handle:
347
+ writer = csv.writer(file_handle)
348
+ writer.writerow(header)
349
+ writer.writerows(rows)
350
+
351
+ def _read_rows_and_labels(
352
+ self,
353
+ data_path: Path,
354
+ smiles_col: str,
355
+ label_cols: list[str] | None,
356
+ ) -> tuple[list[dict[str, str]], list[str]]:
357
+ """Read CSV rows and resolve label columns.
358
+
359
+ Args:
360
+ data_path: Path to CSV file (optionally gzipped).
361
+ smiles_col: Name of the SMILES column.
362
+ label_cols: Explicit label columns, or None to infer.
363
+
364
+ Returns:
365
+ Tuple of (all_rows, resolved_label_columns).
366
+ """
367
+ import contextlib
368
+ import gzip
369
+
370
+ with contextlib.ExitStack() as stack:
371
+ if str(data_path).endswith(".gz"):
372
+ file_handle = stack.enter_context(
373
+ gzip.open(data_path, "rt", newline="", encoding="utf-8")
374
+ )
375
+ else:
376
+ file_handle = stack.enter_context(
377
+ open(data_path, newline="", encoding="utf-8") # noqa: SIM115
378
+ )
379
+
380
+ reader = csv.DictReader(file_handle)
381
+ all_rows: list[dict[str, str]] = list(reader)
382
+
383
+ resolved_labels = (
384
+ [c for c in all_rows[0].keys() if c != smiles_col]
385
+ if label_cols is None and all_rows
386
+ else label_cols
387
+ )
388
+ return all_rows, ([] if resolved_labels is None else list(resolved_labels))
389
+
390
+ def _rows_for_split(self, all_rows: list[dict[str, str]]) -> list[dict[str, str]]:
391
+ """Select rows for the configured split."""
392
+ n_total = len(all_rows)
393
+ n_train = int(0.8 * n_total)
394
+ n_valid = int(0.1 * n_total)
395
+
396
+ if self.config.split == "train":
397
+ return all_rows[:n_train]
398
+ if self.config.split == "valid":
399
+ return all_rows[n_train : n_train + n_valid]
400
+ return all_rows[n_train + n_valid :]
401
+
402
+ @staticmethod
403
+ def _parse_float_or_nan(value: str) -> float:
404
+ """Parse a float value, falling back to NaN for empty/invalid values."""
405
+ try:
406
+ return float(value) if value else float("nan")
407
+ except ValueError:
408
+ return float("nan")
409
+
410
+ def _parse_labels(self, row: dict[str, str], label_cols: list[str]) -> float | jnp.ndarray:
411
+ """Parse one or multiple label columns from a CSV row."""
412
+ if len(label_cols) == 1:
413
+ return self._parse_float_or_nan(row.get(label_cols[0], ""))
414
+ values = [self._parse_float_or_nan(row.get(col, "")) for col in label_cols]
415
+ return jnp.array(values)
416
+
417
+ def _parse_csv(self, data_path: Path, dataset_info: dict) -> list[Element]:
418
+ """Parse CSV file into Elements.
419
+
420
+ Args:
421
+ data_path: Path to CSV file
422
+ dataset_info: Dataset metadata
423
+
424
+ Returns:
425
+ List of Element objects
426
+ """
427
+ smiles_col = dataset_info["smiles_col"]
428
+ label_cols = dataset_info["label_cols"]
429
+
430
+ all_rows, resolved_label_cols = self._read_rows_and_labels(
431
+ data_path, smiles_col, label_cols
432
+ )
433
+ rows = self._rows_for_split(all_rows)
434
+ elements: list[Element] = []
435
+
436
+ for idx, row in enumerate(rows):
437
+ smiles = row.get(smiles_col, "")
438
+ if not smiles:
439
+ continue
440
+
441
+ y = self._parse_labels(row, resolved_label_cols)
442
+
443
+ element = Element(
444
+ data={"smiles": smiles, "y": y},
445
+ state={},
446
+ metadata={ # pyright: ignore[reportArgumentType]
447
+ "idx": idx,
448
+ "dataset": self.config.dataset_name,
449
+ },
450
+ )
451
+ elements.append(element)
452
+
453
+ return elements
454
+
455
+ def __len__(self) -> int:
456
+ """Return the number of elements in the source."""
457
+ return len(self._data)
458
+
459
+ def __getitem__(self, idx: int) -> Element | None:
460
+ """Get element by index.
461
+
462
+ Args:
463
+ idx: Index of the element
464
+
465
+ Returns:
466
+ Element at the given index, or None if out of bounds
467
+ """
468
+ if idx < 0 or idx >= len(self._data):
469
+ return None
470
+ return self._data[idx]
471
+
472
+ def __iter__(self) -> Iterator[Element]: # type: ignore[override]
473
+ """Return iterator over elements."""
474
+ self._current_idx = 0
475
+ return self
476
+
477
+ def __next__(self) -> Element:
478
+ """Get next element in iteration."""
479
+ if self._current_idx >= len(self._data):
480
+ raise StopIteration
481
+ elem = self._data[self._current_idx]
482
+ self._current_idx += 1
483
+ return elem
484
+
485
+ @property
486
+ def task_type(self) -> str:
487
+ """Get the task type for this dataset."""
488
+ return MOLNET_DATASETS[self.config.dataset_name]["task_type"]
489
+
490
+ @property
491
+ def n_tasks(self) -> int:
492
+ """Get the number of tasks for this dataset."""
493
+ return MOLNET_DATASETS[self.config.dataset_name]["n_tasks"]