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.
Files changed (103) hide show
  1. D4CMPP2/_Data/AGENTS.md +24 -0
  2. D4CMPP2/_Data/Aqsoldb.csv +9291 -0
  3. D4CMPP2/_Data/BradleyMP.csv +3042 -0
  4. D4CMPP2/_Data/Lipophilicity.csv +1131 -0
  5. D4CMPP2/_Data/README.md +8 -0
  6. D4CMPP2/_Data/__init__.py +26 -0
  7. D4CMPP2/_Data/optical.csv +20237 -0
  8. D4CMPP2/_Data/test.csv +190 -0
  9. D4CMPP2/__init__.py +16 -0
  10. D4CMPP2/__main__.py +5 -0
  11. D4CMPP2/_main.py +500 -0
  12. D4CMPP2/cli.py +7 -0
  13. D4CMPP2/exceptions.py +53 -0
  14. D4CMPP2/grid_search.py +259 -0
  15. D4CMPP2/network_refer.yaml +160 -0
  16. D4CMPP2/networks/AFP_model.py +72 -0
  17. D4CMPP2/networks/AFPwithSolv_model.py +72 -0
  18. D4CMPP2/networks/DMPNN_model.py +90 -0
  19. D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
  20. D4CMPP2/networks/GAT_model.py +48 -0
  21. D4CMPP2/networks/GATwithSolv_model.py +63 -0
  22. D4CMPP2/networks/GCN_model.py +113 -0
  23. D4CMPP2/networks/GCNwithSolv_model.py +103 -0
  24. D4CMPP2/networks/GC_model.py +122 -0
  25. D4CMPP2/networks/ISATPM_model.py +14 -0
  26. D4CMPP2/networks/ISATPN_model.py +199 -0
  27. D4CMPP2/networks/ISAT_model.py +90 -0
  28. D4CMPP2/networks/MPNN_model.py +56 -0
  29. D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
  30. D4CMPP2/networks/__init__.py +25 -0
  31. D4CMPP2/networks/base.py +250 -0
  32. D4CMPP2/networks/registry.py +187 -0
  33. D4CMPP2/networks/src/AFP.py +118 -0
  34. D4CMPP2/networks/src/BiDropout.py +29 -0
  35. D4CMPP2/networks/src/DMPNN.py +35 -0
  36. D4CMPP2/networks/src/GAT.py +69 -0
  37. D4CMPP2/networks/src/GC.py +85 -0
  38. D4CMPP2/networks/src/GCN.py +71 -0
  39. D4CMPP2/networks/src/ISAT.py +153 -0
  40. D4CMPP2/networks/src/Linear.py +49 -0
  41. D4CMPP2/networks/src/MPNN.py +56 -0
  42. D4CMPP2/networks/src/SolventLayer.py +62 -0
  43. D4CMPP2/networks/src/__init__.py +0 -0
  44. D4CMPP2/networks/src/distGCN.py +21 -0
  45. D4CMPP2/networks/src/pyg_hetero.py +24 -0
  46. D4CMPP2/optimize.py +472 -0
  47. D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
  48. D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
  49. D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
  50. D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
  51. D4CMPP2/src/Analyzer/__init__.py +54 -0
  52. D4CMPP2/src/Analyzer/core.py +480 -0
  53. D4CMPP2/src/Analyzer/factory.py +166 -0
  54. D4CMPP2/src/Analyzer/interpretation.py +232 -0
  55. D4CMPP2/src/Analyzer/results.py +101 -0
  56. D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
  57. D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
  58. D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
  59. D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
  60. D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
  61. D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
  62. D4CMPP2/src/DataManager/ISADataManager.py +67 -0
  63. D4CMPP2/src/DataManager/MolDataManager.py +735 -0
  64. D4CMPP2/src/DataManager/__init__.py +14 -0
  65. D4CMPP2/src/DataManager/contracts.py +179 -0
  66. D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
  67. D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
  68. D4CMPP2/src/NetworkManager/__init__.py +14 -0
  69. D4CMPP2/src/PostProcessor.py +160 -0
  70. D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
  71. D4CMPP2/src/TrainManager/TrainManager.py +254 -0
  72. D4CMPP2/src/TrainManager/__init__.py +14 -0
  73. D4CMPP2/src/TrainManager/callbacks.py +119 -0
  74. D4CMPP2/src/__init__.py +0 -0
  75. D4CMPP2/src/utils/PATH.py +246 -0
  76. D4CMPP2/src/utils/__init__.py +0 -0
  77. D4CMPP2/src/utils/argparser.py +56 -0
  78. D4CMPP2/src/utils/checkpointing.py +90 -0
  79. D4CMPP2/src/utils/config_resolution.py +123 -0
  80. D4CMPP2/src/utils/config_validation.py +370 -0
  81. D4CMPP2/src/utils/csv_validation.py +105 -0
  82. D4CMPP2/src/utils/data_quality.py +181 -0
  83. D4CMPP2/src/utils/featureizer.py +202 -0
  84. D4CMPP2/src/utils/functional_group.csv +169 -0
  85. D4CMPP2/src/utils/graph_cache.py +213 -0
  86. D4CMPP2/src/utils/leaderboard.py +212 -0
  87. D4CMPP2/src/utils/metrics.py +31 -0
  88. D4CMPP2/src/utils/module_loader.py +147 -0
  89. D4CMPP2/src/utils/output.py +80 -0
  90. D4CMPP2/src/utils/reproducibility.py +70 -0
  91. D4CMPP2/src/utils/run_manifest.py +175 -0
  92. D4CMPP2/src/utils/scaler.py +40 -0
  93. D4CMPP2/src/utils/sculptor.py +713 -0
  94. D4CMPP2/src/utils/splitting.py +250 -0
  95. D4CMPP2/src/utils/supportfile_saver.py +94 -0
  96. D4CMPP2/src/utils/tools.py +156 -0
  97. D4CMPP2/src/utils/transfer_learning.py +111 -0
  98. d4cmpp2-0.4.0.dist-info/METADATA +420 -0
  99. d4cmpp2-0.4.0.dist-info/RECORD +103 -0
  100. d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
  101. d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
  102. d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
  103. 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)