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,347 @@
|
|
|
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.BatchMD.constrained_md import ConstrNVT, ConstrNVE
|
|
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.BatchGenerate.coords_interp import linear_interpolation_tens
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConstrainedMolecularDynamics(_CONFIGS):
|
|
25
|
+
"""
|
|
26
|
+
Class of constrained molecular dynamics simulation.
|
|
27
|
+
Users need to set the dataset, dataloader, and constraint functions 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
|
+
ENSEMBLE: Literal[NVE, NVT], the ensemble for MD.
|
|
35
|
+
CONSTR_MD_SCHEME: Literal[BLUE_MOON, SLOW_GROWTH], the scheme of constrained MD.
|
|
36
|
+
NIMAGE: int, the number of images in BLUE_MOON MD. SLOW_GROWTH will ignore it.
|
|
37
|
+
THERMOSTAT: Literal[Langevin, VR, CSVR, Nose-Hoover], the thermostat type. only used for ENSEMBLE=NVT.
|
|
38
|
+
'VR' is Velocity Rescaling and 'CSVR' is Canonical Sampling Velocity Rescaling by Bussi et al. [1].
|
|
39
|
+
THERMOSTAT_CONFIG: Dict, the configs of thermostat.
|
|
40
|
+
* 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
|
|
41
|
+
* 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
|
|
42
|
+
TIME_STEP: float, the time step (fs) for MD. Default: 1
|
|
43
|
+
MAX_STEP: int, total time (fs) = TIME_STEP * MAX_STEP
|
|
44
|
+
REQUIRE_GRAD: bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs). Default: False.
|
|
45
|
+
T_INIT: float, initial Temperature (K). Default: 298.15
|
|
46
|
+
* For ENSEMBLE=NVE, T_INIT is only used to generate ramdom initial velocities by Boltzmann dist if V_init was not given.
|
|
47
|
+
* For ENSEMBLE=NVT, T_INIT is the target temperature of thermostat.
|
|
48
|
+
OUTPUT_COORDS_PER_STEP: int, to control the frequency of outputting atom coordinates. If verbose = 3, atom velocities would also be outputted. Default: 1
|
|
49
|
+
|
|
50
|
+
References:
|
|
51
|
+
[1] J. Chem. Phys., 2007, 126, 014101.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
config_file: str,
|
|
57
|
+
data_type: Literal['pyg', 'dgl'] = 'pyg',
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(config_file)
|
|
60
|
+
|
|
61
|
+
self.config_file = config_file
|
|
62
|
+
assert data_type in {'pyg', 'dgl'}, f'Invalid data type {data_type}. It must be "pyg" or "dgl".'
|
|
63
|
+
self.data_type = data_type
|
|
64
|
+
self.reload_config(config_file)
|
|
65
|
+
if self.VERBOSE: self.logger.info('Config File Was Successfully Read.')
|
|
66
|
+
self.param = None
|
|
67
|
+
self._has_load_data = False
|
|
68
|
+
self._data_loader = None
|
|
69
|
+
self.constr_func = None
|
|
70
|
+
self.constr_val = None
|
|
71
|
+
|
|
72
|
+
__ensembles = {'NVE': ConstrNVE, 'NVT': ConstrNVT}
|
|
73
|
+
if self.MD is None:
|
|
74
|
+
raise RuntimeError('Molecular Dynamics Configs was NOT Set.')
|
|
75
|
+
self.MD: Dict
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
self.MDType = __ensembles[self.MD['ENSEMBLE']] # The Main Function of MD. MD_config is its parameters.
|
|
79
|
+
except KeyError:
|
|
80
|
+
raise NotImplementedError(f'Unknown Ensemble {self.MD["ENSEMBLE"]}.')
|
|
81
|
+
self.require_grad = self.MD.get('REQUIRE_GRAD', False)
|
|
82
|
+
# check modes
|
|
83
|
+
self.CMD_MODE = self.MD['CONSTR_MD_SCHEME']
|
|
84
|
+
if self.CMD_MODE == 'BLUE_MOON':
|
|
85
|
+
# interp images read & check
|
|
86
|
+
self.NIMAGE = self.MD['NIMAGE']
|
|
87
|
+
if self.NIMAGE <= 0:
|
|
88
|
+
raise RuntimeError(f'Images of configurations must be greater than 0, but got {self.NIMAGE}.')
|
|
89
|
+
elif self.CMD_MODE == 'SLOW_GROWTH':
|
|
90
|
+
pass
|
|
91
|
+
else:
|
|
92
|
+
raise NotImplementedError(f'Unknown Constrained MD Scheme {self.CMD_MODE}.')
|
|
93
|
+
|
|
94
|
+
self.MD_config = {'time_step': self.MD.get('TIME_STEP', 1),
|
|
95
|
+
'max_step': self.MD.get('MAX_STEP'),
|
|
96
|
+
'T_init': self.MD.get('T_INIT', 298.15),
|
|
97
|
+
'output_structures_per_step': self.MD.get('OUTPUT_COORDS_PER_STEP', 1),
|
|
98
|
+
'device': self.DEVICE,
|
|
99
|
+
'verbose': self.VERBOSE}
|
|
100
|
+
if self.REDIRECT:
|
|
101
|
+
self.MD_config['output_file'] = os.path.join(self.OUTPUT_PATH, f'{time.strftime("%Y%m%d_%H_%M_%S")}_{self.OUTPUT_POSTFIX}.out')
|
|
102
|
+
if self.MD['ENSEMBLE'] == 'NVT':
|
|
103
|
+
self.MD_config['thermostat'] = self.MD.get('THERMOSTAT', 'CSVR')
|
|
104
|
+
self.MD_config['thermostat_config'] = self.MD.get('THERMOSTAT_CONFIG', dict())
|
|
105
|
+
|
|
106
|
+
def set_constr_func(self, constr_func: Callable) -> None:
|
|
107
|
+
self.constr_func = constr_func
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def set_constr_val(self, constr_val: Callable[[th.Tensor], th.Tensor|Tuple[th.Tensor]] | th.Tensor) -> None:
|
|
111
|
+
self.constr_val = constr_val
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def run(self, model):
|
|
115
|
+
"""
|
|
116
|
+
Parameters:
|
|
117
|
+
model: the input model which is non-instantiated nn.Module class.
|
|
118
|
+
"""
|
|
119
|
+
# check logger
|
|
120
|
+
if not self.logger.hasHandlers(): self.logger.addHandler(self.log_handler)
|
|
121
|
+
# check vars
|
|
122
|
+
_model: nn.Module = model(**self.MODEL_CONFIG)
|
|
123
|
+
if self.START == 'resume' or self.START == 1:
|
|
124
|
+
chk_data = th.load(self.LOAD_CHK_FILE_PATH)
|
|
125
|
+
if self.param is None:
|
|
126
|
+
_model.load_state_dict(chk_data['model_state_dict'], strict=False)
|
|
127
|
+
else:
|
|
128
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
129
|
+
epoch_now = chk_data['epoch']
|
|
130
|
+
elif self.START == 'from_scratch' or self.START == 0:
|
|
131
|
+
self.logger.warning(
|
|
132
|
+
'WARNING: The model was not read the trained parameters from checkpoint file. I HOPE YOU KNOW WHAT YOU ARE DOING!'
|
|
133
|
+
)
|
|
134
|
+
epoch_now = 0
|
|
135
|
+
if self.param is not None:
|
|
136
|
+
_model.load_state_dict(self.param, self.is_strict, self.is_assign)
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError('Invalid START value. It should be "from_scratch" / 0 or "resume" / 1 ')
|
|
139
|
+
# model vars
|
|
140
|
+
_model = _model.to(self.DEVICE)
|
|
141
|
+
|
|
142
|
+
# preprocessing data # TODO
|
|
143
|
+
if self._data_loader is None: raise RuntimeError('Please Set the DataLoader.')
|
|
144
|
+
if not self._has_load_data: raise RuntimeError('Please Set the Data to Predict.')
|
|
145
|
+
#if self.constr_val is None: raise RuntimeError('Please Set the Constraint Values `constr_val`.')
|
|
146
|
+
if self.constr_func is None: raise RuntimeError('Please Set the Constraint Function `constr_func`.')
|
|
147
|
+
self.MD_config['constr_func'] = self.constr_func
|
|
148
|
+
self.MD_config['constr_val'] = self.constr_val
|
|
149
|
+
|
|
150
|
+
# initialize
|
|
151
|
+
self.n_samp = len(self.TRAIN_DATA['dataIS']) # sample number
|
|
152
|
+
self.n_batch = math.ceil(self.n_samp / self.BATCH_SIZE) # total batch number per epoch
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# PRINT TASK INFO
|
|
156
|
+
if self.VERBOSE > 0:
|
|
157
|
+
self.logout_task_information(_model, 'CMD', self.MD_config, self.n_samp)
|
|
158
|
+
|
|
159
|
+
time_tol = time.perf_counter()
|
|
160
|
+
_model.eval()
|
|
161
|
+
# MAIN LOOP
|
|
162
|
+
# define the model wrapper & batch size getter & cell vector getter for different data type
|
|
163
|
+
if self.data_type == 'pyg':
|
|
164
|
+
_pyg = check_module('torch_geometric.data')
|
|
165
|
+
if _pyg is not None:
|
|
166
|
+
import torch_geometric.data as _pyg
|
|
167
|
+
self.pygBatch = _pyg.Batch
|
|
168
|
+
else:
|
|
169
|
+
ImportError('The method is unavailable because the `torch-geometric` cannot be imported.')
|
|
170
|
+
|
|
171
|
+
model_wrap = _Model_Wrapper_pyg(_model)
|
|
172
|
+
def get_batch_size(data):
|
|
173
|
+
return len(data)
|
|
174
|
+
|
|
175
|
+
def get_cell_vec(data):
|
|
176
|
+
return data.cell.numpy(force=True)
|
|
177
|
+
|
|
178
|
+
def get_atomic_number(data):
|
|
179
|
+
return data.atomic_numbers.tolist() # note: here is different from MD/Relaxations that NOT unsqueeze(0)
|
|
180
|
+
|
|
181
|
+
def get_indx(data):
|
|
182
|
+
_indx: Dict = getattr(data, 'idx', None)
|
|
183
|
+
return _indx
|
|
184
|
+
|
|
185
|
+
def get_fixed_mask(data):
|
|
186
|
+
mask = getattr(data, 'fixed', None)
|
|
187
|
+
if mask is not None:
|
|
188
|
+
mask = mask.unsqueeze(0)
|
|
189
|
+
return mask
|
|
190
|
+
|
|
191
|
+
def get_batch_indx(data):
|
|
192
|
+
return [len(dat.pos) for dat in data.to_data_list()]
|
|
193
|
+
|
|
194
|
+
def get_init_veloc(data):
|
|
195
|
+
veloc = getattr(data, 'velocity', None)
|
|
196
|
+
if veloc is not None:
|
|
197
|
+
veloc = veloc.unsqueeze(0)
|
|
198
|
+
return veloc
|
|
199
|
+
|
|
200
|
+
def rebatched_graph(single_graph, X):
|
|
201
|
+
""" expand batches """
|
|
202
|
+
X_in = X.flatten(0, 1) # convert X: (n_batch, n_atom, n_dim) into X': (n_batch * n_atom, 3)
|
|
203
|
+
batch_size = X.size(0)
|
|
204
|
+
graph = self.pygBatch.from_data_list([single_graph] * batch_size)
|
|
205
|
+
graph.pos = X_in
|
|
206
|
+
return graph
|
|
207
|
+
|
|
208
|
+
else:
|
|
209
|
+
model_wrap = _Model_Wrapper_dgl(_model)
|
|
210
|
+
raise NotImplementedError # TODO <<<<
|
|
211
|
+
def get_batch_size(data):
|
|
212
|
+
return data.num_nodes('atom')
|
|
213
|
+
|
|
214
|
+
def get_cell_vec(data):
|
|
215
|
+
return data.nodes['cell'].data['cell'].numpy(force=True)
|
|
216
|
+
|
|
217
|
+
def get_fixed_mask(data):
|
|
218
|
+
mask = data.nodes['atom'].data.get('fix', None)
|
|
219
|
+
if mask is not None:
|
|
220
|
+
mask = mask.unsqueeze(0)
|
|
221
|
+
return mask
|
|
222
|
+
|
|
223
|
+
def get_batch_indx(data):
|
|
224
|
+
return val_data.batch_num_nodes('atom')
|
|
225
|
+
|
|
226
|
+
def get_init_veloc(data):
|
|
227
|
+
veloc = data.nodes['atom'].data.get('velocity', None)
|
|
228
|
+
if veloc is not None:
|
|
229
|
+
veloc = veloc.unsqueeze(0)
|
|
230
|
+
return veloc
|
|
231
|
+
|
|
232
|
+
def get_indx(data):
|
|
233
|
+
_indx: Dict = data.nodes['atom'].data
|
|
234
|
+
return _indx.get('idx', None)
|
|
235
|
+
|
|
236
|
+
def get_atomic_number(data):
|
|
237
|
+
return data.nodes['atom'].data['Z'].unsqueeze(0).tolist()
|
|
238
|
+
|
|
239
|
+
mole_dynam = self.MDType(**self.MD_config)
|
|
240
|
+
val_set: Any = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, **self._data_loader_configs)
|
|
241
|
+
if getattr(val_set, '_LOADER_TYPE', None) != 'ISFS':
|
|
242
|
+
__err_msg = f'Data loader of ConstrainedMolecularDynamics requires ISFS, but got {getattr(val_set, '_LOADER_TYPE', None)}'
|
|
243
|
+
if self.VERBOSE: self.logger.error(__err_msg)
|
|
244
|
+
raise ValueError(__err_msg)
|
|
245
|
+
n_c = 1 # running batch now
|
|
246
|
+
n_s = 0
|
|
247
|
+
for dataIS, dataFS in val_set:
|
|
248
|
+
try:
|
|
249
|
+
# Check Batch
|
|
250
|
+
if get_batch_size(dataIS) != get_batch_size(dataFS):
|
|
251
|
+
__err_msg = f'The batch size of {dataIS} and {dataFS} do not match: {get_batch_size(dataIS)} != {get_batch_size(dataFS)}'
|
|
252
|
+
if self.VERBOSE: self.logger.error(__err_msg)
|
|
253
|
+
raise RuntimeError(__err_msg)
|
|
254
|
+
if get_batch_size(dataIS) <= 0:
|
|
255
|
+
if self.VERBOSE: self.logger.info(f'An empty batch occurred. Skipped.')
|
|
256
|
+
continue
|
|
257
|
+
elif get_batch_size(dataIS) > 1:
|
|
258
|
+
if self.VERBOSE: self.logger.error(f'Constrained MD do not support batched calculation yet. You should set BATCH_SIZE to 1.')
|
|
259
|
+
raise RuntimeError(f'Constrained MD do not support batched calculation yet. You should set BATCH_SIZE to 1.')
|
|
260
|
+
# MD
|
|
261
|
+
# cells & fixations
|
|
262
|
+
_cell = get_cell_vec(dataIS)
|
|
263
|
+
fixed_mask = get_fixed_mask(dataIS)
|
|
264
|
+
# get batch
|
|
265
|
+
batch_indx = get_batch_indx(dataIS)
|
|
266
|
+
# get id
|
|
267
|
+
idx = get_indx(dataIS)
|
|
268
|
+
idx = idx if idx is not None else [f'Untitled{_}' for _ in range(n_s, n_s + len(batch_indx))]
|
|
269
|
+
n_s += len(batch_indx)
|
|
270
|
+
if self.VERBOSE > 0:
|
|
271
|
+
self.logger.info('*' * 89)
|
|
272
|
+
self.logger.info(f'Running Batch {n_c}.')
|
|
273
|
+
self.logger.info('*' * 89)
|
|
274
|
+
cell_str = np.array2string(
|
|
275
|
+
get_cell_vec(dataIS), **FLOAT_ARRAY_FORMAT
|
|
276
|
+
).replace("[", " ").replace("]", " ") # TODO, Now it supports pygData and DGLGraph.
|
|
277
|
+
self.logger.info(f'Structure names: {idx}\n')
|
|
278
|
+
self.logger.info(f'Cell Vectors:\n{cell_str}')
|
|
279
|
+
|
|
280
|
+
if self.data_type == 'pyg':
|
|
281
|
+
batch_indx = [len(dat.pos) for dat in dataIS.to_data_list()]
|
|
282
|
+
_check_batch_indx = [len(dat.pos) for dat in dataFS.to_data_list()]
|
|
283
|
+
elif self.data_type == 'dgl':
|
|
284
|
+
batch_indx = dataIS.batch_num_nodes('atom')
|
|
285
|
+
_check_batch_indx = dataFS.batch_num_nodes('atom')
|
|
286
|
+
else:
|
|
287
|
+
raise RuntimeError(f'Data type {self.data_type} is not supported.')
|
|
288
|
+
# check atom numbers
|
|
289
|
+
for _, __ in enumerate(batch_indx):
|
|
290
|
+
if __ != _check_batch_indx[_]:
|
|
291
|
+
__err_msg = f'The atom numbers of {_}-th sample do not match: {__} != {_check_batch_indx[_]}'
|
|
292
|
+
if self.VERBOSE > 0: self.logger.error(__err_msg)
|
|
293
|
+
raise RuntimeError(__err_msg)
|
|
294
|
+
|
|
295
|
+
# initial atom coordinates
|
|
296
|
+
if self.data_type == 'pyg':
|
|
297
|
+
X_is = dataIS.pos
|
|
298
|
+
X_fs = dataFS.pos
|
|
299
|
+
else:
|
|
300
|
+
X_is = dataIS.nodes['atom'].data['pos']
|
|
301
|
+
X_fs = dataFS.nodes['atom'].data['pos']
|
|
302
|
+
X_init_ = linear_interpolation_tens(X_is, X_fs, self.NIMAGE)
|
|
303
|
+
# rebatch data
|
|
304
|
+
origin_elem_list = get_atomic_number(dataIS)
|
|
305
|
+
dataIS = rebatched_graph(dataIS, X_init_)
|
|
306
|
+
|
|
307
|
+
# run
|
|
308
|
+
mole_dynam.run(
|
|
309
|
+
model_wrap.Energy,
|
|
310
|
+
X_init_,
|
|
311
|
+
[origin_elem_list]*self.NIMAGE,
|
|
312
|
+
V_init=get_init_veloc(dataIS),
|
|
313
|
+
grad_func=model_wrap.Grad,
|
|
314
|
+
func_args=(dataIS,), grad_func_args=(dataIS,),
|
|
315
|
+
is_grad_func_contain_y=False,
|
|
316
|
+
require_grad=self.require_grad,
|
|
317
|
+
fixed_atom_tensor=fixed_mask,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Print info
|
|
321
|
+
if self.VERBOSE > 0:
|
|
322
|
+
self.logger.info(f'Batch {n_c} done.')
|
|
323
|
+
n_c += 1
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
self.logger.warning(f'WARNING: An error occurred in {n_c}th batch. Error: {e}.')
|
|
327
|
+
if self.VERBOSE > 0:
|
|
328
|
+
excp = traceback.format_exc()
|
|
329
|
+
self.logger.warning(f"Traceback:\n{excp}")
|
|
330
|
+
n_c += 1
|
|
331
|
+
|
|
332
|
+
if self.VERBOSE:
|
|
333
|
+
self.logger.info(
|
|
334
|
+
f'Constrained Molecular Dynamics Done. Total Time: {time.perf_counter() - time_tol:<.4f}'
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
th.cuda.synchronize()
|
|
339
|
+
excp = traceback.format_exc()
|
|
340
|
+
self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:\n{excp}')
|
|
341
|
+
|
|
342
|
+
finally:
|
|
343
|
+
th.cuda.synchronize()
|
|
344
|
+
self.logger.removeHandler(self.log_handler)
|
|
345
|
+
if isinstance(self.log_handler, logging.FileHandler):
|
|
346
|
+
self.log_handler.close()
|
|
347
|
+
pass
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
5
|
+
# Authors: Pu Pengxin, Song Xin
|
|
6
|
+
# Version: 0.9a
|
|
7
|
+
# File: DataLoaders.py
|
|
8
|
+
# Environment: Python 3.12
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
from typing import Any, Dict, List, Self, Tuple, Literal, Sequence
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch as th
|
|
15
|
+
|
|
16
|
+
from BUCToolkit.utils._CheckModules import check_module
|
|
17
|
+
|
|
18
|
+
# check modules
|
|
19
|
+
_pyg = check_module('torch_geometric.data')
|
|
20
|
+
if _pyg is not None:
|
|
21
|
+
pygBatch = _pyg.Batch
|
|
22
|
+
else:
|
|
23
|
+
pygBatch = None
|
|
24
|
+
|
|
25
|
+
dgl = check_module('dgl')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DglGraphLoader:
|
|
29
|
+
"""
|
|
30
|
+
A Data loader to form dgl graph IN MEMORY
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data:
|
|
34
|
+
when is_train == True:
|
|
35
|
+
Dict: {
|
|
36
|
+
'data': List[dgl.Graph],
|
|
37
|
+
'label': Dict{'energy': Sequence[float], 'forces': Sequence[np.NDArray[n_atom, 3]]}
|
|
38
|
+
}. Wherein 'force' is optional.
|
|
39
|
+
else see `data_names`.
|
|
40
|
+
batch_size: batch size.
|
|
41
|
+
device: the device that data put on.
|
|
42
|
+
shuffle: whether shuffle data.
|
|
43
|
+
is_train: if `is_train` = True, data need to contain labels; else data need not contain labels and the return of labels [i.e., next(iter(dataloader))] was depended on `contain_names`.
|
|
44
|
+
data_names: only works when `is_train` = False.
|
|
45
|
+
if `data_names` is not None, it should be a Sequence(data names) with the same order as data,
|
|
46
|
+
and the returned `labels` [i.e., next(iter(dataloader))] would be data_names instead of "energy" or "forces",
|
|
47
|
+
else `labels` would be None.
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
(dgl.DGLGraph, {'energy': energy, 'forces': force})
|
|
51
|
+
or (dgl.DGLGraph, {'energy': energy, })
|
|
52
|
+
or (dgl.DGLGraph, data_names | None) [when is_train == False]
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
data: Dict[Literal['data', 'labels'] | Literal['data', 'names'], Any],
|
|
59
|
+
batch_size: int,
|
|
60
|
+
device: str | th.device = 'cpu',
|
|
61
|
+
shuffle: bool = True,
|
|
62
|
+
is_train: bool = True,
|
|
63
|
+
data_names: Sequence[str] | None = None
|
|
64
|
+
) -> None:
|
|
65
|
+
# check import
|
|
66
|
+
if dgl is None:
|
|
67
|
+
raise ImportError('`DglGraphLoader` requires package `dgl` which could not be imported.')
|
|
68
|
+
self.data: List = data['data']
|
|
69
|
+
self.is_train = is_train
|
|
70
|
+
if is_train:
|
|
71
|
+
self.energy: Any = data['labels']['energy']
|
|
72
|
+
self.forces: Any = data['labels'].get('forces', None)
|
|
73
|
+
if shuffle: self.shuffle()
|
|
74
|
+
if not isinstance(self.energy, th.Tensor):
|
|
75
|
+
self.energy_ = th.from_numpy(np.array(self.energy, dtype=np.float32))
|
|
76
|
+
self.data_names = data_names
|
|
77
|
+
self.batchsize = batch_size
|
|
78
|
+
self._n_samp = len(self.data)
|
|
79
|
+
self._index = 0
|
|
80
|
+
self.device = device
|
|
81
|
+
# set the flag
|
|
82
|
+
self._LOADER_TYPE = 'Train'
|
|
83
|
+
|
|
84
|
+
def shuffle(self, ):
|
|
85
|
+
_seed = random.randint(0, 2147483647)
|
|
86
|
+
random.seed(_seed)
|
|
87
|
+
random.shuffle(self.data)
|
|
88
|
+
random.seed(_seed)
|
|
89
|
+
random.shuffle(self.energy)
|
|
90
|
+
if self.forces is not None:
|
|
91
|
+
random.seed(_seed)
|
|
92
|
+
random.shuffle(self.forces)
|
|
93
|
+
|
|
94
|
+
def __iter__(self, ) -> Self:
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __next__(self, ) -> "Tuple[dgl.DGLGraph, Dict[Literal['energy', 'forces'], th.Tensor] | None]":
|
|
98
|
+
if self._index * self.batchsize < self._n_samp:
|
|
99
|
+
data = self.data[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
100
|
+
data = dgl.batch(data)
|
|
101
|
+
data = data.to(self.device) # type: ignore
|
|
102
|
+
|
|
103
|
+
if self.is_train: # when training
|
|
104
|
+
energy = self.energy_[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
105
|
+
energy = energy.to(self.device)
|
|
106
|
+
|
|
107
|
+
if self.forces is not None:
|
|
108
|
+
force = self.forces[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
109
|
+
force = th.from_numpy(np.concatenate(force)).to(self.device)
|
|
110
|
+
label: Dict[Literal['energy', 'forces'], th.Tensor] | None = {'energy': energy, 'forces': force}
|
|
111
|
+
else:
|
|
112
|
+
label: Dict[Literal['energy'], th.Tensor] | None = {'energy': energy}
|
|
113
|
+
|
|
114
|
+
elif self.data_names is not None: # when validating && testing
|
|
115
|
+
label: Sequence[str] = self.data_names[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
label = None
|
|
119
|
+
|
|
120
|
+
self._index += 1
|
|
121
|
+
return data, label
|
|
122
|
+
else:
|
|
123
|
+
raise StopIteration
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PyGDataLoader:
|
|
127
|
+
"""
|
|
128
|
+
A Data loader to form pygData IN MEMORY
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
data: Dict: {'data': D, label: L}, where
|
|
132
|
+
D is a List of pyg.Data that contains attributes `pos`, `cell`, `atomic_numbers`, `natoms`, `tags`, `fixed`, `pbc`, `idx`.
|
|
133
|
+
`pos`: Tensor, atom coordinates.
|
|
134
|
+
`cell`: Tensor, cell vectors.
|
|
135
|
+
`atomic_numbers`: Tensor, atomic numbers, corresponding to `pos` one by one.
|
|
136
|
+
`natoms`: int, number of atoms.
|
|
137
|
+
`tags`: Tensor, to be compatible with 'FAIR-CHEM' (https://fair-chem.github.io/),
|
|
138
|
+
which fixed slab part is set to 0, free slab part is 1, adsorbate is 2.
|
|
139
|
+
`fixed`: Tensor, fixed tag, which fixed atoms are 0, free atoms are 1.
|
|
140
|
+
`pbc`: List[bool, bool, bool], where to be periodic at x, y, z directions.
|
|
141
|
+
`idx`: str, the name of this structure.
|
|
142
|
+
L is Dict{'energy': Sequence[float], 'forces': Sequence[np.NDArray[n_atom, 3]]}, 'forces' is optional.
|
|
143
|
+
batch_size: batch size.
|
|
144
|
+
device: the device that data put on.
|
|
145
|
+
shuffle: whether shuffle data.
|
|
146
|
+
is_train: if `is_train` = True, data need to contain labels; else data need not contain labels and the return of labels [i.e., next(iter(dataloader))] was depended on `contain_names`.
|
|
147
|
+
data_names: only works when `is_train` = False.
|
|
148
|
+
if `data_names` is not None, it should be a Sequence(data names) with the same order as data,
|
|
149
|
+
and the returned `labels` [i.e., next(iter(dataloader))] would be data_names instead of "energy" or "forces",
|
|
150
|
+
else `labels` would be None.
|
|
151
|
+
|
|
152
|
+
Yields:
|
|
153
|
+
(pyg.Data, {'energy': energy, 'forces': force})
|
|
154
|
+
or (pyg.Data, {'energy': energy, })
|
|
155
|
+
or (pyg.Data, data_names | None) [when is_train == False]
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
data: Dict[Literal['data', 'labels'] | Literal['data', 'names'], Any],
|
|
161
|
+
batch_size: int,
|
|
162
|
+
device: str | th.device = 'cpu',
|
|
163
|
+
shuffle: bool = True,
|
|
164
|
+
is_train: bool = True,
|
|
165
|
+
data_names: Sequence[str] | None = None
|
|
166
|
+
) -> None:
|
|
167
|
+
|
|
168
|
+
# check module
|
|
169
|
+
if pygBatch is None:
|
|
170
|
+
raise ImportError('`PyGDataLoader` requires package `torch-geometric` which could not be imported.')
|
|
171
|
+
self.data: List = data['data']
|
|
172
|
+
self.is_train = is_train
|
|
173
|
+
if is_train:
|
|
174
|
+
self.energy: Any = data['labels']['energy']
|
|
175
|
+
self.forces: Any = data['labels'].get('forces', None)
|
|
176
|
+
if shuffle: self.shuffle()
|
|
177
|
+
if not isinstance(self.energy, th.Tensor):
|
|
178
|
+
self.energy_ = th.from_numpy(np.array(self.energy, dtype=np.float32))
|
|
179
|
+
self.data_names = data_names
|
|
180
|
+
self.batchsize = batch_size
|
|
181
|
+
self._n_samp = len(self.data)
|
|
182
|
+
self._index = 0
|
|
183
|
+
self.device = device
|
|
184
|
+
# set the flag
|
|
185
|
+
self._LOADER_TYPE = 'Train'
|
|
186
|
+
|
|
187
|
+
def shuffle(self, ):
|
|
188
|
+
_seed = random.randint(0, 2147483647)
|
|
189
|
+
random.seed(_seed)
|
|
190
|
+
random.shuffle(self.data)
|
|
191
|
+
random.seed(_seed)
|
|
192
|
+
random.shuffle(self.energy)
|
|
193
|
+
if self.forces is not None:
|
|
194
|
+
random.seed(_seed)
|
|
195
|
+
random.shuffle(self.forces)
|
|
196
|
+
|
|
197
|
+
def __iter__(self, ) -> Self:
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __next__(self, ) -> Tuple[pygBatch, Dict[Literal['energy', 'forces'], th.Tensor] | None]:
|
|
201
|
+
if self._index * self.batchsize < self._n_samp:
|
|
202
|
+
data = self.data[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
203
|
+
data = pygBatch.from_data_list(data)
|
|
204
|
+
data = data.to(self.device) # type: ignore
|
|
205
|
+
|
|
206
|
+
if self.is_train:
|
|
207
|
+
energy = self.energy_[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
208
|
+
energy = energy.to(self.device)
|
|
209
|
+
|
|
210
|
+
if self.forces is not None:
|
|
211
|
+
force = self.forces[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
212
|
+
force = th.from_numpy(np.concatenate(force)).to(self.device)
|
|
213
|
+
label: Dict[Literal['energy', 'forces'], th.Tensor] | None = {'energy': energy, 'forces': force}
|
|
214
|
+
else:
|
|
215
|
+
label: Dict[Literal['energy'], th.Tensor] | None = {'energy': energy}
|
|
216
|
+
|
|
217
|
+
elif self.data_names is not None:
|
|
218
|
+
label: Sequence[str] = self.data_names[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
label = None
|
|
222
|
+
|
|
223
|
+
self._index += 1
|
|
224
|
+
return data, label
|
|
225
|
+
else:
|
|
226
|
+
raise StopIteration
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class BatchStructuresDataLoader:
|
|
230
|
+
"""
|
|
231
|
+
A Data loader to form BatchStructure
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
data_path: str,
|
|
237
|
+
batch_size: int,
|
|
238
|
+
device: str | th.device = 'cpu',
|
|
239
|
+
shuffle: bool = True,
|
|
240
|
+
is_train: bool = True,
|
|
241
|
+
data_names: Sequence[str] | None = None
|
|
242
|
+
) -> None:
|
|
243
|
+
self.data_path = data_path
|
|
244
|
+
self.data_names = data_names
|
|
245
|
+
self.batchsize = batch_size
|
|
246
|
+
self._index = 0
|
|
247
|
+
self.device = device
|
|
248
|
+
# TODO <<<
|
|
249
|
+
raise NotImplementedError
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class ISFSPyGDataLoader:
|
|
253
|
+
"""
|
|
254
|
+
A Data loader to form pygData IN MEMORY.
|
|
255
|
+
This Data loader yields Tuple(pygData1, pygData2) instead of (pygData, label).
|
|
256
|
+
Wherein, pygData1 and pygData2 are corresponding to the initial state and finale state of structures.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
data: Dict: {'dataIS': D1, 'dataFS': D2}, where
|
|
260
|
+
D1 and D2 are 2 Lists of pyg.Data that contain attributes `pos`, `cell`, `atomic_numbers`, `natoms`, `tags`, `fixed`, `pbc`, `idx`.
|
|
261
|
+
`pos`: Tensor, atom coordinates.
|
|
262
|
+
`cell`: Tensor, cell vectors.
|
|
263
|
+
`atomic_numbers`: Tensor, atomic numbers, corresponding to `pos` one by one.
|
|
264
|
+
`natoms`: int, number of atoms.
|
|
265
|
+
`tags`: Tensor, to be compatible with 'FAIR-CHEM' (https://fair-chem.github.io/),
|
|
266
|
+
which fixed slab part is set to 0, free slab part is 1, adsorbate is 2.
|
|
267
|
+
`fixed`: Tensor, fixed tag, which fixed atoms are 0, free atoms are 1.
|
|
268
|
+
`pbc`: List[bool, bool, bool], where to be periodic at x, y, z directions.
|
|
269
|
+
`idx`: str, the name of this structure.
|
|
270
|
+
batch_size: batch size.
|
|
271
|
+
device: the device that data put on.
|
|
272
|
+
data_names: if `data_names` is not None, it should be a Sequence(data names) with the same order as data,
|
|
273
|
+
and the returned `labels` [i.e., next(iter(dataloader))] would be data_names instead of "energy" or "forces",
|
|
274
|
+
else `labels` would be None.
|
|
275
|
+
|
|
276
|
+
Yields:
|
|
277
|
+
(pyg.Data, pyg.Data)
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
data: Dict[Literal['dataIS', 'dataFS'], Any],
|
|
283
|
+
batch_size: int,
|
|
284
|
+
device: str | th.device = 'cpu',
|
|
285
|
+
data_names: Sequence[str] | None = None
|
|
286
|
+
) -> None:
|
|
287
|
+
|
|
288
|
+
# check module
|
|
289
|
+
if pygBatch is None:
|
|
290
|
+
raise ImportError('`PyGDataLoader` requires package `torch-geometric` which could not be imported.')
|
|
291
|
+
self.dataIS: List = data['dataIS']
|
|
292
|
+
self.dataFS: List = data['dataFS']
|
|
293
|
+
self.data_names = data_names
|
|
294
|
+
self.batchsize = batch_size
|
|
295
|
+
self._n_samp = len(self.dataIS)
|
|
296
|
+
_n_samp_check = len(self.dataFS)
|
|
297
|
+
if _n_samp_check != self._n_samp:
|
|
298
|
+
raise RuntimeError(
|
|
299
|
+
f"The number of samples in `dataIS` and `dataFS` do not match. Number of IS: {self._n_samp} & number of FS: {_n_samp_check}."
|
|
300
|
+
)
|
|
301
|
+
self._index = 0
|
|
302
|
+
self.device = device
|
|
303
|
+
# set the flag
|
|
304
|
+
self._LOADER_TYPE = 'ISFS' # initial state & finale state
|
|
305
|
+
|
|
306
|
+
def __iter__(self, ) -> Self:
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def __next__(self, ) -> Tuple[pygBatch, pygBatch]:
|
|
310
|
+
if self._index * self.batchsize < self._n_samp:
|
|
311
|
+
dataIS = self.dataIS[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
312
|
+
dataIS = pygBatch.from_data_list(dataIS)
|
|
313
|
+
dataIS = dataIS.to(self.device) # type: ignore
|
|
314
|
+
dataFS = self.dataFS[self._index * self.batchsize: (self._index + 1) * self.batchsize]
|
|
315
|
+
dataFS = pygBatch.from_data_list(dataFS)
|
|
316
|
+
dataFS = dataFS.to(self.device) # type: ignore
|
|
317
|
+
|
|
318
|
+
self._index += 1
|
|
319
|
+
return dataIS, dataFS
|
|
320
|
+
else:
|
|
321
|
+
raise StopIteration
|