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,1680 @@
1
+ r"""
2
+ Methods of reading and transform various files
3
+ """
4
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
5
+ # Authors: Pu Pengxin, Song Xin
6
+ # Version: 0.9a
7
+ # File: load_files.py
8
+ # Environment: Python 3.12
9
+
10
+ import re
11
+ import sys
12
+ import traceback
13
+ from typing import Any, Dict, List, Sequence, Set, Tuple, Optional, Literal
14
+ import time
15
+ import os
16
+ import copy
17
+ import warnings
18
+
19
+ import joblib as jb
20
+ import numpy as np
21
+
22
+ from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
23
+ from BUCToolkit.utils._CheckModules import check_module
24
+ from BUCToolkit.utils.ElemListReduce import elem_list_reduce
25
+
26
+ __all__ = [
27
+ 'POSCARs2Feat',
28
+ 'OUTCAR2Feat',
29
+ 'ASETraj2Feat',
30
+ 'ExtXyz2Feat',
31
+ 'load_from_csv',
32
+ 'Cif2Feat',
33
+ 'load_from_structures',
34
+ 'Output2Feat'
35
+
36
+ ]
37
+
38
+ ''' CONSTANCE '''
39
+ _ALL_ELEMENT = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar',
40
+ 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr',
41
+ 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe',
42
+ 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
43
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn',
44
+ 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr',
45
+ 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', ] # element List
46
+
47
+ ''' Load labels from local file '''
48
+
49
+
50
+ def load_from_csv(
51
+ file_name: str,
52
+ label_type: type = float,
53
+ ignore_None_label_samp: bool = True,
54
+ read_column: Tuple[int, int] = (0, 1),
55
+ has_title_line: bool = False
56
+ ) -> Dict[str, Any]:
57
+ r"""
58
+ load information of a csv file into a dict of {samp_1:label_1, ...}.
59
+
60
+ Parameters:
61
+ file_name: str, csv file of 2 columns (sample name, sample label)
62
+ label_type: type, type of sample label
63
+ ignore_None_label_samp: bool, whether ignore None label samples i.e., not put them into output dict
64
+ read_column: Tuple[int,int], the 2 columns to read, where the 1st column as keys and the 2nd as values.
65
+ has_title_line: bool, whether ignore the 1st line which is the title or descriptions.
66
+
67
+ Return:
68
+ dict of {samp_1:label_1, ...} | dict, id_list of [samp_names]
69
+ """
70
+ f = np.loadtxt(file_name, dtype=str, delimiter=',')
71
+ if has_title_line:
72
+ f = f[1:]
73
+ col1 = read_column[0]
74
+ col2 = read_column[1]
75
+
76
+ if ignore_None_label_samp:
77
+ #print('load_from_file : The sample which has None label was ignored...')
78
+ energy_dict = {cont[col1]: label_type(cont[col2])
79
+ for cont in f if cont[col2] not in {'None', '', 'nan'}} # a dict of {sample_id: energy}
80
+ else:
81
+ energy_dict = {
82
+ cont[col1]: label_type(cont[col2]) if not (cont[col2] in {'None', 'nan', ''})
83
+ else None
84
+ for cont in f
85
+ } # a dict of {sample_id: energy}
86
+
87
+ return energy_dict
88
+
89
+ def load_from_structures(path: str):
90
+ """
91
+ Load BatchStructure memory-mapping files from `path` and return this BatchStructures.
92
+ Args:
93
+ path: the path of BatchStructure memory-mapping files.
94
+
95
+ Returns: BatchStructures with read data.
96
+
97
+ """
98
+ f = BatchStructures()
99
+ f.load(path)
100
+ return f
101
+
102
+
103
+ class POSCARs2Feat(BatchStructures):
104
+ """
105
+ Read and convert a folder of POSCAR files from given path into arrays of atoms, coordinates, cell vectors, etc.
106
+ """
107
+
108
+ def __init__(self, path: str = './', verbose: int = 0, *args, **kwargs) -> None:
109
+ """
110
+ Read and convert a folder of POSCAR files from given path into arrays of atoms, coordinates, cell vectors, etc.
111
+
112
+ Parameters:
113
+ path: str, the path of POSCAR files
114
+ verbose: int, the verbose of print information
115
+
116
+ Method:
117
+ read: read files
118
+ para_read: read files in parallel
119
+
120
+ Attributes:
121
+ file_list: list, name list of structure files
122
+ Coord: list, coordinates of atoms. Shape: List[NDArray[(n_atoms, 3), dtype=float32]]
123
+ Cells: list, cell vector of crystals. Shape: List[NDArray[(3,3), dtype=float32]]
124
+ Elements: list, elements of crystals. Shape: List[List[str(element symbols)]]
125
+ Numbers: list, element numbers of crystals. Shape: List[List[int(atom number of each element)]]. The same order of Elements.
126
+
127
+ Returns: None
128
+ """
129
+ super().__init__()
130
+ self.path = path
131
+ self.verbose = verbose
132
+ self.files_list = list()
133
+
134
+ def para_read(
135
+ self,
136
+ file_list: Optional[List] = None,
137
+ output_coord_type: str = 'cartesian',
138
+ n_core: int = -1,
139
+ backend='loky'
140
+ ):
141
+ r"""
142
+ Reading file in parallel.
143
+ Parameters:
144
+ file_list: list (or other Sequences), the list of selected files to be read. 'None' means read all files in the input path.
145
+ output_coord_type: str, 'cartesian' or 'direct'. The coordination type of output atom coordination.
146
+ n_core: int, the number of CPU cores used in reading files.
147
+ backend: backend for parallelization in joblib. Options: 'loky', 'threading', 'multiprocessing'.
148
+ """
149
+ time_st = time.perf_counter()
150
+
151
+ # loading files
152
+ if self.verbose > 0: print('*' * 60 + '\nReading files...')
153
+ if file_list is None:
154
+ self.files_list = os.listdir(self.path)
155
+ else:
156
+ self.files_list = file_list
157
+ if self.files_list is None: raise RuntimeError('occurred a None file list.')
158
+ self.n_samp = len(self.files_list)
159
+
160
+ # check vars
161
+ _type_converter = {'cartesian': 'C', 'direct': 'D'}
162
+ if output_coord_type != 'cartesian' and output_coord_type != 'direct':
163
+ raise ValueError('Unknown output coordination type. Please input "cartesian" or "direct".')
164
+ self.Coords_type = [_type_converter[output_coord_type], ] * self.n_samp
165
+
166
+ # check parallel and initialize
167
+ if n_core == -1:
168
+ n_core = jb.cpu_count()
169
+ elif n_core <= 0:
170
+ raise ValueError('Invalid CPU numbers.')
171
+ elif n_core > jb.cpu_count():
172
+ warnings.warn('Input n_core is greater than total available CPU number, so it would be set to -1.')
173
+ n_core = jb.cpu_count()
174
+
175
+ # Parallel read
176
+ Z_dict = {key: i for i, key in enumerate(_ALL_ELEMENT, 1)} # a dictionary which map element symbols into their atomic numbers.
177
+ self._Z_dict = Z_dict
178
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose, backend=backend)
179
+ _temp = _para(jb.delayed(self._load)(filename) for filename in self.files_list)
180
+ if _temp is None: raise RuntimeError('Occurred None data.')
181
+ for temp in _temp:
182
+ self._Sample_ids.append(temp[0]) # mp_ids
183
+ self.Cells.append(temp[1]) # Lattice parameters
184
+ self.Elements.append(temp[2]) # Element symbols
185
+ self.Numbers.append(temp[3]) # Element numbers
186
+ self.Coords.append(temp[4]) # Atom coordination (cartesian)
187
+ self.Fixed.append(temp[5])
188
+ self._update_indices()
189
+ self._check_id()
190
+ self._check_len()
191
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - time_st:<5.4f}')
192
+
193
+ def read(self, file_list: Optional[List] = None, output_coord_type: str = 'cartesian', ) -> None:
194
+ """
195
+
196
+ Args:
197
+ file_list: list (or other Sequences), the list of selected files to be read. 'None' means read all files in the input path.
198
+ output_coord_type: str, 'cartesian' or 'direct'. The coordination type of output atom coordination.
199
+
200
+ Returns:
201
+
202
+ """
203
+ time_st = time.perf_counter()
204
+
205
+ Z_dict = {key: i for i, key in enumerate(_ALL_ELEMENT, 1)} # a dictionary which map element symbols into their atomic numbers.
206
+ self._Z_dict = Z_dict
207
+
208
+ # loading files
209
+ if self.verbose > 0: print('*' * 60 + '\nReading files...\n')
210
+ if file_list is None:
211
+ self.files_list = [__f.name for __f in os.scandir(self.path) if __f.is_file()]
212
+ elif isinstance(file_list, (List, Tuple)):
213
+ for __f in file_list:
214
+ if os.path.isfile(os.path.join(self.path, __f)):
215
+ self.files_list.append(__f)
216
+ else:
217
+ warnings.warn(f'No such file: {os.path.join(self.path, __f)}, skipped.', RuntimeWarning)
218
+ else:
219
+ raise TypeError(f'Invalid type of `file_list`: {type(file_list)}')
220
+ self.n_samp = len(self.files_list)
221
+ if self.n_samp == 0: raise RuntimeError('Occurred empty file_list. No file could be read.')
222
+
223
+ # check vars
224
+ _type_converter = {'cartesian': 'C', 'direct': 'D'}
225
+ if output_coord_type != 'cartesian' and output_coord_type != 'direct':
226
+ raise ValueError('Unknown output coordination type. Please input "cartesian" or "direct".')
227
+
228
+ # loading atoms coordinates
229
+ self.Coords_type = [_type_converter[output_coord_type], ] * self.n_samp
230
+ self._Sample_ids = list()
231
+ self.Atom_list = None
232
+ self.Atomic_number_list = None
233
+ self.Cells = list()
234
+ self.Coords = list()
235
+ self.Elements = list()
236
+ self.Numbers = list()
237
+ time_old = copy.deepcopy(time_st)
238
+ err_num = 0
239
+ if self.verbose > 0: print('Progress: ', end='\r')
240
+ for i, fname in enumerate(self.files_list):
241
+ time_mid = time.perf_counter()
242
+ try:
243
+ temp = self._load(fname)
244
+ # update attr
245
+ self._Sample_ids.append(temp[0]) # mp_ids
246
+ self.Cells.append(temp[1]) # Lattice parameters
247
+ self.Elements.append(temp[2]) # Element symbols
248
+ self.Numbers.append(temp[3]) # Element numbers
249
+ self.Coords.append(temp[4]) # Atom coordinates (cartesian)
250
+ self.Fixed.append(temp[5]) # fixed masks
251
+ except Exception as e:
252
+ err_num += 1
253
+ print(f'WARNING : Failed to read file {fname}, because an error "{e}" occurred.'
254
+ f' ERROR NUMBER: {err_num}')
255
+
256
+ # progress bar
257
+ if (self.verbose > 0) and (time_mid - time_old > 1):
258
+ time_old = copy.deepcopy(time_mid)
259
+ prog = ((i + 1) / self.n_samp)
260
+ print('Progress: ' + '>' * int(20 * prog) + f'|{(100 * prog):.1f}%', end='\r')
261
+ i_old = copy.deepcopy(i)
262
+
263
+ if self.verbose > 0: print('Progress: ' + '>' * 20 + '|100% ', end='\r')
264
+ self._indecies = {_id: ii for ii, _id in enumerate(self._Sample_ids)}
265
+ self._update_indices()
266
+ self._check_id()
267
+ self._check_len()
268
+ if self.verbose > 0: print('\nAll files were read successfully!\n' + '*' * 60)
269
+ if self.verbose > 0: print(f'Total time: {(time.perf_counter() - time_st):<5.4f}')
270
+ #print(self.coord_data)
271
+
272
+ def _load(self, fileName: str) -> Tuple[str, np.ndarray, List[str], List[int], np.ndarray, np.ndarray]:
273
+ """
274
+ Loading data from files(POSCAR-like files) in given path.
275
+ """
276
+ with open(os.path.join(self.path, fileName), 'r') as f:
277
+ data = f.readlines() # read raw data
278
+
279
+ # atom type
280
+ atom_type = data[5].split()
281
+
282
+ # atom number
283
+ _atom_num = data[6].split()
284
+ atom_num = [int(num) for num in _atom_num]
285
+ n_atom = sum(atom_num)
286
+ # whether Selective Dynamics
287
+ if data[7].split()[0][0] == 'S' or data[7].split()[0][0] == 's':
288
+ is_selective_dynamics = 1
289
+ else:
290
+ is_selective_dynamics = 0
291
+ # coordination system(Direct or Cartesian)
292
+ coord_sys = data[7 + is_selective_dynamics].split()
293
+ if coord_sys[0][0] == 'D' or coord_sys[0][0] == 'd':
294
+ in_coord_type = 'D'
295
+ elif coord_sys[0][0] == 'C' or coord_sys[0][0] == 'c':
296
+ in_coord_type = 'C'
297
+ else:
298
+ raise RuntimeError(f'Unknown coordination type "{coord_sys[0][0]}" in file {fileName}')
299
+ # cell vectors
300
+ cell = np.empty((3, 3), dtype=np.float32)
301
+ for i in [2, 3, 4]:
302
+ cell[i - 2] = np.array(data[i].split(), dtype=np.float32)
303
+ # atoms cartesian coordinates # TODO: fill list first, and convert to array then.
304
+ atom_coord = [['-'*10, '-'*10, '-'*10]] * n_atom
305
+ atom_fixed = [[1, 1, 1]] * n_atom
306
+ if is_selective_dynamics == 0:
307
+ for i in range(n_atom):
308
+ atom_coord[i] = (data[8 + i].split())[:3]
309
+ else:
310
+ _slec_dynam_convertor = {'T': 1, 'F': 0}
311
+ for i in range(n_atom): # if selective dynamics, coordinates shift down 1 line
312
+ line_info = data[8 + 1 + i].split()
313
+ atom_coord[i] = line_info[:3]
314
+ atom_fixed[i] = [_slec_dynam_convertor[_] for _ in line_info[3:6]]
315
+ atom_coord = np.array(atom_coord, dtype=np.float32)
316
+ atom_fixed = np.array(atom_fixed, dtype=np.int8)
317
+
318
+ if self.Coords_type[0] == in_coord_type:
319
+ pass
320
+ elif in_coord_type == 'D':
321
+ atom_coord = atom_coord @ cell
322
+ elif in_coord_type == 'C':
323
+ atom_coord = atom_coord @ np.linalg.inv(cell)
324
+
325
+ return fileName, cell, atom_type, atom_num, atom_coord, atom_fixed
326
+
327
+
328
+ class ConcatPOSCAR2Feat(BatchStructures):
329
+ r"""
330
+ Read and convert the single file of a batch POSCARs contents from given path into arrays of atoms, coordinates, cell vectors etc.
331
+ All numerical data were stored as float32 in memory.
332
+ The format of the read file:
333
+
334
+ # id1
335
+ TITLE
336
+ SCALE FACTOR
337
+ LATTICE VECTOR AXIS X (x1, x2, x3)
338
+ LATTICE VECTOR AXIS Y (y1, y2, y3)
339
+ LATTICE VECTOR AXIS Z (z1, z2, z3)
340
+ CHEMICAL ELEMENTS LIST (Element1 Element2 Element3 ...)
341
+ ATOM NUMBERS OF EACH ELEMENT (N1 N2 N3 ...)
342
+ COORDINATE TYPE ("Direct" or "Cartesian")
343
+ ATOM COORDINATES IN ORDER OF CHEMICAL ELEMENTS LIST (x1 y1 z1)
344
+ (x2 y2 z2)
345
+ (x3 y3 z3)
346
+ ...
347
+ # id2
348
+ ...
349
+ # id3
350
+ ...
351
+ ...
352
+
353
+ EOF
354
+
355
+ Parameters:
356
+ path: str, the path of POSCAR files
357
+ select_ids: list (or other Sequences), the list of selected id to be read. 'None' means read all files in the input path.
358
+ output_coord_type: str, 'cartesian' or 'direct'. The coordination type of output atom coordination.
359
+
360
+ Attributes:
361
+ Sample_ids: list, name list of structrues
362
+ Coords: list, coordinations of atoms. Shape: List[NDArray[(n_atoms, 3), dtype=float32]]
363
+ Cells: list, cell vector of crystals. Shape: List[NDArray[(3,3), dtype=float32]]
364
+ Elements: list, elements of crystals. Shape: List[List[str(element symbols)]]
365
+ Numbers: list, element numbers of crystals. Shape: List[List[int(atom number of each element)]]. the same order of Elements.
366
+
367
+ Returns: None
368
+ """
369
+
370
+ def __init__(self, path: str = './', select_ids: List[str] | Set[str] | None = None, output_coord_type: str = 'cartesian', verbose: int = 0) -> None:
371
+ warnings.warn('Deprecated.', DeprecationWarning)
372
+ raise Exception('Deprecated.')
373
+ super().__init__()
374
+ time_st = time.perf_counter()
375
+ Z_dict = {key: i for i, key in enumerate(_ALL_ELEMENT, 1)} # a dictionary which map element symbols into their atomic numbers.
376
+ self._Z_dict = Z_dict
377
+
378
+ # loading files
379
+ if verbose: print('*' * 60 + '\nReading files...')
380
+ self.output_coord_type = output_coord_type
381
+ self._Sample_ids = list()
382
+ self.Atom_list = None
383
+ self.Atomic_number_list = None
384
+ self.Cells = list()
385
+ self.Coords = list()
386
+ self.Elements = list()
387
+ self.Numbers = list()
388
+ time_old = copy.deepcopy(time_st)
389
+
390
+ with open(path, 'r') as f:
391
+ if select_ids is None: # read all files
392
+ while True:
393
+ _text = f.readline()
394
+ if _text == '':
395
+ break
396
+ if _text[0] == '#': # whether the id line. Find the headline.
397
+ _id = _text.replace(' ', '') # str
398
+ _id = _id[1:-1]
399
+ self._Sample_ids.append(_id)
400
+
401
+ next(f) # skip title line
402
+ scale = float(f.readline()) # scale factor line
403
+ # Cell Vector
404
+ _cell_vec = list()
405
+ _cell_vec.append((f.readline()).split()) # cell axis a
406
+ _cell_vec.append((f.readline()).split()) # cell axis b
407
+ _cell_vec.append((f.readline()).split()) # cell axis c
408
+ _cell_vec = np.asarray(_cell_vec, dtype=np.float32) * scale # np.NDArray
409
+ self.Cells.append(_cell_vec)
410
+ # Atoms
411
+ _atoms = (f.readline()).split()
412
+ self.Elements.append(_atoms) # List[str]
413
+ # Atom Numbers
414
+ _atom_num = [int(_) for _ in (f.readline()).split()]
415
+ self.Numbers.append(_atom_num) # List[int]
416
+ _atom_tol_num = sum(_atom_num)
417
+ # Coord Type
418
+ _type = (f.readline()).strip()
419
+ # Coords
420
+ if _type[0] == 's' or _type[0] == 'S': # whether 'Selective dynamics'
421
+ _type = (f.readline()).strip()
422
+ _coords = np.empty((_atom_tol_num, 3), dtype=np.float32)
423
+ for ii in range(_atom_tol_num):
424
+ _coords[ii] = np.asarray((f.readline().split())[:3], dtype=np.float32)
425
+ if _type[0] == 'd' or _type[0] == 'D':
426
+ _coords = _coords @ _cell_vec
427
+ elif _type[0] == 'c' or _type[0] == 'C':
428
+ pass
429
+ else:
430
+ warnings.warn(f'Unknown Coordinate type. Coordinate type of sample *** {_id} *** would be considered as "Direct"')
431
+ _coords = _coords @ _cell_vec
432
+ self.Coords.append(_coords)
433
+ else:
434
+ select_ids = set(select_ids)
435
+ _have_read_ids = set()
436
+ while True:
437
+ _text = f.readline()
438
+ if _text == '': # judge EOF
439
+ break
440
+ if _text[0] == '#': # whether the id line. Find the head line.
441
+ _id = _text.replace(' ', '') # str
442
+ _id = _id[1:-1]
443
+ if _id not in select_ids:
444
+ continue
445
+ _have_read_ids.add(_id)
446
+ self._Sample_ids.append(_id)
447
+
448
+ next(f) # skip title line
449
+ scale = float(f.readline()) # scale factor line
450
+ # Cell Vector
451
+ _cell_vec = list()
452
+ _cell_vec.append((f.readline()).split()) # cell axis a
453
+ _cell_vec.append((f.readline()).split()) # cell axis b
454
+ _cell_vec.append((f.readline()).split()) # cell axis c
455
+ _cell_vec = np.asarray(_cell_vec, dtype=np.float32) * scale # np.NDArray
456
+ self.Cells.append(_cell_vec)
457
+ # Atoms
458
+ _atoms = (f.readline()).split()
459
+ self.Elements.append(_atoms) # List[str]
460
+ # Atom Numbers
461
+ _atom_num = [int(_) for _ in (f.readline()).split()]
462
+ self.Numbers.append(_atom_num) # List[int]
463
+ _atom_tol_num = sum(_atom_num)
464
+ # Coord Type
465
+ _type = (f.readline()).strip()
466
+ # Coords
467
+ if _type[0] == 's' or _type[0] == 'S': # whether 'Selective dynamics'
468
+ _type = (f.readline()).strip()
469
+ _coords = np.empty((_atom_tol_num, 3), dtype=np.float32)
470
+ for ii in range(_atom_tol_num):
471
+ _coords[ii] = np.asarray((f.readline().split())[:3], dtype=np.float32)
472
+ if _type[0] == 'd' or _type[0] == 'D':
473
+ _coords = _coords @ _cell_vec
474
+ elif _type[0] == 'c' or _type[0] == 'C':
475
+ pass
476
+ else:
477
+ warnings.warn(f'Unknown Coordinate type. Coordinate type of sample *** {_id} *** would be considered as "Direct"')
478
+ _coords = _coords @ _cell_vec
479
+ self.Coords.append(_coords)
480
+
481
+ if verbose: print('All files were read successfully!\n' + '*' * 60)
482
+
483
+ time_ed = time.perf_counter()
484
+ if verbose: print('Total time: %s' % (time_ed - time_st))
485
+ pass
486
+
487
+
488
+ class OUTCAR2Feat(BatchStructures):
489
+ r"""
490
+ Read atoms, coordinates, atom numbers, energies and force in OUTCARs from given path.
491
+ Notes: Because OUTCAR does not store any information of fixed atom (selective dynamics), all atoms would be set to FREE!!
492
+
493
+ Parameters:
494
+ path: str, the path of batched OUTCAR-like files. The Sample ids would be labeled as its file names.
495
+ """
496
+
497
+ def __init__(self, path: str, verbose: int = 1):
498
+ super().__init__()
499
+ self.verbose = verbose
500
+ self.path = path
501
+ self.Energies = list()
502
+ self.Forces = list()
503
+
504
+ self.__n_atom_partt = re.compile(r"(?<=NIONS =)\s+[0-9]+\n")
505
+ self.__pos_force_partt = re.compile(
506
+ r"(?<=TOTAL-FORCE.\(eV/Angst\)\n.-----------------------------------------------------------------------------------\n)\s+[-|0-9\s\n.]+")
507
+ self.__energy_partt = re.compile(r'FREE ENERGIE OF THE ION-ELECTRON SYSTEM.*\n.*\n.*\n.*\n.*energy\(sigma->0\) =\s*([0-9\-.]+)\n')
508
+ self.__cell_partt = re.compile(r'VOLUME and BASIS-vectors are now.*\n.*\n.*\n.*\n.*direct lattice vectors.*\n(.*\n.*\n.*)')
509
+ self.__is_converge_partt = re.compile(r'aborting loop .')
510
+ self.__voltage_partt = re.compile(r"E-fermi\s+:\s*([-|0-9.]+)\s+XC")
511
+
512
+ @staticmethod
513
+ def _get_atom_info(data: str) -> Tuple[List, List]:
514
+ r""" get atom list and atom number from OUTCAR """
515
+ numbers = re.search(r"(?<=ions per type =)\s+[0-9|\s]+\n", data)
516
+ if numbers is not None:
517
+ numbers = numbers.group().split()
518
+ else:
519
+ raise RuntimeError('atom numbers could not find.')
520
+ n_type = len(numbers)
521
+
522
+ atoms = re.findall(r"POTCAR:\s*PAW_PBE\s*([A-Za-z]{1,2})", data)
523
+ atoms = atoms[:n_type]
524
+
525
+ return atoms, numbers
526
+
527
+ def _read_single_file(self, file_name: str, parallel: bool = False) -> Any:
528
+ r"""
529
+ Read single file.
530
+
531
+ Parameters:
532
+ file_name: str, the OUTCAR-like file name.
533
+ parallel: bool,
534
+ """
535
+ full_path = os.path.join(self.path, file_name)
536
+ if not os.path.isfile(full_path):
537
+ warnings.warn(f'No OUTCAR file in given directory {os.path.join(self.path, file_name)}')
538
+ return None
539
+ try:
540
+ with open(full_path, "r") as file: #打开文件
541
+ data = file.read() #读取文件
542
+ n_atom = re.search(self.__n_atom_partt, data) #从OUTCAR文件搜索原子数
543
+ if n_atom is None:
544
+ warnings.warn(f'No atoms matched in {file_name}, skipped.', RuntimeWarning)
545
+ if parallel:
546
+ return [], [], [], [], [], [], [], [], []
547
+ else:
548
+ return None
549
+ n_atom = int(n_atom.group()) #将输出的原子个数保存为整数形式
550
+ position_iter = re.finditer(self.__pos_force_partt, data) #通过re匹配的迭代器函数,找到原子坐标
551
+ _energies = re.findall(self.__energy_partt, data)
552
+ is_converged = re.findall(self.__is_converge_partt, data)
553
+
554
+ # ATOM & Number
555
+ atoms, numbers = self._get_atom_info(data)
556
+ atoms = np.array(atoms, dtype='<U4')
557
+ numbers = np.array(numbers, dtype=np.int32)
558
+ # Cell Vectors
559
+ cells_str = re.findall(self.__cell_partt, data, )
560
+
561
+ cells = list()
562
+ _data = list()
563
+ energies = list()
564
+ for i, match_for in enumerate(position_iter): #循环的取每次迭代找到的一个结构的原子坐标与受力信息
565
+ # if SCF converged, content in OUTCAR would be 'aborting loop because ...' else 'aborting loop EDIFF was not reached ...'
566
+ if is_converged[i][-1] == 'b':
567
+ _dat = re.split(r"\n+", match_for.group()) #通过换行符进行划分
568
+ _dat = [re.split(r'[0-9][\n\s\t]+', dat_) for dat_ in _dat[:-2]] #去除空字符或都是横线的行,然后循环的对每一行进行划分 <<< # TODO
569
+ _data.append(_dat) #将原子坐标的列表添加进列表中
570
+ energies.append(float(_energies[i]))
571
+ # sometimes the cell vector would not split by \s e.g., "0.000000000-10.955671660 20.223087260"
572
+ _cell_line: List[str] = cells_str[i].split()
573
+ if len(_cell_line) != 3:
574
+ __cell_line = list()
575
+ [__cell_line.extend(_.replace('-', ' -').split()) for _ in _cell_line]
576
+ _cell_line = __cell_line
577
+ del __cell_line
578
+ cells.append(_cell_line)
579
+
580
+ cells = np.array(cells, dtype=np.float32).reshape(-1, 3, 6)
581
+ cells = cells[:, :, :3]
582
+ _data = np.array(_data, dtype=np.float32) # (n_step, n_atom, 3)
583
+ # check if match
584
+ if _data.shape[1] != np.sum(numbers):
585
+ warnings.warn(
586
+ f'Number of the read atoms and coordinates are not match in the file {file_name}, skipped. '
587
+ f'Maybe your elements in this structure are too many (e.g., > 10).',
588
+ RuntimeWarning
589
+ )
590
+ if parallel:
591
+ return [], [], [], [], [], [], [], [], []
592
+ else:
593
+ return None
594
+ if len(_data) == 0:
595
+ warnings.warn(f'Occurred empty data in file {file_name}, skipped.', RuntimeWarning)
596
+ if parallel:
597
+ return [], [], [], [], [], [], [], [], []
598
+ else:
599
+ return None
600
+
601
+ # formatted
602
+ n_step, n_atom, _ = _data.shape
603
+ coords = _data[:, :, :3]
604
+ coords = [coo for coo in coords]
605
+ forces = _data[:, :, 3:]
606
+ forces = [forc for forc in forces]
607
+ atoms = atoms[None, :].repeat(n_step, axis=0)
608
+ numbers = numbers[None, :].repeat(n_step, axis=0)
609
+ fixed = np.ones_like(coords, dtype=np.int8)
610
+
611
+ _id = [file_name + f'_{i}' for i in range(n_step)]
612
+ # output
613
+ if parallel:
614
+ return _id, atoms, numbers, cells, coords, energies, forces, fixed, ['C',] * n_step
615
+ else:
616
+ self._Sample_ids.extend(_id)
617
+ self.Elements.extend(atoms.tolist())
618
+ self.Numbers.extend(numbers.tolist())
619
+ self.Cells.extend(cells)
620
+ self.Coords.extend(coords)
621
+ self.Energies.extend(energies)
622
+ self.Forces.extend(forces)
623
+ self.Fixed.extend(fixed)
624
+ self.Coords_type.extend(["C"] * n_step)
625
+ except Exception as err:
626
+ warnings.warn(f'An Error occurred when reading file {file_name}, skipped.\nError: {err}.')
627
+ if parallel:
628
+ return [], [], [], [], [], [], [], [], []
629
+ else:
630
+ return None
631
+
632
+ def read(self, file_list: Optional[List[str]] = None, n_core: int = -1, backend: str = 'loky'):
633
+ r"""
634
+ Parameters:
635
+ file_list: List[str], the list of files to read. Default for all files under given path.
636
+ n_core: int, the number of CPU cores used in reading files.
637
+ backend: backend for parallelization in joblib. Options: 'loky', 'threading', 'multiprocessing'.
638
+
639
+ Return: None
640
+ Update the attribute self.data.
641
+ """
642
+ t_st = time.perf_counter()
643
+ if file_list is None:
644
+ file_list = os.listdir(self.path)
645
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
646
+ elif not isinstance(file_list, Sequence):
647
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
648
+
649
+ if n_core > len(file_list):
650
+ warnings.warn(f'`ncore` is greater than file numbers, so `ncore` was reset to {len(file_list)}', RuntimeWarning)
651
+ n_core = len(file_list)
652
+ elif n_core == -1:
653
+ n_core = jb.cpu_count()
654
+ elif not(isinstance(n_core, int)) or n_core < -1:
655
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
656
+
657
+ if n_core == 1:
658
+ if self.verbose: print('Sequential Reading...');print('Progress: 0%', end='\r')
659
+ for i, fi_name in enumerate(file_list):
660
+ self._read_single_file(fi_name, parallel=False)
661
+ if self.verbose > 0:
662
+ if (i + 1) % 50 == 0:
663
+ prog_ = (i + 1) / len(file_list)
664
+ prog = round(prog_ / 0.05)
665
+ print('Progress: ' + '>' * prog + f'{prog_ * 100:>3.2f}%', end='\r')
666
+ if self.verbose: print('Progress: ' + '>' * 20 + f'{100:>3d}%')
667
+ else:
668
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
669
+ _dat = _para(jb.delayed(self._read_single_file)(fi_name, parallel=True) for fi_name in file_list)
670
+ for temp in _dat:
671
+ self._Sample_ids.extend(temp[0])
672
+ self.Elements.extend(temp[1])
673
+ self.Numbers.extend(temp[2])
674
+ self.Cells.extend(temp[3])
675
+ self.Coords.extend(temp[4])
676
+ self.Energies.extend(temp[5])
677
+ self.Forces.extend(temp[6])
678
+ self.Fixed.extend(temp[7])
679
+ self.Coords_type.extend(temp[8])
680
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
681
+ self._update_indices()
682
+ self._check_id()
683
+ self._check_len()
684
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
685
+
686
+ def _read_single_file_add_voltage(self, file_name: str, parallel: bool = False) -> Any:
687
+ r"""
688
+ Read single file and output result has voltage.
689
+
690
+ Parameters:
691
+ file_name: str, the OUTCAR-like file name.
692
+ parallel: bool,
693
+ """
694
+ full_path = os.path.join(self.path, file_name)
695
+ if not os.path.isfile(full_path):
696
+ warnings.warn(f'No OUTCAR file in given directory {os.path.join(self.path, file_name)}')
697
+ return None
698
+ try:
699
+ with open(full_path, "r") as file: #打开文件
700
+ data = file.read() #读取文件
701
+ n_atom = re.search(self.__n_atom_partt, data) #从OUTCAR文件搜索原子数
702
+ if n_atom is None:
703
+ warnings.warn(f'No atoms matched in {file_name}, skipped.', RuntimeWarning)
704
+ if parallel:
705
+ return [], [], [], [], [], [], [], [], [], []
706
+ else:
707
+ return None
708
+ n_atom = int(n_atom.group()) #将输出的原子个数保存为整数形式
709
+ position_iter = re.finditer(self.__pos_force_partt, data) #通过re匹配的迭代器函数,找到原子坐标
710
+ _energies = re.findall(self.__energy_partt, data)
711
+ is_converged = re.findall(self.__is_converge_partt, data)
712
+ _voltage = re.findall(self.__voltage_partt,data)
713
+ # ATOM & Number
714
+ atoms, numbers = self._get_atom_info(data)
715
+ atoms = np.array(atoms, dtype='<U4')
716
+ numbers = np.array(numbers, dtype=np.int32)
717
+ # Cell Vectors
718
+ cells_str = re.findall(self.__cell_partt, data, )
719
+
720
+ cells = list()
721
+ _data = list()
722
+ energies = list()
723
+ voltage = list()
724
+ for i, match_for in enumerate(position_iter): #循环的取每次迭代找到的一个结构的原子坐标与受力信息
725
+ # if SCF converged, content in OUTCAR would be 'aborting loop because ...' else 'aborting loop EDIFF was not reached ...'
726
+ if is_converged[i][-1] == 'b':
727
+ _dat = re.split(r"\n+", match_for.group()) #通过换行符进行划分
728
+ _dat = [re.split(r'[0-9][\n\s\t]+', dat_) for dat_ in _dat[:-2]] #去除空字符或都是横线的行,然后循环的对每一行进行划分 <<< # TODO
729
+ _data.append(_dat) #将原子坐标的列表添加进列表中
730
+ energies.append(float(_energies[i]))
731
+ voltage.append([float(_voltage[i])]*len(_dat))
732
+ # sometimes the cell vector would not split by \s e.g., "0.000000000-10.955671660 20.223087260"
733
+ _cell_line: List[str] = cells_str[i].split()
734
+ if len(_cell_line) != 3:
735
+ __cell_line = list()
736
+ [__cell_line.extend(_.replace('-', ' -').split()) for _ in _cell_line]
737
+ _cell_line = __cell_line
738
+ del __cell_line
739
+ cells.append(_cell_line)
740
+
741
+ cells = np.array(cells, dtype=np.float32).reshape(-1, 3, 6)
742
+ cells = cells[:, :, :3]
743
+ _data = np.array(_data, dtype=np.float32) # (n_step, n_atom, 3)
744
+ if len(_data) == 0:
745
+ warnings.warn(f'Occurred empty data in file {file_name}, skipped.', RuntimeWarning)
746
+ if parallel:
747
+ return [], [], [], [], [], [], [], [], [], []
748
+ else:
749
+ return None
750
+
751
+ # formatted
752
+ n_step, n_atom, _ = _data.shape
753
+ coords = _data[:, :, :3]
754
+ coords = [coo for coo in coords]
755
+ forces = _data[:, :, 3:]
756
+ forces = [forc for forc in forces]
757
+ atoms = atoms[None, :].repeat(n_step, axis=0)
758
+ numbers = numbers[None, :].repeat(n_step, axis=0)
759
+ fixed = np.ones_like(coords, dtype=np.int8)
760
+
761
+ _id = [file_name + f'_{i}' for i in range(n_step)]
762
+ # output
763
+ if parallel:
764
+ return _id, atoms, numbers, cells, coords, energies, forces, fixed, ['C',] * n_step, voltage
765
+ else:
766
+ self._Sample_ids.extend(_id)
767
+ self.Elements.extend(atoms.tolist())
768
+ self.Numbers.extend(numbers.tolist())
769
+ self.Cells.extend(cells)
770
+ self.Coords.extend(coords)
771
+ self.Energies.extend(energies)
772
+ self.Forces.extend(forces)
773
+ self.Fixed.extend(fixed)
774
+ self.Coords_type.extend(["C"] * n_step)
775
+ self.Labels.extend(voltage)
776
+ except Exception as err:
777
+ warnings.warn(f'An Error occurred when reading file {file_name}, skipped.\nError: {err}.')
778
+ if parallel:
779
+ return [], [], [], [], [], [], [], [], [], []
780
+ else:
781
+ return None
782
+
783
+ def read_contain_voltage(self, file_list: Optional[List[str]] = None, n_core: int = -1, backend: str = 'loky'):
784
+ r"""
785
+ read the list of OUTCAR files and output every set of id, element, atoms, coordinate, forces, energy, voltage
786
+ Parameters:
787
+ file_list: List[str], the list of files to read. Default for all files under given path.
788
+ n_core: int, the number of CPU cores used in reading files.
789
+ backend: backend for parallelization in joblib. Options: 'loky', 'threading', 'multiprocessing'.
790
+
791
+ Return: None
792
+ Update the attribute self.data.
793
+ """
794
+ t_st = time.perf_counter()
795
+ self.Labels = list()
796
+ if file_list is None:
797
+ file_list = os.listdir(self.path)
798
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
799
+ elif not isinstance(file_list, Sequence):
800
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
801
+
802
+ if n_core > len(file_list):
803
+ warnings.warn(f'`ncore` is greater than file numbers, so `ncore` was reset to {len(file_list)}', RuntimeWarning)
804
+ n_core = len(file_list)
805
+ elif n_core == -1:
806
+ n_core = jb.cpu_count()
807
+ elif not(isinstance(n_core, int)) or n_core < -1:
808
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
809
+
810
+ if n_core == 1:
811
+ if self.verbose: print('Sequential Reading...');print('Progress: 0%', end='\r')
812
+ for i, fi_name in enumerate(file_list):
813
+ self._read_single_file(fi_name, parallel=False)
814
+ if self.verbose > 0:
815
+ if (i + 1) % 50 == 0:
816
+ prog_ = (i + 1) / len(file_list)
817
+ prog = round(prog_ / 0.05)
818
+ print('Progress: ' + '>' * prog + f'{prog_ * 100:>3.2f}%', end='\r')
819
+ if self.verbose: print('Progress: ' + '>' * 20 + f'{100:>3d}%')
820
+ else:
821
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
822
+ _dat = _para(jb.delayed(self._read_single_file_add_voltage)(fi_name, parallel=True) for fi_name in file_list)
823
+ for temp in _dat:
824
+ self._Sample_ids.extend(temp[0])
825
+ self.Elements.extend(temp[1])
826
+ self.Numbers.extend(temp[2])
827
+ self.Cells.extend(temp[3])
828
+ self.Coords.extend(temp[4])
829
+ self.Energies.extend(temp[5])
830
+ self.Forces.extend(temp[6])
831
+ self.Fixed.extend(temp[7])
832
+ self.Coords_type.extend(temp[8])
833
+ self.Labels.extend(temp[9])
834
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
835
+ self._update_indices()
836
+ self._check_id()
837
+ self._check_len()
838
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
839
+
840
+
841
+ class Xyz2Feat(BatchStructures):
842
+ """
843
+ read xyz files to BatchStructures.
844
+ """
845
+ def __init__(self, path: str, verbose: int = 1, has_cell: bool = False, has_forces: bool = False):
846
+ super().__init__()
847
+ #warnings.warn()
848
+ self.verbose = verbose
849
+ self.path = path
850
+ self.has_cell = has_cell
851
+ self.has_forces = has_forces
852
+ if has_forces: self.Forces = list()
853
+
854
+ def _read_single(self, file_name: str, ) -> Tuple[List[str], List, List, List,
855
+ List[List], List[List], List[np.ndarray], List]:
856
+ with open(os.path.join(self.path, file_name), 'r') as _f:
857
+ data = _f.readlines()
858
+ # match atoms number, energy of every structure and generate atom_number_all_list, Energy_list
859
+ atom_list = list()
860
+ labels_list = list()
861
+ elem_coo_list = list()
862
+ is_head = False
863
+ end_ptr = len(data)
864
+ ptr = 0
865
+ while True:
866
+ if ptr >= end_ptr:
867
+ break
868
+ dd = data[ptr]
869
+ if is_head: # next title info line
870
+ labels_list.append(dd) # label info
871
+ ptr += 1
872
+ coo_part = data[ptr: ptr + atom_list[-1]]
873
+ elem_coo_list.append(coo_part)
874
+ is_head = False
875
+ ptr += atom_list[-1]
876
+ continue
877
+ # if head
878
+ try_head = re.match(r"^\s*[0-9]+\n", dd) # pure int line
879
+ if try_head is not None:
880
+ atom_list.append(int(try_head.group()))
881
+ is_head = True
882
+ ptr += 1
883
+ continue
884
+ ptr += 1
885
+
886
+ # Match coordinate, Element_type ,Element_number and generate Coordinate_list, Element_type_Without_repetition_list, Element_Number_Without_repetition_list
887
+ coordinate_list = list()
888
+ forces_list = list()
889
+ element_type_without_repetition_list = list()
890
+ element_number_without_repetition_list = list()
891
+ idx = list()
892
+ for i, P in enumerate(elem_coo_list):
893
+ try:
894
+ position_list_ = [l.split()[1:] for l in P]
895
+ element_list_ = [l.split()[0] for l in P]
896
+ elem, _, numb = elem_list_reduce(element_list_)
897
+ element_type_without_repetition_list.append(elem)
898
+ element_number_without_repetition_list.append(numb)
899
+ pos_forc_array = np.array(position_list_, dtype=np.float32)
900
+ coordinate_list.append(pos_forc_array[:, :3])
901
+ if self.has_forces:
902
+ forces_list.append(pos_forc_array[:, 3:])
903
+ # idx
904
+ idx.append(file_name + '_' + str(i))
905
+ except Exception as e:
906
+ self.logger.warning(f"* Error occurred while loading {i}-th structure: {e}")
907
+ cells = [np.zeros((3, 3))] * len(elem_coo_list)
908
+ coo_type = ['C'] * len(elem_coo_list)
909
+
910
+ return (idx, cells, labels_list, forces_list, element_type_without_repetition_list,
911
+ element_number_without_repetition_list, coordinate_list, coo_type)
912
+
913
+ def read(self, file_list = None):
914
+ t_st = time.perf_counter()
915
+ if file_list is None:
916
+ file_list = os.listdir(self.path)
917
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
918
+ elif not isinstance(file_list, Sequence):
919
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
920
+
921
+ err = 0
922
+ self.Labels = list()
923
+ for i, fil in enumerate(file_list):
924
+ try:
925
+ temp = self._read_single(fil)
926
+ self._Sample_ids.extend(temp[0])
927
+ self.Cells.extend(temp[1])
928
+ self.Labels.extend(temp[2])
929
+ if self.has_forces:
930
+ self.Forces.extend(temp[3])
931
+ self.Elements.extend(temp[4])
932
+ self.Numbers.extend(temp[5])
933
+ self.Coords.extend(temp[6])
934
+ self.Coords_type.extend(temp[7])
935
+ self.Fixed.extend([np.ones_like(_, dtype='|i1') for _ in temp[6]])
936
+
937
+ except Exception as e:
938
+ err += 1
939
+ warnings.warn(
940
+ f"An error occurred in {i}th file: {fil}, "
941
+ f"ERROR: {e}"
942
+ f" skipped. Total errors: {err}, {traceback.format_exc()}",
943
+ RuntimeWarning
944
+ )
945
+ if self.verbose > 0:
946
+ warnings.warn(f"Error: {e}")
947
+ self._update_indices()
948
+ self._check_id()
949
+ self._check_len()
950
+
951
+
952
+ class ExtXyz2Feat(BatchStructures):
953
+ """
954
+ Read extxyz file with multiple structures.
955
+ """
956
+ def __init__(self, path: str, verbose: int = 1):
957
+ super().__init__()
958
+ self.verbose = verbose
959
+ self.path = path
960
+ self.Energies = list()
961
+ self.Forces = list()
962
+ self.fix_mask_trans_func = np.vectorize(self._fixed_info_transformer)
963
+ self.FIXED_DICT = {'T': 1, 'F': 0}
964
+
965
+ @staticmethod
966
+ def _fixed_info_transformer(a: np.ndarray, fixed_dict: dict):
967
+ y = fixed_dict[a]
968
+ return y
969
+
970
+ def _read_single(
971
+ self,
972
+ file_name: str,
973
+ lattice_tag: str = 'lattice',
974
+ energy_tag: str = 'energy',
975
+ column_info_tag: str = 'properties',
976
+ element_tag: str = 'species',
977
+ coordinates_tag: str = 'pos',
978
+ forces_tag: str|None = None,
979
+ fixed_atom_tag: str|None = None
980
+ ) -> Tuple[List[str], List, List, List, List[List], List[List], List[np.ndarray], List, List]:
981
+ """
982
+ read single extxyz file.
983
+ Args:
984
+ file_name:
985
+ lattice_tag:
986
+ energy_tag:
987
+ column_info_tag:
988
+ element_tag:
989
+ coordinates_tag:
990
+ forces_tag:
991
+ fixed_atom_tag:
992
+
993
+ Returns:
994
+
995
+ """
996
+ # save infos
997
+ idx = list()
998
+ Cell_list = list()
999
+ Energy_list = list()
1000
+ Elem_list = list()
1001
+ Number_list = list()
1002
+ Coords_list = list()
1003
+ Forces_list = list()
1004
+ Fixed_list = list()
1005
+ if not os.path.isfile(os.path.join(self.path, file_name)):
1006
+ warnings.warn(f'No OUTCAR file in given directory {os.path.join(self.path, file_name)}')
1007
+ return [], [], [], [], [], [], [], [], []
1008
+
1009
+ try:
1010
+ with open(os.path.join(self.path, file_name), 'r') as _f:
1011
+ data = _f.readlines()
1012
+
1013
+ line_now = 0 # flag of line now, start from 0
1014
+ i_structure = 0 # a counter
1015
+ while line_now + 1 < len(data):
1016
+ n_atom = int(data[line_now])
1017
+ info = data[line_now + 1]
1018
+ # PREFERENCES
1019
+ # cell
1020
+ cell = re.search(self.CELL_PARTTEN, info)
1021
+ assert cell is not None, 'Lattice was not found.'
1022
+ cell = cell.groups()[0].split()
1023
+ cell = np.asarray(cell, dtype=np.float32).reshape(3, 3)
1024
+ # properties & column split
1025
+ col_content = dict() # dict recorded column content that {name: (type, column_number_start, end)}
1026
+ col_split = re.search(self.PROP_PARTTEN, info)
1027
+ if col_split is not None:
1028
+ col_split = col_split.groups()[0].split(':')
1029
+ n_col = len(col_split)
1030
+ assert len(col_split)%3 == 0, 'Invalid properties information format.'
1031
+ _i_now = 0
1032
+ for i_col in range(0, n_col, 3):
1033
+ col_content[col_split[i_col]] = (col_split[i_col + 1], _i_now, _i_now + int(col_split[i_col + 2]))
1034
+ _i_now += int(col_split[i_col + 2])
1035
+ else:
1036
+ col_content = {
1037
+ f'{element_tag}': ('S', 0, 1),
1038
+ f'{coordinates_tag}': ('R', 1, 3)
1039
+ }
1040
+ # energy
1041
+ ener = re.search(self.ENER_PARTTEN, info)
1042
+ if ener is None:
1043
+ has_energy = False # a flag shown whether file contain energy.
1044
+ else:
1045
+ has_energy = True
1046
+ ener = float(ener.groups()[0])
1047
+
1048
+ # MAIN INFO
1049
+ main_info = np.asarray([_.split() for _ in data[line_now + 2 : line_now + 2 + n_atom]])
1050
+ # element & its number
1051
+ element_list_ = main_info[:, col_content[f'{element_tag}'][1]:col_content[f'{element_tag}'][2]].flatten()
1052
+ elements = [element_list_[0], ]
1053
+ numbers = list()
1054
+ count_ = 0
1055
+ for j, el in enumerate(element_list_):
1056
+ if el != elements[-1]:
1057
+ elements.append(el)
1058
+ numbers.append(count_)
1059
+ count_ = 1
1060
+ else:
1061
+ count_ += 1
1062
+ numbers.append(count_)
1063
+ # coords
1064
+ coords = main_info[:, col_content[f'{coordinates_tag}'][1]:col_content[f'{coordinates_tag}'][2]].astype(np.float32)
1065
+ # forces
1066
+ forces_info = col_content.get(f'{forces_tag}', None) if forces_tag is not None else None
1067
+ if forces_info is not None:
1068
+ forces = main_info[:, forces_info[1]:forces_info[2]].astype(np.float32)
1069
+ else:
1070
+ forces = None
1071
+ if forces_tag is not None:
1072
+ warnings.warn('`forces_tag` was set, while it cannot be found in the column information.', RuntimeWarning)
1073
+ # fixed masks
1074
+ mask_info = col_content.get(f'{fixed_atom_tag}', None) if fixed_atom_tag is not None else None
1075
+ if mask_info is not None:
1076
+ fixed = main_info[:, mask_info[1]:mask_info[2]]
1077
+ fixed: np.ndarray = self.fix_mask_trans_func(fixed, self.FIXED_DICT)
1078
+ if fixed.ndim != 2: raise RuntimeError('Unsupported format of atom fixing.')
1079
+ if fixed.shape[1:2] != (3, ):
1080
+ fixed = fixed.repeat(3, 1)
1081
+ fixed.astype(np.int8)
1082
+ else:
1083
+ fixed = np.ones_like(coords, dtype=np.int8)
1084
+ if fixed_atom_tag is not None:
1085
+ warnings.warn(
1086
+ '`mask_info` was set, while it cannot be found in the column information.'
1087
+ ' All atoms will be viewed as free',
1088
+ RuntimeWarning
1089
+ )
1090
+
1091
+ # write data
1092
+ idx.append(f'{os.path.splitext(file_name)[0]}_{i_structure}')
1093
+ Energy_list.append(ener)
1094
+ Cell_list.append(cell)
1095
+ Coords_list.append(coords)
1096
+ Elem_list.append(elements)
1097
+ Number_list.append(numbers)
1098
+ Fixed_list.append(fixed)
1099
+ Forces_list.append(forces)
1100
+
1101
+ line_now += 2 + n_atom
1102
+ i_structure += 1
1103
+
1104
+ return idx, Cell_list, Energy_list, Forces_list, Elem_list, Number_list, Coords_list, Fixed_list, ['C'] * i_structure
1105
+
1106
+ except Exception as err:
1107
+ warnings.warn(f'An Error occurred when reading file {file_name}, skipped.\nError: {err}.')
1108
+ return [], [], [], [], [], [], [], [], []
1109
+
1110
+ def read(
1111
+ self,
1112
+ file_list = None,
1113
+ n_core: int = -1,
1114
+ backend: Literal['loky', 'threading', 'multiprocessing']|str = 'loky',
1115
+ lattice_tag: str = 'lattice',
1116
+ energy_tag: str = 'energy',
1117
+ column_info_tag: str = 'properties',
1118
+ element_tag: str = 'species',
1119
+ coordinates_tag: str = 'pos',
1120
+ forces_tag: str | None = None,
1121
+ fixed_atom_tag: str | None = None
1122
+ ):
1123
+ """
1124
+ Read *.extxyz files to BatchStructures.
1125
+ Args:
1126
+ file_list: List[str], the list of files to read. Default for all files under given path.
1127
+ n_core: int, the number of CPU cores used in reading files. -1 for all cores.
1128
+ backend: backend of parallel reading in `joblib`.
1129
+ lattice_tag: key-words of lattice information in *.extxyz file, at each structure's 2nd line.
1130
+ energy_tag: key-words of structure's energy in *.extxyz file.
1131
+ column_info_tag: key-words of column content information in *.extxyz file. The column content has such format:
1132
+ `content 1`:`data type 1`:`column occupied number 1`:`content 2`:`data type 2`:`column occupied number 2`: ...
1133
+ detailed see https://wiki.fysik.dtu.dk/ase/dev/_modules/ase/io/extxyz.html.
1134
+ element_tag: As a content in `column_info_tag`, name of element column.
1135
+ coordinates_tag: As a content in `column_info_tag`, name of atom coordinates column.
1136
+ forces_tag: As a content in `column_info_tag`, name of forces column. If the file does not contain forces, set it to None.
1137
+ fixed_atom_tag: As a content in `column_info_tag`, name of the mask column specified which atom was fixed.
1138
+
1139
+ Returns: None
1140
+ All data would update as class attributes.
1141
+ """
1142
+ t_st = time.perf_counter()
1143
+ # file check
1144
+ if file_list is None:
1145
+ file_list = os.listdir(self.path)
1146
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
1147
+ elif not isinstance(file_list, Sequence):
1148
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
1149
+ # para. set
1150
+ if n_core > len(file_list):
1151
+ warnings.warn(f'`n_core` is greater than file numbers, so `n_core` was reset to {len(file_list)}', RuntimeWarning)
1152
+ n_core = len(file_list)
1153
+ elif n_core == -1:
1154
+ n_core = jb.cpu_count(only_physical_cores=True)
1155
+ elif (not isinstance(n_core, int)) or n_core < -1:
1156
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
1157
+ # search pattern
1158
+ self.CELL_PARTTEN = re.compile(rf'{lattice_tag}\s?=\s?\"([-+E0-9.\s]+)\"', re.IGNORECASE)
1159
+ self.PROP_PARTTEN = re.compile(rf'{column_info_tag}\s?=\s?([a-zA-Z:0-9_]+)', re.IGNORECASE)
1160
+ self.ENER_PARTTEN = re.compile(rf'{energy_tag}\s?=\s?([-+.0-9e]+)')
1161
+
1162
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
1163
+ _dat = _para(jb.delayed(self._read_single)(
1164
+ fi_name,
1165
+ lattice_tag,
1166
+ energy_tag,
1167
+ column_info_tag,
1168
+ element_tag,
1169
+ coordinates_tag,
1170
+ forces_tag,
1171
+ fixed_atom_tag
1172
+ ) for fi_name in file_list)
1173
+ # idx, Cell_list, Energy_list, Forces_list, Elem_list, Number_list, Coords_list, Coord_type
1174
+ for temp in _dat:
1175
+ self._Sample_ids.extend(temp[0])
1176
+ self.Cells.extend(temp[1])
1177
+ self.Energies.extend(temp[2])
1178
+ self.Forces.extend(temp[3])
1179
+ self.Elements.extend(temp[4])
1180
+ self.Numbers.extend(temp[5])
1181
+ self.Coords.extend(temp[6])
1182
+ self.Fixed.extend(temp[7])
1183
+ self.Coords_type.extend(temp[8])
1184
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
1185
+ self._update_indices()
1186
+ self._check_id()
1187
+ self._check_len()
1188
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
1189
+
1190
+
1191
+ class Cif2Feat(BatchStructures):
1192
+ """
1193
+ Read CIF file with multiple structures.
1194
+ Notes: CIF file could not contain the fixation information, so all atoms would be free.
1195
+
1196
+ """
1197
+
1198
+ def __init__(self, path: str, verbose: int = 1):
1199
+ super().__init__()
1200
+ self.verbose = verbose
1201
+ self.path = path
1202
+ self.Energies = None
1203
+ self.Forces = None
1204
+
1205
+ def read(
1206
+ self,
1207
+ file_list = None,
1208
+ n_core: int = -1,
1209
+ backend: Literal['loky', 'threading', 'multiprocessing'] | str = 'loky',
1210
+ ):
1211
+ t_st = time.perf_counter()
1212
+ # file check
1213
+ if file_list is None:
1214
+ file_list = os.listdir(self.path)
1215
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
1216
+ elif not isinstance(file_list, Sequence):
1217
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
1218
+ # para. set
1219
+ if n_core > len(file_list):
1220
+ warnings.warn(f'`n_core` is greater than file numbers, so `n_core` was reset to {len(file_list)}', RuntimeWarning)
1221
+ n_core = len(file_list)
1222
+ elif n_core == -1:
1223
+ n_core = jb.cpu_count(only_physical_cores=True)
1224
+ elif (not isinstance(n_core, int)) or n_core < -1:
1225
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
1226
+
1227
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
1228
+ _dat = _para(jb.delayed(self._read_single)(fi_name) for fi_name in file_list)
1229
+ # file_name, cell, elements, elem_numbers, coo,
1230
+ for temp in _dat:
1231
+ if temp is not None:
1232
+ self._Sample_ids.append(temp[0])
1233
+ self.Cells.append(temp[1])
1234
+ self.Elements.append(temp[2])
1235
+ self.Numbers.append(temp[3])
1236
+ self.Coords.append(temp[4])
1237
+ self.Fixed.append(temp[5])
1238
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
1239
+ self.Coords_type = ['D'] * len(self._Sample_ids)
1240
+ self._update_indices()
1241
+ self._check_id()
1242
+ self._check_len()
1243
+
1244
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
1245
+
1246
+ def _read_single(
1247
+ self,
1248
+ file_name: str,
1249
+ ):
1250
+ if not os.path.isfile(os.path.join(self.path, file_name)):
1251
+ warnings.warn(f'No OUTCAR file in given directory {os.path.join(self.path, file_name)}')
1252
+ return None
1253
+
1254
+ try:
1255
+ with open(os.path.join(self.path, file_name), 'r') as _f:
1256
+ data = _f.readlines()
1257
+
1258
+ # manage information
1259
+ cell_info_dict = dict()
1260
+ atom_info_seq_dict = dict()
1261
+ coords_info_list = list()
1262
+ i_seq = 0 # serial numbers of atom information
1263
+ is_read_atom = False # detect if reading atom coo. info.
1264
+ for dat in data[1:]:
1265
+ _data_list = dat.split()
1266
+ if _data_list[0] == 'loop_': is_read_atom = False
1267
+ if is_read_atom and _data_list[0][0] != '_':
1268
+ coords_info_list.append(_data_list)
1269
+ elif _data_list[0][:5] == '_cell':
1270
+ cell_info_dict[_data_list[0]] = _data_list[1]
1271
+ elif _data_list[0][:5] == '_atom':
1272
+ atom_info_seq_dict[_data_list[0]] = i_seq
1273
+ i_seq = i_seq + 1
1274
+ is_read_atom = True
1275
+
1276
+ # manage cell vectors
1277
+ a = float(cell_info_dict['_cell_length_a'])
1278
+ b = float(cell_info_dict['_cell_length_b'])
1279
+ c = float(cell_info_dict['_cell_length_c'])
1280
+ alpha = np.deg2rad(float(cell_info_dict['_cell_angle_alpha']))
1281
+ beta = np.deg2rad(float(cell_info_dict['_cell_angle_beta']))
1282
+ gamma = np.deg2rad(float(cell_info_dict['_cell_angle_gamma']))
1283
+ b1 = b * np.cos(gamma)
1284
+ b2 = np.sqrt(b**2 - b1**2)
1285
+ c1 = c * np.cos(beta)
1286
+ c2 = (b * c * np.cos(alpha) - b1 * c1)/b2
1287
+ c3 = np.sqrt(c**2 - c1**2 - c2**2)
1288
+ cell = np.array(
1289
+ [[a, 0., 0.],
1290
+ [b1, b2, 0.],
1291
+ [c1, c2, c3]],
1292
+ dtype=np.float32
1293
+ )
1294
+
1295
+ # manage coordinates
1296
+ coords_info_list = np.asarray(coords_info_list)
1297
+ ix, iy, iz = atom_info_seq_dict['_atom_site_fract_x'], atom_info_seq_dict['_atom_site_fract_y'], atom_info_seq_dict['_atom_site_fract_z']
1298
+ coo = coords_info_list[:, [ix, iy, iz]].astype(np.float32)
1299
+ fix = np.ones_like(coo, dtype=np.int8)
1300
+ atom_list = coords_info_list[:, atom_info_seq_dict['_atom_site_type_symbol']].tolist()
1301
+ # elements
1302
+ _elem_old = atom_list[0]
1303
+ elements = [atom_list[0], ]
1304
+ elem_numbers = list()
1305
+ count_i = 0
1306
+ for elem in atom_list:
1307
+ if elem == _elem_old:
1308
+ count_i += 1
1309
+ else:
1310
+ _elem_old = elem
1311
+ elem_numbers.append(count_i)
1312
+ elements.append(elem)
1313
+ count_i = 1
1314
+ elem_numbers.append(count_i)
1315
+
1316
+ return file_name, cell, elements, elem_numbers, coo, fix
1317
+
1318
+ except Exception as err:
1319
+ warnings.warn(f'An Error occurred when reading file {file_name}, skipped.\nError: {err}.\n{traceback.format_exc()}')
1320
+ return None
1321
+
1322
+
1323
+ class ASETraj2Feat(BatchStructures):
1324
+ """
1325
+ read ASE files to BatchStructures.
1326
+ """
1327
+
1328
+ def __init__(self, path: str, verbose: int = 1) -> None:
1329
+ super().__init__()
1330
+ ase_io = check_module('ase.io')
1331
+ if ase_io is None:
1332
+ raise ImportError(f'`ASETraj2Feat` requires the ase package which is not found.')
1333
+ self.ase_io = ase_io
1334
+ self.path = path
1335
+ self.verbose = verbose
1336
+ self.Energies = list()
1337
+ self.Forces = list()
1338
+
1339
+ def _read_single(self, ase_file):
1340
+ try:
1341
+ idx = list()
1342
+ Cell_list = list()
1343
+ Energy_list = list()
1344
+ Elem_list = list()
1345
+ Number_list = list()
1346
+ Coords_list = list()
1347
+ Coords_type = list()
1348
+ Forces_list = list()
1349
+ Fixed_list = list()
1350
+ atom_list = self.ase_io.read(ase_file, ':')
1351
+ base_name = os.path.basename(ase_file)
1352
+ base_name, _ = os.path.splitext(base_name) # remove postfix
1353
+ for i, atm in enumerate(atom_list):
1354
+ if getattr(atm, 'calc', None) is None: # skip the energy-empty sample
1355
+ continue
1356
+ Cell_list.append(np.asarray(atm.cell, dtype=np.float32))
1357
+ Coords_list.append(atm.positions.astype(np.float32))
1358
+ coord_type = 'D' if np.all(np.abs(atm.positions) <= 1.) else 'C'
1359
+ Coords_type.append(coord_type)
1360
+ Energy_list.append(atm.get_potential_energy())
1361
+ Forces_list.append(np.asarray(atm.get_forces(), dtype=np.float32))
1362
+ fix = np.ones_like(atm.positions, dtype=np.int8)
1363
+ fix[atm.constraints[0].index] = 0
1364
+ Fixed_list.append(fix)
1365
+ elem_sym, atomic_num, atom_count = elem_list_reduce(atm.numbers)
1366
+ Elem_list.append(elem_sym)
1367
+ Number_list.append(atom_count)
1368
+ idx.append(f'{base_name}_{i}')
1369
+
1370
+ return idx, Cell_list, Energy_list, Forces_list, Elem_list, Number_list, Coords_list, Coords_type, Fixed_list
1371
+
1372
+ except Exception as err:
1373
+ warnings.warn(f'An Error occurred when reading file {ase_file}, skipped.\nError: {err}.')
1374
+ return [], [], [], [], [], [], [], [], []
1375
+
1376
+ def read(
1377
+ self,
1378
+ file_list = None,
1379
+ n_core: int = -1,
1380
+ backend: Literal['loky', 'threading', 'multiprocessing'] | str = 'loky',
1381
+ ):
1382
+ t_st = time.perf_counter()
1383
+ # file check
1384
+ if file_list is None:
1385
+ file_list = os.listdir(self.path)
1386
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
1387
+ elif not isinstance(file_list, Sequence):
1388
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
1389
+ # para. set
1390
+ if n_core > len(file_list):
1391
+ warnings.warn(f'`n_core` is greater than file numbers, so `n_core` was reset to {len(file_list)}', RuntimeWarning)
1392
+ n_core = len(file_list)
1393
+ elif n_core == -1:
1394
+ n_core = jb.cpu_count(only_physical_cores=True)
1395
+ elif (not isinstance(n_core, int)) or n_core < -1:
1396
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
1397
+
1398
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
1399
+ _dat = _para(jb.delayed(self._read_single)(os.path.join(self.path, fi_name)) for fi_name in file_list)
1400
+ # file_name, cell, elements, elem_numbers, coo,
1401
+ # idx, Cell_list, Energy_list, Forces_list, Elem_list, Number_list, Coords_list, Coord_type, fixed
1402
+ for temp in _dat:
1403
+ self._Sample_ids.extend(temp[0])
1404
+ self.Cells.extend(temp[1])
1405
+ self.Energies.extend(temp[2])
1406
+ self.Forces.extend(temp[3])
1407
+ self.Elements.extend(temp[4])
1408
+ self.Numbers.extend(temp[5])
1409
+ self.Coords.extend(temp[6])
1410
+ self.Coords_type.extend(temp[7])
1411
+ self.Fixed.extend(temp[8])
1412
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
1413
+ self._update_indices()
1414
+ self._check_id()
1415
+ self._check_len()
1416
+
1417
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
1418
+
1419
+ class Output2Feat(BatchStructures):
1420
+ """
1421
+ A class of post-processing to reading output files of BatchOptimization and BatchMD
1422
+ """
1423
+ def __init__(self, path: str, verbose: int = 1) -> None:
1424
+ super().__init__()
1425
+ self.path = path
1426
+ self.verbose = verbose
1427
+ self.Energies = list()
1428
+ self.Forces = list()
1429
+
1430
+ def read(self, file_list: List[str], n_core: int = -1, backend='loky'):
1431
+ t_st = time.perf_counter()
1432
+ # file check
1433
+ if file_list is None:
1434
+ file_list = os.listdir(self.path)
1435
+ file_list = [f_ for f_ in file_list if os.path.isfile(os.path.join(self.path, f_))]
1436
+ elif not isinstance(file_list, Sequence):
1437
+ raise TypeError(f'Invalid type of files_list: {type(file_list)}')
1438
+ # para. set
1439
+ if n_core > len(file_list):
1440
+ warnings.warn(f'`n_core` is greater than file numbers, so `n_core` was reset to {len(file_list)}', RuntimeWarning)
1441
+ n_core = len(file_list)
1442
+ elif n_core == -1:
1443
+ n_core = min(jb.cpu_count(only_physical_cores=True), len(file_list))
1444
+ elif (not isinstance(n_core, int)) or n_core < -1:
1445
+ raise ValueError(f'Invalid `n_core` number: {n_core}.')
1446
+
1447
+ _para = jb.Parallel(n_jobs=n_core, backend=backend, verbose=self.verbose)
1448
+ _dat = _para(jb.delayed(self._read_single)(fi_name) for fi_name in file_list)
1449
+ # file_name, cell, elements, elem_numbers, coo,
1450
+ # samp_id, cell, elements, elem_numbers, energies, forces, coo, fix
1451
+ is_no_force = False
1452
+ for temp in _dat:
1453
+ self._Sample_ids.extend(temp[0])
1454
+ self.Cells.extend(temp[1])
1455
+ self.Elements.extend(temp[2])
1456
+ self.Numbers.extend(temp[3])
1457
+ self.Energies.extend(temp[4])
1458
+ if len(temp[5]) != 0:
1459
+ self.Forces.extend(temp[5])
1460
+ else:
1461
+ is_no_force = True
1462
+ warnings.warn(f'Forces are not output in some files, so that all forces are set to None.')
1463
+ self.Coords.extend(temp[6])
1464
+ self.Fixed.extend(temp[7])
1465
+ if is_no_force:
1466
+ self.Forces = None
1467
+ self.Coords_type = ['C'] * len(self)
1468
+ if not self._Sample_ids: raise RuntimeError('All data are empty.')
1469
+ self._update_indices()
1470
+ self._check_id()
1471
+ self._check_len()
1472
+
1473
+ if self.verbose > 0: print(f'Done. Total Time: {time.perf_counter() - t_st:>5.4f}')
1474
+ pass
1475
+
1476
+ def _read_single(self, file_name):
1477
+ if not os.path.isfile(os.path.join(self.path, file_name)):
1478
+ warnings.warn(f'No output file in given directory {os.path.join(self.path, file_name)}')
1479
+ return None
1480
+ samp_id = list()
1481
+ cell = list()
1482
+ elements = list()
1483
+ elem_numbers = list()
1484
+ energies = list()
1485
+ forces = list()
1486
+ coo = list()
1487
+ fix = list()
1488
+ number_pattern = re.compile(r'[\s\-.0-9]+')
1489
+ try:
1490
+ with open(os.path.join(self.path, file_name), 'r') as _f:
1491
+ data = _f.readlines()
1492
+ # identify tasks
1493
+ assert len(data) > 0, f'Empty data occurred.'
1494
+ task_tag = None
1495
+ is_task_opt = None
1496
+ is_task_md = None
1497
+ for line in data:
1498
+ tmp = re.search(r'VERBOSITY LEVEL: (.*)$', line)
1499
+ if tmp is not None:
1500
+ data_verbosity = int(tmp.group(1))
1501
+ continue
1502
+ tmp = re.search(r'TASK: Structure Optimization <<', line)
1503
+ if tmp is not None:
1504
+ is_task_opt = tmp
1505
+ continue
1506
+ tmp = re.search(r'TASK: Molecular Dynamics <<', line)
1507
+ if tmp is not None:
1508
+ is_task_md = tmp
1509
+ continue
1510
+ EOF = re.match(r'^ENTERING MAIN', line) # match the end of head information
1511
+ if EOF is not None:
1512
+ break
1513
+ if is_task_opt is not None:
1514
+ task_tag = 'opt'
1515
+ Id_pattern = re.compile(r'Structure names:\s\[(.+)]')
1516
+ Elem_tag = re.compile(r'Structure\s*[0-9]+:\s*(.*)$')
1517
+ Iter_pattern = re.compile(r'ITERATION\s*([0-9]+)')
1518
+ E_pattern = re.compile(r'\s*Energies: +\[([+\s0-9e.-]+)]')
1519
+ F_tag = re.compile(r'\s*Forces:')
1520
+ Coo_tag = re.compile(r'\s*Coordinates:')
1521
+ Cell_tag = re.compile(r'Cell Vectors:')
1522
+ Final_Coo_tag = re.compile(r'^ Final Coordinates:')
1523
+ Final_F_tag = re.compile(r'^ Final Forces:')
1524
+ elif is_task_md is not None:
1525
+ task_tag = 'md'
1526
+ raise NotImplementedError
1527
+ if task_tag is None: raise RuntimeError(f'Unknown task type.')
1528
+
1529
+ i_batch = -1
1530
+ # MAIN LOOP
1531
+ for i, line in enumerate(data):
1532
+ is_name = re.match(Id_pattern, line)
1533
+ if is_name is not None: # it is the name line
1534
+ name = re.split(', *', is_name.group(1).replace('\'', ''))
1535
+ i_batch += 1
1536
+ n_batch = len(name)
1537
+ cell_temp = list()
1538
+ elements_temp = list()
1539
+ elem_numbers_temp = list()
1540
+ continue
1541
+ # cell
1542
+ is_cell = re.match(Cell_tag, line)
1543
+ if is_cell is not None:
1544
+ _cell = list()
1545
+ for _sub_line in data[i+1:]:
1546
+ is_empty = re.match(r'^\s*$', _sub_line)
1547
+ if is_empty is not None:
1548
+ cell_temp.append(np.asarray(_cell, dtype=np.float32))
1549
+ _cell = list()
1550
+ continue
1551
+ is_numbers = re.match(number_pattern, _sub_line)
1552
+ if is_numbers is not None:
1553
+ _numbers = is_numbers.group().split()
1554
+ _cell.append(_numbers)
1555
+ else:
1556
+ break # neither empty line nor numbers, thus skipping
1557
+ continue
1558
+ # element & numbers
1559
+ is_elem = re.match(Elem_tag, line)
1560
+ if is_elem is not None:
1561
+ _elem = list()
1562
+ _number = list()
1563
+ for ii, _ in enumerate(is_elem.group(1).replace(':', '').split()):
1564
+ if ii % 2 == 0:
1565
+ _elem.append(_)
1566
+ else:
1567
+ _number.append(int(_))
1568
+ elements_temp.append(_elem)
1569
+ elem_numbers_temp.append(_number)
1570
+ continue
1571
+ # Main iterations. Here each iteration would repeat the global information, e.g., cell, elements, numbers, etc.
1572
+ if data_verbosity > 1: # for verbosity == 1, only the last coords. will be output
1573
+ is_in_iter = re.match(Iter_pattern, line)
1574
+ if is_in_iter is not None:
1575
+ _samp_id = is_in_iter.group(1)
1576
+ for ii in range(n_batch):
1577
+ samp_id.append(f'{file_name}_{name[ii]}_iter{_samp_id}')
1578
+ cell.append(cell_temp[-n_batch + ii])
1579
+ elements.append(elements_temp[-n_batch + ii])
1580
+ elem_numbers.append(elem_numbers_temp[-n_batch + ii])
1581
+ continue
1582
+ # energy
1583
+ is_energy = re.match(E_pattern, line)
1584
+ if is_energy is not None:
1585
+ _ener = is_energy.group(1).split()
1586
+ for _ in _ener:
1587
+ energies.append(float(_))
1588
+ continue
1589
+ # coords
1590
+ is_coo = re.match(Coo_tag, line)
1591
+ if is_coo is not None:
1592
+ _coo = list()
1593
+ for _sub_line in data[i+2:]:
1594
+ is_empty = re.match(r'^\s*$', _sub_line)
1595
+ if is_empty is not None:
1596
+ if len(_coo[0]) == 0: continue # avoid multiple empty lines are read as coordinates
1597
+ _coo = np.asarray(_coo, dtype=np.float32)
1598
+ coo.append(_coo)
1599
+ fix.append(np.ones_like(_coo, dtype=np.int8))
1600
+ _coo = list()
1601
+ continue
1602
+ is_numbers = re.match(number_pattern, _sub_line)
1603
+ if is_numbers is not None:
1604
+ _numbers = is_numbers.group().split()
1605
+ _coo.append(_numbers)
1606
+ else:
1607
+ break # neither empty line nor numbers, thus skipping
1608
+ continue
1609
+ # forces
1610
+ is_forc = re.match(F_tag, line)
1611
+ if is_forc is not None:
1612
+ _forc = list()
1613
+ for _sub_line in data[i + 2:]:
1614
+ is_empty = re.match(r'^\s*$', _sub_line)
1615
+ if is_empty is not None:
1616
+ forces.append(np.asarray(_forc, dtype=np.float32))
1617
+ _forc = list()
1618
+ continue
1619
+ is_numbers = re.match(number_pattern, _sub_line)
1620
+ if is_numbers is not None:
1621
+ _numbers = is_numbers.group().split()
1622
+ _forc.append(_numbers)
1623
+ else:
1624
+ break # neither empty line nor numbers, thus skipping
1625
+ continue
1626
+ else: # only got the final coords & forces
1627
+ # energy
1628
+ is_energy = re.match(E_pattern, line)
1629
+ if is_energy is not None:
1630
+ _ener = is_energy.group(1).split()
1631
+ is_final = re.match(Final_Coo_tag, line)
1632
+ if is_final is not None:
1633
+ for ii in range(n_batch):
1634
+ samp_id.append(f'{file_name}_{name[ii]}')
1635
+ cell.append(cell_temp[-n_batch + ii])
1636
+ elements.append(elements_temp[-n_batch + ii])
1637
+ elem_numbers.append(elem_numbers_temp[-n_batch + ii])
1638
+ energies.append(float(_ener[ii]))
1639
+ _coo = list()
1640
+ for _sub_line in data[i + 2:]:
1641
+ is_empty = re.match(r'^\s*$', _sub_line)
1642
+ if is_empty is not None:
1643
+ if len(_coo[0]) == 0: continue # avoid multiple empty lines are read as coordinates
1644
+ _coo = np.asarray(_coo, dtype=np.float32)
1645
+ coo.append(_coo)
1646
+ fix.append(np.ones_like(_coo, dtype=np.int8))
1647
+ _coo = list()
1648
+ continue
1649
+ is_numbers = re.match(number_pattern, _sub_line)
1650
+ if is_numbers is not None:
1651
+ _numbers = is_numbers.group().split()
1652
+ _coo.append(_numbers)
1653
+ else:
1654
+ break # neither empty line nor numbers, thus skipping
1655
+ continue
1656
+
1657
+ is_forc = re.match(Final_F_tag, line)
1658
+ if is_forc is not None:
1659
+ _forc = list()
1660
+ for _sub_line in data[i + 2:]:
1661
+ is_empty = re.match(r'^\s*$', _sub_line)
1662
+ if is_empty is not None:
1663
+ forces.append(np.asarray(_forc, dtype=np.float32))
1664
+ _forc = list()
1665
+ continue
1666
+ is_numbers = re.match(number_pattern, _sub_line)
1667
+ if is_numbers is not None:
1668
+ _numbers = is_numbers.group().split()
1669
+ _forc.append(_numbers)
1670
+ else:
1671
+ break # neither empty line nor numbers, thus skipping
1672
+ continue
1673
+
1674
+ return samp_id, cell, elements, elem_numbers, energies, forces, coo, fix
1675
+
1676
+ except Exception as err:
1677
+ warnings.warn(f'An Error occurred when reading file {file_name}, skipped.\nError: {err}.')
1678
+ excp = traceback.format_exc()
1679
+ warnings.warn(f"Traceback:\n{excp}")
1680
+ return None