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,520 @@
1
+ import torch
2
+ import importlib.util
3
+ import os, sys
4
+ import yaml
5
+ import pandas as pd
6
+ import numpy as np
7
+ from D4CMPP2.src.utils.config_validation import validate_runtime_environment
8
+ from D4CMPP2.src.utils.checkpointing import (
9
+ CHECKPOINT_SCHEMA_VERSION,
10
+ atomic_torch_save,
11
+ capture_rng_state,
12
+ load_checkpoint,
13
+ resolve_resume_checkpoint,
14
+ restore_rng_state,
15
+ )
16
+ from D4CMPP2.src.utils.transfer_learning import (
17
+ file_sha256,
18
+ select_compatible_state,
19
+ write_transfer_report,
20
+ )
21
+ from D4CMPP2.src.utils.output import get_output
22
+
23
+ class NetworkManager:
24
+
25
+ def __init__(self, config, tf_path=None, unwrapper=None, temp=False, resume_path=None):
26
+
27
+ self.config = config
28
+ self.output = get_output(config)
29
+ self.device = config.get('device', 'cpu')
30
+ validate_runtime_environment(config, backend="pyg", torch_module=torch)
31
+ self.output.info(f"[Runtime] Compute device: {self.device!r}.")
32
+ self.last_lr = config.get('learning_rate',0.001)
33
+ self.unwrapper = unwrapper
34
+ self.best_loss = float('inf')
35
+ self.es_patience = config.get('early_stopping_patience',50)
36
+ self.es_counter = 0
37
+ self.state= "train"
38
+ self.tf_path = tf_path
39
+ self.schedulers = []
40
+ self.temp = temp
41
+ self.resume_path = resume_path
42
+ self.completed_epoch = -1
43
+ self.next_epoch = 0
44
+ self.best_epoch = None
45
+ self.run_id = config.get("run_id")
46
+
47
+ if self.tf_path:
48
+ self.transferlearn_network()
49
+ else:
50
+ self.init_network(load_existing=not bool(self.resume_path))
51
+
52
+ self.init_optimizer(config.get('lr_dict',{}) )
53
+ self.init_scheduler()
54
+ if self.resume_path:
55
+ self.load_training_checkpoint(self.resume_path)
56
+
57
+ def set_unwrapper(self, unwrapper):
58
+ self.unwrapper = unwrapper
59
+
60
+ def _output(self):
61
+ """Return the configured adapter, including for legacy test fixtures."""
62
+
63
+ return getattr(self, "output", get_output(getattr(self, "config", None)))
64
+
65
+ def get_net_module(self,model_path=None):
66
+ if model_path is None:
67
+ model_path = self.config['MODEL_PATH']
68
+ if os.path.exists(os.path.join(model_path,'network.py')):
69
+ spec1 = importlib.util.spec_from_file_location("network", os.path.join(model_path,"network.py"))
70
+ module = importlib.util.module_from_spec(spec1)
71
+ spec1.loader.exec_module(module)
72
+ net = getattr(module, 'network')
73
+ return net
74
+ network_name = self.config.get("network_id")
75
+ if network_name is not None:
76
+ try:
77
+ from D4CMPP2.networks.registry import get_model
78
+
79
+ return get_model(network_name).network
80
+ except ValueError:
81
+ pass
82
+ raise FileNotFoundError(
83
+ model_path + "/network.py not found and network_id is not registered"
84
+ )
85
+
86
+ # Initialize the network
87
+ def init_network(self, load_existing=True):
88
+ module = self.get_net_module()
89
+ self.network = module(self.config)
90
+ self.network.to(device = self.device)
91
+ parameter_count = sum(
92
+ p.numel() for p in self.network.parameters() if p.requires_grad
93
+ )
94
+ self._output().info(
95
+ f"[Model] Initialized {type(self.network).__name__} with "
96
+ f"{parameter_count:,} trainable parameters."
97
+ )
98
+ from D4CMPP2.networks.base import MolecularNetwork
99
+
100
+ if isinstance(self.network, MolecularNetwork):
101
+ self.loss_fn = self.network.compute_loss
102
+ else:
103
+ self.loss_fn = self.network.loss_fn
104
+
105
+ if os.path.exists(os.path.join(self.config['MODEL_PATH'],'result','learning_curve.csv')):
106
+ self.learning_curve = pd.read_csv(os.path.join(self.config['MODEL_PATH'],'result','learning_curve.csv'))
107
+ elif os.path.exists(os.path.join(self.config['MODEL_PATH'],'learning_curve.csv')):
108
+ self.learning_curve = pd.read_csv(os.path.join(self.config['MODEL_PATH'],'learning_curve.csv'))
109
+ else:
110
+ self.learning_curve = None
111
+
112
+ if not load_existing:
113
+ return
114
+ if os.path.exists(os.path.join(self.config['MODEL_PATH'],'final.pth')):
115
+ self.load_params(os.path.join(self.config['MODEL_PATH'],'final.pth'))
116
+ else:
117
+ losses = []
118
+ for file in os.listdir(self.config['MODEL_PATH']):
119
+ if file.startswith("param_") and file.endswith(".pth"):
120
+ losses.append(float(file.split("_")[1].replace(".pth","")))
121
+ if losses:
122
+ self.best_loss = min(losses)
123
+ self.load_params(os.path.join(self.config['MODEL_PATH'],'param_'+str(self.best_loss)+'.pth')
124
+ )
125
+
126
+
127
+ # Transfer learning
128
+ def transferlearn_network(self):
129
+ self.init_network()
130
+ self.load_params_transfer_learn(self.tf_path)
131
+
132
+ # Initialize the optimizer
133
+ def init_optimizer(self,lr_dict=None):
134
+ lr_dict = {} if lr_dict is None else lr_dict
135
+ if not isinstance(lr_dict, dict):
136
+ raise TypeError(
137
+ f"lr_dict must be a mapping of layer-name components to learning rates, "
138
+ f"got {type(lr_dict).__name__}. Example: lr_dict={{'GCNs': 0.0001}}."
139
+ )
140
+ invalid = {
141
+ key: value for key, value in lr_dict.items()
142
+ if (
143
+ not isinstance(key, str)
144
+ or not key.strip()
145
+ or isinstance(value, bool)
146
+ or not isinstance(value, (int, float))
147
+ or value <= 0
148
+ )
149
+ }
150
+ if invalid:
151
+ raise ValueError(
152
+ f"lr_dict keys must be non-empty layer-name strings and values must be "
153
+ f"positive learning rates; invalid entries: {invalid!r}."
154
+ )
155
+ params = []
156
+ matched_lr_keys = set()
157
+ for name, param in self.network.named_parameters():
158
+ matching_keys = [key for key in lr_dict if key in name.split(".")]
159
+ if len(matching_keys) > 1:
160
+ raise ValueError(
161
+ f"lr_dict entries {matching_keys!r} both match parameter {name!r}. "
162
+ "Use one unambiguous layer-name component for this parameter."
163
+ )
164
+ if matching_keys:
165
+ key = matching_keys[0]
166
+ matched_lr_keys.add(key)
167
+ lr = lr_dict[key]
168
+ self._output().info(
169
+ f"[Optimizer] Parameter {name!r} uses learning rate {lr:g}."
170
+ )
171
+ else:
172
+ lr = self.config.get('learning_rate',0.001)
173
+
174
+ params.append({'params': param, 'lr': lr})
175
+
176
+ unmatched = sorted(set(lr_dict) - matched_lr_keys)
177
+ if unmatched:
178
+ available = sorted(
179
+ {component for name, _ in self.network.named_parameters() for component in name.split(".")}
180
+ )
181
+ raise ValueError(
182
+ f"lr_dict layer names {unmatched!r} did not match any trainable parameter. "
183
+ f"Available name components include: {available!r}."
184
+ )
185
+
186
+ self.optimizer = getattr(torch.optim, self.config['optimizer'])(params,
187
+ lr=self.config.get('learning_rate',0.001),
188
+ weight_decay=self.config.get('weight_decay',0.0005)
189
+ )
190
+
191
+ # Initialize the scheduler
192
+ def init_scheduler(self):
193
+ self.schedulers.append( torch.optim.lr_scheduler.ReduceLROnPlateau(self.optimizer,
194
+ patience=self.config.get('lr_patience',10),
195
+ min_lr=self.config.get('min_lr',1e-7),
196
+ factor=self.config.get('lr_plateau_decay',0.1),
197
+ ))
198
+ self.schedulers.append( torch.optim.lr_scheduler.StepLR(self.optimizer,
199
+ step_size=self.config.get('lr_step',40),
200
+ gamma=self.config.get('lr_step_decay',0.98)))
201
+
202
+ "----------------------------------------------------------------------------------------------------------------------"
203
+ "Below are the functions to manage the network during the training, and called by the Trainer class"
204
+
205
+ def train(self):
206
+ self.state = "train"
207
+ self.network.train()
208
+
209
+ def eval(self):
210
+ self.state = "eval"
211
+ self.network.eval()
212
+
213
+ def get_lr(self):
214
+ lrs = []
215
+ for param_group in self.optimizer.param_groups:
216
+ lrs.append(param_group['lr'])
217
+ return np.mean(lrs)
218
+
219
+ # One step of the training including forward and backward
220
+ def step(self, loader, flag= False, **kargs):
221
+ self.optimizer.zero_grad()
222
+ if type(loader) is dict:
223
+ loader.update({'device': self.device})
224
+ x = self.unwrapper(**loader)
225
+ else:
226
+ x= self.unwrapper(*loader,device=self.device)
227
+ y= x['target']
228
+ x.update(kargs)
229
+ self._validate_network_input(x)
230
+ y_pred = self.network(**x)
231
+ if kargs.get('get_score',False) or kargs.get('get_feature',False):
232
+ return y_pred
233
+
234
+ loss = self.loss_fn(y_pred, y)
235
+ if self.state == "train":
236
+ loss.backward()
237
+ self.optimizer.step()
238
+ if flag:
239
+ return y, y_pred.detach(), loss.detach().item(), x
240
+ return y, y_pred.detach(), loss.detach().item()
241
+
242
+ # One step of the prediction including forward
243
+ def predict(self,loader):
244
+ self.optimizer.zero_grad()
245
+ if type(loader) is dict:
246
+ x = self.unwrapper(**loader, device=self.device)
247
+ else:
248
+ x= self.unwrapper(*loader,device=self.device)
249
+ y= x['target']
250
+ self._validate_network_input(x)
251
+ y_pred = self.network(**x)
252
+ return y_pred, y
253
+
254
+ def _validate_network_input(self, batch):
255
+ from D4CMPP2.networks.base import MolecularNetwork
256
+
257
+ if isinstance(self.network, MolecularNetwork):
258
+ self.network.validate_input(batch)
259
+
260
+ def scheduler_step(self, val_loss=None, completed_epoch=None):
261
+ if completed_epoch is None:
262
+ completed_epoch = getattr(self, "completed_epoch", -1) + 1
263
+ for scheduler in self.schedulers:
264
+ if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
265
+ scheduler.step(val_loss)
266
+ else:
267
+ scheduler.step()
268
+
269
+ # save the best model and reset the early stopping counter
270
+ if val_loss < self.best_loss:
271
+ self.best_loss = val_loss
272
+ self.best_epoch = completed_epoch
273
+ self.save_checkpoint(val_loss)
274
+ self.es_counter = 0
275
+ self.save_training_checkpoint("best", completed_epoch)
276
+ else:
277
+ self.es_counter += 1
278
+
279
+ # early stopping
280
+ if self.es_counter > self.es_patience:
281
+ self.load_best_checkpoint()
282
+ self.completed_epoch = completed_epoch
283
+ self.next_epoch = completed_epoch + 1
284
+ self.save_training_checkpoint("latest", completed_epoch)
285
+ return True
286
+
287
+ # load the best model if the learning rate is reduced
288
+ current_lr = self.get_lr()
289
+ if current_lr<self.last_lr/2:
290
+ self.last_lr=current_lr
291
+ self.load_best_checkpoint()
292
+ self._output().info(
293
+ f"[Training] Learning rate reduced to {self.last_lr:g}; "
294
+ "restored the best validation checkpoint."
295
+ )
296
+ self.completed_epoch = completed_epoch
297
+ self.next_epoch = completed_epoch + 1
298
+ self.save_training_checkpoint("latest", completed_epoch)
299
+
300
+ def save_checkpoint(self, val_loss):
301
+ self.save_params(os.path.join(self.config['MODEL_PATH'],"param_"+str(val_loss)+".pth"))
302
+
303
+ def load_best_checkpoint(self):
304
+ path = os.path.join(self.config['MODEL_PATH'],"param_"+str(self.best_loss)+".pth")
305
+ if os.path.exists(path):
306
+ self.load_params(path)
307
+ return path
308
+ else:
309
+ return None
310
+
311
+ def load_params(self, path):
312
+ self._output().info(f"[Checkpoint] Loading model weights from {path!r}.")
313
+ self.network.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
314
+
315
+ def load_params_transfer_learn(self, tf_path):
316
+ self._output().info(
317
+ f"[Transfer] Loading compatible parameters from {tf_path!r}."
318
+ )
319
+ config_path = os.path.join(tf_path, "config.yaml")
320
+ weights_path = os.path.join(tf_path, "final.pth")
321
+ if not os.path.isfile(config_path):
322
+ raise FileNotFoundError(
323
+ f"Transfer source config {config_path!r} was not found. "
324
+ "Select a completed saved-model directory containing config.yaml."
325
+ )
326
+ if not os.path.isfile(weights_path):
327
+ raise FileNotFoundError(
328
+ f"Transfer source weights {weights_path!r} were not found. "
329
+ "Select a completed saved-model directory containing final.pth."
330
+ )
331
+ try:
332
+ with open(config_path, "r", encoding="utf-8") as stream:
333
+ source_config = yaml.safe_load(stream)
334
+ except (OSError, yaml.YAMLError) as exc:
335
+ raise ValueError(
336
+ f"Transfer source config {config_path!r} could not be read as YAML: {exc}."
337
+ ) from exc
338
+ if not isinstance(source_config, dict):
339
+ raise TypeError(
340
+ f"Transfer source config {config_path!r} must contain a YAML mapping, "
341
+ f"got {type(source_config).__name__}."
342
+ )
343
+
344
+ try:
345
+ module = self.get_net_module(tf_path)
346
+ pretrained_network = module(source_config)
347
+ except Exception as exc:
348
+ raise RuntimeError(
349
+ f"Transfer source network snapshot in {tf_path!r} could not be constructed "
350
+ f"from config.yaml: {exc}"
351
+ ) from exc
352
+ try:
353
+ params = torch.load(
354
+ weights_path,
355
+ weights_only=True,
356
+ map_location=self.device,
357
+ )
358
+ pretrained_network.load_state_dict(params, strict=True)
359
+ except (OSError, RuntimeError, TypeError, ValueError) as exc:
360
+ raise RuntimeError(
361
+ f"Transfer source weights {weights_path!r} are incompatible with its "
362
+ f"saved network.py/config.yaml snapshot: {exc}"
363
+ ) from exc
364
+
365
+ selected, report = select_compatible_state(
366
+ pretrained_network.state_dict(),
367
+ self.network.state_dict(),
368
+ )
369
+ if not selected:
370
+ raise ValueError(
371
+ f"Transfer source {tf_path!r} has no name-and-shape-compatible state "
372
+ "entries for the target network. Check the selected source and target architectures."
373
+ )
374
+ result = self.network.load_state_dict(selected, strict=False)
375
+ report.update(
376
+ {
377
+ "source_model_path": os.path.abspath(tf_path),
378
+ "source_weights": os.path.abspath(weights_path),
379
+ "source_weights_sha256": file_sha256(weights_path),
380
+ "source_network": source_config.get(
381
+ "network_id", source_config.get("network")
382
+ ),
383
+ "target_network": self.config.get(
384
+ "network_id", self.config.get("network")
385
+ ),
386
+ "load_state_missing_keys": list(result.missing_keys),
387
+ "load_state_unexpected_keys": list(result.unexpected_keys),
388
+ }
389
+ )
390
+ report["report_path"] = write_transfer_report(
391
+ report, self.config["MODEL_PATH"]
392
+ )
393
+ self.transfer_report = report
394
+ counts = report["counts"]
395
+ self._output().info(
396
+ "[Transfer] Parameter selection complete: "
397
+ f"loaded={counts['loaded']}, "
398
+ f"shape_mismatch={counts['shape_mismatch']}, "
399
+ f"source_only={counts['source_only']}, "
400
+ f"target_only={counts['target_only']}. "
401
+ f"Report: {report['report_path']!r}."
402
+ )
403
+
404
+ def save_params(self, path):
405
+ torch.save(self.network.state_dict(), path)
406
+
407
+ def _checkpoint_identity(self):
408
+ return {
409
+ "network_id": self.config.get("network_id", self.config.get("network")),
410
+ "network_module": self.config.get("network"),
411
+ "target": list(self.config.get("target", [])),
412
+ "target_dim": self.config.get("target_dim"),
413
+ "optimizer": self.config.get("optimizer"),
414
+ "scheduler_policy": self.config.get("scheduler_policy", "legacy_dual"),
415
+ "graph_backend": "pyg",
416
+ "graph_schema_version": 1,
417
+ "random_seed": self.config.get("random_seed"),
418
+ "deterministic_algorithms": self.config.get("deterministic_algorithms", False),
419
+ "split_strategy": self.config.get("split_strategy", "auto"),
420
+ "scaffold_column": self.config.get("scaffold_column"),
421
+ "scaffold_include_chirality": self.config.get(
422
+ "scaffold_include_chirality", False
423
+ ),
424
+ }
425
+
426
+ def save_training_checkpoint(self, kind, completed_epoch):
427
+ if completed_epoch is None:
428
+ return None
429
+ path = os.path.join(self.config["MODEL_PATH"], "checkpoints", f"{kind}.ckpt")
430
+ payload = {
431
+ "checkpoint_schema_version": CHECKPOINT_SCHEMA_VERSION,
432
+ "kind": kind,
433
+ "run_id": getattr(self, "run_id", None),
434
+ "completed_epoch": int(completed_epoch),
435
+ "next_epoch": int(completed_epoch) + 1,
436
+ "model_state_dict": self.network.state_dict(),
437
+ "optimizer_state_dict": self.optimizer.state_dict(),
438
+ "schedulers": [
439
+ {"class": scheduler.__class__.__name__, "state_dict": scheduler.state_dict()}
440
+ for scheduler in self.schedulers
441
+ ],
442
+ "best_epoch": getattr(self, "best_epoch", None),
443
+ "best_metric": self.best_loss,
444
+ "metric_name": "val_loss",
445
+ "early_stopping_counter": self.es_counter,
446
+ "last_lr": self.last_lr,
447
+ "rng_state": capture_rng_state(),
448
+ "identity": self._checkpoint_identity(),
449
+ }
450
+ atomic_torch_save(payload, path)
451
+ return path
452
+
453
+ def load_training_checkpoint(self, value):
454
+ path = resolve_resume_checkpoint(value)
455
+ payload = load_checkpoint(path, self.device)
456
+ expected = self._checkpoint_identity()
457
+ actual = payload.get("identity", {})
458
+ for key in (
459
+ "network_module",
460
+ "target",
461
+ "target_dim",
462
+ "optimizer",
463
+ "scheduler_policy",
464
+ "graph_backend",
465
+ "graph_schema_version",
466
+ ):
467
+ if actual.get(key) != expected.get(key):
468
+ raise ValueError(
469
+ f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
470
+ f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
471
+ "Use LOAD_PATH for weight-only continuation or TRANSFER_PATH for a different model/target."
472
+ )
473
+ for key in ("random_seed", "deterministic_algorithms"):
474
+ if key in actual and actual.get(key) != expected.get(key):
475
+ raise ValueError(
476
+ f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
477
+ f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
478
+ "Use the saved reproducibility policy for full resume."
479
+ )
480
+ for key in (
481
+ "split_strategy",
482
+ "scaffold_column",
483
+ "scaffold_include_chirality",
484
+ ):
485
+ if key in actual and actual.get(key) != expected.get(key):
486
+ raise ValueError(
487
+ f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
488
+ f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
489
+ "Use the saved split policy for full resume, or start a new run."
490
+ )
491
+ scheduler_states = payload.get("schedulers", [])
492
+ current_names = [scheduler.__class__.__name__ for scheduler in self.schedulers]
493
+ saved_names = [item.get("class") for item in scheduler_states]
494
+ if saved_names != current_names:
495
+ raise ValueError(
496
+ f"Resume checkpoint {str(path)!r} scheduler classes {saved_names!r} "
497
+ f"do not match current classes {current_names!r}."
498
+ )
499
+ self.network.load_state_dict(payload["model_state_dict"])
500
+ self.optimizer.load_state_dict(payload["optimizer_state_dict"])
501
+ for scheduler, item in zip(self.schedulers, scheduler_states):
502
+ scheduler.load_state_dict(item["state_dict"])
503
+ self.best_epoch = payload.get("best_epoch")
504
+ self.best_loss = payload["best_metric"]
505
+ self.es_counter = payload["early_stopping_counter"]
506
+ self.last_lr = payload["last_lr"]
507
+ self.completed_epoch = payload["completed_epoch"]
508
+ self.next_epoch = payload["next_epoch"]
509
+ self.parent_run_id = payload.get("run_id")
510
+ restore_rng_state(payload["rng_state"])
511
+ self._output().info(
512
+ f"[Checkpoint] Restored full training state from {str(path)!r}; "
513
+ f"next epoch: {self.next_epoch}."
514
+ )
515
+ return str(path)
516
+
517
+ def dropout_on(self):
518
+ for m in self.network.modules():
519
+ if m.__class__.__name__.startswith('Dropout'):
520
+ m.train()
@@ -0,0 +1,14 @@
1
+ """Lazy exports for built-in network-manager modules."""
2
+
3
+ from importlib import import_module
4
+ from types import ModuleType
5
+
6
+ __all__ = ["NetworkManager", "ISANetworkManager"]
7
+
8
+
9
+ def __getattr__(name: str) -> ModuleType:
10
+ if name not in __all__:
11
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
12
+ module = import_module(f"{__name__}.{name}")
13
+ globals()[name] = module
14
+ return module