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/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
|
+
|