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,2261 @@
1
+ """
2
+ BatchStructure base class.
3
+ """
4
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
5
+ # Authors: Pu Pengxin, Song Xin
6
+ # Version: 0.9a
7
+ # File: BatchStructuresBase.py
8
+ # Environment: Python 3.12
9
+
10
+ import gc
11
+ import re
12
+ import logging
13
+ import pickle
14
+ import os
15
+ import random
16
+ import sys
17
+ import copy
18
+ import time
19
+ import traceback
20
+ import warnings
21
+ from itertools import accumulate
22
+ import hashlib
23
+ from typing import Dict, List, Literal, Optional, Sequence, Set, Tuple, Any, Iterable, Self
24
+
25
+ import joblib as jb
26
+ import numpy as np
27
+
28
+ from BUCToolkit.utils._para_flatt_list import flatten
29
+ from BUCToolkit.Preprocessing.write_files import WritePOSCARs, write_xyz, write_cif
30
+
31
+
32
+ class BatchStructures(object):
33
+ r"""
34
+ The base class of batch structures.
35
+ """
36
+
37
+ LIST_ATTR_NAME = (
38
+ '_Sample_ids',
39
+ 'Cells',
40
+ 'Coords_type',
41
+ 'Coords',
42
+ 'Fixed',
43
+ 'Elements',
44
+ 'Numbers',
45
+ 'Energies',
46
+ 'Forces',
47
+ 'Labels'
48
+ )
49
+
50
+ def __init__(self) -> None:
51
+ self._ALL_ELEMENTS = ('H', 'He',
52
+ 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
53
+ 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar',
54
+ 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr',
55
+ 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe',
56
+ 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
57
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn',
58
+ 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr',
59
+ 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg',) # element List
60
+ self._ATTR_NAMES = (
61
+ 'Sample_ids',
62
+ 'Batch_indices',
63
+ 'Elements_batch_indices',
64
+ 'Cells',
65
+ 'Coords_type',
66
+ 'Coords',
67
+ 'Fixed',
68
+ 'Elements',
69
+ 'Numbers',
70
+ 'Energies',
71
+ 'Forces',
72
+ 'Labels'
73
+ )
74
+ self._ATTR_TYPE = {
75
+ 'Sample_ids': '<U128',
76
+ 'Batch_indices': '<i8',
77
+ 'Elements_batch_indices': '<i8',
78
+ 'Cells': '<f4',
79
+ 'Coords_type': '<U2',
80
+ 'Coords': '<f4',
81
+ 'Fixed': '|i1',
82
+ 'Elements': '<U3',
83
+ 'Numbers': '<i4',
84
+ 'Energies': '<f4',
85
+ 'Forces': '<f4',
86
+ 'Labels': '|O'
87
+ }
88
+
89
+ self._Sample_ids = list()
90
+ self.Mode: Literal['A', 'L'] = 'L'
91
+ # Python List Version
92
+ # Structure part
93
+ self.Atom_list = None
94
+ self.Atomic_number_list = None
95
+ self.Cells: List[np.ndarray] = list() # cell vectors, float32
96
+ self.Coords_type: List[Literal['C', 'D']] = list() # coordinate type
97
+ self.Coords: List[np.ndarray] = list() # atomic coordinates, float32
98
+ self.Fixed: List[np.ndarray] = list() # fixed atoms masks, 0 for fixed and 1 for free. int8
99
+ self.Elements: List[List[str]] = list() # elements
100
+ self.Numbers: List[List[int]] = list() # atom number of each element. int4
101
+ # Properties part
102
+ self.Energies = None # energies of structures. None | List[float]
103
+ self.Forces = None # forces of structures. None | List[np.NdArray[N, 3]]
104
+ self.Dist_mat = None # distance matrices of structures. None | List[np.NdArray[N, N]]
105
+ # Others
106
+ self._indices = None # a dict of {sample_id: index}, str -> int.
107
+ Z_dict = {key: i for i, key in enumerate(self._ALL_ELEMENTS, 1)} # a dict which maps element symbols into their atomic numbers.
108
+ self._Z_dict = Z_dict
109
+ self.Labels = None # structure labels
110
+
111
+ # NumPy.NdArray Version
112
+ self._Sample_ids_: np.ndarray | None = None
113
+ # Batch info part
114
+ self.Batch_indices_: np.ndarray | None = None # storing the split point (ptr) in each array with n_atom length. It both concludes 0 and len(arr).
115
+ self.Elements_batch_indices_: np.ndarray | None = None # storing the split point (ptr) in each array with n_elem length. It both concludes 0 and len(arr).
116
+ # Structure part
117
+ self.Atom_list_ = None
118
+ self.Atomic_number_list_ = None
119
+ self.Cells_: np.ndarray | None = None # cell vectors
120
+ self.Coords_type_: np.ndarray[Literal['C', 'D']] | None = None # coordinate type
121
+ self.Coords_: np.ndarray | None = None # atomic coordinates
122
+ self.Fixed_: np.ndarray | None = None # fixed atoms masks, 0 for fixed and 1 for free.
123
+ self.Elements_: np.ndarray[str] | None = None # elements
124
+ self.Numbers_: np.ndarray[int] | None = None # atom number of each element
125
+ # Properties part
126
+ self.Energies_ = None # energies of structures. None | List[float]
127
+ self.Forces_ = None # forces of structures. None | List[np.NdArray[N, 3]]
128
+ self.Dist_mat_ = None # distance matrices of structures. None | List[np.NdArray[N, N]]
129
+ # Others
130
+ self.Labels_ = None # structure labels
131
+ # Security
132
+ self._checksum = None
133
+
134
+ # logging
135
+ self.logger = logging.getLogger('Main.BS')
136
+ self.logger.setLevel(logging.INFO)
137
+ formatter = logging.Formatter('%(message)s')
138
+
139
+ from BUCToolkit.utils.setup_loggers import has_any_handler
140
+ if not has_any_handler(self.logger):
141
+ log_handler = logging.StreamHandler(sys.stdout, )
142
+ log_handler.setLevel(logging.INFO)
143
+ log_handler.setFormatter(formatter)
144
+ self.logger.addHandler(log_handler)
145
+
146
+ @property
147
+ def Sample_ids(self, ):
148
+ return self._Sample_ids
149
+
150
+ @Sample_ids.setter
151
+ def Sample_ids(self, val):
152
+ raise ValueError('Sample_ids cannot be directly modified.')
153
+
154
+ @classmethod
155
+ def load_from_file(cls, path: str, data_slice: Tuple[int, int] | None = None) -> Self:
156
+ """
157
+ The class method version for loading data from saved files.
158
+ WARNING: It would overwrite the existing BatchStructure data. Use `self.load(..., mode='a')` to retain existing data.
159
+ Args:
160
+ path: the file path.
161
+ data_slice: The piece of data in files to be read. If None, all data in files would be read into memory.
162
+
163
+ Returns: Self with loaded data.
164
+
165
+ """
166
+ c = cls()
167
+ c.load(path, data_slice, 'w')
168
+ return c
169
+
170
+ def _generate_checksum_in_mem(self, ):
171
+ checksum = hashlib.sha256()
172
+ checksum.update(pickle.dumps(self))
173
+ return checksum.hexdigest()
174
+
175
+ def _hash_check_in_mem(self, ):
176
+ if self._checksum is None:
177
+ warnings.warn('Could not check data integrity due to missing checksum. It would be generate right now.', RuntimeWarning)
178
+ self._checksum = self._generate_checksum_in_mem()
179
+ else:
180
+ if self._checksum != self._generate_checksum_in_mem():
181
+ raise RuntimeError('Hash check failed. Data might be modified.')
182
+
183
+ def _clear_np(self, ):
184
+ """
185
+ Set all np.NdArray information to None.
186
+ """
187
+ self._Sample_ids_: np.ndarray | None = None
188
+ # Batch info part
189
+ self.Batch_indices_: np.ndarray | None = None
190
+ self.Elements_batch_indices_: np.ndarray | None = None
191
+ # Structure part
192
+ self.Atom_list_ = None
193
+ self.Atomic_number_list_ = None
194
+ self.Cells_: np.ndarray | None = None # cell vectors
195
+ self.Coords_type_: np.ndarray[Literal['C', 'D']] | None = None # coordinate type
196
+ self.Coords_: np.ndarray | None = list() # atomic coordinates
197
+ self.Fixed_: np.ndarray | None = list() # fixed atoms masks, 0 for fixed and 1 for free.
198
+ self.Elements_: np.ndarray[str] | None = None # elements
199
+ self.Numbers_: np.ndarray[int] | None = None # atom number of each element
200
+ # Properties part
201
+ self.Energies_ = None # energies of structures. None | List[float]
202
+ self.Forces_ = None # forces of structures. None | List[np.NdArray[N, 3]]
203
+ self.Dist_mat_ = None # distance matrices of structures. None | List[np.NdArray[N, N]]
204
+ # Others
205
+ self.Labels_ = None # structure labels
206
+
207
+ def _np2list(self, release_mem: bool = False):
208
+ """
209
+ Convert data from np.NdArray-format to python-List-format.
210
+ It hardly Consumes Memory.
211
+ """
212
+ self.Coords = np.split(self.Coords_, self.Batch_indices_[1:-1], )
213
+ self.Fixed = np.split(self.Fixed_, self.Batch_indices_[1:-1], )
214
+ if self.Forces_ is not None: self.Forces = np.split(self.Forces_, self.Batch_indices_[1:-1], )
215
+
216
+ #self.Numbers = np.split(self.Numbers_, self.Elements_batch_indices_[1:-1], )
217
+ #self.Elements = np.split(self.Elements_, self.Elements_batch_indices_[1:-1], )
218
+
219
+ self.Numbers = [
220
+ self.Numbers_[_indx: self.Elements_batch_indices_[i + 1]].tolist()
221
+ for i, _indx in enumerate(self.Elements_batch_indices_[:-1])
222
+ ]
223
+ self.Elements = [
224
+ self.Elements_[_indx: self.Elements_batch_indices_[i + 1]].tolist()
225
+ for i, _indx in enumerate(self.Elements_batch_indices_[:-1])
226
+ ]
227
+
228
+ self.Cells = [_ for _ in self.Cells_]
229
+
230
+ self._Sample_ids = self._Sample_ids_.tolist()
231
+ self.Coords_type = self.Coords_type_.tolist()
232
+ if self.Energies_ is not None: self.Energies = self.Energies_.tolist()
233
+ if self.Labels_ is not None: self.Labels = self.Labels_.tolist()
234
+
235
+ if release_mem:
236
+ self.Numbers_ = None
237
+ self.Elements_ = None
238
+ self._Sample_ids_ = None
239
+ self.Coords_type_ = None
240
+ self.Energies_ = None
241
+ self.Labels_ = None
242
+ gc.collect()
243
+
244
+ self.Mode = 'L'
245
+
246
+ def _list2np(self, release_mem: bool = False):
247
+ """
248
+ Convert data from python-List-format to np.NdArray-format.
249
+ It Consumes Memory.
250
+ """
251
+ self.Batch_indices_ = np.array([0] + list(accumulate([len(_) for _ in self.Coords])), dtype=np.int64)
252
+ #self.Batch_indices__ = np.apply_along_axis(lambda x: len(x), 0, self.Coords)
253
+ #self.Batch_indices__ = np.cumsum(self.Batch_indices__) - self.Batch_indices__
254
+ self.Elements_batch_indices_ = np.array([0] + list(accumulate([len(_) for _ in self.Numbers])), dtype=np.int64)
255
+ if release_mem:
256
+ # Batch_indices control the coords, atom_list, atomic_number_list, fixed, forces
257
+ self.Coords_ = np.concatenate(self.Coords, dtype=np.float32)
258
+ self.Coords = list()
259
+ self.Fixed_ = np.concatenate(self.Fixed, dtype=np.int8)
260
+ self.Fixed = list()
261
+ if self.Forces is not None:
262
+ self.Forces_ = np.concatenate(self.Forces, dtype=np.float32)
263
+ self.Forces = None
264
+ # Element_batch_indices control the Elements, Numbers
265
+ self.Numbers_ = np.asarray(flatten(self.Numbers, 1), dtype=np.int32)
266
+ self.Numbers = list()
267
+ self.Elements_ = np.asarray(flatten(self.Elements, 1), dtype='<U3')
268
+ self.Elements = list()
269
+ # Others are sequentially filled
270
+ self.Cells_ = np.asarray(self.Cells, dtype=np.float32)
271
+ self.Cells = None
272
+ self.Coords_type_ = np.asarray(self.Coords_type, dtype='<U2')
273
+ self.Coords_type = list()
274
+ self._Sample_ids_ = np.asarray(self._Sample_ids, dtype='<U128')
275
+ self._Sample_ids = list()
276
+ if self.Energies is not None:
277
+ self.Energies_ = np.asarray(self.Energies, dtype=np.float32)
278
+ self.Energies = None
279
+ if self.Labels is not None:
280
+ self.Labels_ = np.asarray(self.Labels)
281
+ self.Labels = None
282
+ gc.collect()
283
+ else:
284
+ # Batch_indices control the coords, atom_list, atomic_number_list, fixed, forces
285
+ self.Coords_ = np.concatenate(self.Coords, dtype=np.float32)
286
+ self.Fixed_ = np.concatenate(self.Fixed, dtype=np.int8)
287
+ if self.Forces is not None: self.Forces_ = np.concatenate(self.Forces, dtype=np.float32)
288
+ # Element_batch_indices control the Elements, Numbers
289
+ self.Numbers_ = np.asarray(flatten(self.Numbers), dtype=np.int32)
290
+ self.Elements_ = np.asarray(flatten(self.Elements), dtype='<U3')
291
+ # Others are sequentially filled
292
+ self.Cells_ = np.asarray(self.Cells, dtype=np.float32)
293
+ self.Coords_type_ = np.asarray(self.Coords_type, dtype='<U2')
294
+ self._Sample_ids_ = np.asarray(self._Sample_ids, dtype='<U128')
295
+ if self.Energies is not None: self.Energies_ = np.asarray(self.Energies, dtype=np.float32)
296
+ if self.Labels is not None: self.Labels_ = np.asarray(self.Labels)
297
+
298
+ self.Mode = 'A'
299
+
300
+ def change_mode(self, mode: Literal['A', 'L'], release_mem: bool = True):
301
+ """
302
+ Change BatchStructure mode.
303
+ Args:
304
+ mode: 'A' is 'array mode' that all data are stored as concatenated np.ndarray,
305
+ and two ptr arrays of split each structure's atom number are accordingly stored. Some methods in this mode are unavailable.
306
+ 'L' is 'list mode' that all data are stored as Lists of np.ndarray/List with (different) shapes of atom numbers.
307
+ release_mem: if True, the data of other mode will be released after change.
308
+
309
+ Returns: None
310
+
311
+ """
312
+ if mode == 'A':
313
+ if self.Mode == 'L':
314
+ self._list2np(release_mem=release_mem)
315
+ self.Mode = 'A'
316
+ elif mode == 'L':
317
+ if self.Mode == 'A':
318
+ self._np2list(release_mem=release_mem)
319
+ self.Mode = 'L'
320
+ else:
321
+ raise ValueError(f"'mode' must be 'A' or 'L', but got '{mode}'.")
322
+
323
+ def _update_indices(self, ):
324
+ if self.Mode == 'L':
325
+ self._indices = {_: ii for ii, _ in enumerate(self._Sample_ids)}
326
+ else:
327
+ self._indices = {_: ii for ii, _ in enumerate(self._Sample_ids_.tolist())}
328
+
329
+ def _check_id(self, ):
330
+ """ check whether duplication in Sample_ids """
331
+ self._update_indices()
332
+ if len(self._indices) != len(self):
333
+ raise RuntimeError(
334
+ f'There are duplicate names in the Sample_ids: {set([x for x in self._Sample_ids if self._Sample_ids.count(x) > 1])}'
335
+ )
336
+
337
+ def _check_len(self, ):
338
+ """ check whether the length of all prop. consistent. """
339
+ if self.Mode == 'A':
340
+ self._np2list()
341
+ is_convert = True
342
+ else:
343
+ is_convert = False
344
+
345
+ qe = True
346
+ qf = True
347
+ ql = True
348
+ q = (len(self.Coords) == len(self.Coords_type) == len(self.Numbers)
349
+ == len(self.Elements) == len(self._Sample_ids) == len(self.Coords_type) == len(self.Cells) == len(self.Fixed))
350
+ if self.Energies is not None: qe = (len(self.Energies) == len(self))
351
+ if self.Labels is not None: ql = (len(self.Labels) == len(self))
352
+ if self.Forces is not None: qf = (len(self.Forces) == len(self))
353
+
354
+ if not (q & qe & qf & ql): raise RuntimeError(
355
+ 'Sample size (numbers) of some properties does not match. PLEASE DOUBLE CHECK!\n'
356
+ 'If you did not manually modify any attributes in this class, '
357
+ 'please report this bug to us.\nSample size of all properties:'
358
+ f' Sample ids: {len(self._Sample_ids)}\n Coordinates: {len(self.Coords)}\n Coordinates type: {len(self.Coords_type)}\n'
359
+ f' Elements: {len(self.Elements)}\n Element Numbers: {len(self.Numbers)}\n Cells: {len(self.Cells)}\n Fixations: {len(self.Fixed)}\n'
360
+ f' Energies: {len(self.Energies) if self.Energies is not None else None}\n'
361
+ f' Forces: {len(self.Forces) if self.Forces is not None else None}\n'
362
+ f' Labels: {len(self.Labels) if self.Labels is not None else None}'
363
+ )
364
+ if is_convert:
365
+ self._list2np()
366
+
367
+ def check_full(self, ):
368
+ """
369
+ Fully check all data's information
370
+ Returns:
371
+
372
+ """
373
+ for ii, _data in enumerate(self):
374
+ try:
375
+ lcoo = _data.Coords[0].shape
376
+ lfix = _data.Fixed[0].shape
377
+ lelem = len(_data.Elements[0])
378
+ lnumb = len(_data.Numbers[0])
379
+ natm = sum(_data.Numbers[0])
380
+ # check coo
381
+ if lcoo[1] != 3 or len(lcoo) != 2:
382
+ self.logger.warning(f"Wrong coordinates shape of {ii}-th data ({_data.Sample_ids[0]}): {lcoo}. It should be (N, 3).")
383
+ continue
384
+ if lcoo != lfix:
385
+ self.logger.warning(
386
+ f"The {ii}-th data ({_data.Sample_ids[0]}) has inconsistent shape of coordinates and fixations: {lcoo} != {lfix}. "
387
+ )
388
+ continue
389
+ if lelem != lnumb:
390
+ self.logger.warning(
391
+ f"The {ii}-th data ({_data.Sample_ids[0]}) has inconsistent shape of elements and element numbers: {lelem} != {lnumb}. "
392
+ )
393
+ continue
394
+ if lcoo[0] != natm:
395
+ self.logger.warning(
396
+ f"The {ii}-th data ({_data.Sample_ids[0]}) has inconsistent atom number of coordinates and elements: {lcoo[0]} != {natm}. "
397
+ )
398
+ continue
399
+ if (_data.Forces is not None) and (_data.Forces[0].shape != lcoo):
400
+ self.logger.warning(
401
+ f"The {ii}-th data ({_data.Sample_ids[0]}) has inconsistent atom number of coordinates and forces: "
402
+ f"{lcoo} != {_data.Forces.shape}. "
403
+ )
404
+ continue
405
+
406
+ except Exception as e:
407
+ self.logger.warning(f"Unknown error occurred while checking {ii}-th data ({_data.Sample_ids[0]}): {e}.")
408
+
409
+ def save(self, path: str, mode: Literal['w', 'a'] = 'w'):
410
+ """
411
+ Saving this BatchStructures to a numpy memory-mapping file.
412
+ Each attribute in BatchStructures will be saved in the numpy memmap file with the same name.
413
+ Additionally, file 'head' will record the information which is data type & shape of all attributes, and record which attributes are `None`.
414
+
415
+ Args:
416
+ path: the saving directory.
417
+ mode: if 'w', create or overwrite the existing file for reading and writing. If 'a', data will append to the existing file.
418
+
419
+ Returns: None
420
+
421
+ """
422
+ # check files
423
+ _STANDARD_FILE_LIST = set(list(self._ATTR_NAMES) + ['head', ])
424
+ if mode not in {'w', 'a'}:
425
+ raise ValueError(f'Unknown `mode` value {mode}. It must be "w" or "a".')
426
+ if not os.path.isdir(path): # path do not exist
427
+ mode = 'w'
428
+ os.makedirs(path, )
429
+ elif len(os.listdir(path)) > 0:
430
+ if mode == 'a':
431
+ file_set = set(os.listdir(path))
432
+ assert file_set == _STANDARD_FILE_LIST, 'Existing files do not match the saving data.'
433
+ else:
434
+ for ff in os.listdir(path):
435
+ if ff not in _STANDARD_FILE_LIST:
436
+ raise RuntimeError(f'The file `{ff}` have already exist in given path.')
437
+ elif mode == 'a': # exist the path, but it is empty
438
+ mode = 'w'
439
+ # check self
440
+ if len(self) == 0:
441
+ self.logger.error('ERROR: BatchStructures are empty now!')
442
+ return None
443
+ self._update_indices()
444
+ self._check_id()
445
+ #self._check_len()
446
+
447
+ if self.Mode != 'A':
448
+ self._list2np()
449
+ has_conv = True
450
+ else:
451
+ has_conv = False
452
+ _temp_attr_list = [
453
+ self._Sample_ids_,
454
+ self.Batch_indices_,
455
+ self.Elements_batch_indices_,
456
+ self.Cells_,
457
+ self.Coords_type_,
458
+ self.Coords_,
459
+ self.Fixed_,
460
+ self.Elements_,
461
+ self.Numbers_,
462
+ self.Energies_,
463
+ self.Forces_,
464
+ self.Labels_
465
+ ]
466
+ if mode == 'w':
467
+ #############################################################################################################################
468
+ # head file: A pickle file containing the tag which attr. is None, the batch number, the shape of each attr., and the type of attr.
469
+ # head_info: Dict, {
470
+ # "which_None": Set[str], {_attr_names1, _attr_names2, ...},
471
+ # "n_batch": int, the number of samples,
472
+ # `_attr1`(str): Tuple[str, Tuple[int, ...]], (_dtype, _shape),
473
+ # `_attr2`(str): Tuple[str, Tuple[int, ...]], (_dtype, _shape),
474
+ # ...
475
+ # }
476
+ #############################################################################################################################
477
+ head_info = {'which_None': set(), 'n_batch': len(self)}
478
+ for i, filename in enumerate(self._ATTR_NAMES):
479
+ if _temp_attr_list[i] is not None:
480
+ _dtype = _temp_attr_list[i].dtype.str
481
+ _shape = _temp_attr_list[i].shape
482
+ else:
483
+ _dtype = '|O'
484
+ _shape = (1,)
485
+ head_info['which_None'].add(filename)
486
+ _data = np.memmap(os.path.join(path, filename), dtype=_dtype, mode='w+', offset=0, shape=_shape)
487
+ _data[:] = _temp_attr_list[i]
488
+ _data.flush()
489
+ del _data
490
+ gc.collect()
491
+ # head information: dtype shape
492
+ head_info[filename] = (_dtype, _shape)
493
+ with open(os.path.join(path, 'head'), 'wb') as head:
494
+ head_info = pickle.dumps(head_info)
495
+ head.write(head_info)
496
+ del _temp_attr_list
497
+
498
+ else: # mode == 'a', append
499
+ # Read head file
500
+ with open(os.path.join(path, 'head'), 'rb') as head:
501
+ head_info = pickle.load(head)
502
+ # rearrange batch indices & element batch indices (change it which start at 0 into starting at the last indices in old indices)
503
+ # batch indx.
504
+ bat_indx = np.memmap(
505
+ os.path.join(path, 'Batch_indices'),
506
+ dtype=head_info['Batch_indices'][0],
507
+ mode='r',
508
+ shape=head_info['Batch_indices'][1]
509
+ )
510
+ last_indx = bat_indx[-1].copy()
511
+ del bat_indx
512
+ _temp_attr_list[1] = _temp_attr_list[1][1:] + last_indx
513
+ # elem. indx.
514
+ elem_indx = np.memmap(
515
+ os.path.join(path, 'Elements_batch_indices'),
516
+ dtype=head_info['Elements_batch_indices'][0],
517
+ mode='r',
518
+ shape=head_info['Elements_batch_indices'][1]
519
+ )
520
+ last_indx = elem_indx[-1].copy()
521
+ _temp_attr_list[2] = _temp_attr_list[2][1:] + last_indx
522
+ del elem_indx, last_indx
523
+ # Append to file
524
+ new_head_info = {'which_None': set()}
525
+ for i, filename in enumerate(self._ATTR_NAMES):
526
+ old_dtype = head_info[filename][0]
527
+ old_shape = head_info[filename][1]
528
+ if _temp_attr_list[i] is not None:
529
+ if filename in head_info['which_None']:
530
+ raise ValueError(f'The existing data {filename} is None, while appending data is not. The type of both should match.')
531
+ _dtype = _temp_attr_list[i].dtype.str
532
+ _shape = _temp_attr_list[i].shape
533
+ else:
534
+ if filename not in head_info['which_None']:
535
+ raise ValueError(f'The existing data {filename} is not None, while appending data is None. The type of both should match.')
536
+ _dtype = '|O'
537
+ _shape = (1,)
538
+ new_head_info['which_None'].add(filename)
539
+ # check the consistency of old & new data
540
+ if old_shape[1:] != _shape[1:]:
541
+ raise RuntimeError(f'The existing file has a shape on non-1st dim {old_shape[1:]} '
542
+ f'does not match the new data {_temp_attr_list[i].shape[1:]}')
543
+ if old_dtype != _dtype:
544
+ raise RuntimeError(f'The data type of existing file is {old_dtype} '
545
+ f'which is not match the new data type {_temp_attr_list[i].dtype}')
546
+
547
+ # write data
548
+ if _temp_attr_list[i] is not None:
549
+ new_shape = tuple([old_shape[0] + _temp_attr_list[i].shape[0]] + list(_temp_attr_list[i].shape[1:]))
550
+ _data = np.memmap(os.path.join(path, filename), dtype=old_dtype, mode='r+', shape=new_shape)
551
+ _data[old_shape[0]:] = _temp_attr_list[i]
552
+ _data.flush()
553
+ del _data
554
+ gc.collect()
555
+ new_head_info[filename] = (old_dtype, new_shape)
556
+ else:
557
+ new_head_info[filename] = head_info[filename]
558
+
559
+ # Update the head file
560
+ new_head_info['n_batch'] = head_info['n_batch'] + len(self)
561
+ with open(os.path.join(path, 'head'), 'wb') as head:
562
+ head_info = pickle.dumps(new_head_info)
563
+ head.write(head_info)
564
+ del _temp_attr_list
565
+ if has_conv:
566
+ self._np2list(True)
567
+ gc.collect()
568
+ return None
569
+
570
+ def load(self, path: str, data_slice: Tuple[int, int] | None = None, mode: Literal['w', 'a'] = 'w'):
571
+ """
572
+ Load data from saved files.
573
+ Args:
574
+ path: the file path.
575
+ data_slice: The piece of data in files to be read. If None, all data in files would be read into memory.
576
+ mode: the data load mode. 'W' for write or overwrite existing data from data in file; 'a' for appending data in file to existing data.
577
+
578
+ Returns: None
579
+
580
+ """
581
+ # check vars
582
+ if data_slice is not None:
583
+ assert isinstance(data_slice[0], int) and isinstance(data_slice[1], int), (f"`data_slice` must be an integer tuple, "
584
+ f"but got ({type(data_slice[0])}, {type(data_slice[1])})")
585
+ assert data_slice[0] <= data_slice[1], f"`data_slice[0]` cannot be greater than `data_slice[1]`, but got {data_slice}"
586
+ # Read the head file
587
+ with open(os.path.join(path, 'head'), 'rb') as head:
588
+ head_info = pickle.load(head)
589
+ # Read mmap file
590
+ _data_dict = dict()
591
+ if data_slice is None:
592
+ for i, filename in enumerate(self._ATTR_NAMES):
593
+ _dtype, _shape = head_info[filename]
594
+ if filename not in head_info['which_None']:
595
+ _mmdata = np.memmap(os.path.join(path, filename), dtype=_dtype, mode='r', shape=_shape)
596
+ _data_dict[filename] = np.array(_mmdata)
597
+ del _mmdata
598
+ gc.collect()
599
+ else:
600
+ _data_dict[filename] = None
601
+ else:
602
+ #if data_slice[1] > head_info['n_batch']: raise ValueError(f'`data_slice` out of range. Sample number: {head_info['n_batch']}')
603
+ _dtype, _shape = head_info['Batch_indices']
604
+ natom_slice = np.memmap(os.path.join(path, 'Batch_indices'), dtype=_dtype, mode='r', shape=_shape)
605
+ natom_slice = np.array(natom_slice[data_slice[0]: data_slice[1] + 1])
606
+ _dtype, _shape = head_info['Elements_batch_indices']
607
+ elem_slice = np.memmap(os.path.join(path, 'Elements_batch_indices'), dtype=_dtype, mode='r', shape=_shape)
608
+ elem_slice = np.array(elem_slice[data_slice[0]: data_slice[1] + 1])
609
+ for filename in ('Coords', 'Fixed', 'Forces'):
610
+ if filename not in head_info['which_None']:
611
+ _dtype, _shape = head_info[filename]
612
+ _mmdata = np.memmap(os.path.join(path, filename), dtype=_dtype, mode='r', shape=_shape)
613
+ _data_dict[filename] = np.array(_mmdata[natom_slice[0]:natom_slice[-1]])
614
+ del _mmdata
615
+ gc.collect()
616
+ else:
617
+ _data_dict[filename] = None
618
+ for filename in ('Elements', 'Numbers'):
619
+ if filename not in head_info['which_None']:
620
+ _dtype, _shape = head_info[filename]
621
+ _mmdata = np.memmap(os.path.join(path, filename), dtype=_dtype, mode='r', shape=_shape)
622
+ _data_dict[filename] = np.array(_mmdata[elem_slice[0]:elem_slice[-1]])
623
+ del _mmdata
624
+ gc.collect()
625
+ else:
626
+ _data_dict[filename] = None
627
+ for filename in ('Sample_ids', 'Cells', 'Coords_type', 'Energies', 'Labels'):
628
+ if filename not in head_info['which_None']:
629
+ _dtype, _shape = head_info[filename]
630
+ _mmdata = np.memmap(os.path.join(path, filename), dtype=_dtype, mode='r', shape=_shape)
631
+ _data_dict[filename] = np.array(_mmdata[data_slice[0]:data_slice[1]])
632
+ del _mmdata
633
+ gc.collect()
634
+ else:
635
+ _data_dict[filename] = None
636
+ _data_dict['Batch_indices'] = natom_slice - natom_slice[0]
637
+ _data_dict['Elements_batch_indices'] = elem_slice - elem_slice[0]
638
+
639
+ # load to memory
640
+ if mode == 'w':
641
+ self._Sample_ids_ = _data_dict['Sample_ids']
642
+ self.Batch_indices_ = _data_dict['Batch_indices']
643
+ self.Elements_batch_indices_ = _data_dict['Elements_batch_indices']
644
+ self.Cells_ = _data_dict['Cells']
645
+ self.Coords_type_ = _data_dict['Coords_type']
646
+ self.Coords_ = _data_dict['Coords']
647
+ self.Fixed_ = _data_dict['Fixed']
648
+ self.Elements_ = _data_dict['Elements']
649
+ self.Numbers_ = _data_dict['Numbers']
650
+ self.Energies_ = _data_dict['Energies']
651
+ self.Forces_ = _data_dict['Forces']
652
+ self.Labels_ = _data_dict['Labels']
653
+ if self.Mode == 'L':
654
+ self._np2list(release_mem=True)
655
+ self._clear_np()
656
+ else:
657
+ subcls = BatchStructures()
658
+ subcls._Sample_ids_ = _data_dict['Sample_ids']
659
+ subcls.Batch_indices_ = _data_dict['Batch_indices']
660
+ subcls.Elements_batch_indices_ = _data_dict['Elements_batch_indices']
661
+ subcls.Cells_ = _data_dict['Cells']
662
+ subcls.Coords_type_ = _data_dict['Coords_type']
663
+ subcls.Coords_ = _data_dict['Coords']
664
+ subcls.Fixed_ = _data_dict['Fixed']
665
+ subcls.Elements_ = _data_dict['Elements']
666
+ subcls.Numbers_ = _data_dict['Numbers']
667
+ subcls.Energies_ = _data_dict['Energies']
668
+ subcls.Forces_ = _data_dict['Forces']
669
+ subcls.Labels_ = _data_dict['Labels']
670
+ subcls._np2list()
671
+ if self.Mode == 'L':
672
+ self.append(subcls)
673
+ del subcls
674
+ else:
675
+ self._np2list(release_mem=True)
676
+ self._clear_np()
677
+ self.append(subcls)
678
+ self._list2np(release_mem=True)
679
+ del subcls
680
+ gc.collect()
681
+ self._update_indices()
682
+ self._check_id()
683
+ self._check_len()
684
+
685
+ def write2text(
686
+ self,
687
+ output_path: str = './',
688
+ indices: int | str | List[int] | Tuple[int, int] | None = None,
689
+ file_format: Literal['POSCAR', 'cif', 'xyz', 'xyz_forces'] = 'POSCAR',
690
+ file_name_list: str | Sequence[str] | None = None,
691
+ n_core: int = -1
692
+ ) -> None:
693
+ """
694
+ Write selected structures to text files in given format.
695
+ Args:
696
+ indices: the selection indices of `self`. If Tuple, structures between `indices[0]` and `indices[1]` will be selected.
697
+ file_format: the format of written files.
698
+ 'POSCAR': vasp POSCAR format
699
+ 'cif': crystallographic information file
700
+ 'xyz': ext-xyz file that only contains atomic positions
701
+ 'xyz_forces': ext-xyz file that contains atomic positions and forces
702
+ output_path: the directory of output file.
703
+ file_name_list: the list of file names. If None, it would be set to `Sample_ids`.
704
+ n_core: number of CPU cores to write in parallel. `-1` for all available CPU cores.
705
+
706
+ Returns: None
707
+
708
+ """
709
+ # check vars
710
+ if file_format not in {'POSCAR', 'cif', 'xyz', 'xyz_forces'}:
711
+ raise ValueError(f'Invalid value of `file_format`: {file_format}.')
712
+ if indices is None:
713
+ sub_self = self
714
+ elif isinstance(indices, Tuple):
715
+ sub_self = self[indices[0]: indices[1]]
716
+ else:
717
+ sub_self = self[indices]
718
+ if file_name_list is None:
719
+ file_name_list = sub_self.Sample_ids
720
+ self._check_id()
721
+ self._check_len()
722
+ if self.Mode == 'A':
723
+ self._np2list()
724
+ is_convert = True
725
+ else:
726
+ is_convert = False
727
+ if not os.path.isdir(output_path):
728
+ os.makedirs(output_path)
729
+
730
+ try:
731
+ file_name_list = [f'{_}' for _ in file_name_list]
732
+ if file_format == 'POSCAR':
733
+ WritePOSCARs(
734
+ sub_self.Cells,
735
+ sub_self.Coords,
736
+ sub_self.Elements,
737
+ sub_self.Numbers,
738
+ sub_self.Fixed,
739
+ output_path,
740
+ file_name_list,
741
+ sub_self.Sample_ids,
742
+ sub_self.Coords_type,
743
+ n_core
744
+ )
745
+ elif file_format == 'xyz':
746
+ write_xyz(
747
+ sub_self.Elements,
748
+ sub_self.Coords,
749
+ sub_self.Cells,
750
+ sub_self.Energies,
751
+ sub_self.Numbers,
752
+ sub_self.Forces,
753
+ output_path,
754
+ file_name_list,
755
+ output_xyz_type='only_position_xyz',
756
+ n_core=n_core
757
+ )
758
+ elif file_format == 'xyz_forces':
759
+ write_xyz(
760
+ sub_self.Elements,
761
+ sub_self.Coords,
762
+ sub_self.Cells,
763
+ sub_self.Energies,
764
+ sub_self.Numbers,
765
+ sub_self.Forces,
766
+ output_path,
767
+ file_name_list,
768
+ output_xyz_type='write_position_and_force',
769
+ n_core=n_core
770
+ )
771
+ elif file_format == 'cif':
772
+ write_cif(
773
+ sub_self.Cells,
774
+ sub_self.Coords,
775
+ sub_self.Elements,
776
+ sub_self.Numbers,
777
+ output_path,
778
+ file_name_list,
779
+ sub_self.Sample_ids,
780
+ sub_self.Coords_type,
781
+ n_core
782
+ )
783
+
784
+ else:
785
+ raise NotImplementedError
786
+ except Exception as e:
787
+ self.logger.error(f'An error occurred:\n\t{e}\ntraceback:\n\t{traceback.print_exc()}')
788
+ finally:
789
+ if is_convert:
790
+ self._list2np(release_mem=True)
791
+
792
+ def set_Labels(self, val: Sequence | Dict | np.ndarray) -> None:
793
+ """
794
+ Set or reset self.Labels from input `val`.
795
+ Args:
796
+ val: the input values of self.Labels to set.
797
+ If Dict, val: {sample_id: energy, ...}; if Sequence, it must have the same order of self.Sample_ids.
798
+
799
+ Returns: None
800
+
801
+ """
802
+ if len(val) != len(self):
803
+ raise ValueError(f'labels (length{len(val)}) must have the same number of structures (length: {len(self)}).')
804
+ if isinstance(val, Dict):
805
+ try:
806
+ self.Labels = [val[k_] for k_ in self._Sample_ids]
807
+ except KeyError as e:
808
+ raise KeyError(f'Some keys: {e} in Sample_ids do not in labels.')
809
+ elif isinstance(val, Sequence):
810
+ self.Labels = list(val)
811
+ elif isinstance(val, np.ndarray):
812
+ self.Labels = val.tolist()
813
+ else:
814
+ raise ValueError('Unknown type of input labels.')
815
+
816
+ def set_Energies(self, val: Sequence | Dict):
817
+ """
818
+ Set or reset self.Energies from input `val`.
819
+ Args:
820
+ val: the input values to set self.Energies
821
+ if Dict, val: {sample_id: energy, ...}; if Sequence, it must have the same order of self.Sample_ids.
822
+
823
+ Returns: None
824
+
825
+ """
826
+ if len(val) != len(self):
827
+ raise ValueError(f'labels (length: {len(val)}) must have the same number of structures (length: {len(self)}).')
828
+ if isinstance(val, Dict):
829
+ try:
830
+ self.Energies = [val[k_] for k_ in self._Sample_ids]
831
+ except KeyError as e:
832
+ raise KeyError(f'Some keys: {e} in Sample_ids do not in val.')
833
+ elif isinstance(val, Sequence):
834
+ self.Energies = list(val)
835
+ elif isinstance(val, np.ndarray):
836
+ self.Energies = val.tolist()
837
+ else:
838
+ raise ValueError('Unknown type of input val.')
839
+
840
+ def set_Forces(self, val: Sequence[np.ndarray] | Dict[str, np.ndarray]):
841
+ """
842
+ Set or reset self.Forces from `val`.
843
+ Args:
844
+ val: the input values to set self.Forces
845
+ if Dict, val: {sample_id: forces, ...}; if Sequence, it must have the same order of self.Sample_ids.
846
+
847
+ Returns:
848
+
849
+ """
850
+ if len(val) != len(self):
851
+ raise ValueError(f'labels (length{len(val)}) must have the same number of structures (length: {len(self)}).')
852
+ if isinstance(val, Dict):
853
+ try:
854
+ self.Forces = [val[k_] for k_ in self._Sample_ids]
855
+ except KeyError as e:
856
+ raise KeyError(f'Some keys: {e} in Sample_ids do not in val.')
857
+ elif isinstance(val, Sequence):
858
+ self.Forces = list(val)
859
+ else:
860
+ raise ValueError('Unknown type of input val.')
861
+ # check
862
+ for i, forces in enumerate(self.Forces):
863
+ if forces.shape != self.Coords[i].shape:
864
+ self.Forces = None
865
+ raise ValueError('Some forces data do not have the same shape of corresponding coordinates.')
866
+
867
+ def generate_dist_mat(self, supercell_indices=np.zeros((1, 3))):
868
+ r"""
869
+ Parameters:
870
+ supercell_indices: NDArray[n_supercell, 3], the indices of supercells that calculate distance.
871
+
872
+ self.Dist_mat Format: List[array[(n_prim_cells, n_atom, n_atom)]]
873
+ """
874
+ self.Dist_mat = list()
875
+ for i, atomic_coordinates in enumerate(self.Coords):
876
+ cell_vectors: np.ndarray = self.Cells[i]
877
+ # calculate cross-cell dist.; cell_diff = supercell_indices @ cell_vec; <<<
878
+ # shape: (n_prim_cells, 1, 3)@(1, 3, 3) -> (n_prim_cells, 1, 3)
879
+ cell_diff = (supercell_indices[:, None, :]) @ (cell_vectors[None, :, :])
880
+ # convert direct to cartesian
881
+ if self.Coords_type[i] == 'D':
882
+ atomic_coordinates = atomic_coordinates @ cell_vectors
883
+ # calculate the atom coords across cells (x_j + R_k) <<<
884
+ # shape: (1, n_atom, 3) + (n_prim_cells, 1, 3)
885
+ # -> (n_prim_cells, n_atom, 3)
886
+ coord_cross = atomic_coordinates[None, :, :] + cell_diff
887
+ n_prim_cells, n_atom, _ = coord_cross.shape
888
+ # calculate actual dist. vec.; dist_vec = r_ijk = ||x_i - (x_j + R_k)|| where R_k is the cell vector; <<<
889
+ # shape: (1, n_atom, 1, 3) - (n_prim_cells, 1, n_atom, 3)
890
+ # -> (n_prim_cells, n_atom, n_atom, 3) # coord_diff
891
+ # -norm-> (n_prim_cells, n_atom, n_atom) # euclid distance
892
+ distance = atomic_coordinates[None, :, None, :] - coord_cross[:, None, :, :]
893
+ distance = np.linalg.norm(distance, ord=2, axis=-1)
894
+ self.Dist_mat.append(distance)
895
+
896
+ def generate_atom_list(self, force_update: bool = False) -> None:
897
+ r"""
898
+ Generate an attribute self.Atom_list (List[str]) which is the atom lists in order of the atomic coordinates sequence.
899
+
900
+ Parameters:
901
+ force_update: bool. if False, Atom_list would not be updated when it's not None.
902
+ """
903
+ if (self.Atom_list is None) or force_update:
904
+ self.Atom_list = list()
905
+ for i, list_ in enumerate(self.Elements):
906
+ self.Atom_list.append([])
907
+ for j, elem in enumerate(list_):
908
+ self.Atom_list[i] += [elem, ] * self.Numbers[i][j]
909
+
910
+ elif isinstance(self.Atom_list, Sequence):
911
+ warnings.warn('Atom_list already existed, so it would not be updated.')
912
+ else:
913
+ raise RuntimeError(f'An Unknown Atom_list occurred. Atom_list == {self.Atom_list}')
914
+
915
+ def generate_atomic_number_list(self, force_update: bool = False) -> None:
916
+ r"""
917
+ Generate attribute self.Atomic_number_list (List[int]) which is the atomic number lists in order of the sequence of atom coordinates.
918
+
919
+ Parameters:
920
+ force_update: bool. if False, Atomic_number_list would not be updated when it's not None.
921
+ """
922
+ if (self.Atomic_number_list is None) or force_update:
923
+ if self.Atom_list is None:
924
+ self.Atom_list = list()
925
+ for i, list_ in enumerate(self.Elements):
926
+ self.Atom_list.append([])
927
+ for j, elem in enumerate(list_):
928
+ self.Atom_list[i] += [elem, ] * self.Numbers[i][j]
929
+
930
+ self.Atomic_number_list = list()
931
+ for i, list_ in enumerate(self.Atom_list):
932
+ self.Atomic_number_list.append([self._Z_dict[symb] for symb in list_])
933
+ elif isinstance(self.Atomic_number_list, Sequence):
934
+ warnings.warn('Atomic_number_list already existed, so it would not be updated.')
935
+ else:
936
+ raise RuntimeError(f'An Unknown Atomic_number_list occurred. Atomic_number_list == {self.Atomic_number_list}')
937
+
938
+ def cartesian2direct(self, ):
939
+ """ Convert Cartesian coordinates to Direct coordinates. Only work in 'L' Mode. """
940
+ assert self.Mode == 'L'
941
+ for i, cootype in enumerate(self.Coords_type):
942
+ if cootype == 'C':
943
+ cell = self.Cells[i]
944
+ self.Coords[i] @= np.linalg.inv(cell)
945
+ self.Coords_type[i] = 'D'
946
+
947
+ def direct2cartesian(self, ):
948
+ """ Convert Direct coordinates to Cartesian coordinates. Only work in 'L' Mode. """
949
+ assert self.Mode == 'L'
950
+ for i, cootype in enumerate(self.Coords_type):
951
+ if cootype == 'D':
952
+ cell = self.Cells[i]
953
+ self.Coords[i] @= cell
954
+ self.Coords_type[i] = 'C'
955
+
956
+ def sort_ids(self, ref_attr: str = 'Sample_ids', reverse: bool = False):
957
+ """
958
+ Sort structures by their Sample_ids order.
959
+ Args:
960
+ ref_attr: str, the attribute to be the reference for structure sort.
961
+ reverse: bool. if True, the sort order is reversed, i.e., the descending order.
962
+ Returns: None
963
+
964
+ """
965
+ if self.Mode == 'A':
966
+ raise NotImplementedError(f"Sort for Array mode is not implemented yet.")
967
+ lst = getattr(self, ref_attr, None)
968
+ if lst is None:
969
+ raise ValueError(f"Attribute '{ref_attr}' not found.")
970
+ _sort_idx = sorted(range(len(lst)), key=lambda i: lst[i], reverse=reverse)
971
+
972
+ for attr_name in self.LIST_ATTR_NAME:
973
+ orig_attr = getattr(self, attr_name)
974
+ if orig_attr is None:
975
+ continue
976
+ setattr(self, attr_name, [orig_attr[_] for _ in _sort_idx])
977
+
978
+
979
+ def standardize(self, ):
980
+ """
981
+ Standardize the atomic coordinates, i.e., translation the centre of all coordinates to their lattice centre.
982
+ Only work in 'L' Mode.
983
+
984
+ Returns: None
985
+ """
986
+ # TODO, standardize coo
987
+ raise NotImplementedError('Standardize not implemented yet.')
988
+ _CELL_CENTRE = np.asarray([0.5, 0.5, 0.5])
989
+ for i, coo in enumerate(self.Coords):
990
+ if self.Coords_type[i] == 'C': # cartesian
991
+ # shape center
992
+ c1 = np.mean(coo, axis=0, keepdims=True)
993
+ # check out of cell
994
+ fc1 = c1 @ np.linalg.inv(self.Cells[i])
995
+ if np.any((fc1 < 0.) | (fc1 > 1.)):
996
+ self.logger.warning(f'Center out of cell in {self.Sample_ids[i]}')
997
+ fc1 -= np.floor(fc1)
998
+ self.Coords[i] = fc1 @ self.Cells[i]
999
+ lc1 = _CELL_CENTRE @ self.Cells[i]
1000
+ self.Coords[i] -= c1 - lc1 # (n_atom, 3) - (1, 3)
1001
+ elif self.Coords_type[i] == 'D': # frac
1002
+ # shape center
1003
+ fc1 = np.mean(coo, axis=0, keepdims=True)
1004
+ if np.any((fc1 < 0.) | (fc1 > 1.)):
1005
+ self.logger.warning(f'Center out of cell in {self.Sample_ids[i]}')
1006
+ fc1 -= np.floor(fc1)
1007
+ self.Coords[i] -= fc1 - _CELL_CENTRE # (n_atom, 3) - (1, 3)
1008
+
1009
+ def shuffle(self, seed:int = None):
1010
+ """
1011
+ Shuffle the samples with given random seed.
1012
+ Args:
1013
+ seed: random seed.
1014
+
1015
+ Returns: None
1016
+
1017
+ """
1018
+ # generate indices
1019
+ indx = list(range(len(self)))
1020
+ random.seed(seed)
1021
+ random.shuffle(indx)
1022
+ self.rearrange(indx)
1023
+
1024
+ def rearrange(self, indices: List[int]):
1025
+ """
1026
+ Rearrange the order of samples by given indices.
1027
+ Args:
1028
+ indices: indices of sample orders.
1029
+
1030
+ Returns: None
1031
+
1032
+ """
1033
+ indx = list(indices)
1034
+ if self.Energies is not None:
1035
+ self.Energies = [self.Energies[_] for _ in indx]
1036
+ if self.Forces is not None:
1037
+ self.Forces = [self.Forces[_] for _ in indx]
1038
+ if self.Labels is not None:
1039
+ self.Labels = [self.Labels[_] for _ in indx]
1040
+
1041
+ self._Sample_ids = [self._Sample_ids[_] for _ in indx] # mp_ids
1042
+ self.Cells = [self.Cells[_] for _ in indx] # Lattice parameters
1043
+ self.Elements = [self.Elements[_] for _ in indx] # Element symbols
1044
+ self.Numbers = [self.Numbers[_] for _ in indx] # Element numbers
1045
+ self.Coords_type = [self.Coords_type[_] for _ in indx] # coordinates type
1046
+ self.Coords = [self.Coords[_] for _ in indx] # Atom coordinates
1047
+ self.Fixed = [self.Fixed[_] for _ in indx] # fixed masks
1048
+ self._check_id()
1049
+ self._check_len()
1050
+
1051
+ def fix_atoms_by_height(self, height:float, coord_type: Literal['C', 'D']='C', direction: Literal['x', 'y', 'z'] = 'z'):
1052
+ """
1053
+ Fix atoms by height at the given direction.
1054
+ All atoms with coordinates lower than the given height will be fixed.
1055
+ Returns: None
1056
+
1057
+ """
1058
+ _DIRECT_DICT = {'x': slice(None, 1), 'y': slice(1, 2), 'z': slice(-1, None)}
1059
+ if coord_type == 'C':
1060
+ self.direct2cartesian()
1061
+ elif coord_type == 'D':
1062
+ self.cartesian2direct()
1063
+ else:
1064
+ self.logger.error(f'ERROR: Unknown coord_type `{coord_type}`.')
1065
+ return
1066
+
1067
+ if direction not in _DIRECT_DICT:
1068
+ self.logger.error(f'ERROR: Unknown direction `{direction}`.')
1069
+ return
1070
+
1071
+ i = 0
1072
+ try:
1073
+ for i, m in enumerate(self.Coords):
1074
+ mask = np.where(self.Coords[i][:, _DIRECT_DICT[direction]] <= height, 0, 1)
1075
+ mask = np.repeat(mask, 3, axis=-1)
1076
+ self.Fixed[i] = mask.astype(np.int8)
1077
+ except Exception as e:
1078
+ self.logger.error(f'ERROR: Fixation failed in the {i}-th structure:')
1079
+ self.logger.error(e)
1080
+ self.logger.error(traceback.format_exc())
1081
+
1082
+ def revise(
1083
+ self,
1084
+ index: int|str,
1085
+ rev_Sample_ids: None | str = None,
1086
+ rev_Cells: None | np.ndarray = None,
1087
+ rev_Elements: None | List[str] = None,
1088
+ rev_Numbers: None | List[int] = None,
1089
+ rev_Coords_type: None | Literal['C', 'D'] = None,
1090
+ rev_Coords: None | np.ndarray = None,
1091
+ rev_Fixed: None | np.ndarray = None,
1092
+ rev_Energies: None | float = None,
1093
+ rev_Forces: None | np.ndarray = None,
1094
+ rev_Labels: Any | None = None,
1095
+ ) -> None:
1096
+ """
1097
+ Revising data of given `index`. None is for not changing.
1098
+ Args:
1099
+ index: the index of structure to revise.
1100
+ rev_Sample_ids: Sample ids of structure to revise.
1101
+ rev_Cells: Cell ids of structure to revise.
1102
+ rev_Elements: Elements ids of structure to revise.
1103
+ rev_Numbers: Numbers of structure to revise.
1104
+ rev_Coords_type: Coords of structure to revise.
1105
+ rev_Coords: Coords of structure to revise.
1106
+ rev_Fixed: Fixation mask of atoms to revise.
1107
+ rev_Energies: Energies of structure to revise.
1108
+ rev_Forces: Forces of structure to revise.
1109
+ rev_Labels: Labels of structure to revise.
1110
+
1111
+ """
1112
+ assert self.Mode == 'L', f'Mode "A" does not support yet.'
1113
+ if isinstance(index, str):
1114
+ if self._indices is None:
1115
+ self._update_indices()
1116
+ _indx:int = self._indices[index]
1117
+ elif isinstance(index, int):
1118
+ _indx:int = index
1119
+ else:
1120
+ raise TypeError(f'Expected `index` to be int or str, but got {type(index)}.')
1121
+ # check types
1122
+ if rev_Sample_ids is not None:
1123
+ assert isinstance(rev_Sample_ids, type(self._Sample_ids[0]))
1124
+ else:
1125
+ rev_Sample_ids = self._Sample_ids[_indx]
1126
+ if rev_Cells is not None:
1127
+ assert isinstance(rev_Cells, np.ndarray)
1128
+ assert rev_Cells.shape == (3, 3)
1129
+ else:
1130
+ rev_Cells = self.Cells[_indx]
1131
+ if rev_Elements is not None:
1132
+ assert isinstance(rev_Elements, List)
1133
+ else:
1134
+ rev_Elements = self.Elements[_indx]
1135
+ if rev_Numbers is not None:
1136
+ assert isinstance(rev_Numbers, List)
1137
+ else:
1138
+ rev_Numbers = self.Numbers[_indx]
1139
+ if rev_Coords_type is not None:
1140
+ assert rev_Coords_type in {'C', 'D'}, f'rev_Coords_type must be either "C" or "D", but got {rev_Coords_type}.'
1141
+ else:
1142
+ rev_Coords_type = self.Coords_type[_indx]
1143
+ if rev_Coords is not None:
1144
+ assert isinstance(rev_Coords, np.ndarray)
1145
+ else:
1146
+ rev_Coords = self.Coords[_indx]
1147
+ if rev_Fixed is not None:
1148
+ assert isinstance(rev_Fixed, np.ndarray)
1149
+ assert np.all(rev_Fixed - 2 < 0) and np.all(rev_Fixed >= 0), '`Fixed` must be either 0 or 1'
1150
+ else:
1151
+ rev_Fixed = self.Fixed[_indx]
1152
+ if rev_Energies is None:
1153
+ if self.Energies is not None: rev_Energies = self.Energies[_indx]
1154
+ elif self.Energies is None:
1155
+ warnings.warn('`rev_Energies` is given while `self.Energies` is None. Hence `rev_Energies` will be skipped.', RuntimeWarning)
1156
+ if rev_Forces is None:
1157
+ if self.Forces is not None:
1158
+ rev_Forces = self.Forces[_indx]
1159
+ force_shape = rev_Forces.shape
1160
+ else:
1161
+ force_shape = rev_Coords.shape
1162
+ else:
1163
+ assert isinstance(rev_Forces, np.ndarray)
1164
+ if self.Forces is None:
1165
+ warnings.warn('`rev_Forces` is given while `self.Forces` is None. Hence `rev_Force` will be skipped.', RuntimeWarning)
1166
+ force_shape = rev_Forces.shape
1167
+ if rev_Labels is None:
1168
+ if not self.Labels is None: rev_Labels = self.Labels[_indx]
1169
+ elif self.Labels is None:
1170
+ warnings.warn('`rev_Labels` is given while `self.Labels` is None. Hence `rev_Labels` will be skipped.', RuntimeWarning)
1171
+ # check numbers
1172
+ assert (rev_Coords.shape == rev_Fixed.shape == force_shape), f'Coords, Fixed, and Forces (optional) have different shapes.'
1173
+ assert len(rev_Elements) == len(rev_Numbers), f'Elements & Numbers have different shapes.'
1174
+ assert sum(rev_Numbers) == len(rev_Coords), f'Number of atoms is mismatch in Numbers and Coord.'
1175
+ # load
1176
+ self._Sample_ids[_indx] = rev_Sample_ids
1177
+ self.Cells[_indx] = rev_Cells.astype(np.float32)
1178
+ self.Elements[_indx] = rev_Elements
1179
+ self.Numbers[_indx] = rev_Numbers
1180
+ self.Coords_type[_indx] = rev_Coords_type
1181
+ self.Coords[_indx] = rev_Coords.astype(np.float32)
1182
+ self.Fixed[_indx] = rev_Fixed.astype(np.int8)
1183
+ if self.Energies is not None:
1184
+ self.Energies[_indx] = float(rev_Energies)
1185
+ if self.Forces is not None:
1186
+ self.Forces[_indx] = rev_Forces.astype(np.float32)
1187
+ if self.Labels is not None:
1188
+ self.Labels[_indx] = rev_Labels
1189
+ # postprocess
1190
+ if self.Atom_list is not None:
1191
+ self.generate_atom_list(True)
1192
+ if self.Atomic_number_list is not None:
1193
+ self.generate_atomic_number_list(True)
1194
+ if self.Dist_mat is not None:
1195
+ warnings.warn(f'Due to the data change, old Dist_mat might be incorrect. Hence Dist_mat is reset to None.', RuntimeWarning)
1196
+ self.Dist_mat = None
1197
+
1198
+ def append_from_lists(
1199
+ self,
1200
+ add_Sample_ids: List,
1201
+ add_Cells: List[np.ndarray],
1202
+ add_Elements: List[List[str]],
1203
+ add_Numbers: List[List[int]],
1204
+ add_Coords_type: List[Literal['C', 'D']],
1205
+ add_Coords: List[np.ndarray],
1206
+ add_Fixed: List[np.ndarray],
1207
+ add_Energies: None | List[float] = None,
1208
+ add_Forces: None | List[np.ndarray] = None,
1209
+ add_Labels: List[Any] | None = None,
1210
+ ) -> None:
1211
+ """
1212
+ Appending data from given Lists of ids, cells, elements, numbers, and coords.
1213
+ Args:
1214
+ add_Sample_ids: the data in List to be appended.
1215
+ add_Cells: the data in List to be appended.
1216
+ add_Elements: the data in List to be appended.
1217
+ add_Numbers: the data in List to be appended.
1218
+ add_Coords_type: the data in List to be appended.
1219
+ add_Coords: the data in List to be appended.
1220
+ add_Fixed: the data in List to be appended.
1221
+ add_Energies: the data in List to be appended.
1222
+ add_Forces: the data in List to be appended.
1223
+ add_Labels: the data in List to be appended.
1224
+
1225
+ """
1226
+ assert self.Mode == 'L', f'Mode "A" does not support yet.'
1227
+ # check types
1228
+ if not isinstance(add_Sample_ids, list):
1229
+ raise TypeError(f'`add_Sample_ids` must be a list, but got {type(add_Sample_ids)}.')
1230
+ n_samp = len(add_Sample_ids)
1231
+
1232
+ if not isinstance(add_Cells, list):
1233
+ raise TypeError(f'`add_Cells` must be a list, but got {type(add_Cells)}.')
1234
+ else:
1235
+ _chk = [type(_) for _ in add_Cells if not isinstance(_, np.ndarray)]
1236
+ if len(_chk) > 0:
1237
+ raise TypeError(f'`add_Cells` must contain only np.ndarray elements, but got {_chk}.')
1238
+ n_cells = len(add_Cells)
1239
+
1240
+ if not isinstance(add_Elements, list):
1241
+ raise TypeError(f'`add_Elements` must be a list, but got {type(add_Elements)}.')
1242
+ else:
1243
+ _chk = [_ for _ in add_Elements if (not isinstance(_, List)) or ({type(__) for __ in _} != {str})]
1244
+ if len(_chk) > 0:
1245
+ raise TypeError(f'`add_Elements` must contain only List[str] elements, but got {_chk}.')
1246
+ n_elems = len(add_Elements)
1247
+
1248
+ if not isinstance(add_Numbers, list):
1249
+ raise TypeError(f'`add_Numbers` must be a list, but got {type(add_Numbers)}.')
1250
+ else:
1251
+ _chk = [_ for _ in add_Numbers if (not isinstance(_, List)) or ({type(__) for __ in _} != {int})]
1252
+ if len(_chk) > 0:
1253
+ raise TypeError(f'`add_Numbers` must contain only List[int] elements, but got {_chk}.')
1254
+ n_nums = len(add_Numbers)
1255
+
1256
+ if not isinstance(add_Coords_type, list):
1257
+ raise TypeError(f'`add_Coords_type` must be a list, but got {type(add_Coords_type)}.')
1258
+ else:
1259
+ _chk = [_ for _ in add_Coords_type if _ not in {'C', 'D'}]
1260
+ if len(_chk) > 0:
1261
+ raise ValueError(f'`add_Coords_type` must contain only "C" or "D" as elements, but got {_chk}.')
1262
+ n_coords_type = len(add_Coords_type)
1263
+
1264
+ if not isinstance(add_Coords, list):
1265
+ raise TypeError(f'`add_Coords` must be a list, but got {type(add_Coords)}.')
1266
+ else:
1267
+ _chk = [_ for _ in add_Coords if not isinstance(_, np.ndarray)]
1268
+ if len(_chk) > 0:
1269
+ raise TypeError(f'`add_Coords` must contain only np.ndarray elements, but got {_chk}.')
1270
+ n_coords = len(add_Coords)
1271
+
1272
+ if not isinstance(add_Fixed, list):
1273
+ raise TypeError(f'`add_Fixed` must be a list, but got {type(add_Fixed)}.')
1274
+ else:
1275
+ _chk = [_ for _ in add_Fixed if not isinstance(_, np.ndarray)]
1276
+ if len(_chk) > 0:
1277
+ raise TypeError(f'`add_Fixed` must contain only np.ndarray elements, but got {_chk}.')
1278
+ n_fixed = len(add_Fixed)
1279
+
1280
+ if add_Energies is None:
1281
+ if self.Energies is not None:
1282
+ raise ValueError(f"Other structures have energy but here given `add_Energies` is None, which is not allowed.")
1283
+ n_energies = n_samp
1284
+ elif not isinstance(add_Energies, List):
1285
+ raise TypeError(f'Expected `add_Energies` of list, but got {type(add_Energies)}.')
1286
+ else:
1287
+ _chk = [_ for _ in add_Energies if not isinstance(_, float)]
1288
+ if len(_chk) > 0:
1289
+ raise TypeError(f'`add_Energies` must contain only float elements, but got {type(_chk)}.')
1290
+ n_energies = len(add_Energies)
1291
+ if self.Energies is None: # adding data into None
1292
+ if len(self) == 0:
1293
+ self.Energies = list()
1294
+ else:
1295
+ raise ValueError(f'Other structures do not have energy, but here some energies is adding.')
1296
+
1297
+ if add_Forces is None:
1298
+ if self.Forces is not None:
1299
+ raise ValueError(f"Other structures have forces but here given `add_Forces` is None, which is not allowed.")
1300
+ n_forces = n_samp
1301
+ elif not isinstance(add_Forces, List):
1302
+ raise TypeError(f'Expected `add_Forces` of list, but got {type(add_Forces)}.')
1303
+ else:
1304
+ _chk = [_ for _ in add_Forces if not isinstance(_, np.ndarray)]
1305
+ if len(_chk) > 0:
1306
+ raise TypeError(f'`add_Forces` must contain only np.ndarray elements, but got {type(_chk)}.')
1307
+ n_forces = len(add_Forces)
1308
+ if self.Forces is None: # adding data into None
1309
+ if len(self) == 0:
1310
+ self.Forces = list()
1311
+ else:
1312
+ raise ValueError(f'Other structures do not have forces, but here some forces is adding.')
1313
+
1314
+ if add_Labels is None:
1315
+ if self.Labels is not None:
1316
+ raise ValueError(f"Other structures have labels but here given `add_Labels` is None, which is not allowed.")
1317
+ n_labels = n_samp
1318
+ elif not isinstance(add_Labels, List):
1319
+ raise TypeError(f'Expected `add_Labels` of list, but got {type(add_Labels)}.')
1320
+ else:
1321
+ n_labels = len(add_Labels)
1322
+ if self.Labels is None: # adding data into None
1323
+ if len(self) == 0:
1324
+ self.Labels = list()
1325
+ else:
1326
+ raise ValueError(f'Other structures do not have forces, but here some forces is adding.')
1327
+
1328
+ samp_number_set = {n_samp, n_cells, n_elems, n_nums, n_coords_type, n_coords, n_fixed, n_energies, n_forces, n_labels}
1329
+ if len(samp_number_set) > 1:
1330
+ # i.e., some sample numbers are not equal
1331
+ raise ValueError(
1332
+ f'Sample numbers must be the same, '
1333
+ f'but [n_samp, n_cells, n_elems, n_nums, n_coords_type, n_coords, n_fixed, n_energies, n_forces, n_labels] '
1334
+ f'have sample numbers of {n_samp, n_cells, n_elems, n_nums, n_coords_type, n_coords, n_fixed, n_energies, n_forces, n_labels}.'
1335
+ )
1336
+
1337
+ # load
1338
+ _new_attr = [
1339
+ add_Sample_ids, # self.Sample_ids,
1340
+ add_Cells, # self.Cells,
1341
+ add_Elements, # self.Elements,
1342
+ add_Numbers, # self.Numbers,
1343
+ add_Coords_type, # self.Coords_type,
1344
+ add_Coords, # self.Coords,
1345
+ add_Fixed, # self.Fixed,
1346
+ add_Energies, # self.Energies,
1347
+ add_Forces, # self.Forces,
1348
+ add_Labels, # self.Labels
1349
+ ]
1350
+ _new_attr_type = [
1351
+ None,
1352
+ np.float32,
1353
+ None,
1354
+ None,
1355
+ None,
1356
+ np.float32,
1357
+ np.int8,
1358
+ None,
1359
+ np.float32,
1360
+ None,
1361
+ ]
1362
+ for _i, _attr in enumerate([
1363
+ self.Sample_ids,
1364
+ self.Cells,
1365
+ self.Elements,
1366
+ self.Numbers,
1367
+ self.Coords_type,
1368
+ self.Coords,
1369
+ self.Fixed,
1370
+ self.Energies,
1371
+ self.Forces,
1372
+ self.Labels
1373
+ ]):
1374
+ if (_new_attr_type[_i] is not None) and (_new_attr[_i] is not None):
1375
+ _at = [_.astype(_new_attr_type[_i]) for _ in _new_attr[_i]]
1376
+ else:
1377
+ _at = _new_attr[_i]
1378
+
1379
+ if _attr is not None:
1380
+ _attr.extend(_at)
1381
+ # postprocess
1382
+ if self.Atom_list is not None:
1383
+ self.generate_atom_list(True)
1384
+ if self.Atomic_number_list is not None:
1385
+ self.generate_atomic_number_list(True)
1386
+ if self.Dist_mat is not None:
1387
+ warnings.warn(f'Due to the data change, old Dist_mat might be incorrect. Hence Dist_mat is reset to None.', RuntimeWarning)
1388
+ self.Dist_mat = None
1389
+ # check
1390
+ self._check_id()
1391
+ self._check_len()
1392
+
1393
+ def append_from_array(
1394
+ self,
1395
+ add_atom_batch_sizes: np.ndarray,
1396
+ add_element_batch_sizes: np.ndarray,
1397
+ add_Sample_ids: np.ndarray,
1398
+ add_Cells: np.ndarray,
1399
+ add_Elements: np.ndarray,
1400
+ add_Numbers: np.ndarray,
1401
+ add_Coords_type: np.ndarray,
1402
+ add_Coords: np.ndarray,
1403
+ add_Fixed: np.ndarray,
1404
+ add_Energies: None | np.ndarray = None,
1405
+ add_Forces: None | np.ndarray = None,
1406
+ add_Labels: np.ndarray | None = None,
1407
+ is_check_length: bool = False,
1408
+ ):
1409
+ """
1410
+ Appending datta from np.ndarray, only work in self.Mode = 'A'.
1411
+ Args:
1412
+ add_atom_batch_sizes: array of atom numbers in each structure.
1413
+ add_element_batch_sizes: array of element numbers in each structure.
1414
+ add_Sample_ids: array of element numbers in each structure.
1415
+ add_Cells: array of element numbers in each structure.
1416
+ add_Elements: array of element numbers in each structure.
1417
+ add_Numbers: array of element numbers in each structure.
1418
+ add_Coords_type: array of element numbers in each structure.
1419
+ add_Coords: array of element numbers in each structure.
1420
+ add_Fixed: array of element numbers in each structure.
1421
+ add_Energies: array of element numbers in each structure.
1422
+ add_Forces: array of element numbers in each structure.
1423
+ add_Labels: array of element numbers in each structure.
1424
+ is_check_length: bool, whether to check the consistency of sample numbers of all `add_*` vars. Very expensive!
1425
+
1426
+ Returns:
1427
+
1428
+ """
1429
+ # check
1430
+ if self.Mode != 'A':
1431
+ raise ValueError(f'`append_from_array` only work in `A` mode.')
1432
+ if add_atom_batch_sizes.ndim != 1:
1433
+ raise ValueError(f'Expected `add_atom_batch_sizes` of 1 dimension, but got {add_atom_batch_sizes.ndim}.')
1434
+ if add_element_batch_sizes.ndim != 1:
1435
+ raise ValueError(f'Expected `add_element_batch_sizes` of 1 dimension, but got {add_element_batch_sizes.shape}.')
1436
+
1437
+ _new_atom_ptr = np.empty(len(add_atom_batch_sizes) + 1, dtype=int)
1438
+ _new_atom_ptr[0] = 0
1439
+ np.cumsum(add_atom_batch_sizes, out=_new_atom_ptr[1:])
1440
+ _new_element_ptr = np.empty(len(add_element_batch_sizes) + 1, dtype=int)
1441
+ _new_element_ptr[0] = 0
1442
+ np.cumsum(add_element_batch_sizes, out=_new_element_ptr[1:])
1443
+ # standarize shape
1444
+ add_Sample_ids = add_Sample_ids.astype('<U128')
1445
+ add_Cells = add_Cells.astype('<f4')
1446
+ add_Elements = add_Elements.astype('<U3')
1447
+ add_Numbers = add_Numbers.astype('<i4')
1448
+ add_Coords = add_Coords.astype('<f4')
1449
+ add_Coords_type = add_Coords_type.astype('<U2')
1450
+ add_Fixed = add_Fixed.astype('|i1')
1451
+ if add_Energies is not None: add_Energies = add_Energies.astype('<f4')
1452
+ if add_Forces is not None: add_Forces = add_Forces.astype('<f4')
1453
+ if add_Labels is not None: add_Labels = add_Labels.astype('|O')
1454
+ # write
1455
+ if self._Sample_ids_ is None: # a new container
1456
+ self.Batch_indices_ = _new_atom_ptr
1457
+ self.Elements_batch_indices_ = _new_element_ptr
1458
+ self._Sample_ids_ = add_Sample_ids
1459
+ self.Cells_ = add_Cells
1460
+ self.Elements_ = add_Elements
1461
+ self.Numbers_ = add_Numbers
1462
+ self.Coords_ = add_Coords
1463
+ self.Coords_type_ = add_Coords_type
1464
+ self.Fixed_ = add_Fixed
1465
+ self.Energies_ = add_Energies
1466
+ self.Forces_ = add_Forces
1467
+ self.Labels_ = add_Labels
1468
+ else:
1469
+ _new_atom_ptr += self.Batch_indices_[-1]
1470
+ _new_element_ptr += self.Elements_batch_indices_[-1]
1471
+ self.Batch_indices_ = np.append(self.Batch_indices_, _new_atom_ptr[1:]) # drop the 1st element '0' in `_new_atom_ptr`
1472
+ self.Elements_batch_indices_ = np.append(self.Elements_batch_indices_, _new_element_ptr[1:])
1473
+ self._Sample_ids_ = np.append(self._Sample_ids_, add_Sample_ids)
1474
+ self.Cells_ = np.append(self.Cells_, add_Cells, axis=0)
1475
+ self.Elements_ = np.append(self.Elements_, add_Elements)
1476
+ self.Numbers_ = np.append(self.Numbers_, add_Numbers)
1477
+ self.Coords_type_ = np.append(self.Coords_type_, add_Coords_type)
1478
+ self.Coords_ = np.append(self.Coords_, add_Coords, axis=0)
1479
+ self.Fixed_ = np.append(self.Fixed_, add_Fixed, axis=0)
1480
+ if self.Energies_ is not None:
1481
+ self.Energies_ = np.append(self.Energies_, add_Energies)
1482
+ elif add_Energies is not None:
1483
+ warnings.warn(f'`add_Energies` will be dropped because `self.Energies` is None.')
1484
+ if self.Forces_ is not None:
1485
+ self.Forces_ = np.append(self.Forces_, add_Forces)
1486
+ elif add_Forces is not None:
1487
+ warnings.warn(f'`add_Forces` will be dropped because `self.Forces` is None.')
1488
+ if self.Labels_ is not None:
1489
+ self.Labels_ = np.append(self.Labels_, add_Labels)
1490
+ elif add_Labels is not None:
1491
+ warnings.warn(f'`add_Labels` will be dropped because `self.Labels` is None.')
1492
+
1493
+ self._check_id()
1494
+ if is_check_length: self._check_len()
1495
+
1496
+ def sort_split_by_natoms(
1497
+ self,
1498
+ labels: Optional[Dict | List] = None,
1499
+ split_ratio: float = 0.2,
1500
+ n_core: int = 1,
1501
+ verbose: int = 0
1502
+ ) -> (Tuple[List[List[np.ndarray]], List[List[np.ndarray]], List[np.ndarray], List[np.ndarray], List[np.ndarray], List[np.ndarray]]):
1503
+ """
1504
+ Rearrange samples with different atom number into List[np.array] that list of NDArrays with the same atom number.
1505
+ Only work in 'L' Mode.
1506
+
1507
+ Parameters:
1508
+ labels: Dict[id, label]|List[label]|None, the labels of samples. If labels is a List, it must have the same order as self.Sample_ids. If None, labels would be set to self.Labels.
1509
+ split_ratio: float between 0 and 1, the ratio of validation set.
1510
+ n_core:
1511
+ verbose: control the verboseness of output
1512
+
1513
+ Returns:
1514
+ training_batches, val_batches: List_1[List_2[NDArray]]. List_1 is of diff. n_atom cells, List_2 is of (Cell, Atomic_number_list, Atom_coords)
1515
+ training_labels, val_labels: List[NDArray]. List is of diff. n_atom cells, NDArray is label.
1516
+ training_args, val_args: List[NDArray]. List is of diff. n_atom cells, NDArray is the indices of samples in self.Samp_id.
1517
+ """
1518
+ t_st = time.perf_counter()
1519
+ if self.Atomic_number_list is None:
1520
+ self.generate_atomic_number_list()
1521
+ if labels is None:
1522
+ if self.Labels is None:
1523
+ raise ValueError('Input labels and self.Labels are both None. Please assign values to at least one variable.')
1524
+ else:
1525
+ labels = {_id: self.Labels[i] for i, _id in enumerate(self._Sample_ids)}
1526
+ elif isinstance(labels, Sequence):
1527
+ labels = {_id: labels[i] for i, _id in enumerate(self._Sample_ids)}
1528
+ elif not isinstance(labels, Dict):
1529
+ raise TypeError('Invalid type of labels')
1530
+
1531
+ n_samp = len(self.Numbers)
1532
+ if (split_ratio < 0.) or (split_ratio > 1.):
1533
+ raise ValueError(f'Invalid value of split_ratio: {split_ratio}')
1534
+ VAL_SPLIT_RATIO = split_ratio
1535
+ count = np.empty(n_samp, dtype=np.int32)
1536
+ for i, list_ in enumerate(self.Numbers): # total atom number list
1537
+ count[i] = np.sum(np.array(list_, dtype=np.int32))
1538
+ batch_args = np.argsort(count)
1539
+ temp_ = 0
1540
+ batch_dict = dict() # the dict with format: {atom number: indices of sample}
1541
+ for arg in batch_args:
1542
+ if count[arg] > temp_:
1543
+ temp_ = count[arg]
1544
+ batch_dict[count[arg]] = [arg, ]
1545
+ elif count[arg] == temp_ and (count[arg] in batch_dict.keys()):
1546
+ batch_dict[count[arg]].append(arg)
1547
+ elif count[arg] == temp_ and (count[arg] not in batch_dict.keys()):
1548
+ warnings.warn(f'ERROR, unexpected situation has occurred in {arg}th sample.')
1549
+ else:
1550
+ warnings.warn(f'ERROR2, unexpected situation has occurred in {arg}th sample.')
1551
+
1552
+ ''' Split Batches '''
1553
+ tot_batches = list() # total feature batches <<<
1554
+ # list of NDArrays. The list shape is (batches_with_diff_n_atom, 3),
1555
+ # and '3' contains 3 NDArrays that are [cell(n_samp, 3, 3), atomic_number(n_samp, n_atom), coord(n_samp, n_atom, 3)]
1556
+ label_batches = list() # total labels <<<
1557
+ # list of NDArrays. The list of energies NDArray with shape (n_samp, 1), wherein '1' contains the energy values.
1558
+ samp_args = list() # sample indices
1559
+
1560
+ # closure to parallel
1561
+ def _rearrange_single(n_atom, sample_args_, Cells, Atomic_number_list, Coords, labels_, Sample_ids) -> Tuple[np.ndarray, ...]:
1562
+ Cell_batch = np.empty((len(sample_args_), 3, 3), dtype=np.float32)
1563
+ Atomic_number_batch = np.empty((len(sample_args_), n_atom), dtype=np.int32, )
1564
+ Coord_batch = np.empty((len(sample_args_), n_atom, 3), dtype=np.float32)
1565
+ Energies_batch = np.empty(len(sample_args_), dtype=np.float32)
1566
+ Samp_args = np.empty((len(sample_args_)), dtype=np.int32, )
1567
+ for j, arg_ in enumerate(sample_args_):
1568
+ Cell_batch[j] = np.array(Cells[arg_], dtype=np.float32)
1569
+ Atomic_number_batch[j] = np.array(Atomic_number_list[arg_], dtype=np.int32) # type: ignore
1570
+ Coord_batch[j] = np.array(Coords[arg_], dtype=np.float32)
1571
+ Energies_batch[j] = np.array(labels_[Sample_ids[arg_]]) # grep DFT energies by mp-ids in energy dict; files_list[arg_][:-5] is mp-id.
1572
+ Samp_args[j] = np.array(arg_)
1573
+ return Energies_batch, Cell_batch, Atomic_number_batch, Coord_batch, Samp_args
1574
+
1575
+ if n_core != 1:
1576
+ _parallel = jb.Parallel(n_jobs=n_core, verbose=verbose, backend='loky')
1577
+ _dat = _parallel(
1578
+ jb.delayed(_rearrange_single)(n_atom, sample_args_, self.Cells, self.Atomic_number_list, self.Coords, labels, self._Sample_ids) for
1579
+ n_atom, sample_args_ in
1580
+ batch_dict.items())
1581
+ if _dat is None: raise RuntimeError('Occurred None data.')
1582
+ for temp in _dat:
1583
+ label_batches.append(temp[0], )
1584
+ tot_batches.append([temp[1], temp[2], temp[3]])
1585
+ samp_args.append(temp[4])
1586
+ else:
1587
+ if verbose > 0: self.logger.info('Rearrange sequentially...')
1588
+ for n_atom, sample_args_ in batch_dict.items():
1589
+ temp = _rearrange_single(n_atom, sample_args_, self.Cells, self.Atomic_number_list, self.Coords, labels, self._Sample_ids)
1590
+ label_batches.append(temp[0], )
1591
+ tot_batches.append([temp[1], temp[2], temp[3]])
1592
+ samp_args.append(temp[4])
1593
+
1594
+ if verbose > 0: self.logger.info('Done.\nSplitting...')
1595
+ # Split training and validation set
1596
+ k__ = 0
1597
+ training_batches: List[
1598
+ List[np.ndarray]] = list() # List_1[List_2[NDArray]]. List_1 is of diff. n_atom cells, List_2 is of (Cell, Atom_list, Atom_coord)
1599
+ val_batches: List[List[np.ndarray]] = list()
1600
+ training_labels: List[np.ndarray] = list() # List[NDArray]. List is of diff. n_atom cells, NDArray is Energy
1601
+ val_labels: List[np.ndarray] = list()
1602
+ training_args: List[np.ndarray] = list() # List[NDArray]. List is of diff. n_atom cells, NDArray is the indices of samples in f.Samp_id.
1603
+ val_args: List[np.ndarray] = list()
1604
+ if VAL_SPLIT_RATIO > 1e-8:
1605
+ for i, batches_with_diff_n_atom in enumerate(tot_batches):
1606
+ n_samp_in_batch_ = len(label_batches[i])
1607
+ val_samples_temp = (np.ceil(n_samp_in_batch_ * VAL_SPLIT_RATIO)).astype(int)
1608
+ # To avoid empty samples
1609
+ if val_samples_temp == n_samp_in_batch_:
1610
+ _ = batches_with_diff_n_atom[1].shape
1611
+ k__ += 1
1612
+ self.logger.info(
1613
+ f'WARNING: Too few samples occurred. The number of samples with {_[1]} atoms is only {n_samp_in_batch_}, '
1614
+ f'so it would be put in neither training set nor validation set. Total {k__} warnings.')
1615
+ continue
1616
+ # split
1617
+ training_batches.append([batches_with_diff_n_atom[j][:-val_samples_temp] for j in range(3)])
1618
+ val_batches.append([batches_with_diff_n_atom[j][-val_samples_temp:] for j in range(3)])
1619
+ training_labels.append(label_batches[i][:-val_samples_temp])
1620
+ val_labels.append(label_batches[i][-val_samples_temp:])
1621
+ training_args.append(samp_args[i][:-val_samples_temp])
1622
+ val_args.append(samp_args[i][-val_samples_temp:])
1623
+
1624
+ elif abs(VAL_SPLIT_RATIO) <= 1e-8:
1625
+ training_batches = tot_batches
1626
+ training_labels = label_batches
1627
+ training_args = samp_args
1628
+
1629
+ if verbose > 0: self.logger.info(f'Done. Total Time: {time.perf_counter() - t_st}')
1630
+ return training_batches, val_batches, training_labels, val_labels, training_args, val_args
1631
+
1632
+ def _raw_append(self, batch_structures):
1633
+ """
1634
+ Directly append without a check.
1635
+ """
1636
+ if batch_structures.Energies is not None:
1637
+ if self.Energies is None:
1638
+ self.Energies = [None, ] * len(self)
1639
+ self.Energies.extend(batch_structures.Energies)
1640
+ elif self.Energies is not None:
1641
+ self.Energies.extend([None] * len(batch_structures))
1642
+ if batch_structures.Forces is not None:
1643
+ if self.Forces is None:
1644
+ self.Forces = [None, ] * len(self)
1645
+ self.Forces.extend(batch_structures.Forces)
1646
+ elif self.Forces is not None:
1647
+ self.Forces.extend([None, ] * len(batch_structures))
1648
+ if batch_structures.Labels is not None:
1649
+ if self.Labels is None:
1650
+ self.Labels = [None, ] * len(self)
1651
+ self.Labels.extend(batch_structures.Labels)
1652
+ elif self.Labels is not None:
1653
+ self.Labels.extend([None] * len(batch_structures))
1654
+
1655
+ self._Sample_ids.extend(batch_structures.Sample_ids) # mp_ids
1656
+ self.Cells.extend(batch_structures.Cells) # Lattice parameters
1657
+ self.Elements.extend(batch_structures.Elements) # Element symbols
1658
+ self.Numbers.extend(batch_structures.Numbers) # Element numbers
1659
+ self.Coords_type.extend(batch_structures.Coords_type) # coordinates type
1660
+ self.Coords.extend(batch_structures.Coords) # Atom coordinates
1661
+ self.Fixed.extend(batch_structures.Fixed) # fixed masks
1662
+
1663
+ def append(self, batch_structures, strict: bool = True):
1664
+ """
1665
+ Append information of another BatchStructures to self.
1666
+ If original data do not have some properties but appending data do,
1667
+ these properties of original data would be padded with `None`, and vice versa.
1668
+ Args:
1669
+ batch_structures: the BatchStructures to append
1670
+ strict: If True, `batch_structures` contains any id which is already in `self` would raise an Error;
1671
+ otherwise, the data will be overwritten if encountered the same id.
1672
+
1673
+ Returns: None
1674
+
1675
+ """
1676
+ if not isinstance(batch_structures, BatchStructures):
1677
+ raise TypeError('`batch_structures` must be BatchStructures')
1678
+ if self._indices is None:
1679
+ self._update_indices()
1680
+ for i, _ids in enumerate(batch_structures.Sample_ids):
1681
+ if _ids in self._indices:
1682
+ if strict:
1683
+ raise RuntimeError(f'The {i}th sample in given `batch_structures` has the same sample_id ({_ids}) as existing samples.')
1684
+ else:
1685
+ self.remove(_ids)
1686
+ self._raw_append(batch_structures)
1687
+ self._update_indices()
1688
+ self._check_id()
1689
+ self._check_len()
1690
+
1691
+ def extend(self, batch_structures_list: Sequence, strict: bool = True):
1692
+ """
1693
+ Extend a list of BatchStructures to self.
1694
+ If original data do not have some properties but appending data do,
1695
+ these properties of original data would be padded with `None`, and vice versa.
1696
+ Args:
1697
+ batch_structures_list: the BatchStructures to append
1698
+ strict: If True, `batch_structures` contains any id which is already in `self` would raise an Error;
1699
+ otherwise, the data will be overwritten if encountered the same id.
1700
+
1701
+ Returns: None
1702
+
1703
+ """
1704
+ if not isinstance(batch_structures_list, Sequence):
1705
+ raise TypeError('`batch_structures_list` must be BatchStructures')
1706
+ _temp_bs = BatchStructures()
1707
+ _bs: BatchStructures
1708
+ for _bs in batch_structures_list:
1709
+ assert isinstance(_bs, BatchStructures), f'Encountered invalid type {type(_bs)} in `batch_structures_list`'
1710
+ _temp_bs._raw_append(_bs)
1711
+ _temp_bs._check_id()
1712
+ # check repetitions of id
1713
+ if self._indices is None:
1714
+ self._update_indices()
1715
+ for i, _ids in enumerate(_temp_bs.Sample_ids):
1716
+ if _ids in self._indices:
1717
+ if strict:
1718
+ raise RuntimeError(f'The {i}th sample in given `batch_structures` has the same sample_id ({_ids}) as existing samples.')
1719
+ else:
1720
+ self.remove(_ids)
1721
+ self._raw_append(_temp_bs)
1722
+ self._update_indices()
1723
+ self._check_id()
1724
+ self._check_len()
1725
+
1726
+ def remove(self, key: int | str | slice | List):
1727
+ """
1728
+ Remove the data of given `key` in-place.
1729
+ Args:
1730
+ key: int for index, str for Sample_ids, slice for a slice of indices.
1731
+
1732
+ Returns: None
1733
+
1734
+ """
1735
+ # delete ops
1736
+ def _remove_single(indx):
1737
+ if self.Atom_list is not None:
1738
+ del self.Atom_list[indx]
1739
+ if self.Atomic_number_list is not None:
1740
+ del self.Atomic_number_list[indx]
1741
+ del self.Sample_ids[indx]
1742
+ del self.Cells[indx]
1743
+ del self.Elements[indx]
1744
+ del self.Numbers[indx]
1745
+ del self.Coords[indx]
1746
+ del self.Coords_type[indx]
1747
+ del self.Fixed[indx]
1748
+ if self.Energies is not None: del self.Energies[indx]
1749
+ if self.Forces is not None: del self.Forces[indx]
1750
+ if self.Labels is not None: del self.Labels[indx]
1751
+ # check key
1752
+ if isinstance(key, int):
1753
+ if key >= len(self): raise ValueError(f'key {key} is out of range {len(self)}')
1754
+ _remove_single(key)
1755
+ elif isinstance(key, slice):
1756
+ _remove_single(key)
1757
+ elif isinstance(key, str):
1758
+ if self._indices is None:
1759
+ self._update_indices()
1760
+ self._check_id()
1761
+ if key not in self._indices: raise KeyError(f'the input key `"{key}"` does not exist.')
1762
+ indices = self._indices[key]
1763
+ _remove_single(indices)
1764
+ elif isinstance(key, list):
1765
+ _sort_key = sorted(key, reverse=True)
1766
+ if _sort_key[-1] < 0: raise IndexError(f'Negative index {_sort_key[-1]} in the list is not supported.')
1767
+ if _sort_key[0] > len(self): raise ValueError(f'key {_sort_key[0]} is out of range {len(self)}')
1768
+ _check_repeat = _sort_key[0]
1769
+ _remove_single(_check_repeat)
1770
+ for del_id in _sort_key[1:]:
1771
+ if not isinstance(del_id, int):
1772
+ raise TypeError(f'Expected all elements in key list are int, but got {type(del_id)}.')
1773
+ if del_id != _check_repeat: # avoid duplicated indices
1774
+ _remove_single(del_id)
1775
+ _check_repeat = del_id
1776
+ else:
1777
+ raise TypeError(f'Invalid type of key: {type(key)}')
1778
+
1779
+ # update id dict
1780
+ self._update_indices()
1781
+
1782
+ def remove_copy(self, key: int | str | slice):
1783
+ """
1784
+ Remove the data of given `key` in a copy of self and return. The original data is not affected.
1785
+ Args:
1786
+ key: int for index, str for Sample_ids, slice for a slice of indices.
1787
+
1788
+ Returns: copy of
1789
+
1790
+ """
1791
+ sub_self = copy.deepcopy(self)
1792
+ sub_self.remove(key)
1793
+
1794
+ return sub_self
1795
+
1796
+ ########################################################################################
1797
+ # QUERIES
1798
+ #
1799
+ ########################################################################################
1800
+ def where(self, key: str | List[str]):
1801
+ """
1802
+ Return the index/indices of given sample id(s) `key` in self
1803
+ Returns: int | List[int]
1804
+
1805
+ """
1806
+ if isinstance(key, list):
1807
+ indx_list = list()
1808
+ for kk in key:
1809
+ indx_list.append(self._indices[kk])
1810
+ else:
1811
+ indx_list = self._indices[key]
1812
+
1813
+ return indx_list
1814
+
1815
+ def contain_any(self, elements: List[str] | Set[str]):
1816
+ """
1817
+ Return a view of BatchStructures that contain any of given elements.
1818
+ """
1819
+ sub_self = BatchStructures()
1820
+ if self.Energies is not None: sub_self.Energies = list()
1821
+ if self.Forces is not None: sub_self.Forces = list()
1822
+ if self.Labels is not None: sub_self.Labels = list()
1823
+
1824
+ if not isinstance(elements, (Sequence, Set)): raise TypeError(f'elements must be a Sequence but occurred {type(elements)}')
1825
+ if not isinstance(elements, Set): elements = set(elements)
1826
+ for i, elems in enumerate(self.Elements):
1827
+ for elem_ in elems:
1828
+ if elem_ in elements:
1829
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1830
+ sub_self.Cells.append(self.Cells[i])
1831
+ sub_self.Elements.append(self.Elements[i])
1832
+ sub_self.Numbers.append(self.Numbers[i])
1833
+ sub_self.Coords.append(self.Coords[i])
1834
+ sub_self.Coords_type.append(self.Coords_type[i])
1835
+ sub_self.Fixed.append(self.Fixed[i])
1836
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
1837
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1838
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1839
+ break
1840
+ return sub_self
1841
+
1842
+ def contain_all(self, elements: List[str] | Set[str]):
1843
+ """
1844
+ Return a view of BatchStructures that contain all given elements.
1845
+ """
1846
+ sub_self = BatchStructures()
1847
+ if self.Energies is not None: sub_self.Energies = list()
1848
+ if self.Forces is not None: sub_self.Forces = list()
1849
+ if self.Labels is not None: sub_self.Labels = list()
1850
+
1851
+ if not isinstance(elements, (Sequence, Set)): raise TypeError(f'elements must be a Sequence but occurred {type(elements)}')
1852
+ if not isinstance(elements, Set): elements = set(elements)
1853
+ for i, elems in enumerate(self.Elements):
1854
+ _elem = set(elems)
1855
+ if _elem >= elements:
1856
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1857
+ sub_self.Cells.append(self.Cells[i])
1858
+ sub_self.Elements.append(self.Elements[i])
1859
+ sub_self.Numbers.append(self.Numbers[i])
1860
+ sub_self.Coords.append(self.Coords[i])
1861
+ sub_self.Coords_type.append(self.Coords_type[i])
1862
+ sub_self.Fixed.append(self.Fixed[i])
1863
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
1864
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1865
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1866
+
1867
+ return sub_self
1868
+
1869
+ def contain_only_in(self, elements: List[str] | Set[str]):
1870
+ """
1871
+ Return a view of BatchStructures that only contain given elements.
1872
+ """
1873
+ sub_self = BatchStructures()
1874
+ if self.Energies is not None: sub_self.Energies = list()
1875
+ if self.Forces is not None: sub_self.Forces = list()
1876
+ if self.Labels is not None: sub_self.Labels = list()
1877
+
1878
+ if not isinstance(elements, (List, Tuple, Set)): raise TypeError(f'elements must be a List or Set but occurred {type(elements)}')
1879
+ if not isinstance(elements, Set): elements = set(elements)
1880
+ for i, elems in enumerate(self.Elements):
1881
+ Not_in = False
1882
+ for elem_ in elems:
1883
+ if elem_ not in elements:
1884
+ Not_in = True
1885
+ break
1886
+ if not Not_in:
1887
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1888
+ sub_self.Cells.append(self.Cells[i])
1889
+ sub_self.Elements.append(self.Elements[i])
1890
+ sub_self.Numbers.append(self.Numbers[i])
1891
+ sub_self.Coords.append(self.Coords[i])
1892
+ sub_self.Coords_type.append(self.Coords_type[i])
1893
+ sub_self.Fixed.append(self.Fixed[i])
1894
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
1895
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1896
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1897
+
1898
+ return sub_self
1899
+
1900
+ def not_contain_any(self, elements: List[str] | Set[str], verbose: int = 0) -> "BatchStructures":
1901
+ """
1902
+ Return a view of BatchStructures that Not contain any of given elements.
1903
+ """
1904
+ sub_self = BatchStructures()
1905
+ if self.Energies is not None: sub_self.Energies = list()
1906
+ if self.Forces is not None: sub_self.Forces = list()
1907
+ if self.Labels is not None: sub_self.Labels = list()
1908
+
1909
+ if not isinstance(elements, (Sequence, Set)): raise TypeError(f'elements must be a Sequence but occurred {type(elements)}')
1910
+ if not isinstance(elements, Set): elements = set(elements)
1911
+ for i, elems in enumerate(self.Elements):
1912
+ Not_in = False
1913
+ for elem_ in elems:
1914
+ if elem_ in elements:
1915
+ Not_in = True
1916
+ break
1917
+ if not Not_in:
1918
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1919
+ sub_self.Cells.append(self.Cells[i])
1920
+ sub_self.Elements.append(self.Elements[i])
1921
+ sub_self.Numbers.append(self.Numbers[i])
1922
+ sub_self.Coords.append(self.Coords[i])
1923
+ sub_self.Coords_type.append(self.Coords_type[i])
1924
+ sub_self.Fixed.append(self.Fixed[i])
1925
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
1926
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1927
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1928
+
1929
+ if verbose > 0:
1930
+ if i % round(0.1 * len(self.Elements)) == 0:
1931
+ self.logger.info(f'Progress: {100 * i / len(self.Elements):>5.2f}%')
1932
+
1933
+ return sub_self
1934
+
1935
+ def not_contain_all(self, elements: List[str] | Set[str]):
1936
+ """
1937
+ Return a view of BatchStructures that Not contain all given elements at the same time.
1938
+ """
1939
+ sub_self = BatchStructures()
1940
+ if self.Energies is not None: sub_self.Energies = list()
1941
+ if self.Forces is not None: sub_self.Forces = list()
1942
+ if self.Labels is not None: sub_self.Labels = list()
1943
+
1944
+ if not isinstance(elements, (Sequence, Set)): raise TypeError(f'elements must be a Sequence but occurred {type(elements)}')
1945
+ if not isinstance(elements, Set): elements = set(elements)
1946
+ for i, elems in enumerate(self.Elements):
1947
+ Put_in = False
1948
+ for elem_ in elements:
1949
+ if elem_ not in elems:
1950
+ Put_in = True
1951
+ break
1952
+ if Put_in:
1953
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1954
+ sub_self.Cells.append(self.Cells[i])
1955
+ sub_self.Elements.append(self.Elements[i])
1956
+ sub_self.Numbers.append(self.Numbers[i])
1957
+ sub_self.Coords.append(self.Coords[i])
1958
+ sub_self.Coords_type.append(self.Coords_type[i])
1959
+ sub_self.Fixed.append(self.Fixed[i])
1960
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
1961
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1962
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1963
+
1964
+ return sub_self
1965
+
1966
+ def select_by_energies(self, energy_range: Tuple[float, float]):
1967
+ """
1968
+ Return a view of BatchStructures that Energies between energy_range.
1969
+ """
1970
+ sub_self = BatchStructures()
1971
+ if self.Energies is not None: sub_self.Energies = list()
1972
+ if self.Forces is not None: sub_self.Forces = list()
1973
+ if self.Labels is not None: sub_self.Labels = list()
1974
+
1975
+ if not isinstance(energy_range, (Tuple, List)): raise TypeError(f'energy_range must be a Tuple but occurred {type(energy_range)}')
1976
+ if energy_range[0] >= energy_range[1]: raise ValueError('Interval error.')
1977
+ if self.Energies is None: raise RuntimeError('No Energy Information yet.')
1978
+
1979
+ for i, ener in enumerate(self.Energies):
1980
+ # judge
1981
+ if (ener < energy_range[1]) and (ener >= energy_range[0]):
1982
+ sub_self._Sample_ids.append(self.Sample_ids[i])
1983
+ sub_self.Cells.append(self.Cells[i])
1984
+ sub_self.Elements.append(self.Elements[i])
1985
+ sub_self.Numbers.append(self.Numbers[i])
1986
+ sub_self.Coords.append(self.Coords[i])
1987
+ sub_self.Coords_type.append(self.Coords_type[i])
1988
+ sub_self.Fixed.append(self.Fixed[i])
1989
+ sub_self.Energies.append(self.Energies[i]) # type: ignore
1990
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
1991
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
1992
+
1993
+ return sub_self
1994
+
1995
+ def select_by_element_number(self, element_number_range: Tuple[float, float]):
1996
+ """
1997
+ Return a view of BatchStructures that element number between element_number_range.
1998
+ """
1999
+ sub_self = BatchStructures()
2000
+ if self.Energies is not None: sub_self.Energies = list()
2001
+ if self.Forces is not None: sub_self.Forces = list()
2002
+ if self.Labels is not None: sub_self.Labels = list()
2003
+
2004
+ if not isinstance(element_number_range, (Tuple, List)): raise TypeError(
2005
+ f'energy_range must be a Tuple but occurred {type(element_number_range)}')
2006
+ if element_number_range[0] >= element_number_range[1]: raise ValueError('Interval error.')
2007
+ if self.Energies is None: raise RuntimeError('No Energy Information yet.')
2008
+
2009
+ for i, nums in enumerate(self.Numbers):
2010
+ num = len(nums)
2011
+ # judge
2012
+ if (num < element_number_range[1]) and (num >= element_number_range[0]):
2013
+ sub_self._Sample_ids.append(self.Sample_ids[i])
2014
+ sub_self.Cells.append(self.Cells[i])
2015
+ sub_self.Elements.append(self.Elements[i])
2016
+ sub_self.Numbers.append(self.Numbers[i])
2017
+ sub_self.Coords.append(self.Coords[i])
2018
+ sub_self.Coords_type.append(self.Coords_type[i])
2019
+ sub_self.Fixed.append(self.Fixed[i])
2020
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
2021
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
2022
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
2023
+
2024
+ return sub_self
2025
+
2026
+ def select_by_prop(self, prop: str, prop_range: Tuple[float, float]):
2027
+ """
2028
+ Return a view of BatchStructures that given properties of BatchStructures between prop_range.
2029
+ The given properties must be comparable.
2030
+
2031
+ Args:
2032
+ prop: an attribute name of self e.g., "Number", "Energies"
2033
+ prop_range: Tuple[float, float], range of prop to select. It is a left closed right open interval.
2034
+
2035
+ Returns:
2036
+ BatchStructures which is a view of self
2037
+ """
2038
+ sub_self = BatchStructures()
2039
+ if self.Energies is not None: sub_self.Energies = list()
2040
+ if self.Forces is not None: sub_self.Forces = list()
2041
+ if self.Labels is not None: sub_self.Labels = list()
2042
+
2043
+ if not isinstance(prop_range, (Tuple, List)): raise TypeError(f'energy_range must be a Tuple but occurred {type(prop_range)}')
2044
+ if prop_range[0] >= prop_range[1]: raise ValueError('Interval error.')
2045
+ if self.Energies is None: raise RuntimeError('No Energy Information yet.')
2046
+
2047
+ properties = getattr(self, prop)
2048
+ for i, ener in enumerate(properties):
2049
+ # judge
2050
+ if (ener < prop_range[1]) and (ener >= prop_range[0]):
2051
+ sub_self._Sample_ids.append(self.Sample_ids[i])
2052
+ sub_self.Cells.append(self.Cells[i])
2053
+ sub_self.Elements.append(self.Elements[i])
2054
+ sub_self.Numbers.append(self.Numbers[i])
2055
+ sub_self.Coords.append(self.Coords[i])
2056
+ sub_self.Coords_type.append(self.Coords_type[i])
2057
+ sub_self.Fixed.append(self.Fixed[i])
2058
+ if sub_self.Energies is not None: sub_self.Energies.append(self.Energies[i]) # type: ignore
2059
+ if sub_self.Forces is not None: sub_self.Forces.append(self.Forces[i]) # type: ignore
2060
+ if sub_self.Labels is not None: sub_self.Labels.append(self.Labels[i]) # type: ignore
2061
+
2062
+ return sub_self
2063
+
2064
+ def select_by_sample_id(self, pattern: str):
2065
+ """
2066
+ Return a view of BatchStructures in which sample id could match the given pattern.
2067
+ Regular expressions are supported.
2068
+ For non-string sample id, str(*) would be applied first to convert id into string.
2069
+ Args:
2070
+ pattern: the (regular expressions) pattern to match sample ids in self.
2071
+
2072
+ Returns: BatchStructures
2073
+
2074
+ """
2075
+ _pattern = re.compile(pattern)
2076
+ sub_self = BatchStructures()
2077
+ for i, name in enumerate(self.Sample_ids):
2078
+ _name = str(name)
2079
+ has_match = re.match(_pattern, _name)
2080
+ if has_match is not None:
2081
+ sub_self._raw_append(self[i])
2082
+ sub_self._check_id()
2083
+ sub_self._check_len()
2084
+ return sub_self
2085
+
2086
+ def elem_distribution(self, count_for: Literal['Atom', 'Structure'] = 'Structure'):
2087
+ """
2088
+ return a Dict[str, int] of {element: frequency} that shows the distribution of elements in BatchStructures.
2089
+
2090
+ Parameters:
2091
+ count_for: 'Atom' or 'Structure', count element frequency by each atom in structures or each structure.
2092
+ """
2093
+ elem_distribution_dict = {elem_: 0 for elem_ in self._ALL_ELEMENTS}
2094
+ if count_for == 'Atom':
2095
+ for i, elems in enumerate(self.Elements):
2096
+ for elem_ in elems:
2097
+ elem_distribution_dict[elem_] += 1
2098
+ if count_for == 'Structure':
2099
+ for elem_ in elem_distribution_dict.keys():
2100
+ for i, elems in enumerate(self.Elements):
2101
+ if elem_ in elems:
2102
+ elem_distribution_dict[elem_] += 1
2103
+ return elem_distribution_dict
2104
+
2105
+ def eq(self, other, rtol: float = 1e-6, atol: float = 1e-6, verbose: bool = False):
2106
+ """
2107
+ Check if two BatchStructures are equivalent.
2108
+ Args:
2109
+ other: the BatchStructures to compare.
2110
+ rtol: relative tolerance for comparing float number data.
2111
+ atol: absolute tolerance for comparing float number data.
2112
+ verbose: To control the verbosity. if True, the equality of each attribute will be printed.
2113
+
2114
+ Returns: bool
2115
+
2116
+ """
2117
+ other._check_len()
2118
+ self._check_len()
2119
+ try:
2120
+ if len(self) != len(other): return False
2121
+ Qid = (self.Sample_ids == other.Sample_ids)
2122
+ Qcoo = all([np.allclose(self.Coords[_], other.Coords[_], rtol=rtol, atol=atol) for _ in range(len(self))])
2123
+ Qct = (self.Coords_type == other.Coords_type)
2124
+ Qcel = all([np.allclose(self.Cells[_], other.Cells[_], rtol=rtol, atol=atol) for _ in range(len(self))])
2125
+ Qelm = all([np.all(self.Elements[_] == other.Elements[_]) for _ in range(len(self))])
2126
+ Qnum = all([np.all(self.Numbers[_] == other.Numbers[_]) for _ in range(len(self))])
2127
+ Qfix = all([np.allclose(self.Fixed[_], other.Fixed[_], rtol=rtol, atol=atol) for _ in range(len(self))])
2128
+ Qe = True
2129
+ Qf = True
2130
+ Ql = True
2131
+ if self.Energies is not None:
2132
+ Qe = (self.Energies == other.Energies)
2133
+ if self.Forces is not None:
2134
+ Qf = all([np.allclose(self.Forces[_], other.Forces[_], rtol=rtol, atol=atol) for _ in range(len(self))])
2135
+ if self.Labels is not None:
2136
+ Ql = (self.Labels == other.Label)
2137
+ qq = (Qid & Qcoo & Qct & Qcel & Qelm & Qnum & Qfix & Qe & Qf & Ql)
2138
+ if verbose:
2139
+ self.logger.info(
2140
+ f'Sample_ids: {Qid}\n'
2141
+ f'Cells: {Qcel}\n'
2142
+ f'Elements: {Qelm}\n'
2143
+ f'Numbers: {Qnum}\n'
2144
+ f'Coords: {Qcoo}\n'
2145
+ f'Fixed: {Qfix}\n'
2146
+ f'Energies: {Qe}\n'
2147
+ f'Forces: {Qf}\n'
2148
+ f'Labels: {Ql}'
2149
+ )
2150
+ return qq
2151
+ except Exception as e:
2152
+ if verbose:
2153
+ self.logger.warning(
2154
+ f"An Error occurred while comparing self and other: {e}\n"
2155
+ f"Hence, `False` is returned as the result."
2156
+ )
2157
+ return False
2158
+
2159
+ def __len__(self, ) -> int:
2160
+ if self.Mode == 'L':
2161
+ return len(self._Sample_ids)
2162
+ elif self.Mode == 'A':
2163
+ return len(self._Sample_ids_) if self._Sample_ids_ is not None else 0
2164
+ else:
2165
+ raise ValueError(f'Mode {self.Mode} not supported.')
2166
+
2167
+ def __repr__(self) -> str:
2168
+ info = (
2169
+ f'BatchStructures[\n\ttotal {len(self)} structures\n\thas Atom_list: {self.Atom_list is not None}\n\t'
2170
+ f'has Atomic_number_list: {self.Atomic_number_list is not None}'
2171
+ f'\n\thas Energies: {self.Energies is not None}\n\thas Forces: {self.Forces is not None}\n\t'
2172
+ f'has Labels: {self.Labels is not None}\n{" " * 15}]'
2173
+ )
2174
+ return info
2175
+
2176
+ def __contains__(self, val) -> bool:
2177
+ if val in self._Sample_ids:
2178
+ return True
2179
+ else:
2180
+ return False
2181
+
2182
+ def __eq__(self, other):
2183
+ return self.eq(other)
2184
+
2185
+ def __getitem__(self, key: int | str | slice | Iterable): # tips: this method was not efficient and used to occasionally inquire information
2186
+ """
2187
+ Args:
2188
+ key: if `int`, inquiring the `key`th data;
2189
+ if other `Hashable` types (e.g., str), inquire the data which Samp_id is `key`;
2190
+ if `slice`, inquiring the range which slice indicates.
2191
+ if `Iterable`, each element in the `key` will be recursively inquired and appended to return.
2192
+
2193
+ Returns: a subclass of BatchStructure.
2194
+ """
2195
+ # check key
2196
+ if isinstance(key, int):
2197
+ if key >= len(self): raise IndexError(f'key {key} is out of range {len(self)}')
2198
+ indx = slice(key, key + 1) if key != -1 else slice(key, None)
2199
+
2200
+ elif isinstance(key, slice):
2201
+ indx = key
2202
+
2203
+ elif isinstance(key, str):
2204
+ if self._indices is None:
2205
+ self._update_indices()
2206
+ self._check_id()
2207
+ if key not in self._indices: raise KeyError(f'the input key `"{key}"` does not exist.')
2208
+ indx = self._indices[key]
2209
+ indx = slice(indx, indx + 1)
2210
+
2211
+ elif isinstance(key, Iterable): # use a recursive strategy
2212
+ sub_self = BatchStructures()
2213
+ for _indx_ in key:
2214
+ sub_self._raw_append(self[_indx_])
2215
+ return sub_self
2216
+
2217
+ else:
2218
+ raise TypeError(f'Invalid type of key: {type(key)}')
2219
+ # load data
2220
+ if self.Atom_list is None:
2221
+ atomlist = None
2222
+ else:
2223
+ atomlist = self.Atom_list[indx]
2224
+ if self.Atomic_number_list is None:
2225
+ atomicnumberlist = None
2226
+ else:
2227
+ atomicnumberlist = self.Atomic_number_list[indx]
2228
+
2229
+ sub_self = BatchStructures()
2230
+ sub_self._Sample_ids = self.Sample_ids[indx]
2231
+ sub_self.Cells = self.Cells[indx]
2232
+ sub_self.Elements = self.Elements[indx]
2233
+ sub_self.Numbers = self.Numbers[indx]
2234
+ sub_self.Coords = self.Coords[indx]
2235
+ sub_self.Coords_type = self.Coords_type[indx]
2236
+ sub_self.Fixed = self.Fixed[indx]
2237
+ sub_self.Atom_list = atomlist
2238
+ sub_self.Atomic_number_list = atomicnumberlist
2239
+ if self.Energies is not None: sub_self.Energies = self.Energies[indx]
2240
+ if self.Forces is not None: sub_self.Forces = self.Forces[indx]
2241
+ if self.Labels is not None: sub_self.Labels = self.Labels[indx]
2242
+
2243
+ return sub_self
2244
+
2245
+ def __setitem__(self, key: int | str | slice, value, *args, **kwargs):
2246
+ if not isinstance(value, BatchStructures):
2247
+ raise TypeError('The set value must be a BatchStructure type.')
2248
+ if not isinstance(key, (int, slice, str)):
2249
+ raise TypeError(f'`key` does not support type {type(key)}')
2250
+ if key not in value:
2251
+ raise ValueError(f'The sample id `{key}` does not in given value.')
2252
+ elif key not in self:
2253
+ self.append(value)
2254
+ else:
2255
+ raise NotImplementedError(
2256
+ 'Directly modifying the data of `key` is not implemented yet. You may use self.append with `strict = False` instead.'
2257
+ )
2258
+
2259
+ def __iter__(self):
2260
+ for i in range(len(self)):
2261
+ yield self[i]