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
|
@@ -0,0 +1,282 @@
|
|
|
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
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch as th
|
|
15
|
+
from torch import nn
|
|
16
|
+
|
|
17
|
+
from BUCToolkit.BatchMD import NVE, NVT
|
|
18
|
+
from ._io import _CONFIGS, _LoggingEnd, _Model_Wrapper_pyg, _Model_Wrapper_dgl
|
|
19
|
+
from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT
|
|
20
|
+
from BUCToolkit.BatchMD._BaseMD import _BaseMD
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MolecularDynamics(_CONFIGS):
|
|
24
|
+
"""
|
|
25
|
+
Class of molecular dynamics simulation.
|
|
26
|
+
Users need to set the dataset and dataloader manually.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config_file: path to input file.
|
|
30
|
+
data_type: graph data type. 'pyg' for torch-geometric BatchData, 'dgl' for dgl DGLGraph.
|
|
31
|
+
|
|
32
|
+
Input file parameters: (Under the section `MD` in input files)
|
|
33
|
+
ENSEMBLE: Literal[NVE, NVT], the ensemble for MD.
|
|
34
|
+
THERMOSTAT: Literal[Langevin, VR, CSVR, Nose-Hoover], the thermostat type. only used for ENSEMBLE=NVT.
|
|
35
|
+
'VR' is Velocity Rescaling and 'CSVR' is Canonical Sampling Velocity Rescaling by Bussi et al. [1].
|
|
36
|
+
THERMOSTAT_CONFIG: Dict, the configs of thermostat.
|
|
37
|
+
* For 'Langevin', option key 'damping_coeff' (fs^-1) is to control the damping coefficient. Large damping_coeff lead to a strong coupling. Default: 0.01
|
|
38
|
+
* For 'CSVR', option key 'time_const' (fs) is to control the characteristic timescale. Large time_const leads to a weak coupling. Default: 10*TIME_STEP
|
|
39
|
+
TIME_STEP: float, the time step (fs) for MD. Default: 1
|
|
40
|
+
MAX_STEP: int, total time (fs) = TIME_STEP * MAX_STEP
|
|
41
|
+
T_INIT: float, initial Temperature (K). Default: 298.15
|
|
42
|
+
* For ENSEMBLE=NVE, T_INIT is only used to generate ramdom initial velocities by Boltzmann dist if V_init was not given.
|
|
43
|
+
* For ENSEMBLE=NVT, T_INIT is the target temperature of thermostat.
|
|
44
|
+
OUTPUT_COORDS_PER_STEP: int, to control the frequency of outputting atom coordinates. If verbose = 3, atom velocities would also be outputted. Default: 1
|
|
45
|
+
|
|
46
|
+
References:
|
|
47
|
+
[1] J. Chem. Phys., 2007, 126, 014101.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
config_file: str,
|
|
53
|
+
data_type: Literal['pyg', 'dgl'] = 'pyg',
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(config_file)
|
|
56
|
+
|
|
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
|
+
__ensembles = {'NVE': NVE, 'NVT': NVT}
|
|
67
|
+
if self.MD is None:
|
|
68
|
+
raise RuntimeError('Molecular Dynamics Configs was NOT Set.')
|
|
69
|
+
self.MD: Dict
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
self.MDType = __ensembles[self.MD['ENSEMBLE']] # The Main Function of MD. MD_config is its parameters.
|
|
73
|
+
except KeyError:
|
|
74
|
+
raise NotImplementedError(f'Unknown Ensemble {self.MD["ENSEMBLE"]}.')
|
|
75
|
+
self.require_grad = self.MD.get('REQUIRE_GRAD', False)
|
|
76
|
+
self.MD_config = {
|
|
77
|
+
'time_step': self.MD.get('TIME_STEP', 1),
|
|
78
|
+
'max_step': self.MD.get('MAX_STEP'),
|
|
79
|
+
'T_init': self.MD.get('T_INIT', 298.15),
|
|
80
|
+
'output_structures_per_step': self.MD.get('OUTPUT_COORDS_PER_STEP', 1),
|
|
81
|
+
'device': self.DEVICE,
|
|
82
|
+
'verbose': self.VERBOSE
|
|
83
|
+
}
|
|
84
|
+
if self.SAVE_PREDICTIONS:
|
|
85
|
+
self.MD_config['output_file'] = self.PREDICTIONS_SAVE_FILE
|
|
86
|
+
if self.MD['ENSEMBLE'] == 'NVT':
|
|
87
|
+
self.MD_config['thermostat'] = self.MD.get('THERMOSTAT', 'CSVR')
|
|
88
|
+
self.MD_config['thermostat_config'] = self.MD.get('THERMOSTAT_CONFIG', dict())
|
|
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) or (self.START == 2):
|
|
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=self.STRICT_LOAD)
|
|
103
|
+
else:
|
|
104
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
105
|
+
elif self.START == 'from_scratch' or self.START == 0:
|
|
106
|
+
self.logger.warning(
|
|
107
|
+
'WARNING: The model does not read the trained parameters from checkpoint file. I HOPE YOU KNOW WHAT YOU ARE DOING!'
|
|
108
|
+
)
|
|
109
|
+
if self.param is not None:
|
|
110
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError('Invalid START value. It should be "from_scratch" / 0 or "resume" / 1 ')
|
|
113
|
+
# model vars
|
|
114
|
+
_model = _model.to(self.DEVICE)
|
|
115
|
+
|
|
116
|
+
# preprocessing data # TODO
|
|
117
|
+
if self._data_loader is None: raise RuntimeError('Please Set the DataLoader.')
|
|
118
|
+
if not self._has_load_data: raise RuntimeError('Please Set the Data to Predict.')
|
|
119
|
+
|
|
120
|
+
# initialize
|
|
121
|
+
self.n_samp = len(self.TRAIN_DATA['data']) # sample number
|
|
122
|
+
self.n_batch = math.ceil(self.n_samp / self.BATCH_SIZE) # total batch number per epoch
|
|
123
|
+
|
|
124
|
+
mole_dynam = None
|
|
125
|
+
try:
|
|
126
|
+
# I/O
|
|
127
|
+
if self.VERBOSE > 0:
|
|
128
|
+
self.logout_task_information(_model, 'MD', self.MD_config, self.n_samp)
|
|
129
|
+
|
|
130
|
+
time_tol = time.perf_counter()
|
|
131
|
+
_model.eval()
|
|
132
|
+
# MAIN LOOP
|
|
133
|
+
# define the model wrapper & batch size getter & cell vector getter for different data type
|
|
134
|
+
if self.data_type == 'pyg':
|
|
135
|
+
model_wrap = _Model_Wrapper_pyg(_model)
|
|
136
|
+
def get_batch_size(data):
|
|
137
|
+
return len(data)
|
|
138
|
+
|
|
139
|
+
def get_cell_vec(data):
|
|
140
|
+
return data.cell.numpy(force=True)
|
|
141
|
+
|
|
142
|
+
def get_atomic_number(data):
|
|
143
|
+
return data.atomic_numbers.unsqueeze(0).tolist()
|
|
144
|
+
|
|
145
|
+
def get_fixed_mask(data):
|
|
146
|
+
mask = getattr(data, 'fixed', None)
|
|
147
|
+
if mask is not None:
|
|
148
|
+
mask = mask.unsqueeze(0)
|
|
149
|
+
return mask
|
|
150
|
+
|
|
151
|
+
def get_batch_indx(data):
|
|
152
|
+
return [len(dat.pos) for dat in data.to_data_list()]
|
|
153
|
+
|
|
154
|
+
def get_init_veloc(data):
|
|
155
|
+
veloc = getattr(data, 'velocity', None)
|
|
156
|
+
if veloc is not None:
|
|
157
|
+
veloc = veloc.unsqueeze(0)
|
|
158
|
+
return veloc
|
|
159
|
+
|
|
160
|
+
def get_indx(data):
|
|
161
|
+
_indx: Dict = getattr(data, 'idx', None)
|
|
162
|
+
return _indx
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
model_wrap = _Model_Wrapper_dgl(_model)
|
|
166
|
+
def get_batch_size(data):
|
|
167
|
+
return data.num_nodes('atom')
|
|
168
|
+
|
|
169
|
+
def get_cell_vec(data):
|
|
170
|
+
return data.nodes['cell'].data['cell'].numpy(force=True)
|
|
171
|
+
|
|
172
|
+
def get_atomic_number(data):
|
|
173
|
+
return data.nodes['atom'].data['Z'].unsqueeze(0).tolist()
|
|
174
|
+
|
|
175
|
+
def get_fixed_mask(data):
|
|
176
|
+
mask = data.nodes['atom'].data.get('fix', None)
|
|
177
|
+
if mask is not None:
|
|
178
|
+
mask = mask.unsqueeze(0)
|
|
179
|
+
return mask
|
|
180
|
+
|
|
181
|
+
def get_batch_indx(data):
|
|
182
|
+
return data.batch_num_nodes('atom')
|
|
183
|
+
|
|
184
|
+
def get_init_veloc(data):
|
|
185
|
+
veloc = data.nodes['atom'].data.get('velocity', None)
|
|
186
|
+
if veloc is not None:
|
|
187
|
+
veloc = veloc.unsqueeze(0)
|
|
188
|
+
return veloc
|
|
189
|
+
|
|
190
|
+
def get_indx(data):
|
|
191
|
+
_indx: Dict = data.nodes['atom'].data
|
|
192
|
+
return _indx.get('idx', None)
|
|
193
|
+
|
|
194
|
+
mole_dynam = self.MDType(**self.MD_config)
|
|
195
|
+
#if self.REDIRECT:
|
|
196
|
+
# _file_handler = os.path.join(self.OUTPUT_PATH, f'{time.strftime("%Y%m%d_%H_%M_%S")}_{self.OUTPUT_POSTFIX}.out')
|
|
197
|
+
# mole_dynam.reset_logger_handler(_file_handler)
|
|
198
|
+
if self.SAVE_PREDICTIONS:
|
|
199
|
+
mole_dynam._HOLD_DUMPER = True
|
|
200
|
+
# change dumping mode to 'a' to contiguously dumper within the whole for-loop
|
|
201
|
+
mole_dynam.dumper.reset_args(self.PREDICTIONS_SAVE_FILE, mode='a', cache_size=4096, )
|
|
202
|
+
val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, is_train=False, **self._data_loader_configs)
|
|
203
|
+
n_c = 1 # running batch now
|
|
204
|
+
n_s = 0 # number of calculated samples. each sample in batches in each for-loop += 1.
|
|
205
|
+
for val_data, val_label in val_set:
|
|
206
|
+
try:
|
|
207
|
+
# to avoid get an empty batch
|
|
208
|
+
if get_batch_size(val_data) <= 0:
|
|
209
|
+
if self.VERBOSE: self.logger.info(f'An empty batch occurred. Skipped.')
|
|
210
|
+
continue
|
|
211
|
+
# MD
|
|
212
|
+
# cells & fixations
|
|
213
|
+
_cell = get_cell_vec(val_data)
|
|
214
|
+
fixed_mask = get_fixed_mask(val_data)
|
|
215
|
+
# get batch
|
|
216
|
+
batch_indx = get_batch_indx(val_data)
|
|
217
|
+
# get id
|
|
218
|
+
idx = get_indx(val_data)
|
|
219
|
+
idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
|
|
220
|
+
n_s += len(batch_indx)
|
|
221
|
+
element_tensor = get_atomic_number(val_data)
|
|
222
|
+
if self.VERBOSE > 0:
|
|
223
|
+
self.logger.info('*' * 89)
|
|
224
|
+
self.logger.info(f'Running Batch {n_c}.')
|
|
225
|
+
cell_str = np.array2string(
|
|
226
|
+
_cell, **FLOAT_ARRAY_FORMAT
|
|
227
|
+
).replace("[", " ").replace("]", " ")
|
|
228
|
+
self.logger.info(f'Structure names: {idx}\n')
|
|
229
|
+
self.logger.info(f'Cell Vectors:\n{cell_str}\n')
|
|
230
|
+
# print structures titles with elements
|
|
231
|
+
self.logout_element_information(element_tensor, batch_indx)
|
|
232
|
+
|
|
233
|
+
# initial atom coordinates
|
|
234
|
+
if self.data_type == 'pyg':
|
|
235
|
+
X_init = val_data.pos.unsqueeze(0)
|
|
236
|
+
else:
|
|
237
|
+
X_init = val_data.nodes['atom'].data['pos'].unsqueeze(0)
|
|
238
|
+
|
|
239
|
+
mv2cent_freq = int(self.MD.get('MOVE_TO_CENTER_FREQ', -1))
|
|
240
|
+
mole_dynam.run(
|
|
241
|
+
model_wrap.Energy,
|
|
242
|
+
X_init,
|
|
243
|
+
Element_list=get_atomic_number(val_data),
|
|
244
|
+
Cell_vector=_cell,
|
|
245
|
+
V_init=get_init_veloc(val_data), # user-defined initial velocities.
|
|
246
|
+
grad_func=model_wrap.Grad,
|
|
247
|
+
func_args=(val_data,),
|
|
248
|
+
grad_func_args=(val_data,),
|
|
249
|
+
is_grad_func_contain_y=False,
|
|
250
|
+
require_grad=self.require_grad,
|
|
251
|
+
batch_indices=batch_indx,
|
|
252
|
+
fixed_atom_tensor=fixed_mask, # The Selective Dynamics.
|
|
253
|
+
move_to_center_freq=mv2cent_freq
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Print info
|
|
257
|
+
if self.VERBOSE > 0:
|
|
258
|
+
self.logger.info(f'Batch {n_c} done.')
|
|
259
|
+
n_c += 1
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self.logger.warning(f'WARNING: An error occurred in {n_c}th batch. Error: {e}.')
|
|
263
|
+
if self.VERBOSE > 0:
|
|
264
|
+
excp = traceback.format_exc()
|
|
265
|
+
self.logger.warning(f"Traceback:\n{excp}")
|
|
266
|
+
n_c += 1
|
|
267
|
+
|
|
268
|
+
if self.VERBOSE: self.logger.info(f'Molecular Dynamics Done. Total Time: {time.perf_counter() - time_tol:<.4f}')
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
th.cuda.synchronize()
|
|
272
|
+
excp = traceback.format_exc()
|
|
273
|
+
self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:\n{excp}')
|
|
274
|
+
|
|
275
|
+
finally:
|
|
276
|
+
th.cuda.synchronize()
|
|
277
|
+
if mole_dynam is not None:
|
|
278
|
+
mole_dynam.dumper.close()
|
|
279
|
+
self.logger.removeHandler(self.log_handler)
|
|
280
|
+
if isinstance(self.log_handler, logging.FileHandler):
|
|
281
|
+
self.log_handler.close()
|
|
282
|
+
pass
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch as th
|
|
15
|
+
from torch import nn
|
|
16
|
+
|
|
17
|
+
from BUCToolkit.BatchMC import MMC
|
|
18
|
+
from ._io import _CONFIGS, _LoggingEnd, _Model_Wrapper_pyg, _Model_Wrapper_dgl
|
|
19
|
+
from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MonteCarlo(_CONFIGS):
|
|
23
|
+
"""
|
|
24
|
+
Class of Monte Carlo algorithms, including NVT g Monte Carlo & simulated annealing (temperature varied MC).
|
|
25
|
+
Users need to set the dataset and dataloader manually.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config_file: path to input file.
|
|
29
|
+
data_type: graph data type. 'pyg' for torch-geometric BatchData, 'dgl' for dgl DGLGraph.
|
|
30
|
+
|
|
31
|
+
Input file parameters: (Under the section `MC` in input files.)
|
|
32
|
+
ITER_SCHEME: Literal['Gaussian', 'Cauchy', 'Uniform'], the scheme of atoms moving. Default: Gaussian.
|
|
33
|
+
COORDINATE_UPDATE_PARAM: float, the scale parameter for coordinates update. variation for Gaussian/range for Uniform/scale for Cauchy.
|
|
34
|
+
MAXITER: int, the maximum iteration numbers. Default: 300.
|
|
35
|
+
TEMPERATURE_INIT: initial temperature.
|
|
36
|
+
TEMPERATURE_SCHEME: Literal['constant', 'linear', 'exponential', 'log', 'fast'], temperature update scheme.
|
|
37
|
+
* `constant` for a fixed temperature during the whole simulation.
|
|
38
|
+
* `linear` for a linearly changed temperature from `TEMPERATURE_INIT` to `TEMPERATURE_SCHEME_PARAM` during MAXITER steps.
|
|
39
|
+
* `exponential` for temperature (T) changing by T^(i + 1) = `TEMPERATURE_SCHEME_PARAM` * T^(i).
|
|
40
|
+
* `log` for temperature (T) changing by T^(i) = `TEMPERATURE_INIT`/(1. + log(1. + `TEMPERATURE_SCHEME_PARAM` * i)), `i` is the step number.
|
|
41
|
+
* `fast` for temperature (T) changing by T^(i) = `TEMPERATURE_INIT`/(1. + `TEMPERATURE_SCHEME_PARAM` * i), `i` is the step number.
|
|
42
|
+
TEMPERATURE_SCHEME_PARAM: float, to control the temperature scheme. See args `TEMPERATURE_SCHEME`.
|
|
43
|
+
TEMPERATURE_UPDATE_FREQ: update temperature per `TEMPERATURE_UPDATE_FREQ` step.
|
|
44
|
+
OUTPUT_COORDS_PER_STEP: int, to control the frequency of outputting atom coordinates. If verbose = 3, atom velocities would also be outputted. Default: 1
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config_file: str,
|
|
51
|
+
data_type: Literal['pyg', 'dgl'] = 'pyg',
|
|
52
|
+
) -> None:
|
|
53
|
+
super().__init__(config_file)
|
|
54
|
+
|
|
55
|
+
self.config_file = config_file
|
|
56
|
+
assert data_type in {'pyg', 'dgl'}, f'Invalid data type {data_type}. It must be "pyg" or "dgl".'
|
|
57
|
+
self.data_type = data_type
|
|
58
|
+
self.reload_config(config_file)
|
|
59
|
+
if self.VERBOSE: self.logger.info('Config File Was Successfully Read.')
|
|
60
|
+
self.param = None
|
|
61
|
+
self._has_load_data = False
|
|
62
|
+
self._data_loader = None
|
|
63
|
+
|
|
64
|
+
__ensembles = {'METROPOLIS': MMC, }
|
|
65
|
+
if self.MC is None:
|
|
66
|
+
raise RuntimeError('Monte Carlo Configs was NOT Set.')
|
|
67
|
+
self.MC: Dict
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
self.MCType = __ensembles[self.MC['TYPE'].upper()]
|
|
71
|
+
except KeyError:
|
|
72
|
+
raise NotImplementedError(f'Type {self.MC['TYPE']} is not supported yet.')
|
|
73
|
+
self.MC_config = {
|
|
74
|
+
'maxiter': int(self.MC.get('MAXITER', 10000)),
|
|
75
|
+
'output_structures_per_step': int(self.MC.get('OUTPUT_COORDS_PER_STEP', 1)),
|
|
76
|
+
'device': self.DEVICE,
|
|
77
|
+
'verbose': self.VERBOSE
|
|
78
|
+
}
|
|
79
|
+
if self.REDIRECT:
|
|
80
|
+
self.MC_config['output_file'] = self.PREDICTIONS_SAVE_FILE
|
|
81
|
+
if self.MC['TYPE'].upper() == 'METROPOLIS':
|
|
82
|
+
self.MC_config.update(
|
|
83
|
+
{
|
|
84
|
+
'iter_scheme': str(self.MC.get('ITER_SCHEME', 'Gaussian')),
|
|
85
|
+
'temperature_init': float(self.MC.get('T_INIT', 298.15)),
|
|
86
|
+
'temperature_scheme': str(self.MC.get('T_SCHEME', 'constant')),
|
|
87
|
+
'temperature_update_freq': int(self.MC.get('T_UPDATE_FREQ', 1)),
|
|
88
|
+
'temperature_scheme_param': float(self.MC.get('T_SCHEME_PARAM', None)),
|
|
89
|
+
'coordinate_update_param': float(self.MC.get('COORDINATE_UPDATE_PARAM', 0.2)),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def run(self, model):
|
|
94
|
+
"""
|
|
95
|
+
Parameters:
|
|
96
|
+
model: the input model which is non-instantiated nn.Module class.
|
|
97
|
+
"""
|
|
98
|
+
# check logger
|
|
99
|
+
if not self.logger.hasHandlers(): self.logger.addHandler(self.log_handler)
|
|
100
|
+
# check vars
|
|
101
|
+
_model: nn.Module = model(**self.MODEL_CONFIG)
|
|
102
|
+
if (self.START == 'resume') or (self.START == 1) or (self.START == 2):
|
|
103
|
+
chk_data = th.load(self.LOAD_CHK_FILE_PATH, weights_only=True)
|
|
104
|
+
if self.param is None:
|
|
105
|
+
_model.load_state_dict(chk_data['model_state_dict'], strict=self.STRICT_LOAD)
|
|
106
|
+
else:
|
|
107
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
108
|
+
elif self.START == 'from_scratch' or self.START == 0:
|
|
109
|
+
self.logger.warning(
|
|
110
|
+
'WARNING: The model does not read the trained parameters from checkpoint file. I HOPE YOU KNOW WHAT YOU ARE DOING!'
|
|
111
|
+
)
|
|
112
|
+
if self.param is not None:
|
|
113
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError('Invalid START value. It should be "from_scratch" / 0 or "resume" / 1 ')
|
|
116
|
+
# model vars
|
|
117
|
+
_model = _model.to(self.DEVICE)
|
|
118
|
+
|
|
119
|
+
# preprocessing data # TODO
|
|
120
|
+
if self._data_loader is None: raise RuntimeError('Please Set the DataLoader.')
|
|
121
|
+
if not self._has_load_data: raise RuntimeError('Please Set the Data to Predict.')
|
|
122
|
+
|
|
123
|
+
# initialize
|
|
124
|
+
self.n_samp = len(self.TRAIN_DATA['data']) # sample number
|
|
125
|
+
self.n_batch = math.ceil(self.n_samp / self.BATCH_SIZE) # total batch number per epoch
|
|
126
|
+
|
|
127
|
+
mont_carlo = None
|
|
128
|
+
try:
|
|
129
|
+
# I/O
|
|
130
|
+
if self.VERBOSE > 0:
|
|
131
|
+
self.logout_task_information(_model, 'MC', self.MC_config, self.n_samp)
|
|
132
|
+
|
|
133
|
+
time_tol = time.perf_counter()
|
|
134
|
+
_model.eval()
|
|
135
|
+
# MAIN LOOP
|
|
136
|
+
# define the model wrapper & batch size getter & cell vector getter for different data type
|
|
137
|
+
if self.data_type == 'pyg':
|
|
138
|
+
model_wrap = _Model_Wrapper_pyg(_model)
|
|
139
|
+
def get_batch_size(data):
|
|
140
|
+
return len(data)
|
|
141
|
+
|
|
142
|
+
def get_cell_vec(data):
|
|
143
|
+
return data.cell.numpy(force=True)
|
|
144
|
+
|
|
145
|
+
def get_atomic_number(data):
|
|
146
|
+
return data.atomic_numbers.unsqueeze(0).tolist()
|
|
147
|
+
|
|
148
|
+
def get_fixed_mask(data):
|
|
149
|
+
mask = getattr(data, 'fixed', None)
|
|
150
|
+
if mask is not None:
|
|
151
|
+
mask = mask.unsqueeze(0)
|
|
152
|
+
return mask
|
|
153
|
+
|
|
154
|
+
def get_batch_indx(data):
|
|
155
|
+
return [len(dat.pos) for dat in data.to_data_list()]
|
|
156
|
+
|
|
157
|
+
def get_init_veloc(data):
|
|
158
|
+
veloc = getattr(data, 'velocity', None)
|
|
159
|
+
if veloc is not None:
|
|
160
|
+
veloc = veloc.unsqueeze(0)
|
|
161
|
+
return veloc
|
|
162
|
+
|
|
163
|
+
def get_indx(data):
|
|
164
|
+
_indx: Dict = getattr(data, 'idx', None)
|
|
165
|
+
return _indx
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
model_wrap = _Model_Wrapper_dgl(_model)
|
|
169
|
+
def get_batch_size(data):
|
|
170
|
+
return data.num_nodes('atom')
|
|
171
|
+
|
|
172
|
+
def get_cell_vec(data):
|
|
173
|
+
return data.nodes['cell'].data['cell'].numpy(force=True)
|
|
174
|
+
|
|
175
|
+
def get_atomic_number(data):
|
|
176
|
+
return data.nodes['atom'].data['Z'].unsqueeze(0).tolist()
|
|
177
|
+
|
|
178
|
+
def get_fixed_mask(data):
|
|
179
|
+
mask = data.nodes['atom'].data.get('fix', None)
|
|
180
|
+
if mask is not None:
|
|
181
|
+
mask = mask.unsqueeze(0)
|
|
182
|
+
return mask
|
|
183
|
+
|
|
184
|
+
def get_batch_indx(data):
|
|
185
|
+
return data.batch_num_nodes('atom')
|
|
186
|
+
|
|
187
|
+
def get_init_veloc(data):
|
|
188
|
+
veloc = data.nodes['atom'].data.get('velocity', None)
|
|
189
|
+
if veloc is not None:
|
|
190
|
+
veloc = veloc.unsqueeze(0)
|
|
191
|
+
return veloc
|
|
192
|
+
|
|
193
|
+
def get_indx(data):
|
|
194
|
+
_indx: Dict = data.nodes['atom'].data
|
|
195
|
+
return _indx.get('idx', None)
|
|
196
|
+
|
|
197
|
+
mont_carlo = self.MCType(**self.MC_config)
|
|
198
|
+
#if self.REDIRECT:
|
|
199
|
+
# _file_handler = os.path.join(self.OUTPUT_PATH, f'{time.strftime("%Y%m%d_%H_%M_%S")}_{self.OUTPUT_POSTFIX}.out')
|
|
200
|
+
# mont_carlo.reset_logger_handler(_file_handler)
|
|
201
|
+
if self.SAVE_PREDICTIONS:
|
|
202
|
+
mont_carlo._HOLD_DUMPER = True
|
|
203
|
+
# change dumping mode to 'a' to contiguously dumper within the whole for-loop
|
|
204
|
+
mont_carlo.dumper.reset_args(self.PREDICTIONS_SAVE_FILE, mode='a', cache_size=4096, )
|
|
205
|
+
val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, is_train=False, **self._data_loader_configs)
|
|
206
|
+
n_c = 1 # running batch now
|
|
207
|
+
n_s = 0 # number of calculated samples. each sample in batches in each for-loop += 1.
|
|
208
|
+
for val_data, val_label in val_set:
|
|
209
|
+
try:
|
|
210
|
+
# to avoid get an empty batch
|
|
211
|
+
if get_batch_size(val_data) <= 0:
|
|
212
|
+
if self.VERBOSE: self.logger.info(f'An empty batch occurred. Skipped.')
|
|
213
|
+
continue
|
|
214
|
+
# MCc
|
|
215
|
+
# cells & fixations
|
|
216
|
+
_cell = get_cell_vec(val_data)
|
|
217
|
+
fixed_mask = get_fixed_mask(val_data)
|
|
218
|
+
# get batch
|
|
219
|
+
batch_indx = get_batch_indx(val_data)
|
|
220
|
+
# get id
|
|
221
|
+
idx = get_indx(val_data)
|
|
222
|
+
idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
|
|
223
|
+
n_s += len(batch_indx)
|
|
224
|
+
element_tensor = get_atomic_number(val_data)
|
|
225
|
+
if self.VERBOSE > 0:
|
|
226
|
+
self.logger.info('*' * 89)
|
|
227
|
+
self.logger.info(f'Running Batch {n_c}.')
|
|
228
|
+
cell_str = np.array2string(
|
|
229
|
+
_cell, **FLOAT_ARRAY_FORMAT
|
|
230
|
+
).replace("[", " ").replace("]", " ")
|
|
231
|
+
self.logger.info(f'Structure names: {idx}\n')
|
|
232
|
+
self.logger.info(f'Cell Vectors:\n{cell_str}\n')
|
|
233
|
+
# print structures titles with elements
|
|
234
|
+
self.logout_element_information(element_tensor, batch_indx)
|
|
235
|
+
|
|
236
|
+
# initial atom coordinates
|
|
237
|
+
if self.data_type == 'pyg':
|
|
238
|
+
X_init = val_data.pos.unsqueeze(0)
|
|
239
|
+
else:
|
|
240
|
+
X_init = val_data.nodes['atom'].data['pos'].unsqueeze(0)
|
|
241
|
+
|
|
242
|
+
mv2cent_freq = int(self.MC.get('MOVE_TO_CENTER_FREQ', -1))
|
|
243
|
+
mont_carlo.run(
|
|
244
|
+
model_wrap.Energy,
|
|
245
|
+
X_init,
|
|
246
|
+
Element_list=get_atomic_number(val_data),
|
|
247
|
+
Cell_vector=_cell,
|
|
248
|
+
func_args=(val_data,),
|
|
249
|
+
fixed_atom_tensor=fixed_mask,
|
|
250
|
+
batch_indices=batch_indx,
|
|
251
|
+
move_to_center_freq=mv2cent_freq
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Print info
|
|
255
|
+
if self.VERBOSE > 0:
|
|
256
|
+
self.logger.info(f'Batch {n_c} done.')
|
|
257
|
+
n_c += 1
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.logger.warning(f'WARNING: An error occurred in {n_c}th batch. Error: {e}.')
|
|
261
|
+
if self.VERBOSE > 0:
|
|
262
|
+
excp = traceback.format_exc()
|
|
263
|
+
self.logger.warning(f"Traceback:\n{excp}")
|
|
264
|
+
n_c += 1
|
|
265
|
+
|
|
266
|
+
if self.VERBOSE: self.logger.info(f'Monte Carlo Simulation Done. Total Time: {time.perf_counter() - time_tol:<.4f}')
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
th.cuda.synchronize()
|
|
270
|
+
excp = traceback.format_exc()
|
|
271
|
+
self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:\n{excp}')
|
|
272
|
+
|
|
273
|
+
finally:
|
|
274
|
+
th.cuda.synchronize()
|
|
275
|
+
if mont_carlo is not None:
|
|
276
|
+
mont_carlo.dumper.close()
|
|
277
|
+
self.logger.removeHandler(self.log_handler)
|
|
278
|
+
if isinstance(self.log_handler, logging.FileHandler):
|
|
279
|
+
self.log_handler.close()
|
|
280
|
+
pass
|