BUCToolkit 1.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
@@ -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