papyrus-scripts 3.0.0__tar.gz → 3.0.2__tar.gz
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.
- {papyrus_scripts-3.0.0/src/papyrus_scripts.egg-info → papyrus_scripts-3.0.2}/PKG-INFO +7 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/README.md +6 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/__init__.py +1 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/download.py +49 -11
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/fingerprint.py +22 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/modelling.py +9 -2
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/neuralnet.py +27 -9
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/oop.py +24 -3
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/preprocess.py +3 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/subsim_search.py +21 -13
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/IO.py +12 -2
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/links.json +1524 -1524
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2/src/papyrus_scripts.egg-info}/PKG-INFO +7 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_download.py +94 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_modelling.py +161 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_neuralnet.py +162 -1
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_oop.py +4 -2
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_oop_unit.py +70 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/LICENSE +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/pyproject.toml +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/setup.cfg +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/__main__.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/cli.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/matchRCSB.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/reader.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/UniprotMatch.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/__init__.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/aliases.json +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts/utils/mol_reader.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts.egg-info/SOURCES.txt +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts.egg-info/dependency_links.txt +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts.egg-info/entry_points.txt +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts.egg-info/requires.txt +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/src/papyrus_scripts.egg-info/top_level.txt +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_cli.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_fingerprint.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_io.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_matchRCSB.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_modelling_real_data.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_mol_reader.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_preprocess.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_reader.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_reader_offline.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_reader_real_data.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_subsim_search.py +0 -0
- {papyrus_scripts-3.0.0 → papyrus_scripts-3.0.2}/tests/test_uniprot_match.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: papyrus_scripts
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: A collection of scripts to handle the Papyrus bioactivity dataset
|
|
5
5
|
Author: Brandon J. Bongers, Willem Jespers
|
|
6
6
|
Author-email: "Olivier J. M. Béquignon" <olivier.bequignon.maintainer@gmail.com>
|
|
@@ -104,6 +104,12 @@ Optional extras enable additional functionality:
|
|
|
104
104
|
> **Note:** `openbabel` (needed only for FP2/FP3/FP4 fingerprints) must be installed via conda-forge, not pip, when used alongside RDKit/FPSim2/cupy in the same environment: `conda install -c conda-forge openbabel`.
|
|
105
105
|
>
|
|
106
106
|
> **Note:** the `gpu` extra pins `cupy-cuda12x[ctk]`, whose `ctk` extra pulls in the CUDA 12.x toolkit as a pip package — no separate toolkit install needed, only an NVIDIA driver compatible with CUDA 12.x.
|
|
107
|
+
>
|
|
108
|
+
> **Note:** on Windows, plain `pip install papyrus-scripts[dnn]` installs a CPU-only PyTorch build — CUDA-enabled Windows wheels aren't published on PyPI, only on PyTorch's own index. Get GPU support with:
|
|
109
|
+
> ```
|
|
110
|
+
> pip install papyrus-scripts[dnn] --extra-index-url https://download.pytorch.org/whl/cu130
|
|
111
|
+
> ```
|
|
112
|
+
> (swap `cu130` for the CUDA tag matching your driver; see the [PyTorch install matrix](https://pytorch.org/get-started/locally/)). On Linux, the plain PyPI wheel already bundles CUDA, so no extra index is needed there.
|
|
107
113
|
|
|
108
114
|
## 🛠️ Requirements
|
|
109
115
|
|
|
@@ -52,6 +52,12 @@ Optional extras enable additional functionality:
|
|
|
52
52
|
> **Note:** `openbabel` (needed only for FP2/FP3/FP4 fingerprints) must be installed via conda-forge, not pip, when used alongside RDKit/FPSim2/cupy in the same environment: `conda install -c conda-forge openbabel`.
|
|
53
53
|
>
|
|
54
54
|
> **Note:** the `gpu` extra pins `cupy-cuda12x[ctk]`, whose `ctk` extra pulls in the CUDA 12.x toolkit as a pip package — no separate toolkit install needed, only an NVIDIA driver compatible with CUDA 12.x.
|
|
55
|
+
>
|
|
56
|
+
> **Note:** on Windows, plain `pip install papyrus-scripts[dnn]` installs a CPU-only PyTorch build — CUDA-enabled Windows wheels aren't published on PyPI, only on PyTorch's own index. Get GPU support with:
|
|
57
|
+
> ```
|
|
58
|
+
> pip install papyrus-scripts[dnn] --extra-index-url https://download.pytorch.org/whl/cu130
|
|
59
|
+
> ```
|
|
60
|
+
> (swap `cu130` for the CUDA tag matching your driver; see the [PyTorch install matrix](https://pytorch.org/get-started/locally/)). On Linux, the plain PyPI wheel already bundles CUDA, so no extra index is needed there.
|
|
55
61
|
|
|
56
62
|
## 🛠️ Requirements
|
|
57
63
|
|
|
@@ -83,17 +83,29 @@ _SIZE_KEY_BY_FTYPE = {
|
|
|
83
83
|
'3D_structures': 'structures_3D',
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
#: data_size.json omits 'prodec' on every 2022.11.x release (.1-.4), even
|
|
87
|
+
#: though the file itself is published. All four share one sha256; row
|
|
88
|
+
#: count independently verified via ``xz -dc <file> | wc -l``.
|
|
89
|
+
_ROW_COUNT_OVERRIDE_BY_SHA256: dict[str, int] = {
|
|
90
|
+
'3211a62f18ccb7ccc13f885374c1462efeb83ab0e98ed62d2645723f7dc9f1a1': 7505,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _size_for_ftype(sizes: dict, ftype: str, sha256: str | None = None) -> int | None:
|
|
87
95
|
"""Look up *ftype*'s row count in a parsed data_size.json, or ``None`` if absent.
|
|
88
96
|
|
|
89
97
|
Releases up to 2022.11.4 key papyrus++ as ``"papyrus++"``; 2024.09.1+
|
|
90
98
|
use ``"papyrus_++"`` (``_SIZE_KEY_BY_FTYPE['papyrus++']``) - try both.
|
|
99
|
+
|
|
100
|
+
:param sha256: fallback key into :data:`_ROW_COUNT_OVERRIDE_BY_SHA256`.
|
|
91
101
|
"""
|
|
92
102
|
key = _SIZE_KEY_BY_FTYPE.get(ftype)
|
|
93
103
|
if key in sizes:
|
|
94
104
|
return sizes[key]
|
|
95
105
|
if ftype == 'papyrus++' and 'papyrus++' in sizes:
|
|
96
106
|
return sizes['papyrus++']
|
|
107
|
+
if sha256 is not None and sha256 in _ROW_COUNT_OVERRIDE_BY_SHA256:
|
|
108
|
+
return _ROW_COUNT_OVERRIDE_BY_SHA256[sha256]
|
|
97
109
|
return None
|
|
98
110
|
|
|
99
111
|
|
|
@@ -807,7 +819,11 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
807
819
|
_update_current_file_description()
|
|
808
820
|
|
|
809
821
|
def _wait_for_converter() -> None:
|
|
810
|
-
"""Block until the converter process exits, draining progress messages meanwhile.
|
|
822
|
+
"""Block until the converter process exits, draining progress messages meanwhile.
|
|
823
|
+
|
|
824
|
+
Does not close converting_pbar - callers do that via
|
|
825
|
+
_close_converting_pbar, once success/failure is known.
|
|
826
|
+
"""
|
|
811
827
|
# Only ever called once converter_process has been started (see call sites).
|
|
812
828
|
if converter_process is None: # noqa: B023
|
|
813
829
|
raise RuntimeError('converter_process not started')
|
|
@@ -815,8 +831,27 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
815
831
|
_drain_progress_queue()
|
|
816
832
|
converter_process.join(timeout=0.1) # noqa: B023
|
|
817
833
|
_drain_progress_queue()
|
|
818
|
-
|
|
819
|
-
|
|
834
|
+
|
|
835
|
+
def _close_converting_pbar(success: bool) -> None:
|
|
836
|
+
"""Close converting_pbar, topping it up to 100% first on success.
|
|
837
|
+
|
|
838
|
+
total is a naive line count that can exceed the true row count
|
|
839
|
+
(see convert_xz_to_parquet's docstring), so n often lands just
|
|
840
|
+
under total even on success - tqdm.notebook then marks the bar
|
|
841
|
+
'danger' (red) as if it had failed. Topping up avoids that;
|
|
842
|
+
skipped on failure, where n < total is a real signal.
|
|
843
|
+
"""
|
|
844
|
+
if converting_pbar is None: # noqa: B023
|
|
845
|
+
return
|
|
846
|
+
if (
|
|
847
|
+
success # noqa: B023
|
|
848
|
+
and isinstance(converting_pbar.total, (int, float)) # noqa: B023
|
|
849
|
+
and isinstance(converting_pbar.n, (int, float)) # noqa: B023
|
|
850
|
+
):
|
|
851
|
+
shortfall = converting_pbar.total - converting_pbar.n # noqa: B023
|
|
852
|
+
if shortfall > 0:
|
|
853
|
+
converting_pbar.update(shortfall) # noqa: B023
|
|
854
|
+
converting_pbar.close() # noqa: B023
|
|
820
855
|
|
|
821
856
|
def _enqueue(item) -> None:
|
|
822
857
|
"""Put *item* on task_queue, staying responsive while backpressured.
|
|
@@ -850,15 +885,16 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
850
885
|
_drain_progress_queue()
|
|
851
886
|
|
|
852
887
|
if not keep_xz:
|
|
853
|
-
# ftype per file still needing conversion
|
|
854
|
-
#
|
|
855
|
-
|
|
888
|
+
# (ftype, sha256) per file still needing conversion, for
|
|
889
|
+
# converting_pbar's total - sha256 lets _size_for_ftype fall
|
|
890
|
+
# back to _ROW_COUNT_OVERRIDE_BY_SHA256 if needed.
|
|
891
|
+
to_convert_ftypes: list[tuple[str, str]] = []
|
|
856
892
|
for ftype in ordered_ftypes:
|
|
857
893
|
for entry in _iter_entries(version_files[ftype]):
|
|
858
894
|
fpath = _file_path(papyrus_version_root, ftype, entry['name'])
|
|
859
895
|
parquet_path = _parquet_sibling(fpath)
|
|
860
896
|
if parquet_path is not None and not parquet_path.is_file():
|
|
861
|
-
to_convert_ftypes.append(ftype)
|
|
897
|
+
to_convert_ftypes.append((ftype, entry['sha256']))
|
|
862
898
|
|
|
863
899
|
task_queue = mp.Queue(maxsize=1)
|
|
864
900
|
error_queue = mp.Queue()
|
|
@@ -972,8 +1008,8 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
972
1008
|
# unless any is unknown (avoid understating it).
|
|
973
1009
|
if progress and converting_pbar is not None and to_convert_ftypes:
|
|
974
1010
|
per_file_rows = [
|
|
975
|
-
_size_for_ftype(sizes, ft)
|
|
976
|
-
for ft in to_convert_ftypes
|
|
1011
|
+
_size_for_ftype(sizes, ft, sha)
|
|
1012
|
+
for ft, sha in to_convert_ftypes
|
|
977
1013
|
]
|
|
978
1014
|
if all(n is not None for n in per_file_rows):
|
|
979
1015
|
# reset() syncs the widget's max too, unlike plain `.total =`.
|
|
@@ -985,7 +1021,7 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
985
1021
|
'parquet_path': parquet_path,
|
|
986
1022
|
'schema_overrides': schemas.get(_SCHEMA_KEY_BY_FTYPE.get(ftype)),
|
|
987
1023
|
'null_values': _NULL_VALUES_BY_FTYPE.get(ftype),
|
|
988
|
-
'total_rows': _size_for_ftype(sizes, ftype),
|
|
1024
|
+
'total_rows': _size_for_ftype(sizes, ftype, dhash),
|
|
989
1025
|
# ftype (e.g. 'papyrus++', '2D_mold2') rather
|
|
990
1026
|
# than fpath.name: the real filenames (e.g.
|
|
991
1027
|
# '05.6++_combined_set_without_stereochemistry
|
|
@@ -1012,6 +1048,7 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
1012
1048
|
raise RuntimeError('task_queue not created') from None
|
|
1013
1049
|
_enqueue(_CONVERSION_DONE)
|
|
1014
1050
|
_wait_for_converter()
|
|
1051
|
+
_close_converting_pbar(success=False)
|
|
1015
1052
|
if progress:
|
|
1016
1053
|
pbar.close()
|
|
1017
1054
|
raise
|
|
@@ -1028,6 +1065,7 @@ def download_papyrus(outdir: str | Path | None = None,
|
|
|
1028
1065
|
_enqueue(_CONVERSION_DONE)
|
|
1029
1066
|
_wait_for_converter()
|
|
1030
1067
|
error = error_queue.get()
|
|
1068
|
+
_close_converting_pbar(success=error is None)
|
|
1031
1069
|
if error is not None:
|
|
1032
1070
|
if progress:
|
|
1033
1071
|
pbar.close()
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import contextlib
|
|
7
8
|
import hashlib
|
|
8
9
|
import json
|
|
10
|
+
import warnings
|
|
9
11
|
from abc import ABC, abstractmethod
|
|
10
12
|
from collections.abc import Callable
|
|
11
13
|
from typing import Any
|
|
@@ -20,8 +22,27 @@ try:
|
|
|
20
22
|
HAS_PYBEL = True
|
|
21
23
|
except ImportError: # pragma: no cover - only taken when openbabel isn't installed
|
|
22
24
|
HAS_PYBEL = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextlib.contextmanager
|
|
28
|
+
def _suppress_missing_cuda_path_warning():
|
|
29
|
+
"""Silence cupy's harmless "CUDA path could not be detected" warning, nothing else."""
|
|
30
|
+
with warnings.catch_warnings():
|
|
31
|
+
warnings.filterwarnings(
|
|
32
|
+
'ignore',
|
|
33
|
+
message=r'^CUDA path could not be detected\.',
|
|
34
|
+
category=UserWarning,
|
|
35
|
+
module=r'^cupy(\..*)?$',
|
|
36
|
+
)
|
|
37
|
+
yield
|
|
38
|
+
|
|
39
|
+
|
|
23
40
|
try:
|
|
24
|
-
|
|
41
|
+
# Importing any FPSim2 submodule runs FPSim2/__init__.py, which
|
|
42
|
+
# unconditionally imports cupy - this is the first FPSim2 import in the
|
|
43
|
+
# package's import chain, so the warning suppression belongs here.
|
|
44
|
+
with _suppress_missing_cuda_path_warning():
|
|
45
|
+
from FPSim2.FPSim2lib.utils import BitStrToIntList, PyPopcount
|
|
25
46
|
HAS_FPSIM2 = True # pragma: no cover - only taken when FPSim2 is installed
|
|
26
47
|
except ImportError:
|
|
27
48
|
HAS_FPSIM2 = False
|
|
@@ -377,7 +377,9 @@ def _fit_and_evaluate(data: pd.DataFrame,
|
|
|
377
377
|
crossvalidate_model's per-fold-plus-"Full model" dict
|
|
378
378
|
"""
|
|
379
379
|
if split_by.lower() == 'year':
|
|
380
|
-
|
|
380
|
+
# 'Year' may come in as string/object dtype
|
|
381
|
+
years = pd.to_numeric(data['Year'], errors='coerce')
|
|
382
|
+
test_set = data[years >= split_year]
|
|
381
383
|
if test_set.empty:
|
|
382
384
|
raise _InsufficientDataError(f'No test data for temporal split at {split_year}')
|
|
383
385
|
training_set = data[~data.index.isin(test_set.index)]
|
|
@@ -400,7 +402,8 @@ def _fit_and_evaluate(data: pd.DataFrame,
|
|
|
400
402
|
elif split_by.lower() == 'cluster':
|
|
401
403
|
if cluster_method is None:
|
|
402
404
|
raise RuntimeError('cluster_method missing despite qsar()/pcm() validating it upfront')
|
|
403
|
-
|
|
405
|
+
# errors='ignore': merge_on/target_id are already dropped from data by this point
|
|
406
|
+
groups = cluster_method.fit_predict(data.drop(columns=features_to_ignore, errors='ignore'))
|
|
404
407
|
training_set, test_set, training_groups, _ = train_test_proportional_group_split(data, groups,
|
|
405
408
|
test_set_size,
|
|
406
409
|
verbose=verbose)
|
|
@@ -450,6 +453,10 @@ def _fit_and_evaluate(data: pd.DataFrame,
|
|
|
450
453
|
random_state=random_state).to_pandas()
|
|
451
454
|
test_set.index = test_index
|
|
452
455
|
# Make sure enough data
|
|
456
|
+
# (applies to every split_by mode, not just 'year')
|
|
457
|
+
if training_set.shape[0] < folds:
|
|
458
|
+
raise _InsufficientDataError(
|
|
459
|
+
f'Not enough training data ({training_set.shape[0]} rows) for {folds} folds')
|
|
453
460
|
if model_type == 'classifier':
|
|
454
461
|
train_data_classes = Counter(training_set[endpoint])
|
|
455
462
|
if not np.all(np.array(list(train_data_classes.values())) > folds):
|
|
@@ -25,6 +25,7 @@ try:
|
|
|
25
25
|
from skorch.callbacks import Checkpoint, EarlyStopping, LRScheduler
|
|
26
26
|
from skorch.dataset import Dataset as SkorchDataset
|
|
27
27
|
from skorch.helper import predefined_split
|
|
28
|
+
from skorch.utils import to_tensor
|
|
28
29
|
from torch import nn
|
|
29
30
|
HAS_TORCH = True
|
|
30
31
|
except ImportError: # pragma: no cover - exercised only when torch/skorch aren't installed
|
|
@@ -151,6 +152,7 @@ class BaseNN:
|
|
|
151
152
|
# subclass (see SingleTaskNNClassifier etc.), which supplies the
|
|
152
153
|
# rest of this __init__ signature and the fit/predict_proba/
|
|
153
154
|
# initialize members used below - invisible to mypy from here.
|
|
155
|
+
kwargs.setdefault('device', _default_device()) # allow overriding via device= kwarg
|
|
154
156
|
super().__init__( # type: ignore[call-arg]
|
|
155
157
|
module=_MLP,
|
|
156
158
|
optimizer=torch.optim.Adam,
|
|
@@ -160,7 +162,6 @@ class BaseNN:
|
|
|
160
162
|
callbacks=callbacks,
|
|
161
163
|
callbacks__valid_acc=None, # replaced by our own early-stopping/checkpoint logic
|
|
162
164
|
predict_nonlinearity=None, # real value set per-subclass, see set_architecture()/__init__
|
|
163
|
-
device=_default_device(),
|
|
164
165
|
train_split=None, # require an explicit validation set, see set_validation()
|
|
165
166
|
**kwargs,
|
|
166
167
|
)
|
|
@@ -305,6 +306,7 @@ class SingleTaskNNRegressor(BaseNN, skorch.NeuralNetRegressor if HAS_TORCH else
|
|
|
305
306
|
|
|
306
307
|
def __init__(self, *args, **kwargs) -> None:
|
|
307
308
|
"""Neural Network regressor to predict a unique endpoint."""
|
|
309
|
+
_require_torch()
|
|
308
310
|
super().__init__(*args, criterion=nn.MSELoss, **kwargs)
|
|
309
311
|
|
|
310
312
|
def set_architecture(self, n_dim: int) -> None:
|
|
@@ -315,14 +317,28 @@ class SingleTaskNNRegressor(BaseNN, skorch.NeuralNetRegressor if HAS_TORCH else
|
|
|
315
317
|
self._dims = [n_dim, *self.hidden_layers, 1]
|
|
316
318
|
|
|
317
319
|
|
|
318
|
-
class
|
|
319
|
-
"""
|
|
320
|
+
class _MaskedMultiTaskLoss:
|
|
321
|
+
"""Mixin: excludes ``NaN`` targets from the loss, so sparse multi-task rows need no imputation."""
|
|
322
|
+
|
|
323
|
+
def get_loss(self, y_pred, y_true, X=None, training=False):
|
|
324
|
+
"""Mean loss over non-``NaN`` targets; requires criterion(reduction='none')."""
|
|
325
|
+
y_true = to_tensor(y_true, device=self.device)
|
|
326
|
+
mask = ~torch.isnan(y_true)
|
|
327
|
+
if isinstance(self.criterion_, torch.nn.Module):
|
|
328
|
+
self.criterion_.train(training)
|
|
329
|
+
elementwise = self.criterion_(y_pred, torch.where(mask, y_true, torch.zeros_like(y_true)))
|
|
330
|
+
return (elementwise * mask).sum() / mask.sum().clamp(min=1)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class MultiTaskNNClassifier(_MaskedMultiTaskLoss, BaseNN,
|
|
334
|
+
skorch.NeuralNetClassifier if HAS_TORCH else object): # type: ignore[misc]
|
|
335
|
+
"""Neural Network classifier to predict multiple (independent, binary) endpoints; supports ``NaN`` labels."""
|
|
320
336
|
|
|
321
337
|
def __init__(self, *args, **kwargs) -> None:
|
|
322
338
|
"""Neural Network classifier to predict multiple endpoints."""
|
|
323
|
-
|
|
324
|
-
#
|
|
325
|
-
super().__init__(*args, criterion=nn.BCEWithLogitsLoss, **kwargs)
|
|
339
|
+
_require_torch()
|
|
340
|
+
# Raw logits + BCEWithLogitsLoss (see SingleTaskNNClassifier); reduction='none' for masking.
|
|
341
|
+
super().__init__(*args, criterion=nn.BCEWithLogitsLoss, criterion__reduction='none', **kwargs)
|
|
326
342
|
self.predict_nonlinearity = torch.sigmoid
|
|
327
343
|
|
|
328
344
|
def set_architecture(self, n_dim: int, n_task: int) -> None:
|
|
@@ -350,12 +366,14 @@ class MultiTaskNNClassifier(BaseNN, skorch.NeuralNetClassifier if HAS_TORCH else
|
|
|
350
366
|
return np.round(self.predict_proba(X))
|
|
351
367
|
|
|
352
368
|
|
|
353
|
-
class MultiTaskNNRegressor(BaseNN,
|
|
354
|
-
|
|
369
|
+
class MultiTaskNNRegressor(_MaskedMultiTaskLoss, BaseNN,
|
|
370
|
+
skorch.NeuralNetRegressor if HAS_TORCH else object): # type: ignore[misc]
|
|
371
|
+
"""Neural Network regressor to predict multiple endpoints; supports ``NaN`` targets."""
|
|
355
372
|
|
|
356
373
|
def __init__(self, *args, **kwargs) -> None:
|
|
357
374
|
"""Neural Network regressor to predict multiple endpoints."""
|
|
358
|
-
|
|
375
|
+
_require_torch()
|
|
376
|
+
super().__init__(*args, criterion=nn.MSELoss, criterion__reduction='none', **kwargs) # 'none' for masking
|
|
359
377
|
|
|
360
378
|
def set_architecture(self, n_dim: int, n_task: int) -> None:
|
|
361
379
|
"""Set dimension of input and number of tasks to be predicted.
|
|
@@ -427,6 +427,7 @@ class PapyrusDataset:
|
|
|
427
427
|
chunksize: int | None = None,
|
|
428
428
|
keep_original_files: bool = False,
|
|
429
429
|
disk_margin: float = 0.10,
|
|
430
|
+
download_if_missing: bool = False,
|
|
430
431
|
) -> PapyrusDataset:
|
|
431
432
|
"""Create a :class:`PapyrusDataset` from an existing DataFrame.
|
|
432
433
|
|
|
@@ -443,6 +444,11 @@ class PapyrusDataset:
|
|
|
443
444
|
lazily through this dataset (default: False)
|
|
444
445
|
:param disk_margin: safety margin for any download later triggered
|
|
445
446
|
through this dataset (default: 0.10)
|
|
447
|
+
:param download_if_missing: download the protein-target file if not
|
|
448
|
+
found locally, instead of raising (default: False)
|
|
449
|
+
:raises FileNotFoundError | NotADirectoryError | OSError | ValueError:
|
|
450
|
+
if the protein-target file isn't found and *download_if_missing*
|
|
451
|
+
is False
|
|
446
452
|
:returns: a :class:`PapyrusDataset` wrapping *df*
|
|
447
453
|
"""
|
|
448
454
|
pv = _ensure_papyrus_version(version)
|
|
@@ -450,9 +456,24 @@ class PapyrusDataset:
|
|
|
450
456
|
df = pl.from_pandas(df)
|
|
451
457
|
dataset = PapyrusDataset.__new__(PapyrusDataset)
|
|
452
458
|
dataset.papyrus_bioactivity_data = df
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
try:
|
|
460
|
+
dataset.papyrus_protein_data = reader.read_protein_set(
|
|
461
|
+
source_path=source_path, version=pv,
|
|
462
|
+
)
|
|
463
|
+
except _NOT_AVAILABLE_LOCALLY:
|
|
464
|
+
if not download_if_missing:
|
|
465
|
+
raise
|
|
466
|
+
download.download_papyrus(
|
|
467
|
+
outdir=source_path,
|
|
468
|
+
version=pv.pystow_path_key, # not .version: must match the folder key reads use
|
|
469
|
+
nostereo=not is3d, stereo=is3d, only_pp=plusplus,
|
|
470
|
+
structures=False, descriptors=None,
|
|
471
|
+
progress=download_progress, disk_margin=disk_margin,
|
|
472
|
+
keep_xz=keep_original_files,
|
|
473
|
+
)
|
|
474
|
+
dataset.papyrus_protein_data = reader.read_protein_set(
|
|
475
|
+
source_path=source_path, version=pv,
|
|
476
|
+
)
|
|
456
477
|
dataset.papyrus_params = dict(
|
|
457
478
|
is3d=is3d, version=pv, plusplus=plusplus,
|
|
458
479
|
chunksize=chunksize, source_path=source_path,
|
|
@@ -290,7 +290,9 @@ def process_groups(
|
|
|
290
290
|
|
|
291
291
|
if has_pchembl:
|
|
292
292
|
pv = pl.col('pchembl_value')
|
|
293
|
-
|
|
293
|
+
# Raw values can carry padding (e.g. " 6.700") - strip before the
|
|
294
|
+
# strict numeric cast, which otherwise rejects it.
|
|
295
|
+
pv_num = pv.str.strip_chars().cast(pl.Float64)
|
|
294
296
|
median = pv_num.median()
|
|
295
297
|
n = pv_num.drop_nulls().len()
|
|
296
298
|
std = pv_num.std()
|
|
@@ -27,6 +27,8 @@ from rdkit import Chem
|
|
|
27
27
|
from rdkit.Chem.rdSubstructLibrary import CachedSmilesMolHolder, PatternHolder, SubstructLibrary
|
|
28
28
|
from tqdm.auto import tqdm
|
|
29
29
|
|
|
30
|
+
from .fingerprint import Fingerprint, MorganFingerprint, _suppress_missing_cuda_path_warning, get_fp_from_name
|
|
31
|
+
|
|
30
32
|
try:
|
|
31
33
|
import tables as tb
|
|
32
34
|
HAS_TABLES = True # pragma: no cover - exercised only with pytables installed
|
|
@@ -36,16 +38,16 @@ except ImportError:
|
|
|
36
38
|
try:
|
|
37
39
|
# This whole block only runs with FPSim2 installed; every line below the
|
|
38
40
|
# first import is unreachable otherwise (the first import raises first).
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
with _suppress_missing_cuda_path_warning():
|
|
42
|
+
from FPSim2.base import BaseEngine # pragma: no cover
|
|
43
|
+
from FPSim2.FPSim2 import FPSim2Engine
|
|
44
|
+
from FPSim2.io.backends.base import BaseStorageBackend # pragma: no cover
|
|
45
|
+
from FPSim2.io.backends.pytables import ( # pragma: no cover
|
|
46
|
+
BATCH_WRITE_SIZE,
|
|
47
|
+
calc_popcnt_bins_pytables,
|
|
48
|
+
create_schema,
|
|
49
|
+
)
|
|
50
|
+
from FPSim2.io.chem import load_molecule # pragma: no cover
|
|
49
51
|
HAS_FPSIM2 = True # pragma: no cover
|
|
50
52
|
except ImportError:
|
|
51
53
|
HAS_FPSIM2 = False
|
|
@@ -60,12 +62,18 @@ except ImportError:
|
|
|
60
62
|
|
|
61
63
|
class FPSim2Engine: # type: ignore[no-redef]
|
|
62
64
|
"""Stub for FPSim2.FPSim2.FPSim2Engine when FPSim2 is absent."""
|
|
65
|
+
BATCH_WRITE_SIZE = 32_000 # FPSim2's own default; only used for queue sizing here
|
|
63
66
|
|
|
67
|
+
try:
|
|
68
|
+
# FPSim2.FPSim2Cuda unconditionally imports cupy, so this is kept
|
|
69
|
+
# separate from HAS_FPSIM2 above: a missing GPU stack must not disable
|
|
70
|
+
# CPU-only search.
|
|
71
|
+
with _suppress_missing_cuda_path_warning():
|
|
72
|
+
from FPSim2.FPSim2Cuda import FPSim2CudaEngine # pragma: no cover
|
|
73
|
+
except ImportError:
|
|
64
74
|
class FPSim2CudaEngine: # type: ignore[no-redef]
|
|
65
|
-
"""Stub for FPSim2.FPSim2Cuda.FPSim2CudaEngine when
|
|
66
|
-
BATCH_WRITE_SIZE = 32_000 # FPSim2's own default; only used for queue sizing here
|
|
75
|
+
"""Stub for FPSim2.FPSim2Cuda.FPSim2CudaEngine when cupy is absent."""
|
|
67
76
|
|
|
68
|
-
from .fingerprint import Fingerprint, MorganFingerprint, get_fp_from_name
|
|
69
77
|
from .utils.IO import PapyrusVersion, _prefer_parquet, _set_root_folder, get_num_rows_in_file, locate_file
|
|
70
78
|
from .utils.mol_reader import MolSupplier
|
|
71
79
|
|
|
@@ -294,6 +294,11 @@ def to_polars_schema(dtypes: dict) -> dict:
|
|
|
294
294
|
return {col: to_polars_dtype(t) for col, t in dtypes.items()}
|
|
295
295
|
|
|
296
296
|
|
|
297
|
+
#: 'Year' is absent from shipped data_types.json (unlike 'all_years', a
|
|
298
|
+
#: semicolon-joined list, it's always a clean single integer or null).
|
|
299
|
+
_EXTRA_PAPYRUS_SCHEMA_OVERRIDES: dict = {'Year': pl.Int32}
|
|
300
|
+
|
|
301
|
+
|
|
297
302
|
def load_data_type_schemas(source_module: pystow.Module) -> dict:
|
|
298
303
|
"""Read a version folder's ``data_types.json`` and return ``{section: {col: polars_dtype}}``.
|
|
299
304
|
|
|
@@ -302,10 +307,14 @@ def load_data_type_schemas(source_module: pystow.Module) -> dict:
|
|
|
302
307
|
dtype_file = source_module.join(name='data_types.json')
|
|
303
308
|
with open(dtype_file) as fh:
|
|
304
309
|
raw = json.load(fh, cls=TypeDecoder)
|
|
305
|
-
|
|
310
|
+
schemas = {
|
|
306
311
|
key: to_polars_schema(val) if isinstance(val, dict) else val
|
|
307
312
|
for key, val in raw.items()
|
|
308
313
|
}
|
|
314
|
+
if 'papyrus' in schemas:
|
|
315
|
+
for col, dtype in _EXTRA_PAPYRUS_SCHEMA_OVERRIDES.items():
|
|
316
|
+
schemas['papyrus'].setdefault(col, dtype)
|
|
317
|
+
return schemas
|
|
309
318
|
|
|
310
319
|
|
|
311
320
|
# ---------------------------------------------------------------------------
|
|
@@ -1097,7 +1106,8 @@ def _downcast_integer_overrides(
|
|
|
1097
1106
|
``None`` if every eligible column downcast cleanly.
|
|
1098
1107
|
"""
|
|
1099
1108
|
for col, target in overrides.items():
|
|
1100
|
-
|
|
1109
|
+
# A shared schema may name a column one file variant lacks.
|
|
1110
|
+
if col not in chunk.columns or target not in _NULLABLE_INT_DTYPES or col in forced_float_cols:
|
|
1101
1111
|
continue
|
|
1102
1112
|
non_null = chunk[col].dropna()
|
|
1103
1113
|
if len(non_null) == 0 or (non_null % 1 == 0).all():
|