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/_io.py ADDED
@@ -0,0 +1,1026 @@
1
+ """ Input / Output Module """
2
+ import gc
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: _io.py
7
+ # Environment: Python 3.12
8
+
9
+ import logging
10
+ import os
11
+ import re
12
+ import sys
13
+ import time
14
+ import traceback
15
+ import warnings
16
+ from typing import Optional, Dict, Callable, Any, Literal, Sequence, List
17
+
18
+ import numpy as np
19
+ import torch as th
20
+ import yaml
21
+ from torch import nn
22
+
23
+ from BUCToolkit.utils._CheckModules import check_module
24
+ from BUCToolkit.cli.print_logo import generate_display_art
25
+ from BUCToolkit.utils.setup_loggers import has_any_handler
26
+ from BUCToolkit.utils._Element_info import ATOMIC_NUMBER, ATOMIC_SYMBOL
27
+ from BUCToolkit.utils.function_utils import _BaseWrapper
28
+ from BUCToolkit.BatchStructures.StructuresIO import structures_io_dumper
29
+
30
+
31
+ class _LoggingEnd:
32
+ """
33
+ A Context Manager of Setting Terminator of Logger.
34
+ Temporarily change the terminator of log handler into `end`, and reset to '\n' when it closed.
35
+
36
+ Parameters:
37
+ logger: the handler of logging.
38
+ end: str, the set terminator of logger.
39
+ """
40
+
41
+ def __init__(self, logger: logging.StreamHandler, end: str = ''):
42
+ self.handler = logger
43
+ self.end = end
44
+
45
+ def __enter__(self):
46
+ self.handler.terminator = self.end
47
+
48
+ def __exit__(self, exc_type, exc_val, exc_tb):
49
+ self.handler.terminator = '\n'
50
+
51
+
52
+ class _CONFIGS(object):
53
+ r"""
54
+ A base class of loading configs.
55
+ """
56
+
57
+ def __init__(self, config_file: str) -> None:
58
+ self.config_file = config_file
59
+ self.logger = None
60
+ self.config = dict()
61
+ self.DEVICE = 'cpu'
62
+ self.param = None
63
+ self._has_load_data = False
64
+ self._data_loader = None
65
+ self.reload_config(config_file)
66
+ self.reset_logger()
67
+
68
+ def set_device(self, device: str | th.device) -> None:
69
+ """ reset the device that model would train on """
70
+ self.DEVICE = device
71
+
72
+ def set_model_config(self, model_config: Dict[str, Any] | None = None) -> None:
73
+ """
74
+ Set the new configs (hyperparameters) of model.
75
+ """
76
+ if model_config is None: model_config = dict()
77
+ self.MODEL_CONFIG = model_config
78
+
79
+ def set_model_param(self, model_state_dict: Dict, is_strict: bool = True, is_assign: bool = False) -> None:
80
+ """
81
+ Set the trained model parameters from direct input.
82
+ Parameters:
83
+ model_state_dict: Dict, a dict containing parameters and persistent buffers.
84
+ is_strict: bool,whether to strictly enforce that the keys in state_dict match the keys returned by this module's state_dict() function.
85
+ is_assign: bool, When False, the properties of the tensors in the current module are preserved while when True,
86
+ the properties of the Tensors in the state dict are preserved.
87
+ """
88
+ if isinstance(model_state_dict, Dict):
89
+ self.param = model_state_dict
90
+ else:
91
+ raise TypeError(f'model_state_dict must be a Dict, but occurred {type(model_state_dict)}')
92
+ self.is_strict = is_strict
93
+ self.is_assign = is_assign
94
+
95
+ def set_dataloader(self, DataLoader, DataLoader_configs: Dict | None = None) -> None:
96
+ r"""
97
+ Set the data loader which is :
98
+ * DataLoader(data, batchsize, device, **kwargs) -> Iterable
99
+ * next(iter( DataLoader(data) )) -> (data, label)
100
+ The argument 'batchsize' and 'device' of DataLoader would be read from self.BATCH_SIZE and self.DEVICE respectively.
101
+ """
102
+ if DataLoader_configs is None: DataLoader_configs = dict()
103
+ self._data_loader = DataLoader
104
+ self._data_loader_configs = DataLoader_configs
105
+
106
+ def set_dataset(
107
+ self,
108
+ train_data: Dict[Literal['data', 'labels', 'dataIS', 'dataFS'], Any],
109
+ valid_data: Optional[Dict[Literal['data', 'labels'], Any]] = None
110
+ ):
111
+ r"""
112
+ Load the data that put into DataLoader.
113
+ Parameters:
114
+ train_data: {'data': Any, 'labels':Any}, the Dict of training set.
115
+ valid_data: {'data': Any, 'labels':Any}, the Dict of validation set.
116
+ Both training and validation set data must implement __len__() method, and they are correspond to the input of dataloader.
117
+ """
118
+ self._has_load_data = True
119
+ self.TRAIN_DATA = train_data
120
+ if valid_data is not None:
121
+ self.VALID_DATA = valid_data
122
+ else:
123
+ self.VALID_DATA = {'data': list(), 'label': list()}
124
+
125
+ def logout_element_information(self, element_tensor, batch_indx):
126
+ """
127
+ Logout element information.
128
+ Args:
129
+ element_tensor: the atom-wise element tensor.
130
+ batch_indx: the number of atoms in a sample within a batch.
131
+
132
+ Returns:
133
+
134
+ """
135
+ element_list = element_tensor.tolist() if isinstance(element_tensor, th.Tensor) else list(element_tensor)
136
+ elem_list = list()
137
+ _element_list = list()
138
+ if batch_indx is not None:
139
+ indx_old = 0
140
+ for indx in batch_indx:
141
+ _element_list.append(element_list[0][indx_old: indx_old + indx])
142
+ indx_old += indx
143
+ else:
144
+ _element_list = element_list
145
+ for elements in _element_list:
146
+ __element_now = ''
147
+ __elem = ''
148
+ elem_info = ''
149
+ __elem_count = ''
150
+ for i, elem in enumerate(elements, 1):
151
+ # get element symbol
152
+ if isinstance(elem, int):
153
+ __elem = ATOMIC_NUMBER[elem]
154
+ else:
155
+ __elem = elem
156
+ # count element number
157
+ if __elem == __element_now:
158
+ __elem_count += 1
159
+ else:
160
+ elem_info = elem_info + str(__elem_count) + ' '
161
+ elem_info = elem_info + __elem + ': '
162
+ __elem_count = 1
163
+ __element_now = __elem
164
+ elem_info = elem_info + str(__elem_count)
165
+ elem_list.append(elem_info)
166
+ # log out
167
+ for i, ee in enumerate(elem_list):
168
+ self.logger.info(f'Structure {i:>5d}: {ee}')
169
+ self.logger.info('*' * 89)
170
+
171
+ def logout_task_information(
172
+ self,
173
+ _model: nn.Module,
174
+ mode: str,
175
+ algo_config: Dict[str, Any] | None,
176
+ n_samp: int | None
177
+ ) -> None:
178
+ """
179
+ Logout task head information.
180
+ Args:
181
+ _model: nn.Module, a user-defind model to count & print parameter number.
182
+ mode: task mode keyword.
183
+ algo_config: Dict[str, Any], kwargs/configs of this task.
184
+ n_samp: int, number of samples loaded in.
185
+
186
+ Returns: None
187
+
188
+ """
189
+ if algo_config is None: algo_config = dict()
190
+ __time = time.strftime("%Y%m%d_%H:%M:%S")
191
+ para_count = sum(p.numel() for p in _model.parameters())
192
+ self.logger.info('\n' + generate_display_art())
193
+ self.logger.info('\n' + '*' * 89 + f'\n TIME: {__time}')
194
+ # parse mode
195
+ algo_info = None
196
+ if mode == 'OPT':
197
+ self.logger.info(' TASK: Structure Optimization (minimize) <<')
198
+ algo_info = self.RELAXATION["ALGO"]
199
+ elif mode == 'TS':
200
+ self.logger.info(' TASK: Structure Optimization (TS) <<')
201
+ algo_info = self.TRANSITION_STATE["ALGO"]
202
+ elif mode == 'MD':
203
+ self.logger.info(' TASK: Molecular Dynamics <<')
204
+ algo_info = f"{self.MD["ENSEMBLE"]} Ensemble"
205
+ elif mode == 'NEB':
206
+ self.logger.info(' TASK: Nudged Elastic Band Transition State Search <<')
207
+ algo_info = self.NEB.get('ALGO', 'CI-NEB')
208
+ elif mode == 'CMD':
209
+ self.logger.info(' TASK: Constrained Molecular Dynamics <<')
210
+ algo_info = f"{self.MD["ENSEMBLE"]} Ensemble"
211
+ elif mode == 'VIB':
212
+ self.logger.info(' TASK: Vibrational Analysis (Harmonic) <<')
213
+ algo_info = "Finite Difference" if self.VIBRATION.get('METHOD', 'Coord') == "Coord" else "Automatic Differentiation"
214
+ elif mode == 'MC':
215
+ self.logger.info(' TASK: Monte Carlo <<')
216
+ algo_info = f"{self.MC["ENSEMBLE"]} Ensemble"
217
+ elif mode == 'TRAIN':
218
+ self.logger.info(' TASK: TRAINING & VALIDATION <<')
219
+ elif mode == 'PREDICT':
220
+ self.logger.info(' TASK: Predict <<')
221
+ else:
222
+ self.logger.info(' TASK: Unknown Mode <<')
223
+ # mode end
224
+ if (self.START == 0) or (self.START == 'from_scratch'):
225
+ self.logger.info(' FROM_SCRATCH <<')
226
+ elif (self.START == 1) or (self.START == 'resume'):
227
+ self.logger.info(' RESUME <<')
228
+ elif (self.START == 2) or (self.START == 'load_param'):
229
+ self.logger.info(' LOAD_PARAMETER <<')
230
+ self.logger.info(f' COMMENTS: {self.COMMENTS}')
231
+ self.logger.info(f' I/O INFORMATION:')
232
+ self.logger.info(f'\tVERBOSITY LEVEL: {self.VERBOSE}')
233
+ if not self.REDIRECT:
234
+ self.logger.info('\tLOG WILL OUTPUT TO STDOUT')
235
+ else:
236
+ output_file = os.path.join(self.OUTPUT_PATH, f'{time.strftime("%Y%m%d_%H_%M_%S")}_{self.OUTPUT_POSTFIX}.out')
237
+ self.logger.info(f'\tLOG WILL OUTPUT TO {output_file}') # type: ignore
238
+ if (self.START == 2) or (self.START == 'load_param'):
239
+ self.logger.info(f'\tMODEL PARAMETERS LOAD FROM: {self.LOAD_CHK_FILE_PATH}')
240
+ elif (self.START == 1) or (self.START == 'resume'):
241
+ self.logger.info(f'\tMODEL CHECKPOINT FILE LOAD FROM: {self.LOAD_CHK_FILE_PATH}')
242
+ if mode == 'TRAIN': self.logger.info(f'\tCHECKPOINT FILE SAVE TO: {self.CHK_SAVE_PATH}_{self.CHK_SAVE_POSTFIX}')
243
+ self.logger.info(f' MODEL NAME: {self.MODEL_NAME}')
244
+ self.logger.info(f' MODEL INFORMATION:')
245
+ self.logger.info(f'\tTOTAL PARAMETERS: {para_count}')
246
+ if mode == 'TRAIN':
247
+ para_count_train = sum(p.numel() for p in _model.parameters() if p.requires_grad)
248
+ self.logger.info(f'\tTOTAL TRAINABLE PARAMETERS: {para_count_train}')
249
+ if self.VERBOSE > 1:
250
+ self.logger.info(f'\tHYPER-PARAMETERS:')
251
+ for hp, hpv in self.MODEL_CONFIG.items():
252
+ self.logger.info(f'\t\t{hp}: {hpv}')
253
+
254
+ self.logger.info(f' TASK WILL RUN ON {self.DEVICE}')
255
+ if mode == 'TRAIN':
256
+ self.logger.info(f' LOSS FUNCTION: {self.loss_name}')
257
+ if len(self.METRICS) > 0:
258
+ self.logger.info(f" # note that `METRICS` only counts the data of last true batch instead of accumulated ones.")
259
+ with _LoggingEnd(self.log_handler):
260
+ self.logger.info(f' METRICS: ')
261
+ for _name in self.METRICS.keys():
262
+ self.logger.info(f'{_name} ')
263
+ self.logger.info('')
264
+ else:
265
+ self.logger.info(' METRICS: None')
266
+ self.logger.info(f' OPTIMIZER INFORMATION:')
267
+ __opt_repr = re.split(
268
+ r'\(\n|\)$|\s{2,}|\n',
269
+ repr(self.OPTIMIZER(list(th.zeros(1)), **self.OPTIM_CONFIG))
270
+ )
271
+ self.logger.info(f'\tOPTIMIZER: {__opt_repr[0]}')
272
+ if (self.VERBOSE > 1) and hasattr(self, '_layerwise_opt_configs'):
273
+ if self._layerwise_opt_configs is not None:
274
+ _layer_grp_name = list(self._layerwise_opt_configs.keys())
275
+ else:
276
+ _layer_grp_name = None
277
+ kk = 0
278
+ for __partt in __opt_repr[1:-2]:
279
+ if __partt.startswith('Parameter Group'):
280
+ self.logger.info(f'\t{__partt}')
281
+ if self._layerwise_opt_configs is not None:
282
+ self.logger.info(
283
+ f'\t\tcontaining layers: {
284
+ _layer_grp_name[kk].pattern if kk < len(_layer_grp_name) else "all remaining layers"
285
+ }'
286
+ )
287
+ kk += 1
288
+ else:
289
+ self.logger.info(f'\t\t{__partt}')
290
+ del kk, _layer_grp_name
291
+ if self.LR_SCHEDULER is not None:
292
+ self.logger.info(f'\tLR_SCHEDULER: {str(self.LR_SCHEDULER)}')
293
+ self.logger.info(f'\tLR_SCHEDULER CONFIG:')
294
+ for hp, hpv in self.LR_SCHEDULER_CONFIG.items(): self.logger.info(f'\t\t{hp}: {hpv}')
295
+ else:
296
+ self.logger.info('\tLR_SCHEDULER: None')
297
+ if self.GRAD_CLIP:
298
+ self.logger.info(f'\tGRAD_CLIP: True')
299
+ self.logger.info(f'\tGRAD_CLIP_MAX_NORM: {self.GRAD_CLIP_MAX_NORM:<.2f}')
300
+ if len(self.GRAD_CLIP_CONFIG) > 0: self.logger.info(f'\tGRAD_CLIP_CONFIG: {self.GRAD_CLIP_CONFIG}')
301
+ else:
302
+ self.logger.info(f'\tGRAD_CLIP: False')
303
+ if self.EMA:
304
+ self.logger.info(f'\tEXPONENTIAL MOVING AVERAGE (EMA): True')
305
+ self.logger.info(f'\tEMA_DECAY: {self.EMA_DECAY:<.5f}')
306
+ self.logger.info(
307
+ f'\tNOTE: `best_checkpoint` will save with EMA parameters, while `checkpoint` and `stop_checkpoint` will not.'
308
+ )
309
+ else:
310
+ self.logger.info(f'\tEXPONENTIAL MOVING AVERAGE (EMA): False')
311
+ self.logger.info(f' ITERATION INFORMATION:')
312
+ self.logger.info(f'\tEPOCH: {self.EPOCH}\n\tBATCH SIZE: {self.BATCH_SIZE}\n\tVALID BATCH SIZE: {self.VAL_BATCH_SIZE}' +
313
+ f'\n\tGRADIENT ACCUMULATION STEPS: {self.ACCUMULATE_STEP}\n\tEVAL PER {self.VAL_PER_STEP} STEPS\n' +
314
+ '*' * 89 + '\n' + 'ENTERING MAIN LOOP...')
315
+ else:
316
+ if self.SAVE_PREDICTIONS:
317
+ self.logger.info(f' PREDICTIONS WILL SAVE TO {self.PREDICTIONS_SAVE_FILE}')
318
+ else:
319
+ self.logger.info(f' PREDICTIONS WILL SAVE IN MEMORY AND RETURN AS A VARIABLE.')
320
+ self.logger.info(f' ITERATION INFORMATION:')
321
+ # ALGORITHM PRINT
322
+ if algo_info is not None:
323
+ self.logger.info(f'\tALGORITHM: {algo_info}')
324
+ for _algo_conf_name, _algo_conf in algo_config.items():
325
+ self.logger.info(f'\t{_algo_conf_name}: {_algo_conf}')
326
+ n_samp = 'Unknown' if n_samp is None else n_samp
327
+ self.logger.info(f'\tBATCH SIZE: {self.BATCH_SIZE}' +
328
+ f'\n\tTOTAL SAMPLE NUMBER: {n_samp}\n' +
329
+ '*' * 89 + '\n' + 'ENTERING MAIN LOOP...')
330
+
331
+ def fixation_resolve(self):
332
+ """
333
+ resolving fixation information.
334
+ Returns:
335
+
336
+ """
337
+ if self.FIXATIONS is None:
338
+ raise ValueError(f'No fixation information is available for fixation resolving.')
339
+
340
+ MODE_DICT = {'FIX':'fix', 'INV_FIX':'inv_fix', 'FREE':'free'}
341
+ SELECT_DICT = {'ELEMENT': 'select_element', 'HEIGHT': 'select_height', 'INDEX': 'atom_index'}
342
+
343
+ fix_mode_list = list()
344
+ for k, v in self.FIXATIONS.items():
345
+ if k not in MODE_DICT:
346
+ raise ValueError(f'Unknown fixation mode: {k}')
347
+ key = MODE_DICT[k]
348
+ _tmp_dict = dict()
349
+ for fix_info, fix_val in v.items():
350
+ if fix_info not in SELECT_DICT:
351
+ raise ValueError(f'Unknown fixation config: {fix_info}')
352
+ _tmp_dict[SELECT_DICT[fix_info]] = fix_val
353
+ _tmp_dict['select_mode'] = key
354
+ # put the inv_fix to the 1st to avoiding overriding
355
+ if key == 'inv_fix':
356
+ fix_mode_list = [_tmp_dict, ] + fix_mode_list
357
+ else:
358
+ fix_mode_list.append(_tmp_dict)
359
+
360
+ return fix_mode_list
361
+
362
+ def reset_logger(self):
363
+ """
364
+ Reset logger & its handlers.
365
+ Returns: None
366
+
367
+ """
368
+ # logging
369
+ # logging.getLogger().disabled = True
370
+ self.logger = logging.getLogger('Main')
371
+ self.logger.propagate = False
372
+ self.logger.setLevel(logging.INFO)
373
+ # remove existing handlers
374
+ for _hdl in list(self.logger.handlers):
375
+ self.logger.removeHandler(_hdl)
376
+ try:
377
+ _hdl.close()
378
+ except Exception as ehdl:
379
+ warnings.warn(f'Failed to close handler {_hdl}: {ehdl}', RuntimeWarning)
380
+ formatter = logging.Formatter('%(message)s')
381
+ if self.REDIRECT:
382
+ output_file = os.path.join(self.OUTPUT_PATH, f'{time.strftime("%Y%m%d_%H_%M_%S")}_{self.OUTPUT_POSTFIX}.out')
383
+ # check whether path exists
384
+ if not os.path.isdir(self.OUTPUT_PATH): os.makedirs(self.OUTPUT_PATH)
385
+ # set log handler
386
+ self.log_handler = logging.FileHandler(output_file, 'w', delay=True)
387
+ self.log_handler.setLevel(logging.INFO)
388
+ self.log_handler.setFormatter(formatter)
389
+ else:
390
+ self.log_handler = logging.StreamHandler(stream=sys.stdout)
391
+ self.log_handler.setLevel(logging.INFO)
392
+ self.log_handler.setFormatter(formatter)
393
+ if not has_any_handler(self.logger): self.logger.addHandler(self.log_handler)
394
+
395
+ def reload_config(self, config_file_path: str| None = None) -> None:
396
+ """
397
+ Reload the yaml configs file.
398
+ """
399
+ # load config file
400
+ config_file_path = self.config_file if config_file_path is None else config_file_path
401
+ with open(config_file_path, 'r', encoding='utf-8') as f:
402
+ config: Dict[str, Any] = yaml.safe_load(f)
403
+ self.config = config
404
+
405
+ # global information
406
+ self.START = self.config.get('START', 0)
407
+ if self.START != 'from_scratch' and self.START != 0:
408
+ self.LOAD_CHK_FILE_PATH: str = self.config['LOAD_CHK_FILE_PATH']
409
+ if not isinstance(self.LOAD_CHK_FILE_PATH, str): raise TypeError('LOAD_CHK_FILE_PATH must be a str.')
410
+ self.STRICT_LOAD: bool = self.config.get('STRICT_LOAD', True)
411
+ if not isinstance(self.STRICT_LOAD, bool): raise TypeError(f'STRICT_LOAD must be a boolean, but got {type(self.STRICT_LOAD)}')
412
+ self.COMMENTS: str = self.config.get('COMMENTS', 'None.')
413
+ self.VERBOSE: int = int(self.config.get('VERBOSE', 1))
414
+ self.DEVICE: str|th.device = self.config.get('DEVICE', 'cpu')
415
+ self.BATCH_SIZE: int = self.config.get('BATCH_SIZE', 1)
416
+
417
+ # model info
418
+ self.MODEL_NAME: str = self.config.get('MODEL_NAME', 'Untitled')
419
+ if not isinstance(self.MODEL_NAME, str): raise TypeError('MODEL_NAME must be a str.')
420
+ self.MODEL_CONFIG = self.config.get('MODEL_CONFIG', dict())
421
+ if not isinstance(self.MODEL_CONFIG, Dict): raise ValueError('MODEL_CONFIG must be a dictionary.')
422
+
423
+ # output info
424
+ self.REDIRECT = self.config.get('REDIRECT', True)
425
+ self.SAVE_CHK = self.config.get('SAVE_CHK', False)
426
+ self.SAVE_PREDICTIONS = self.config.get('SAVE_PREDICTIONS', False)
427
+ if not isinstance(self.SAVE_PREDICTIONS, bool):
428
+ raise TypeError(f'SAVE_PREDICTIONS must be a boolean, but occurred {type(self.SAVE_PREDICTIONS)}.')
429
+ self._PREDICTIONS_SAVE_FILE = self.config.get('PREDICTIONS_SAVE_FILE', './_Predictions')
430
+ while os.path.exists(self._PREDICTIONS_SAVE_FILE): # avoid overwrite existent data. Automatically rename.
431
+ warnings.warn(
432
+ f'`PREDICTIONS_SAVE_FILE`: "{self._PREDICTIONS_SAVE_FILE}" already exists. '
433
+ f'It will be renamed as "{self._PREDICTIONS_SAVE_FILE}_1".',
434
+ RuntimeWarning
435
+ )
436
+ self._PREDICTIONS_SAVE_FILE += '_1'
437
+ if self.SAVE_PREDICTIONS and (not isinstance(self.PREDICTIONS_SAVE_FILE, str)):
438
+ raise TypeError(f'PREDICTIONS_SAVE_PATH must be a str, but occurred {type(self.PREDICTIONS_SAVE_FILE)}.')
439
+ if self.SAVE_PREDICTIONS:
440
+ self.dumper = DumpStructures(self.PREDICTIONS_SAVE_FILE)
441
+ else:
442
+ self.dumper = DumpStructures(None, )
443
+ if not isinstance(self.REDIRECT, bool): raise TypeError('REDIRECT must be a boolean.')
444
+ if self.REDIRECT:
445
+ self.OUTPUT_PATH = self.config.get('OUTPUT_PATH', './')
446
+ self.OUTPUT_POSTFIX = self.config.get('OUTPUT_POSTFIX', 'Untitled')
447
+ self.CHK_SAVE_PATH = self.config.get('CHK_SAVE_PATH', './')
448
+ self.CHK_SAVE_POSTFIX = self.config.get('CHK_SAVE_POSTFIX', '')
449
+ if self.CHK_SAVE_POSTFIX != '': self.CHK_SAVE_POSTFIX = '_' + self.CHK_SAVE_POSTFIX # use '_' to delimit chk name.
450
+ if not isinstance(self.CHK_SAVE_PATH, str): raise TypeError('CHK_SAVE_PATH must be a str.')
451
+
452
+ # debug mode
453
+ self.DEBUG_MODE = self.config.get('DEBUG_MODE', False)
454
+ if not isinstance(self.DEBUG_MODE, bool): raise TypeError('DEBUG_MODE must be a boolean.')
455
+ self.CHECK_NAN = self.config.get('CHECK_NAN', True)
456
+ if not isinstance(self.CHECK_NAN, bool): raise TypeError('CHECK_NAN must be a boolean.')
457
+
458
+ # loading atoms fixation info.
459
+ self.FIXATIONS = self.config.get('FIXATIONS', None)
460
+
461
+ # If Train
462
+ self.TRAIN = self.config.get('TRAIN', None)
463
+
464
+ # If Structure opt.
465
+ self.RELAXATION = self.config.get('RELAXATION', None)
466
+ self.TRANSITION_STATE = self.config.get('TRANSITION_STATE', None)
467
+
468
+ # If Molecular Dynamics
469
+ self.MD = self.config.get('MD', None)
470
+
471
+ # NEB Transition State Search
472
+ self.NEB = self.config.get('NEB', None)
473
+
474
+ # If Vibration Calc.
475
+ self.VIBRATION = self.config.get('VIBRATION', None)
476
+
477
+ # If Monte Carlo
478
+ self.MC = self.config.get('MC', None)
479
+
480
+ @property
481
+ def PREDICTIONS_SAVE_FILE(self):
482
+ return self._PREDICTIONS_SAVE_FILE
483
+
484
+ @PREDICTIONS_SAVE_FILE.setter
485
+ def PREDICTIONS_SAVE_FILE(self, value):
486
+ if not self.SAVE_PREDICTIONS:
487
+ self.logger.warning(
488
+ f'WARNING: You are setting a new predictions save file, while `SAVE_PREDICTIONS` is still False.\n'
489
+ f'Hence, NOTHING WILL HAPPEN. BYE!'
490
+ )
491
+ return
492
+ self._PREDICTIONS_SAVE_FILE = value
493
+ self.dumper = DumpStructures(self.PREDICTIONS_SAVE_FILE)
494
+
495
+ def compare_tensors(X1: th.Tensor, X2: th.Tensor):
496
+ """Compare two tensors. Return True if they are the same, False otherwise."""
497
+ char1 = (X1.untyped_storage().data_ptr(),
498
+ X1.storage_offset(),
499
+ tuple(X1.shape),
500
+ tuple(X1.stride()),
501
+ X1.device,
502
+ X1.dtype)
503
+ char2 = (X2.untyped_storage().data_ptr(),
504
+ X2.storage_offset(),
505
+ tuple(X2.shape),
506
+ tuple(X2.stride()),
507
+ X2.device,
508
+ X2.dtype)
509
+
510
+ return char1 == char2
511
+
512
+
513
+ class _Model_Wrapper_pyg(_BaseWrapper):
514
+
515
+ __slots__ = ('_model', 'forces', 'X', )
516
+
517
+ def __init__(self, model) -> None:
518
+ """
519
+ A format transformer for converting Tensor X into PygData.pos
520
+ Wrap the model(graph, ...) into f(X)
521
+
522
+ Args:
523
+ model: An instantiate nn.Module
524
+
525
+ Methods:
526
+ Energy: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['energy'].
527
+ Grad: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['forces'].
528
+
529
+ """
530
+ super().__init__(model)
531
+ if check_module('torch_geometric') is None:
532
+ ImportError('The method is unavailable because the `torch-geometric` cannot be imported.')
533
+ pass
534
+
535
+ def Energy(self, X, graph):
536
+ self.X = X
537
+ graph.pos = self.X.reshape(-1,3).contiguous()
538
+ y = self._model(graph)
539
+ energy = y['energy']
540
+ self.forces = y['forces']
541
+ return energy
542
+
543
+ def Grad(self, X, graph):
544
+ origin_shape = X.shape
545
+ if (self.X is None) or (not compare_tensors(X, self.X)):
546
+ self.forces = None
547
+ if self.forces is None:
548
+ self.X = X
549
+ graph.pos = self.X.reshape(-1,3)
550
+ return - ((self._model(graph))['forces']).reshape(origin_shape)
551
+ else:
552
+ force = self.forces
553
+ self.forces = None
554
+ return - force.reshape(origin_shape).contiguous()
555
+
556
+
557
+ class _Model_Wrapper_pyg_only_X(_BaseWrapper):
558
+ def __init__(self, model , graph) -> None:
559
+ """
560
+ A format transformer for converting Tensor X into PygData.pos
561
+ Wrap the model(graph, ...) into f(X)
562
+
563
+ Args:
564
+ model: An instantiate nn.Module
565
+
566
+ Methods:
567
+ Energy: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['energy'].
568
+ Grad: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['forces'].
569
+ """
570
+ super().__init__(model)
571
+ self.graph = graph
572
+ if check_module('torch_geometric') is None:
573
+ ImportError('The method is unavailable because the `torch-geometric` cannot be imported.')
574
+ pass
575
+
576
+ def Energy(self, X,):
577
+ self.X = X
578
+ self.graph.pos = self.X.squeeze(0).reshape(-1,3)
579
+ y = self._model(self.graph )
580
+ energy = y['energy']
581
+ energy = th.sum(energy).unsqueeze(0)
582
+ return energy
583
+
584
+ def Grad(self, X):
585
+ origin_shape = X.shape
586
+ if (self.X is None) or (not compare_tensors(X, self.X)):
587
+ self.forces = None
588
+ if self.forces is None:
589
+ self.X = X
590
+ self.graph.pos = self.X.reshape(-1, 3)
591
+ return - ((self._model(self.graph))['forces']).reshape(origin_shape)
592
+ else:
593
+ force = self.forces
594
+ self.forces = None
595
+ return - force.reshape(origin_shape).contiguous()
596
+
597
+
598
+ class _Model_Wrapper_dgl(_BaseWrapper):
599
+ def __init__(self, model) -> None:
600
+ """
601
+ A format transformer for converting Tensor X into DGLGraph.ndata['pos'] i.e., wrapping the model(graph, ...) into f(X)
602
+ The output DGLGraph has format as follows:
603
+ dgl.heterograph(
604
+ {
605
+ ('atom', 'bond', 'atom'): ([], []),
606
+ ('cell', 'disp', 'cell'): ([], [])
607
+ },
608
+ num_nodes_dict={
609
+ 'atom': n_atom,
610
+ 'cell': 1
611
+ }
612
+ )
613
+ data.nodes['atom'].data['pos']: (n_atom, 3), Atom positions in Cartesian coordinates.
614
+ data.nodes['atom'].data['Z']: (n_atom, ), Atomic numbers.
615
+ data.nodes['cell'].data['cell']: (1, 3, 3), Cell vectors.
616
+
617
+ Args:
618
+ model: An instantiate nn.Module subclass
619
+
620
+ Methods:
621
+ Energy: input Tensor `X` and DGLGraph `graph`, it will update data.nodes['atom'].data['pos'] into X and return model(graph)['energy'].
622
+ Grad: input Tensor `X` and DGLGraph `graph`, it will update data.nodes['atom'].data['pos'] into X and return model(graph)['forces'].
623
+
624
+ """
625
+ super().__init__(model)
626
+ if check_module('dgl') is None:
627
+ ImportError('The method is unavailable because the `dgl` cannot be imported.')
628
+ pass
629
+
630
+ def Energy(self, X, graph, return_format: Literal['sum', 'origin'] = 'origin'):
631
+ self.X = X
632
+ graph.nodes['atom'].data['pos'] = self.X.squeeze(0)
633
+ y = self._model(graph)
634
+ energy = y['energy']
635
+ self.forces = y['forces']
636
+ if return_format == 'sum':
637
+ energy = th.sum(energy).unsqueeze(0)
638
+ return energy
639
+
640
+ def Grad(self, X, graph):
641
+ if (self.X is None) or (not compare_tensors(X, self.X)):
642
+ self.forces = None
643
+ if self.forces is None:
644
+ self.X = X
645
+ graph.nodes['atom'].data['pos'] = self.X.squeeze(0)
646
+ return - ((self._model(graph))['forces']).unsqueeze(0)
647
+ else:
648
+ force = self.forces
649
+ self.forces = None
650
+ return - force.unsqueeze(0)
651
+
652
+
653
+ class _Model_Wrapper_regularBatch_pyg(_BaseWrapper):
654
+ def __init__(self, model) -> None:
655
+ """
656
+ A format transformer for converting Tensor X into PygData.pos
657
+ Wrap the model(graph, ...) into f(X), but here the batch size of graph is only 1, and the batch size (1st dimension) of X is many.
658
+ This wrapper would expand batch of graph into the same as X.
659
+
660
+ Args:
661
+ model: An instantiate nn.Module
662
+
663
+ Methods:
664
+ Energy: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['energy'].
665
+ Grad: input Tensor `X` and PygData `graph`, it will update graph.pos into X and return model(graph)['forces'].
666
+
667
+ """
668
+ super().__init__(model)
669
+ _pyg = check_module('torch_geometric.data')
670
+ if _pyg is not None:
671
+ import torch_geometric.data as _pyg
672
+ self.pygBatch = _pyg.Batch
673
+ else:
674
+ ImportError('The method is unavailable because the `torch-geometric` cannot be imported.')
675
+
676
+ def Energy(self, X: th.Tensor, graph):
677
+ self.X = X.flatten(0, 1) # convert X: (n_batch, n_atom, n_dim) into X': (n_batch * n_atom, 3)
678
+ batch_size = X.size(0)
679
+ if graph.batch_size == 1:
680
+ graph = self.pygBatch.from_data_list([graph] * batch_size)
681
+ graph.pos = self.X
682
+ y = self._model(graph) # (n_batch, )
683
+ energy = y['energy']
684
+ self.forces = y['forces']
685
+ return energy
686
+
687
+ def Grad(self, X, graph):
688
+ if (self.X is None) or (not compare_tensors(X, self.X)):
689
+ self.forces = None
690
+ if self.forces is None:
691
+ self.Energy(X, graph)
692
+
693
+ force: th.Tensor = self.forces
694
+ self.forces = None
695
+ return - force.unsqueeze(0)
696
+
697
+
698
+ class ExpMovingAverage:
699
+ """
700
+ Applying exponential moving average for training models.
701
+ """
702
+ def __init__(self, model:nn.Module, decay:float=0.999):
703
+ """
704
+
705
+ Args:
706
+ model: The torch model of nn.Module
707
+ decay: The decay coefficient.
708
+ """
709
+ self.model = model
710
+ self.decay = decay
711
+ self.shadow: Dict[str, th.Tensor] = dict()
712
+ self.backup = dict()
713
+ # initialize
714
+ for name, param in self.model.named_parameters():
715
+ if param.requires_grad:
716
+ self.shadow[name] = param.data.detach().clone()
717
+
718
+ def step(self):
719
+ """
720
+ Applying ema 1 time.
721
+ """
722
+ for name, param in self.model.named_parameters():
723
+ if param.requires_grad:
724
+ assert name in self.shadow
725
+ self.shadow[name].mul_(self.decay).add_(param.data.detach(), alpha=1-self.decay)
726
+
727
+ def apply(self):
728
+ """
729
+ Applying ema shadow parameters to the trained model, and the original parameters of trained model will be stored in `self.backup`.
730
+ """
731
+ for name, param in self.model.named_parameters():
732
+ if param.requires_grad:
733
+ assert name in self.shadow
734
+ self.backup[name] = param.data.detach().clone()
735
+ param.copy_(self.shadow[name])
736
+
737
+ def restore(self):
738
+ """
739
+ Restore backup of original parameters of model from `self.backup` to continue training.
740
+ """
741
+ try:
742
+ if len(self.backup) != 0:
743
+ for name, param in self.model.named_parameters():
744
+ if param.requires_grad:
745
+ assert name in self.backup, f'Parameter {name} is not in backup!'
746
+ param.copy_(self.backup[name])
747
+ else:
748
+ pass
749
+ finally:
750
+ self.backup = dict()
751
+
752
+
753
+ class DumpStructures:
754
+ """
755
+ Dump structures calculated by `Predictor`, `StructureOptimization`, etc.
756
+ Use the `ArrayDumper` as the backend, which dump structure information into binary file.
757
+ """
758
+ def __init__(self, path:str|None=None):
759
+ """
760
+ Args:
761
+ path: the path to dump structures. If None, structures will always store in memory.
762
+ """
763
+ self.dumper = structures_io_dumper(path, 'x', )
764
+ self._step = self._first_step
765
+ self._has_initialized = False
766
+
767
+ def initialize(self):
768
+ self.dumper.initialize()
769
+
770
+ @staticmethod
771
+ def _exit(errt = None, errv = None, tr = None):
772
+ if errt is not None:
773
+ raise errt(f'{errv}, {tr}')
774
+
775
+ @staticmethod
776
+ def _reformat(
777
+ batch_indices: List,
778
+ idx: List[str],
779
+ elements: th.Tensor,
780
+ cells: th.Tensor | np.ndarray,
781
+ pos: th.Tensor,
782
+ fixations: th.Tensor,
783
+ energies: th.Tensor,
784
+ forces: th.Tensor,
785
+ ):
786
+ """
787
+ Reformat input into dumping format.
788
+ Args:
789
+ batch_indices:
790
+ idx:
791
+ elements:
792
+ cells:
793
+ pos:
794
+ fixations:
795
+ energies:
796
+ forces:
797
+
798
+ Returns:
799
+ batch_indices, idx, cells, elements, pos, fixations, energies, forces
800
+ """
801
+ # Postprocessing & save TODO: reformat it in future to apply to all functions.
802
+ n_batch = len(batch_indices)
803
+ batch_indices = np.array(batch_indices, dtype=np.int64)
804
+ if cells.shape != (n_batch, 3, 3):
805
+ raise ValueError(f'`cells` is expected to have 3 dimensions (n_batch, 3, 3), but got {cells.shape}.')
806
+ cells = cells.numpy(force=True).astype(np.float32) if isinstance(cells, th.Tensor) else np.asarray(cells, dtype=np.float32)
807
+ if len(idx) != n_batch:
808
+ raise ValueError(f'The number of `idx` is expected to be batch size {n_batch}, but got {len(idx)}.')
809
+ idx = np.array(idx, dtype='<U128')
810
+ if elements.ndim == 2:
811
+ elements = elements.squeeze(0)
812
+ elif elements.ndim != 1:
813
+ raise ValueError(f'`elements` should be a 1D array, but got {elements.shape}')
814
+ elements = elements.numpy(force=True).astype(np.int64) if isinstance(elements, th.Tensor) else np.asarray(elements, dtype=np.int64)
815
+ # pos_type = np.array(pos_type)
816
+ if pos.ndim == 3:
817
+ pos = pos.squeeze(0)
818
+ elif pos.ndim != 2:
819
+ raise ValueError(f'`pos` should be a 2D array, but got {pos.shape}')
820
+ pos = pos.numpy(force=True).astype(np.float32) if isinstance(pos, th.Tensor) else np.asarray(pos, dtype=np.float32)
821
+ if forces.ndim == 3:
822
+ forces = forces.squeeze(0)
823
+ if forces.shape != pos.shape:
824
+ raise ValueError(f'`forces` should have the same shape as `pos`, but got {forces.shape} rather than {pos.shape}.')
825
+ forces = forces.numpy(force=True).astype(np.float32) if isinstance(forces, th.Tensor) else np.asarray(forces, dtype=np.float32)
826
+ if energies.ndim == 2:
827
+ energies = energies.squeeze(0)
828
+ elif energies.ndim != 1:
829
+ raise ValueError(f'`energies` should be a 1D array, but got {energies.shape}')
830
+ energies = energies.numpy(force=True)
831
+ if fixations.ndim == 3:
832
+ fixations = fixations.squeeze(0)
833
+ if fixations.shape != pos.shape:
834
+ raise ValueError(f'`fixations` should have the same shape as `pos`, but got {fixations.shape} rather than {pos.shape}.')
835
+ fixations = fixations.numpy(force=True).astype(np.float32) if isinstance(fixations, th.Tensor) else np.asarray(fixations, dtype=np.float32)
836
+
837
+ return batch_indices, idx, cells, elements, pos, fixations, energies, forces
838
+
839
+ def _first_step(
840
+ self,
841
+ batch_indices: List,
842
+ idx: List[str],
843
+ elements: th.Tensor,
844
+ cells: th.Tensor | np.ndarray,
845
+ pos: th.Tensor,
846
+ fixations: th.Tensor,
847
+ energies: th.Tensor,
848
+ forces: th.Tensor,
849
+ ):
850
+ """
851
+ First step to initialize
852
+ Args:
853
+ batch_indices:
854
+ idx:
855
+ elements:
856
+ cells:
857
+ pos:
858
+ fixations:
859
+ energies:
860
+ forces:
861
+
862
+ Returns:
863
+
864
+ """
865
+ if self._has_initialized:
866
+ self.initialize()
867
+ batch_indices, idx, cells, elements, pos, fixations, energies, forces = self._reformat(
868
+ batch_indices,
869
+ idx,
870
+ elements,
871
+ cells,
872
+ pos,
873
+ fixations,
874
+ energies,
875
+ forces,
876
+ )
877
+ self.dumper.start_from_arrays(1, batch_indices, idx, cells, elements, pos, fixations, energies, forces)
878
+ self.dumper.step(
879
+ batch_indices, idx, cells, elements, pos, fixations, energies, forces
880
+ )
881
+ self._step = self._continue_step
882
+
883
+ def _continue_step(
884
+ self,
885
+ batch_indices: List,
886
+ idx: List[str],
887
+ elements: th.Tensor,
888
+ cells: th.Tensor | np.ndarray,
889
+ pos: th.Tensor,
890
+ fixations: th.Tensor,
891
+ energies: th.Tensor,
892
+ forces: th.Tensor,
893
+ ):
894
+ batch_indices, idx, cells, elements, pos, fixations, energies, forces = self._reformat(
895
+ batch_indices,
896
+ idx,
897
+ elements,
898
+ cells,
899
+ pos,
900
+ fixations,
901
+ energies,
902
+ forces,
903
+ )
904
+ self.dumper.start_from_arrays(1, batch_indices, idx, cells, elements, pos, fixations, energies, forces)
905
+ self.dumper.step(
906
+ batch_indices, idx, cells, elements, pos, fixations, energies, forces
907
+ )
908
+
909
+ def collect(
910
+ self,
911
+ batch_indices: List,
912
+ idx: List[str],
913
+ elements: th.Tensor,
914
+ cells: th.Tensor | np.ndarray,
915
+ pos: th.Tensor,
916
+ fixations: th.Tensor,
917
+ energies: th.Tensor,
918
+ forces: th.Tensor,
919
+ ):
920
+ """
921
+ Store structures into a `Structures` instance in the ARRAY FORMAT (Mode = 'A') as `self._structres`.
922
+ Args:
923
+ batch_indices: List, the list of atom numbers in each sample.
924
+ idx: List[str],
925
+ elements: th.Tensor[int] | List[List[int]], the atom-wise elements sequence
926
+ cells: th.Tensor | np.ndarray,
927
+ pos: th.Tensor,
928
+ fixations: th.Tensor,
929
+ energies: th.Tensor,
930
+ forces: th.Tensor,
931
+
932
+ Returns: None
933
+
934
+ """
935
+ try:
936
+ # Postprocessing & save TODO: reformat it in future to apply to all functions.
937
+ self._step(
938
+ batch_indices,
939
+ idx,
940
+ elements,
941
+ cells,
942
+ pos,
943
+ fixations,
944
+ energies,
945
+ forces,
946
+ )
947
+
948
+ except Exception as e:
949
+ warnings.warn(f'Failed to collect structures due to ERROR: {e}')
950
+
951
+ def flush(self):
952
+ """
953
+ dump the collected structures in `self._structures` into disk as memory-mapped files.
954
+ Returns:
955
+
956
+ """
957
+ try:
958
+ self.dumper.flush()
959
+ except Exception as e:
960
+ warnings.warn(f'Failed to flush structures due to ERROR: {e}')
961
+
962
+ def close(self):
963
+ self.dumper.close()
964
+
965
+
966
+ class PygBatchUpdater:
967
+ """
968
+ batch updater for torch-geometric objects.
969
+ It can be directly called after initialization.
970
+ Examples:
971
+ ```
972
+ updater = PygBatchUpdater()
973
+ updater.initialize()
974
+ optimizer = CG(...)
975
+ optimizer.set_batch_updater(updater)
976
+ optimizer.run(...)
977
+ ```
978
+ One can use `self.initialize()` to reset this updater.
979
+ """
980
+
981
+ def __init__(self):
982
+ self.__check_old = None
983
+ self.pygData = check_module('torch_geometric.data.batch')
984
+
985
+ def initialize(self):
986
+ self.__check_old = None
987
+
988
+ def _reallocate(
989
+ self,
990
+ converge_check: th.Tensor,
991
+ func_args,
992
+ func_kwargs,
993
+ grad_func_args,
994
+ grad_func_kwargs
995
+ ):
996
+ # main
997
+ g = func_args[0]
998
+ g_list = g.index_select(converge_check)
999
+ g_new = self.pygData.Batch.from_data_list(g_list)
1000
+ self.__check_old = converge_check
1001
+ self.__g_old = g_new
1002
+ return (g_new,), func_kwargs, (g_new,), grad_func_kwargs
1003
+
1004
+ def __call__(
1005
+ self,
1006
+ converge_check: th.Tensor,
1007
+ func_args,
1008
+ func_kwargs,
1009
+ grad_func_args,
1010
+ grad_func_kwargs
1011
+ ):
1012
+ # adding a buffer
1013
+ is_new = (self.__check_old is None) or (converge_check.shape != self.__check_old.shape)
1014
+ if is_new:
1015
+ if th.all(converge_check): # if all are unconverged. usually occurred for new ones.
1016
+ self.__check_old = None
1017
+ return func_args, func_kwargs, grad_func_args, grad_func_kwargs
1018
+ else:
1019
+ self.__check_old = converge_check
1020
+ self.__g_old = func_args[0]
1021
+ return self._reallocate(converge_check, func_args, func_kwargs, grad_func_args, grad_func_kwargs)
1022
+
1023
+ elif th.all(th.eq(self.__check_old, converge_check)):
1024
+ return (self.__g_old,), func_kwargs, (self.__g_old,), grad_func_kwargs
1025
+ else:
1026
+ return self._reallocate(converge_check, func_args, func_kwargs, grad_func_args, grad_func_kwargs)