BUCToolkit 1.0b0__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 (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
BUCToolkit/api/NEB.py ADDED
@@ -0,0 +1,325 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: MolecularDynamics.py
5
+ # Environment: Python 3.12
6
+ import logging
7
+ import math
8
+ import os
9
+ import time
10
+ import traceback
11
+ from typing import Dict, Any, Literal, Callable, Tuple
12
+
13
+ import numpy as np
14
+ import torch as th
15
+ from torch import nn
16
+
17
+ from BUCToolkit.BatchOptim.TS.CI_NEB import CI_NEB
18
+ from BUCToolkit.utils._CheckModules import check_module
19
+ from ._io import _CONFIGS, _LoggingEnd, _Model_Wrapper_pyg, _Model_Wrapper_dgl
20
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT
21
+ from BUCToolkit.utils.AtomSelector import atom_fix_selector
22
+
23
+
24
+ class ClimbingImageNudgedElasticBand(_CONFIGS):
25
+ """
26
+ Class of Climbing Image Nudged Elastic Band algorithm to search transition state.
27
+ Users need to set the dataset and dataloader manually.
28
+
29
+ Args:
30
+ config_file: path to input file.
31
+ data_type: graph data type. 'pyg' for torch-geometric BatchData, 'dgl' for dgl DGLGraph.
32
+
33
+ Input file parameters: (Under the section `MD` in input files)
34
+ ALGO: Literal['CI-NEB'] = 'CI-NEB', the algorithm to use (now only supports CI-NEB).
35
+ N_IMAGES: int, the image number of NEB.
36
+ SPRING_CONST: float = 1, the elastic (spring) constant.
37
+ OPTIMIZER: Literal['FIRE'] = 'FIRE', the optimizer to use (now only support FIRE).
38
+ OPTIMIZER_CONFIGS: Optional[Dict[str, Any]] = None, other kwargs of optimizer.
39
+ STEPLENGTH: float = 0.05, the step length of the optimization.
40
+ E_THRESHOLD: float = 1e-3, the energy threshold of convergence.
41
+ F_THRESHOLD: float = 0.05, the force threshold of convergence.
42
+ MAXITER: int = 100, the maximum number of optimization iterations.
43
+ REQUIRE_GRAD: bool = False, bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs).
44
+
45
+ References:
46
+ [1] G. Henkelman and H. Jónsson, J. Chem. Phys. 2000, 113, 9901-9904.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ config_file: str,
52
+ data_type: Literal['pyg', 'dgl'] = 'pyg',
53
+ ) -> None:
54
+ super().__init__(config_file)
55
+
56
+ __algos = {'CI-NEB': CI_NEB}
57
+ self.config_file = config_file
58
+ assert data_type in {'pyg', 'dgl'}, f'Invalid data type {data_type}. It must be "pyg" or "dgl".'
59
+ self.data_type = data_type
60
+ self.reload_config(config_file)
61
+ if self.VERBOSE: self.logger.info('Config File Was Successfully Read.')
62
+ self.param = None
63
+ self._has_load_data = False
64
+ self._data_loader = None
65
+
66
+ if self.NEB is None:
67
+ raise RuntimeError('Nudged Elastic Band Configs was NOT Set.')
68
+ self.NEB: Dict
69
+
70
+ try:
71
+ self.NEB_ALGO = __algos.get(self.NEB['ALGO'], CI_NEB) # The Main Function of MD. MD_config is its parameters.
72
+ except KeyError:
73
+ raise NotImplementedError(f'Unknown Algorithm {self.NEB['ALGO']}.')
74
+ self.require_grad = self.NEB.get('REQUIRE_GRAD', False)
75
+ # check modes
76
+ self.N_IMAGES = self.NEB['N_IMAGES']
77
+ self.NEB_config = {
78
+ 'N_images': self.NEB['N_IMAGES'],
79
+ 'spring_const': float(self.NEB.get('SPRING_CONST', 1.)),
80
+ 'optimizer': self.NEB.get('OPTIMIZER', 'FIRE'),
81
+ 'optimizer_configs': self.NEB.get('OPTIMIZER_CONFIGS', None),
82
+ 'steplength': self.NEB.get('STEPLENGTH', 0.2),
83
+ 'E_threshold': float(self.NEB.get('E_THRESHOLD', 1e-3)),
84
+ 'F_threshold': float(self.NEB.get('F_THRESHOLD', 0.05)),
85
+ 'maxiter': int(self.NEB.get('MAXITER', 100)),
86
+ 'device': self.DEVICE,
87
+ 'verbose': self.VERBOSE
88
+ }
89
+
90
+ def run(self, model):
91
+ """
92
+ Parameters:
93
+ model: the input model which is non-instantiated nn.Module class.
94
+ """
95
+ # check logger
96
+ if not self.logger.hasHandlers(): self.logger.addHandler(self.log_handler)
97
+ # check vars
98
+ _model: nn.Module = model(**self.MODEL_CONFIG)
99
+ if self.START == 'resume' or self.START == 1:
100
+ chk_data = th.load(self.LOAD_CHK_FILE_PATH, weights_only=True)
101
+ if self.param is None:
102
+ _model.load_state_dict(chk_data['model_state_dict'], strict=False)
103
+ else:
104
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
105
+ epoch_now = chk_data['epoch']
106
+ elif self.START == 'from_scratch' or self.START == 0:
107
+ self.logger.warning(
108
+ 'WARNING: The model was not read the trained parameters from checkpoint file. I HOPE YOU KNOW WHAT YOU ARE DOING!'
109
+ )
110
+ epoch_now = 0
111
+ if self.param is not None:
112
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
113
+ else:
114
+ raise ValueError('Invalid START value. It should be "from_scratch" / 0 or "resume" / 1 ')
115
+ # model vars
116
+ _model = _model.to(self.DEVICE)
117
+
118
+ # preprocessing data # TODO
119
+ if self._data_loader is None: raise RuntimeError('Please Set the DataLoader.')
120
+ if not self._has_load_data: raise RuntimeError('Please Set the Data to Predict.')
121
+
122
+ # initialize
123
+ self.n_samp = len(self.TRAIN_DATA['dataIS']) # sample number
124
+ self.n_batch = math.ceil(self.n_samp / self.BATCH_SIZE) # total batch number per epoch
125
+
126
+ try:
127
+ # I/O
128
+ if self.VERBOSE > 0:
129
+ self.logout_task_information(_model, 'NEB', self.NEB_config, self.n_samp)
130
+
131
+ time_tol = time.perf_counter()
132
+ _model.eval()
133
+ # MAIN LOOP
134
+ # define the model wrapper & batch size getter & cell vector getter for different data type
135
+ if self.data_type == 'pyg':
136
+ _pyg = check_module('torch_geometric.data')
137
+ if _pyg is not None:
138
+ import torch_geometric.data as _pyg
139
+ self.pygBatch = _pyg.Batch
140
+ else:
141
+ ImportError('The method is unavailable because the `torch-geometric` cannot be imported.')
142
+
143
+ model_wrap = _Model_Wrapper_pyg(_model)
144
+ def get_indx(data):
145
+ _indx = getattr(data, 'idx', None)
146
+ # To correctly manage the names after NEB opt. There names will be [[`idx`], [`idx`], ...] (repeat N_images times).
147
+ if (_indx is not None) and isinstance(_indx[0], list):
148
+ _indx = [_[0] + f'_{iii}' for iii, _ in enumerate(_indx)]
149
+
150
+ return _indx
151
+
152
+ def get_batch_size(data):
153
+ return len(data)
154
+
155
+ def get_batch_indx(data):
156
+ return [len(dat.pos) for dat in data.to_data_list()]
157
+
158
+ def get_cell_vec(data):
159
+ return data.cell.numpy(force=True)
160
+
161
+ def get_atomic_number(data):
162
+ return data.atomic_numbers.unsqueeze(0)
163
+
164
+ def get_fixed_mask(data):
165
+ mask = getattr(data, 'fixed', None)
166
+ if mask is not None:
167
+ mask = mask.unsqueeze(0)
168
+ return mask
169
+
170
+ def rebatched_graph(single_graph, N_images):
171
+ """ expand batches """
172
+ graph = self.pygBatch.from_data_list([single_graph] * (N_images + 2))
173
+ return graph
174
+
175
+ else:
176
+ model_wrap = _Model_Wrapper_dgl(_model)
177
+ raise NotImplementedError # TODO <<<<
178
+ def get_batch_size(data):
179
+ return data.num_nodes('atom')
180
+
181
+ def get_cell_vec(data):
182
+ return data.nodes['cell'].data['cell'].numpy(force=True)
183
+
184
+ def get_atomic_number(data):
185
+ return data.nodes['atom'].data['Z'].unsqueeze(0).tolist()
186
+
187
+ def get_fixed_mask(data):
188
+ return data.nodes['atom'].data.get('fix', None)
189
+
190
+ # Instantiate NEB ALGO class
191
+ neb_ops = self.NEB_ALGO(**self.NEB_config)
192
+ val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, **self._data_loader_configs)
193
+ if getattr(val_set, '_LOADER_TYPE', None) != 'ISFS':
194
+ __err_msg = f'Data loader of ConstrainedMolecularDynamics requires ISFS, but got {getattr(val_set, '_LOADER_TYPE', None)}'
195
+ if self.VERBOSE: self.logger.error(__err_msg)
196
+ raise ValueError(__err_msg)
197
+ n_c = 1 # running batch now
198
+ n_s = 0 # number of calculated samples. each sample in batches in each for-loop += 1.
199
+ for dataIS, dataFS in val_set:
200
+ try:
201
+ # get basic information of IS.
202
+ batch_size = get_batch_size(dataIS)
203
+ cells = get_cell_vec(dataIS)
204
+ # Check Batch
205
+ if batch_size != get_batch_size(dataFS):
206
+ __err_msg = f'The batch size of {dataIS} and {dataFS} do not match: {get_batch_size(dataIS)} != {get_batch_size(dataFS)}'
207
+ if self.VERBOSE: self.logger.error(__err_msg)
208
+ raise RuntimeError(__err_msg)
209
+ if batch_size <= 0:
210
+ if self.VERBOSE: self.logger.info(f'An empty batch occurred. Skipped.')
211
+ continue
212
+ elif batch_size > 1:
213
+ if self.VERBOSE: self.logger.error(f'Constrained MD do not support batched calculation yet. You should set BATCH_SIZE to 1.')
214
+ raise RuntimeError(f'Constrained MD do not support batched calculation yet. You should set BATCH_SIZE to 1.')
215
+ if self.data_type == 'pyg':
216
+ batch_indx = [len(dat.pos) for dat in dataIS.to_data_list()]
217
+ _check_batch_indx = [len(dat.pos) for dat in dataFS.to_data_list()]
218
+ elif self.data_type == 'dgl':
219
+ batch_indx = dataIS.batch_num_nodes('atom')
220
+ _check_batch_indx = dataFS.batch_num_nodes('atom')
221
+ else:
222
+ raise RuntimeError(f'Data type {self.data_type} is not supported.')
223
+ # NEB
224
+ if self.VERBOSE > 0:
225
+ self.logger.info('*' * 89)
226
+ self.logger.info(f'Running Batch {n_c}.')
227
+ self.logger.info(f'Structure names: {get_indx(dataIS)}\n')
228
+ cell_str = np.array2string(
229
+ cells, **FLOAT_ARRAY_FORMAT
230
+ ).replace("[", " ").replace("]", " ") # TODO, Now it supports pygData and DGLGraph.
231
+ self.logger.info(f'Cell Vectors:\n{cell_str}')
232
+ self.logout_element_information(get_atomic_number(dataIS), batch_indx)
233
+
234
+ # check atom numbers
235
+ for _, __ in enumerate(batch_indx):
236
+ if __ != _check_batch_indx[_]:
237
+ __err_msg = f'The atom numbers of {_}-th sample do not match: {__} != {_check_batch_indx[_]}'
238
+ if self.VERBOSE > 0: self.logger.error(__err_msg)
239
+ raise RuntimeError(__err_msg)
240
+
241
+ # initial atom coordinates
242
+ if self.data_type == 'pyg':
243
+ X_is = dataIS.pos
244
+ X_fs = dataFS.pos
245
+ else:
246
+ X_is = dataIS.nodes['atom'].data['pos']
247
+ X_fs = dataFS.nodes['atom'].data['pos']
248
+ # rebatch data
249
+ origin_elem_tensor = get_atomic_number(dataIS).squeeze(0)
250
+ # creat fixation
251
+ if self.FIXATIONS is not None:
252
+ fixed_atom_tensor = th.ones_like(X_is, dtype=th.int8)
253
+ fix_list = self.fixation_resolve()
254
+ for fix_info in fix_list:
255
+ fixed_atom_tensor = atom_fix_selector(
256
+ fixed_atom_tensor,
257
+ origin_elem_tensor,
258
+ X_is,
259
+ **fix_info
260
+ )
261
+ fixed_atom_tensor = fixed_atom_tensor.repeat((self.N_IMAGES, 1))
262
+ else:
263
+ fixed_atom_tensor = get_fixed_mask(dataIS)
264
+ dataIS = rebatched_graph(dataIS, self.N_IMAGES)
265
+
266
+ # run
267
+ _energy, _X, out_grad = neb_ops.run(
268
+ model_wrap.Energy,
269
+ X_is,
270
+ X_fs,
271
+ grad_func=model_wrap.Grad,
272
+ func_args=(dataIS,), grad_func_args=(dataIS,),
273
+ is_grad_func_contain_y=False,
274
+ require_grad=self.require_grad,
275
+ output_grad = True,
276
+ fixed_atom_tensor=fixed_atom_tensor,
277
+ )
278
+ _energy.detach_()
279
+ _X.detach_()
280
+ out_grad.detach_()
281
+ idx = get_indx(dataIS)
282
+ idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
283
+ n_s += len(batch_indx)
284
+ self.dumper.collect(
285
+ batch_indx,
286
+ idx,
287
+ get_atomic_number(dataIS).squeeze(0),
288
+ get_cell_vec(dataIS),
289
+ _X.flatten(0, 1),
290
+ fixed_atom_tensor,
291
+ _energy,
292
+ - out_grad.flatten(0, 1),
293
+ )
294
+
295
+ # Print info
296
+ if self.VERBOSE > 0:
297
+ self.logger.info(f'Batch {n_c} done.')
298
+ n_c += 1
299
+ if self.SAVE_PREDICTIONS:
300
+ t_save = time.perf_counter()
301
+ with _LoggingEnd(self.log_handler):
302
+ if self.VERBOSE: self.logger.info(f'SAVING RESULTS TO {self.PREDICTIONS_SAVE_FILE} ...')
303
+ self.dumper.flush()
304
+ if self.VERBOSE: self.logger.info(f'Done. Saving Time: {time.perf_counter() - t_save:<.4f}')
305
+
306
+ except Exception as e:
307
+ self.logger.warning(f'WARNING: An error occurred in {n_c}th batch. Error: {e}.')
308
+ if self.VERBOSE > 0:
309
+ excp = traceback.format_exc()
310
+ self.logger.warning(f"Traceback:\n{excp}")
311
+ n_c += 1
312
+
313
+ if self.VERBOSE: self.logger.info(f'NEB Search Done. Total Time: {time.perf_counter() - time_tol:<.4f}')
314
+
315
+ except Exception as e:
316
+ th.cuda.synchronize()
317
+ excp = traceback.format_exc()
318
+ self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:\n{excp}')
319
+
320
+ finally:
321
+ th.cuda.synchronize()
322
+ self.logger.removeHandler(self.log_handler)
323
+ if isinstance(self.log_handler, logging.FileHandler):
324
+ self.log_handler.close()
325
+ pass
@@ -0,0 +1,292 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: Predictor.py
5
+ # Environment: Python 3.12
6
+ import logging
7
+ import math
8
+ import os
9
+ import time
10
+ import traceback
11
+ import warnings
12
+ from typing import Any, Literal
13
+ from queue import Queue
14
+ import threading
15
+
16
+ import numpy as np
17
+ import torch as th
18
+ from torch import nn
19
+
20
+ from BUCToolkit.api._io import _CONFIGS, _LoggingEnd
21
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
22
+ from BUCToolkit.BatchStructures.StructuresIO import structures_io_dumper, ArrayDumper
23
+
24
+
25
+ class Predictor(_CONFIGS):
26
+ """
27
+ A Base Predictor class.
28
+ Users need to set the dataset and dataloader manually.
29
+
30
+ Args:
31
+ config_file: the path of input file
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ config_file: str,
37
+ data_type: Literal['pyg', 'dgl'] = 'pyg',
38
+ ) -> None:
39
+ super().__init__(config_file)
40
+
41
+ self.config_file = config_file
42
+ self.reload_config(config_file)
43
+ self.data_type = data_type
44
+ if self.VERBOSE: self.logger.info('Config File Was Successfully Read.')
45
+ self.param = None
46
+ self._has_load_data = False
47
+ self._data_loader = None
48
+ self.dumper = None
49
+
50
+ def predict(self, model, test_model: bool = False, warm_up: bool = False):
51
+ """ Alias for `run(...)` """
52
+ self.run(model, test_model, warm_up)
53
+
54
+ @staticmethod
55
+ def _async_dump(q: Queue):
56
+ """
57
+
58
+ Args:
59
+ q: queue of `dumper, data`, where data is a dict of {str: Tensor}
60
+
61
+ Returns:
62
+
63
+ """
64
+ while True:
65
+ _ = q.get()
66
+ if _[0] is None:
67
+ break
68
+ dumper: ArrayDumper = _[0]
69
+ copy_stream = _[1]
70
+ copy_stream.synchronize()
71
+ data = _[2]
72
+ data = [data[_k].numpy() for _k in sorted(data) if isinstance(data[_k], th.Tensor)]
73
+ dumper.start_from_arrays(
74
+ 1,
75
+ *data,
76
+ )
77
+ dumper.step(*data)
78
+
79
+ def _async_print(self, q: Queue):
80
+ """
81
+
82
+ Args:
83
+ q: queue of idx, batch_indx, n_s, data, where data is a dict of {str: Tensor}
84
+
85
+ Returns:
86
+
87
+ """
88
+ while True:
89
+ idx, batch_indx, n_s, copy_stream, data = q.get()
90
+ data: dict
91
+ if idx is None:
92
+ break
93
+ copy_stream.synchronize()
94
+ idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
95
+ self.logger.info(f"\nSample: {idx}")
96
+ for key, dat in data.items():
97
+ dat_print = np.array2string(dat.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT) if dat is not None else None
98
+ self.logger.info(f"\t{key}:\n\t\t{dat_print}")
99
+
100
+ def run(self, model, test_model: bool = False, warm_up: bool = False):
101
+ """
102
+ Parameters:
103
+ model: the input model which is uninstantiated nn.Module class.
104
+ test_model: bool, if True, consumed time per batch and max memory per batch would be returned.
105
+ warm_up: bool, if True, model will idle on some pseudo-samples for warming up.
106
+
107
+ Returns:
108
+ None, if `SAVE_PREDICTIONS` in config_file is true.
109
+ np.NdArray, if `SAVE_PREDICTIONS` in config_file is false.
110
+ """
111
+ # check logger
112
+ if not self.logger.hasHandlers(): self.logger.addHandler(self.log_handler)
113
+ dumper = structures_io_dumper(
114
+ path=self.PREDICTIONS_SAVE_FILE,
115
+ mode='x',
116
+ )
117
+ # check vars
118
+ _model: nn.Module = model(**self.MODEL_CONFIG)
119
+ if self.START == 'resume' or self.START == 1 or self.START == 2:
120
+ chk_data = th.load(self.LOAD_CHK_FILE_PATH)
121
+ if self.param is None:
122
+ _model.load_state_dict(chk_data['model_state_dict'])
123
+ else:
124
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
125
+ epoch_now = chk_data['epoch']
126
+ elif self.START == 'from_scratch' or self.START == 0:
127
+ warnings.warn('The model was not read trained parameters from checkpoint file. \nI HOPE YOU KNOW WHAT YOU ARE DOING!', RuntimeWarning)
128
+ epoch_now = 0
129
+ if self.param is not None:
130
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
131
+ else:
132
+ raise ValueError('Invalid START value. It should be "from_scratch" / 0 or "resume" / 1 ')
133
+ # model vars
134
+ _model = _model.to(self.DEVICE)
135
+
136
+ # preprocessing data # TODO
137
+ if self._data_loader is None: raise RuntimeError('Please Set the DataLoader.')
138
+ if not self._has_load_data: raise RuntimeError('Please Set the Data to Predict.')
139
+
140
+ # initialize
141
+ self.n_samp = len(self.TRAIN_DATA['data']) # sample number
142
+ self.n_batch = math.ceil(self.n_samp / self.BATCH_SIZE) # total batch number per epoch
143
+
144
+ try:
145
+ # I/O
146
+ if self.VERBOSE > 0:
147
+ self.logout_task_information(_model, 'PREDICT', None, self.n_samp)
148
+
149
+ time_tol = time.perf_counter()
150
+ _model.eval()
151
+ # data type parse
152
+ if self.data_type == 'pyg':
153
+ def get_indx(data):
154
+ _indx: dict = getattr(data, 'idx', None)
155
+ return _indx
156
+
157
+ def get_batch_indx(data):
158
+ return [len(dat.pos) for dat in data.to_data_list()]
159
+ else:
160
+ def get_indx(data):
161
+ _indx: dict = data.nodes['atom'].data
162
+ return _indx.get('idx', None)
163
+
164
+ def get_batch_indx(data):
165
+ return val_data.batch_num_nodes('atom')
166
+
167
+ # warm_up
168
+ if warm_up:
169
+ with _LoggingEnd(self.log_handler):
170
+ if self.VERBOSE: self.logger.info('Warm up...')
171
+ t_wu = time.perf_counter()
172
+ val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, is_train=False, **self._data_loader_configs)
173
+ __warm_up = 0
174
+ for val_data, val_label in val_set:
175
+ with th.no_grad():
176
+ pred_y = _model(val_data)
177
+ __warm_up += 1
178
+ if __warm_up >= 5: break
179
+ th.cuda.synchronize()
180
+ th.cuda.empty_cache()
181
+ th.cuda.reset_peak_memory_stats()
182
+ if self.VERBOSE: self.logger.info(f'Done. Warm up time: {time.perf_counter() - t_wu:<.4f}')
183
+ # initialize dumper
184
+ dumper.initialize()
185
+ # MAIN LOOP
186
+ val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, is_train=False, **self._data_loader_configs)
187
+ _results = list()
188
+ _results_names = list()
189
+ n_c = 1
190
+ t_1 = 0.
191
+ t_per_batch = list()
192
+ max_alloc_mem = list()
193
+ val_label = None
194
+ n_s = 0 # number of calculated samples. each sample in batches in each for-loop += 1.
195
+ n_err = 0 # error count
196
+ dump_queue = Queue(maxsize=100)
197
+ dump_thread = threading.Thread(target=self._async_dump, args=(dump_queue, ), daemon=True)
198
+ print_queue = Queue(maxsize=100)
199
+ print_thread = threading.Thread(target=self._async_print, args=(print_queue, ), daemon=True)
200
+ if not self.DEVICE.startswith('cpu'):
201
+ copy_stream = th.cuda.Stream()
202
+ cp_ctx = th.cuda.stream(copy_stream)
203
+ default_stream = th.cuda.default_stream(self.DEVICE)
204
+ else:
205
+ copy_stream = EmptyContextManager()
206
+ cp_ctx = EmptyContextManager() # A placeholder
207
+ default_stream = None
208
+ try:
209
+ dump_thread.start()
210
+ print_thread.start()
211
+ for val_data, val_label in val_set:
212
+ try:
213
+ t_bt = time.perf_counter()
214
+ # to avoid get an empty batch
215
+ if len(val_data) <= 0:
216
+ if self.VERBOSE: self.logger.info(f'An empty batch occurred. Skipped.')
217
+ continue
218
+ # pred & loss
219
+ with th.no_grad():
220
+ pred_y = _model(val_data)
221
+ if not isinstance(pred_y, dict):
222
+ raise ValueError(f'The predictions of models should be a dict as the standard format, but got {type(pred_y)}.')
223
+
224
+ with cp_ctx:
225
+ copy_stream.wait_stream(default_stream)
226
+ _pred_cpu = dict()
227
+ for k, v in pred_y.items():
228
+ if isinstance(v, th.Tensor):
229
+ _pred_cpu[k] = v.to('cpu', non_blocking=True)
230
+
231
+ if self.VERBOSE > 1:
232
+ idx = get_indx(val_data)
233
+ batch_indx = get_batch_indx(val_data)
234
+ idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
235
+ n_s += len(batch_indx)
236
+ print_queue.put((idx, batch_indx, n_s, copy_stream, _pred_cpu))
237
+
238
+ dump_queue.put((dumper, copy_stream, _pred_cpu))
239
+
240
+ if self.VERBOSE > 2 and (not self.DEVICE.startswith('cpu')):
241
+ th.cuda.synchronize()
242
+ self.logger.info(f'batch {n_c}, time: {time.perf_counter() - t_bt:<5.4f}, '
243
+ f'GPU_memory: {th.cuda.max_memory_reserved() / (1024 ** 2):>6.3f}')
244
+ if test_model:
245
+ t_per_batch.append(time.perf_counter() - t_bt)
246
+ max_alloc_mem.append(th.cuda.max_memory_reserved() / (1024 ** 2))
247
+ th.cuda.empty_cache()
248
+ th.cuda.reset_peak_memory_stats()
249
+ n_c += 1
250
+ except Exception as e:
251
+ n_err += 1
252
+ self.logger.error(f"ERROR: An error occurred in the {n_c}-th batch: {e}. Total errors: {n_err}.")
253
+ if n_err >= 100:
254
+ self.logger.fatal(f"** Too many errors (over 100) occurred while running model predictions. Program terminated.")
255
+ raise RuntimeError('** Too many errors (over 100) occurred while running model predictions.')
256
+ finally:
257
+ dump_queue.put([None]*3)
258
+ dump_thread.join()
259
+ print_queue.put([None]*5)
260
+ print_thread.join()
261
+
262
+ if self.VERBOSE: self.logger.info(f'MAIN LOOP DONE. Total Time: {time.perf_counter() - time_tol:<.4f}')
263
+ dumper.close()
264
+ if test_model:
265
+ return _results, t_per_batch, max_alloc_mem
266
+ else:
267
+ return _results
268
+
269
+ except Exception as e:
270
+ self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:\n{traceback.format_exc()}')
271
+
272
+ finally:
273
+ self.logger.removeHandler(self.log_handler)
274
+ if isinstance(self.log_handler, logging.FileHandler):
275
+ self.log_handler.close()
276
+ pass
277
+
278
+
279
+ class EmptyContextManager:
280
+ def __enter__(self) -> 'EmptyContextManager':
281
+ return self
282
+
283
+ def __exit__(self, exc_type, exc_val, exc_tb):
284
+ pass
285
+
286
+ def synchronize(self):
287
+ pass
288
+
289
+ def wait_stream(self, *args, **kwargs):
290
+ pass
291
+
292
+