D4CMPP2 0.4.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.
- D4CMPP2/_Data/AGENTS.md +24 -0
- D4CMPP2/_Data/Aqsoldb.csv +9291 -0
- D4CMPP2/_Data/BradleyMP.csv +3042 -0
- D4CMPP2/_Data/Lipophilicity.csv +1131 -0
- D4CMPP2/_Data/README.md +8 -0
- D4CMPP2/_Data/__init__.py +26 -0
- D4CMPP2/_Data/optical.csv +20237 -0
- D4CMPP2/_Data/test.csv +190 -0
- D4CMPP2/__init__.py +16 -0
- D4CMPP2/__main__.py +5 -0
- D4CMPP2/_main.py +500 -0
- D4CMPP2/cli.py +7 -0
- D4CMPP2/exceptions.py +53 -0
- D4CMPP2/grid_search.py +259 -0
- D4CMPP2/network_refer.yaml +160 -0
- D4CMPP2/networks/AFP_model.py +72 -0
- D4CMPP2/networks/AFPwithSolv_model.py +72 -0
- D4CMPP2/networks/DMPNN_model.py +90 -0
- D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
- D4CMPP2/networks/GAT_model.py +48 -0
- D4CMPP2/networks/GATwithSolv_model.py +63 -0
- D4CMPP2/networks/GCN_model.py +113 -0
- D4CMPP2/networks/GCNwithSolv_model.py +103 -0
- D4CMPP2/networks/GC_model.py +122 -0
- D4CMPP2/networks/ISATPM_model.py +14 -0
- D4CMPP2/networks/ISATPN_model.py +199 -0
- D4CMPP2/networks/ISAT_model.py +90 -0
- D4CMPP2/networks/MPNN_model.py +56 -0
- D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
- D4CMPP2/networks/__init__.py +25 -0
- D4CMPP2/networks/base.py +250 -0
- D4CMPP2/networks/registry.py +187 -0
- D4CMPP2/networks/src/AFP.py +118 -0
- D4CMPP2/networks/src/BiDropout.py +29 -0
- D4CMPP2/networks/src/DMPNN.py +35 -0
- D4CMPP2/networks/src/GAT.py +69 -0
- D4CMPP2/networks/src/GC.py +85 -0
- D4CMPP2/networks/src/GCN.py +71 -0
- D4CMPP2/networks/src/ISAT.py +153 -0
- D4CMPP2/networks/src/Linear.py +49 -0
- D4CMPP2/networks/src/MPNN.py +56 -0
- D4CMPP2/networks/src/SolventLayer.py +62 -0
- D4CMPP2/networks/src/__init__.py +0 -0
- D4CMPP2/networks/src/distGCN.py +21 -0
- D4CMPP2/networks/src/pyg_hetero.py +24 -0
- D4CMPP2/optimize.py +472 -0
- D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
- D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
- D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
- D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
- D4CMPP2/src/Analyzer/__init__.py +54 -0
- D4CMPP2/src/Analyzer/core.py +480 -0
- D4CMPP2/src/Analyzer/factory.py +166 -0
- D4CMPP2/src/Analyzer/interpretation.py +232 -0
- D4CMPP2/src/Analyzer/results.py +101 -0
- D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
- D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
- D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
- D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
- D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
- D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
- D4CMPP2/src/DataManager/ISADataManager.py +67 -0
- D4CMPP2/src/DataManager/MolDataManager.py +735 -0
- D4CMPP2/src/DataManager/__init__.py +14 -0
- D4CMPP2/src/DataManager/contracts.py +179 -0
- D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
- D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
- D4CMPP2/src/NetworkManager/__init__.py +14 -0
- D4CMPP2/src/PostProcessor.py +160 -0
- D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
- D4CMPP2/src/TrainManager/TrainManager.py +254 -0
- D4CMPP2/src/TrainManager/__init__.py +14 -0
- D4CMPP2/src/TrainManager/callbacks.py +119 -0
- D4CMPP2/src/__init__.py +0 -0
- D4CMPP2/src/utils/PATH.py +246 -0
- D4CMPP2/src/utils/__init__.py +0 -0
- D4CMPP2/src/utils/argparser.py +56 -0
- D4CMPP2/src/utils/checkpointing.py +90 -0
- D4CMPP2/src/utils/config_resolution.py +123 -0
- D4CMPP2/src/utils/config_validation.py +370 -0
- D4CMPP2/src/utils/csv_validation.py +105 -0
- D4CMPP2/src/utils/data_quality.py +181 -0
- D4CMPP2/src/utils/featureizer.py +202 -0
- D4CMPP2/src/utils/functional_group.csv +169 -0
- D4CMPP2/src/utils/graph_cache.py +213 -0
- D4CMPP2/src/utils/leaderboard.py +212 -0
- D4CMPP2/src/utils/metrics.py +31 -0
- D4CMPP2/src/utils/module_loader.py +147 -0
- D4CMPP2/src/utils/output.py +80 -0
- D4CMPP2/src/utils/reproducibility.py +70 -0
- D4CMPP2/src/utils/run_manifest.py +175 -0
- D4CMPP2/src/utils/scaler.py +40 -0
- D4CMPP2/src/utils/sculptor.py +713 -0
- D4CMPP2/src/utils/splitting.py +250 -0
- D4CMPP2/src/utils/supportfile_saver.py +94 -0
- D4CMPP2/src/utils/tools.py +156 -0
- D4CMPP2/src/utils/transfer_learning.py +111 -0
- d4cmpp2-0.4.0.dist-info/METADATA +420 -0
- d4cmpp2-0.4.0.dist-info/RECORD +103 -0
- d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
- d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
- d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
- d4cmpp2-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Read-only experiment comparison for current and legacy model directories."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_METRIC = "val_rmse"
|
|
12
|
+
_SUMMARY_PATTERNS = {
|
|
13
|
+
"parameter_count": re.compile(r"#params:\s*(\d+)"),
|
|
14
|
+
"runtime_seconds": re.compile(r"learning_time:\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _model_directories(paths):
|
|
19
|
+
if isinstance(paths, (str, Path)):
|
|
20
|
+
paths = [paths]
|
|
21
|
+
if not paths:
|
|
22
|
+
raise ValueError("At least one model directory or search root is required.")
|
|
23
|
+
|
|
24
|
+
discovered = set()
|
|
25
|
+
for value in paths:
|
|
26
|
+
root = Path(value).expanduser()
|
|
27
|
+
if not root.exists():
|
|
28
|
+
raise FileNotFoundError(
|
|
29
|
+
f"Experiment path {str(root)!r} does not exist. "
|
|
30
|
+
"Provide a model directory or a directory containing model folders."
|
|
31
|
+
)
|
|
32
|
+
if root.is_file():
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Experiment path {str(root)!r} is a file. Provide its model directory instead."
|
|
35
|
+
)
|
|
36
|
+
if (root / "config.yaml").is_file():
|
|
37
|
+
discovered.add(root.resolve())
|
|
38
|
+
continue
|
|
39
|
+
for config_path in root.rglob("config.yaml"):
|
|
40
|
+
discovered.add(config_path.parent.resolve())
|
|
41
|
+
if not discovered:
|
|
42
|
+
shown = ", ".join(repr(str(Path(value))) for value in paths)
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"No model directories containing config.yaml were found below {shown}. "
|
|
45
|
+
"Check the search roots or pass model directories directly."
|
|
46
|
+
)
|
|
47
|
+
return sorted(discovered, key=lambda path: str(path).casefold())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_yaml(path):
|
|
51
|
+
try:
|
|
52
|
+
with path.open("r", encoding="utf-8") as stream:
|
|
53
|
+
value = yaml.safe_load(stream)
|
|
54
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Could not read model config {str(path)!r}: {exc}. "
|
|
57
|
+
"Check that config.yaml is valid UTF-8 YAML."
|
|
58
|
+
) from exc
|
|
59
|
+
if not isinstance(value, dict):
|
|
60
|
+
raise ValueError(f"Model config {str(path)!r} must contain a YAML mapping.")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _read_manifests(model_path):
|
|
65
|
+
manifests = []
|
|
66
|
+
for path in sorted((model_path / "runs").glob("*/run_manifest.json")):
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
69
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Could not read run manifest {str(path)!r}: {exc}. "
|
|
72
|
+
"Repair or remove the malformed manifest before comparison."
|
|
73
|
+
) from exc
|
|
74
|
+
if not isinstance(data, dict):
|
|
75
|
+
raise ValueError(f"Run manifest {str(path)!r} must contain a JSON object.")
|
|
76
|
+
data["_manifest_path"] = str(path)
|
|
77
|
+
manifests.append(data)
|
|
78
|
+
return manifests
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _read_metrics(model_path):
|
|
82
|
+
candidates = [model_path / "result" / "metrics.csv", model_path / "metrics.csv"]
|
|
83
|
+
path = next((candidate for candidate in candidates if candidate.is_file()), None)
|
|
84
|
+
if path is None:
|
|
85
|
+
return None, None
|
|
86
|
+
try:
|
|
87
|
+
frame = pd.read_csv(path, index_col=0)
|
|
88
|
+
except (OSError, pd.errors.ParserError, UnicodeError) as exc:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Could not read metrics file {str(path)!r}: {exc}. "
|
|
91
|
+
"Check that metrics.csv is a valid CSV file."
|
|
92
|
+
) from exc
|
|
93
|
+
frame.index = frame.index.map(str)
|
|
94
|
+
return frame, str(path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _summary(model_path):
|
|
98
|
+
path = model_path / "model_summary.txt"
|
|
99
|
+
if not path.is_file():
|
|
100
|
+
return {}
|
|
101
|
+
try:
|
|
102
|
+
text = path.read_text(encoding="utf-8")
|
|
103
|
+
except (OSError, UnicodeError):
|
|
104
|
+
return {}
|
|
105
|
+
values = {}
|
|
106
|
+
for key, pattern in _SUMMARY_PATTERNS.items():
|
|
107
|
+
match = pattern.search(text)
|
|
108
|
+
if match:
|
|
109
|
+
values[key] = float(match.group(1)) if key == "runtime_seconds" else int(match.group(1))
|
|
110
|
+
return values
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _targets(config, metrics):
|
|
114
|
+
configured = config.get("target")
|
|
115
|
+
if isinstance(configured, str):
|
|
116
|
+
configured = [configured]
|
|
117
|
+
targets = [str(value) for value in configured or []]
|
|
118
|
+
if metrics is not None:
|
|
119
|
+
targets.extend(str(value) for value in metrics.index)
|
|
120
|
+
return list(dict.fromkeys(targets)) or [None]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _base_row(model_path, config, manifest, summary):
|
|
124
|
+
network = manifest.get("network", {}) if manifest else {}
|
|
125
|
+
manifest_config = manifest.get("config", {}) if manifest else {}
|
|
126
|
+
effective = dict(config)
|
|
127
|
+
effective.update(manifest_config if isinstance(manifest_config, dict) else {})
|
|
128
|
+
return {
|
|
129
|
+
"model_path": str(model_path),
|
|
130
|
+
"run_id": manifest.get("run_id") if manifest else "legacy",
|
|
131
|
+
"status": manifest.get("status") if manifest else "legacy",
|
|
132
|
+
"mode": manifest.get("mode") if manifest else "legacy",
|
|
133
|
+
"started_at": manifest.get("started_at") if manifest else None,
|
|
134
|
+
"ended_at": manifest.get("ended_at") if manifest else None,
|
|
135
|
+
"duration_seconds": manifest.get("duration_seconds", summary.get("runtime_seconds")) if manifest else summary.get("runtime_seconds"),
|
|
136
|
+
"network": network.get("id") or effective.get("network_id") or effective.get("network"),
|
|
137
|
+
"data": effective.get("data") or Path(str(effective.get("DATA_PATH", ""))).stem or None,
|
|
138
|
+
"best_epoch": manifest.get("best_epoch") if manifest else None,
|
|
139
|
+
"parameter_count": summary.get("parameter_count"),
|
|
140
|
+
"batch_size": effective.get("batch_size"),
|
|
141
|
+
"learning_rate": effective.get("learning_rate", effective.get("lr")),
|
|
142
|
+
"optimizer": effective.get("optimizer"),
|
|
143
|
+
"max_epoch": effective.get("max_epoch"),
|
|
144
|
+
"manifest_path": manifest.get("_manifest_path") if manifest else None,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def collect_experiments(paths):
|
|
149
|
+
"""Return one row per run and target, including legacy model folders."""
|
|
150
|
+
rows = []
|
|
151
|
+
for model_path in _model_directories(paths):
|
|
152
|
+
config = _read_yaml(model_path / "config.yaml")
|
|
153
|
+
manifests = _read_manifests(model_path)
|
|
154
|
+
metrics, metrics_path = _read_metrics(model_path)
|
|
155
|
+
summary = _summary(model_path)
|
|
156
|
+
latest_completed = None
|
|
157
|
+
completed = [item for item in manifests if item.get("status") == "completed"]
|
|
158
|
+
if completed:
|
|
159
|
+
latest_completed = max(
|
|
160
|
+
completed,
|
|
161
|
+
key=lambda item: (item.get("ended_at") or "", item.get("run_id") or ""),
|
|
162
|
+
)
|
|
163
|
+
entries = manifests or [None]
|
|
164
|
+
for manifest in entries:
|
|
165
|
+
attach_metrics = manifest is None or manifest is latest_completed
|
|
166
|
+
for target in _targets(config, metrics if attach_metrics else None):
|
|
167
|
+
row = _base_row(model_path, config, manifest, summary)
|
|
168
|
+
row["target"] = target
|
|
169
|
+
row["metric_source"] = metrics_path if attach_metrics else None
|
|
170
|
+
if attach_metrics and metrics is not None and target in metrics.index:
|
|
171
|
+
for name, value in metrics.loc[target].items():
|
|
172
|
+
row[str(name)] = value
|
|
173
|
+
rows.append(row)
|
|
174
|
+
return pd.DataFrame(rows)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def compare_experiments(paths, output_path="leaderboard.csv", metric=DEFAULT_METRIC, target=None, ascending=None):
|
|
178
|
+
"""Collect, rank within each target, save CSV, and return a DataFrame."""
|
|
179
|
+
frame = collect_experiments(paths)
|
|
180
|
+
if target is not None:
|
|
181
|
+
frame = frame[frame["target"] == str(target)].copy()
|
|
182
|
+
if frame.empty:
|
|
183
|
+
available = sorted(value for value in collect_experiments(paths)["target"].dropna().unique())
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Target {target!r} was not found. Available targets: {available}."
|
|
186
|
+
)
|
|
187
|
+
if metric not in frame.columns:
|
|
188
|
+
available = sorted(
|
|
189
|
+
column for column in frame.columns
|
|
190
|
+
if column.startswith(("train_", "val_", "test_"))
|
|
191
|
+
)
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Metric {metric!r} was not found. Available metric columns: {available}."
|
|
194
|
+
)
|
|
195
|
+
if ascending is None:
|
|
196
|
+
ascending = not metric.endswith(("_r2", "_accuracy"))
|
|
197
|
+
frame["selected_metric"] = metric
|
|
198
|
+
frame["selected_metric_value"] = pd.to_numeric(frame[metric], errors="coerce")
|
|
199
|
+
frame["rank"] = frame.groupby("target", dropna=False)["selected_metric_value"].rank(
|
|
200
|
+
method="min",
|
|
201
|
+
ascending=ascending,
|
|
202
|
+
na_option="bottom",
|
|
203
|
+
)
|
|
204
|
+
frame = frame.sort_values(
|
|
205
|
+
["target", "rank", "model_path", "run_id"],
|
|
206
|
+
na_position="last",
|
|
207
|
+
kind="stable",
|
|
208
|
+
).reset_index(drop=True)
|
|
209
|
+
destination = Path(output_path)
|
|
210
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
frame.to_csv(destination, index=False)
|
|
212
|
+
return frame
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
def get_metrics(targets, prediction_df, ):
|
|
5
|
+
metrics = {}
|
|
6
|
+
for target in targets:
|
|
7
|
+
metrics[target] = {}
|
|
8
|
+
for set in ['train','val','test']:
|
|
9
|
+
true = prediction_df[prediction_df['set']==set][f"{target}_true"]
|
|
10
|
+
pred = prediction_df[prediction_df['set']==set][f"{target}_pred"]
|
|
11
|
+
metrics[target][set+'_r2'] = r2_score(true,pred)
|
|
12
|
+
metrics[target][set+'_mae'] = mean_absolute_error(true,pred)
|
|
13
|
+
metrics[target][set+'_rmse'] = np.sqrt(mean_squared_error(true,pred))
|
|
14
|
+
metrics[target][set+'_max_error'] = max_error(true,pred)
|
|
15
|
+
metrics = pd.DataFrame(metrics).T
|
|
16
|
+
metrics = metrics[[f"{set}_{metric}" for set in ['train','val','test'] for metric in ['r2','mae','rmse','max_error']]]
|
|
17
|
+
return metrics
|
|
18
|
+
|
|
19
|
+
def r2_score(true,pred):
|
|
20
|
+
ss_res = np.sum((true - pred) ** 2)
|
|
21
|
+
ss_tot = np.sum((true - np.mean(true)) ** 2)
|
|
22
|
+
return 1 - (ss_res / ss_tot)
|
|
23
|
+
|
|
24
|
+
def mean_absolute_error(true,pred):
|
|
25
|
+
return np.mean(np.abs(true-pred))
|
|
26
|
+
|
|
27
|
+
def mean_squared_error(true,pred):
|
|
28
|
+
return np.mean((true-pred)**2)
|
|
29
|
+
|
|
30
|
+
def max_error(true,pred):
|
|
31
|
+
return np.max(np.abs(true-pred))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable, Mapping, Union
|
|
8
|
+
|
|
9
|
+
from D4CMPP2.exceptions import ManagerNotFoundError, ModuleLoadError
|
|
10
|
+
|
|
11
|
+
Config = Mapping[str, Any]
|
|
12
|
+
ManagerFactory = Callable[[str, str], Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_train_manager(config: Config) -> Any:
|
|
16
|
+
"""Construct the configured built-in or external training manager."""
|
|
17
|
+
|
|
18
|
+
return _load_manager(
|
|
19
|
+
"TrainManager_PATH",
|
|
20
|
+
"train_manager_module",
|
|
21
|
+
"train_manager_class",
|
|
22
|
+
load_default_train_manager,
|
|
23
|
+
)(config)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_data_manager(config: Config) -> Any:
|
|
27
|
+
"""Construct the configured built-in or external data manager."""
|
|
28
|
+
|
|
29
|
+
return _load_manager(
|
|
30
|
+
"DataManager_PATH",
|
|
31
|
+
"data_manager_module",
|
|
32
|
+
"data_manager_class",
|
|
33
|
+
load_default_data_manager,
|
|
34
|
+
)(config)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_network_manager(config: Config) -> Any:
|
|
38
|
+
"""Construct the configured built-in or external network manager."""
|
|
39
|
+
|
|
40
|
+
return _load_manager(
|
|
41
|
+
"NetworkManager_PATH",
|
|
42
|
+
"network_manager_module",
|
|
43
|
+
"network_manager_class",
|
|
44
|
+
load_default_network_manager,
|
|
45
|
+
)(config)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_manager(
|
|
49
|
+
path_key: str,
|
|
50
|
+
module_key: str,
|
|
51
|
+
class_key: str,
|
|
52
|
+
default_manager: ManagerFactory,
|
|
53
|
+
) -> Callable[[Config], Any]:
|
|
54
|
+
def load_manager(config: Config) -> Any:
|
|
55
|
+
module_name = config.get(module_key)
|
|
56
|
+
class_name = config.get(class_key)
|
|
57
|
+
if not isinstance(module_name, str) or not isinstance(class_name, str):
|
|
58
|
+
raise ManagerNotFoundError(
|
|
59
|
+
f"Manager config requires string keys {module_key!r} and "
|
|
60
|
+
f"{class_key!r}; got module={module_name!r}, class={class_name!r}."
|
|
61
|
+
)
|
|
62
|
+
if path_key in config:
|
|
63
|
+
path = config[path_key]
|
|
64
|
+
module_path = os.path.join(path, module_name + ".py")
|
|
65
|
+
if not os.path.isfile(module_path):
|
|
66
|
+
raise ManagerNotFoundError(
|
|
67
|
+
f"Custom manager module {module_path!r} was not found. "
|
|
68
|
+
f"Check {path_key}, {module_key}, and {class_key}."
|
|
69
|
+
)
|
|
70
|
+
return load_module(path, module_name, class_name)
|
|
71
|
+
try:
|
|
72
|
+
return default_manager(module_name, class_name)
|
|
73
|
+
except (AttributeError, KeyError) as exc:
|
|
74
|
+
raise ManagerNotFoundError(
|
|
75
|
+
f"Built-in manager {module_name}.{class_name} was not found. "
|
|
76
|
+
f"Check {module_key} and {class_key} in the selected registry."
|
|
77
|
+
) from exc
|
|
78
|
+
|
|
79
|
+
return load_manager
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_default_train_manager(module_name: str, class_name: str) -> Any:
|
|
83
|
+
"""Resolve a training-manager class from the installed package."""
|
|
84
|
+
|
|
85
|
+
return _load_default_manager("TrainManager", module_name, class_name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_default_data_manager(module_name: str, class_name: str) -> Any:
|
|
89
|
+
"""Resolve a data-manager class from the installed package."""
|
|
90
|
+
|
|
91
|
+
return _load_default_manager("DataManager", module_name, class_name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_default_network_manager(module_name: str, class_name: str) -> Any:
|
|
95
|
+
"""Resolve a network-manager class from the installed package."""
|
|
96
|
+
|
|
97
|
+
return _load_default_manager("NetworkManager", module_name, class_name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_default_manager(family: str, module_name: str, class_name: str) -> Any:
|
|
101
|
+
module = importlib.import_module(f"D4CMPP2.src.{family}.{module_name}")
|
|
102
|
+
return getattr(module, class_name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def load_module(
|
|
106
|
+
path: Union[str, os.PathLike[str]],
|
|
107
|
+
module: str,
|
|
108
|
+
class_name: str,
|
|
109
|
+
) -> Any:
|
|
110
|
+
"""Load one custom manager class without permanently changing ``sys.path``."""
|
|
111
|
+
|
|
112
|
+
directory = str(Path(path).resolve())
|
|
113
|
+
module_path = os.path.join(directory, module + ".py")
|
|
114
|
+
identity = hashlib.sha256(
|
|
115
|
+
os.path.normcase(module_path).encode("utf-8")
|
|
116
|
+
).hexdigest()[:16]
|
|
117
|
+
internal_name = f"_d4cmpp2_custom_{module}_{identity}"
|
|
118
|
+
inserted_path = directory not in sys.path
|
|
119
|
+
try:
|
|
120
|
+
if inserted_path:
|
|
121
|
+
sys.path.insert(0, directory)
|
|
122
|
+
spec2 = importlib.util.spec_from_file_location(internal_name, module_path)
|
|
123
|
+
if spec2 is None or spec2.loader is None:
|
|
124
|
+
raise ImportError("Python could not create a module specification.")
|
|
125
|
+
module2 = importlib.util.module_from_spec(spec2)
|
|
126
|
+
sys.modules[internal_name] = module2
|
|
127
|
+
spec2.loader.exec_module(module2)
|
|
128
|
+
except (ImportError, OSError, SyntaxError) as exc:
|
|
129
|
+
sys.modules.pop(internal_name, None)
|
|
130
|
+
raise ModuleLoadError(
|
|
131
|
+
f"Custom module {module_path!r} could not be loaded: {exc}"
|
|
132
|
+
) from exc
|
|
133
|
+
except BaseException:
|
|
134
|
+
sys.modules.pop(internal_name, None)
|
|
135
|
+
raise
|
|
136
|
+
finally:
|
|
137
|
+
if inserted_path:
|
|
138
|
+
try:
|
|
139
|
+
sys.path.remove(directory)
|
|
140
|
+
except ValueError:
|
|
141
|
+
pass
|
|
142
|
+
try:
|
|
143
|
+
return getattr(module2, class_name)
|
|
144
|
+
except AttributeError as exc:
|
|
145
|
+
raise ManagerNotFoundError(
|
|
146
|
+
f"Custom module {module_path!r} does not define class {class_name!r}."
|
|
147
|
+
) from exc
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Optional output/logging adapter that preserves current defaults."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
LOGGER_NAME = "D4CMPP2"
|
|
9
|
+
logger = logging.getLogger(LOGGER_NAME)
|
|
10
|
+
if not any(isinstance(handler, logging.NullHandler) for handler in logger.handlers):
|
|
11
|
+
logger.addHandler(logging.NullHandler())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OutputAdapter:
|
|
15
|
+
"""Route opt-in diagnostics without changing default stdout/warnings."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
verbose: bool = True,
|
|
21
|
+
stream=None,
|
|
22
|
+
error_stream=None,
|
|
23
|
+
diagnostic_logger: Optional[logging.Logger] = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
if not isinstance(verbose, bool):
|
|
26
|
+
raise TypeError(
|
|
27
|
+
f"verbose must be a bool, got {type(verbose).__name__}: {verbose!r}."
|
|
28
|
+
)
|
|
29
|
+
self.verbose = verbose
|
|
30
|
+
# Keep None so redirect_stdout() applied after construction still works.
|
|
31
|
+
self.stream = stream
|
|
32
|
+
self.error_stream = error_stream if error_stream is not None else stream
|
|
33
|
+
self.logger = diagnostic_logger if diagnostic_logger is not None else logger
|
|
34
|
+
|
|
35
|
+
def info(self, *values: Any, **kwargs: Any) -> None:
|
|
36
|
+
"""Match the existing print-based user output."""
|
|
37
|
+
|
|
38
|
+
if self.verbose:
|
|
39
|
+
print(*values, file=self.stream, **kwargs)
|
|
40
|
+
|
|
41
|
+
def error(self, *values: Any, **kwargs: Any) -> None:
|
|
42
|
+
"""Write essential failure information regardless of verbosity."""
|
|
43
|
+
|
|
44
|
+
print(*values, file=self.error_stream, **kwargs)
|
|
45
|
+
|
|
46
|
+
def always(self, *values: Any, **kwargs: Any) -> None:
|
|
47
|
+
"""Write an essential completion summary regardless of verbosity."""
|
|
48
|
+
|
|
49
|
+
print(*values, file=self.stream, **kwargs)
|
|
50
|
+
|
|
51
|
+
def progress(self, iterable, **kwargs):
|
|
52
|
+
"""Wrap an iterable in tqdm while respecting the verbosity policy."""
|
|
53
|
+
|
|
54
|
+
from tqdm import tqdm
|
|
55
|
+
|
|
56
|
+
kwargs.setdefault("disable", not self.verbose)
|
|
57
|
+
return tqdm(iterable, **kwargs)
|
|
58
|
+
|
|
59
|
+
def warning(
|
|
60
|
+
self,
|
|
61
|
+
message: str,
|
|
62
|
+
category=UserWarning,
|
|
63
|
+
*,
|
|
64
|
+
stacklevel: int = 2,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Keep actionable fallback/deprecation messages visible."""
|
|
67
|
+
|
|
68
|
+
warnings.warn(message, category, stacklevel=stacklevel)
|
|
69
|
+
|
|
70
|
+
def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
71
|
+
"""Emit opt-in developer diagnostics through the package logger."""
|
|
72
|
+
|
|
73
|
+
self.logger.debug(message, *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_output(config: Optional[Mapping[str, Any]] = None) -> OutputAdapter:
|
|
77
|
+
"""Build the standard output adapter for a runtime configuration."""
|
|
78
|
+
|
|
79
|
+
verbose = True if config is None else config.get("verbose", True)
|
|
80
|
+
return OutputAdapter(verbose=verbose)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Opt-in training seed and deterministic-backend configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def capture_backend_determinism():
|
|
11
|
+
cudnn = getattr(torch.backends, "cudnn", None)
|
|
12
|
+
return {
|
|
13
|
+
"deterministic_algorithms": torch.are_deterministic_algorithms_enabled(),
|
|
14
|
+
"cudnn_deterministic": getattr(cudnn, "deterministic", None),
|
|
15
|
+
"cudnn_benchmark": getattr(cudnn, "benchmark", None),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def restore_backend_determinism(state):
|
|
20
|
+
torch.use_deterministic_algorithms(state["deterministic_algorithms"])
|
|
21
|
+
cudnn = getattr(torch.backends, "cudnn", None)
|
|
22
|
+
if cudnn is not None:
|
|
23
|
+
if state["cudnn_deterministic"] is not None:
|
|
24
|
+
cudnn.deterministic = state["cudnn_deterministic"]
|
|
25
|
+
if state["cudnn_benchmark"] is not None:
|
|
26
|
+
cudnn.benchmark = state["cudnn_benchmark"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure_reproducibility(config, resume=False):
|
|
30
|
+
"""Apply requested policy and return effective state for reporting."""
|
|
31
|
+
seed = config.get("random_seed")
|
|
32
|
+
deterministic = config.get("deterministic_algorithms", False)
|
|
33
|
+
|
|
34
|
+
if deterministic:
|
|
35
|
+
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
|
|
36
|
+
torch.use_deterministic_algorithms(True)
|
|
37
|
+
if hasattr(torch.backends, "cudnn"):
|
|
38
|
+
torch.backends.cudnn.deterministic = True
|
|
39
|
+
torch.backends.cudnn.benchmark = False
|
|
40
|
+
|
|
41
|
+
if seed is not None and not resume:
|
|
42
|
+
random.seed(seed)
|
|
43
|
+
np.random.seed(seed)
|
|
44
|
+
torch.manual_seed(seed)
|
|
45
|
+
if torch.cuda.is_available():
|
|
46
|
+
torch.cuda.manual_seed_all(seed)
|
|
47
|
+
|
|
48
|
+
return effective_reproducibility_state(config, resume=resume)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def effective_reproducibility_state(config, resume=False):
|
|
52
|
+
cudnn = getattr(torch.backends, "cudnn", None)
|
|
53
|
+
return {
|
|
54
|
+
"random_seed": config.get("random_seed"),
|
|
55
|
+
"split_random_seed": config.get("split_random_seed", 42),
|
|
56
|
+
"deterministic_algorithms_requested": config.get("deterministic_algorithms", False),
|
|
57
|
+
"deterministic_algorithms_enabled": torch.are_deterministic_algorithms_enabled(),
|
|
58
|
+
"cudnn_deterministic": getattr(cudnn, "deterministic", None),
|
|
59
|
+
"cudnn_benchmark": getattr(cudnn, "benchmark", None),
|
|
60
|
+
"python_hash_seed": os.environ.get("PYTHONHASHSEED"),
|
|
61
|
+
"rng_source": "checkpoint" if resume else "configured_seed" if config.get("random_seed") is not None else "ambient",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def seed_data_loader_worker(worker_id):
|
|
66
|
+
"""Seed Python and NumPy from the worker seed assigned by PyTorch."""
|
|
67
|
+
del worker_id
|
|
68
|
+
worker_seed = torch.initial_seed() % (2 ** 32)
|
|
69
|
+
random.seed(worker_seed)
|
|
70
|
+
np.random.seed(worker_seed)
|