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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- 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)
|