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,923 @@
1
+ """ Preprocessing, including data transformations. """
2
+
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: preprocessing.py
7
+ # Environment: Python 3.12
8
+
9
+ import copy
10
+ import gc
11
+ import os.path
12
+ import pickle
13
+ import random
14
+ # basic modules
15
+ import re
16
+ import time
17
+ import warnings
18
+ from math import floor, ceil
19
+ from typing import Dict, Set, Tuple, List, Sequence, Any, Union, Literal, Generator
20
+
21
+ import joblib as jb
22
+ import numpy as np
23
+ import torch as th
24
+
25
+ from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
26
+ from BUCToolkit.utils._CheckModules import check_module
27
+ from .load_files import OUTCAR2Feat, ExtXyz2Feat, POSCARs2Feat
28
+
29
+ ase = check_module('ase')
30
+ _pyg = check_module('torch_geometric.data')
31
+ dgl = check_module('dgl')
32
+ if _pyg is not None:
33
+ pygData = _pyg.Data
34
+ else:
35
+ pygData = None
36
+
37
+ """ Constance """
38
+ ALL_ELEMENT = {'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar',
39
+ 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr',
40
+ 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe',
41
+ 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
42
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn',
43
+ 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr',
44
+ 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', } # element set
45
+
46
+ __all__ = [
47
+ "CreateASE",
48
+ "CreatePygData",
49
+ "CreateDglData",
50
+ "BlockedRW",
51
+ "split_dataset"
52
+ ]
53
+
54
+
55
+ class ScreenSampleByElement:
56
+ """
57
+ Screening samples by given conditions.
58
+
59
+ Methods:
60
+ screen_from_elements: select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
61
+ screen_from_formula: select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
62
+ """
63
+
64
+ def __init__(self, ) -> None:
65
+ """
66
+ Screening samples by given conditions.
67
+
68
+ Methods:
69
+ screen_from_elements: select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
70
+ screen_from_formula: select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
71
+ """
72
+ warnings.warn(
73
+ 'This Method Has Been Deprecated. Use BatchStructure.contain_... or BatchStructures.not_contain_... instead', DeprecationWarning
74
+ )
75
+ self.non_radio = {'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar',
76
+ 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr',
77
+ 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe',
78
+ 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
79
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi'}
80
+
81
+ def screen_from_elements(self, elem_list_dict: Dict[str, List[str]],
82
+ element_contains: str | Set[str] = 'non-radioactive',
83
+ output_removed_formula: bool = False,
84
+ *args, **kwargs) -> Union[Tuple[Dict, List], Dict, Any]:
85
+ r"""
86
+ select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
87
+
88
+ Parameter:
89
+ elem_list_dict: Dict[str, List[str]], the dict of {id:List[elements]}
90
+ element_contains: Set[str], the set of elements.
91
+ """
92
+ __elem_list_dict = copy.deepcopy(elem_list_dict)
93
+ if element_contains == 'non-radioactive':
94
+ Sequence_elements = self.non_radio
95
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
96
+ else:
97
+ Sequence_elements = element_contains
98
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
99
+
100
+ removed_formula = list()
101
+ for elem in remove_elements:
102
+ for __id, __elem in elem_list_dict.items():
103
+ if elem in __elem:
104
+ removed_formula_ = __elem_list_dict.pop(__id, elem_list_dict[__id])
105
+ if output_removed_formula:
106
+ removed_formula.append(removed_formula_)
107
+ if output_removed_formula:
108
+ return __elem_list_dict, removed_formula
109
+ else:
110
+ return __elem_list_dict
111
+
112
+ def screen_from_elements_para(self, elem_list_dict: Dict[str, List[str]],
113
+ element_contains: str | Set[str] = 'non-radioactive',
114
+ output_removed_formula: bool = False,
115
+ n_core=-1) -> Union[Tuple[Dict, List], Dict, Any]:
116
+ r"""
117
+ select samples that only contains elements in input element_contains from input formula_dict, and return selected dict.
118
+
119
+ """
120
+ __elem_list_dict = copy.deepcopy(elem_list_dict)
121
+ if element_contains == 'non-radioactive':
122
+ Sequence_elements = self.non_radio
123
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
124
+ else:
125
+ Sequence_elements = element_contains
126
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
127
+
128
+ removed_formula = list()
129
+
130
+ def _rm_singl(elem, ):
131
+ for __id, __elem in elem_list_dict.items():
132
+ if elem in __elem:
133
+ removed_formula_ = __elem_list_dict.pop(__id, elem_list_dict[__id])
134
+ if output_removed_formula:
135
+ removed_formula.append(removed_formula_)
136
+
137
+ _para = jb.Parallel(n_jobs=n_core, verbose=1, require='sharedmem')
138
+ _para(jb.delayed(_rm_singl)(elem) for elem in remove_elements)
139
+ if output_removed_formula:
140
+ return __elem_list_dict, removed_formula
141
+ else:
142
+ return __elem_list_dict
143
+
144
+ def screen_from_formula(self, formula_dict: 'dict of {id : formula}', # type: ignore
145
+ element_contains='non-radioactive', # type: ignore
146
+ output_removed_formula: 'bool, whether output a list of removed formula' = False, # type: ignore
147
+ *args, **kwargs) -> Any:
148
+ r"""
149
+ select samples that only contain elements in input element_contains from input formula_dict, and return selected dict.
150
+
151
+ Returns:
152
+ 'dict of selected samples | dict, list of removed formula'
153
+ """
154
+
155
+ # copy a formula_dict
156
+ __formula_dict = copy.deepcopy(formula_dict)
157
+ if element_contains == 'non-radioactive':
158
+ Sequence_elements = self.non_radio
159
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
160
+ else:
161
+ Sequence_elements = element_contains
162
+ remove_elements = ALL_ELEMENT.difference(Sequence_elements)
163
+
164
+ removed_formula = list()
165
+ for elem in remove_elements:
166
+ for __id, __formula in formula_dict.items():
167
+ pattern = elem + '[A-Z0-9()]|(%s$)' % elem # match an element either before [A-Z0-9()] or at the end.
168
+ n_temp = re.search(pattern, __formula)
169
+ if n_temp is not None:
170
+ removed_formula_ = __formula_dict.pop(__id, formula_dict[__id])
171
+ if output_removed_formula:
172
+ removed_formula.append(removed_formula_)
173
+ if output_removed_formula:
174
+ return __formula_dict, removed_formula
175
+ else:
176
+ return __formula_dict
177
+
178
+
179
+ def element_distribution(formulas_list: List):
180
+ """
181
+ Counting the element distribution of formulas.
182
+ para:
183
+ formulas: list (or other Sequence type) of formula strings e.g., [NiPt3, Al2OCo4S, Li2O,...]
184
+ """
185
+ # initialize
186
+ elem_freq_dict = dict() # frequencies of every element in all input formulas, {element symbol : freq}
187
+ n_element_component = dict() # frequencies of number of elements in every compound, {element number : freq}
188
+ Sequence_elements = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar',
189
+ 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr',
190
+ 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe',
191
+ 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
192
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi']
193
+
194
+ for elem in Sequence_elements:
195
+ elem_freq_dict[elem] = 0
196
+ for n in range(20):
197
+ n_element_component['%i' % (n + 1)] = 0
198
+
199
+ # counting
200
+ for i, formula in enumerate(formulas_list):
201
+ n_ = 0
202
+ for elem in Sequence_elements:
203
+ pattern = elem + '[A-Z0-9()]|(%s$)' % elem # match an element either before [A-Z0-9()] or at the end.
204
+ n_temp = re.search(pattern, formula)
205
+ if n_temp is not None:
206
+ elem_freq_dict[elem] += 1
207
+ n_ += 1
208
+ try:
209
+ n_element_component['%i' % n_] += 1
210
+ except Exception as e:
211
+ print(f'WARNING : Failed to count up formula {formula}, because an error "{e}" occurred')
212
+
213
+ return elem_freq_dict, n_element_component
214
+
215
+
216
+ class BandKit:
217
+ """
218
+ A Toolkit to load VASP band file EIGENVAL and KPOINT.
219
+
220
+ Attributes:
221
+ self.eig_info: eigenvalues for each (n, k)
222
+ self.kpt_path: k-points information. Format: [[x1, y1, z1, weight1], ...]
223
+ self.is_spin: whether spin polarized. 1 for False, 2 for True.
224
+ self.k_labels: label of k-points.
225
+ """
226
+
227
+ def __init__(self):
228
+ self.zero_weight_indices = None
229
+ self.kpt_path = None
230
+ self.eig_info = None
231
+ self.is_spin = 1
232
+ self.k_labels = None
233
+ self.k_interval = None
234
+ self.band_interp_func = None
235
+
236
+ def load_EIGENVAL(
237
+ self,
238
+ file: str = './EIGENVAL',
239
+ *args,
240
+ **kwargs
241
+ ) -> None:
242
+ """
243
+ Read VASP output file EIGENVAL to np.array
244
+ """
245
+ with open(file, 'r') as f:
246
+ data = f.readlines()
247
+ self.is_spin = int(data[0].split()[-1])
248
+ nn, k_points, n_bands = data[5].split()
249
+ k_points, n_bands = int(k_points), int(n_bands)
250
+
251
+ k_coords = [data[7 + i * (n_bands + 2)].split() for i in range(k_points)]
252
+ k_coords = np.array(k_coords, dtype=np.float64)
253
+
254
+ eigs = [data[8 + i * (n_bands + 2): 6 + (i + 1) * (n_bands + 2)] for i in range(k_points)]
255
+ eigs = [a.split() for eigs_ in eigs for a in eigs_]
256
+ eigs = np.array(eigs, dtype=np.float64).reshape((k_points, n_bands, 5))
257
+
258
+ self.kpt_path = k_coords # ([[x1, y1, z1, weight1], ...])
259
+ self.zero_weight_indices = np.where(np.abs(self.kpt_path[:, -1]) <= 1e-5)[0]
260
+ self.eig_info = eigs
261
+
262
+ def load_k_path(
263
+ self,
264
+ file: str = './KPATH.in'
265
+ ):
266
+ """
267
+ Read k-path file to determine k-path range.
268
+ """
269
+ with open(file, 'r') as f:
270
+ data = f.readlines()
271
+ if (data[2].split()[0][0] != 'L') and (data[2].split()[0][0] != 'l'):
272
+ raise ValueError('k-path file is not a linear mode VASP KPOINTS file.')
273
+
274
+ k_num_in_each_line = int(data[1].split()[0])
275
+ self.k_labels = list()
276
+ self.k_interval = list()
277
+ has_start = False
278
+ main_data = data[4:]
279
+ for i, kl in enumerate(main_data):
280
+ if kl == ' \n': continue
281
+ if not has_start:
282
+ st1 = kl.split()
283
+ st2 = main_data[i + 1].split()
284
+ labels = (st1[-1], st2[-1])
285
+ self.k_labels.append(labels)
286
+ interval = (np.asarray(st1[:-1], dtype=np.float64), np.asarray(st2[:-1], dtype=np.float64))
287
+ self.k_interval.append(interval)
288
+ has_start = True
289
+ else:
290
+ has_start = False
291
+ continue
292
+
293
+ def calc_effective_mass(self, n: int, k: int | Tuple | np.ndarray = (0, 0, 0)):
294
+ """
295
+ Calculate the effective mass at `k` k-point.
296
+ Args:
297
+ n: the index of band to calc. effective mass.
298
+ k: for `k` is int, the `k`th k-point with 0 weight would be selected. The order is as same as given EIGENVAL.
299
+ for `k` is ndarray or List, it must have the shape of (3, ), and the k-point with this given coordinate would be selected.
300
+ If there are more than 1 k-points had same coordinate, the 1st one would be selected.
301
+
302
+ Returns: float, effective mass.
303
+
304
+ """
305
+ k_0w_points = self.kpt_path[self.zero_weight_indices, :-1]
306
+ eigval_0w = self.eig_info[self.zero_weight_indices]
307
+ if isinstance(k, int):
308
+ kpt = k_0w_points[k]
309
+ k_indx = k
310
+ eig = eigval_0w[k_indx, n, 1:1 + self.is_spin]
311
+ elif isinstance(k, (List, Tuple, np.ndarray)):
312
+ k = np.asarray(k)
313
+ k_indx = np.where(np.linalg.norm(k - k_0w_points, axis=-1) < 1e-6)[0]
314
+ if len(k_indx) != 0:
315
+ kpt = k
316
+ k_indx = k_indx[0]
317
+ eig = eigval_0w[k_indx, n, 1:1 + self.is_spin]
318
+ else:
319
+ raise RuntimeError('Given k-point `k` does not in EIGENVAL.')
320
+ else:
321
+ raise TypeError(f'Invalid `k` type: {type(k)}')
322
+
323
+ find_k_intval = False
324
+ for intval in self.k_interval:
325
+ if np.linalg.norm(kpt - intval[0], axis=-1) < 1e-6: # given k-points at interval start
326
+ kpt_front = k_0w_points[k_indx + 1]
327
+ eig_front = eigval_0w[k_indx + 1, n, 1:1 + self.is_spin]
328
+ kpt_back = 2 * kpt - kpt_front # center symmetry
329
+ eig_back = eig_front
330
+ # collinear judge
331
+ cos_t = (((kpt_front - intval[0]) @ (kpt_front - intval[1])) /
332
+ (np.linalg.norm(kpt_front - intval[0]) * np.linalg.norm(kpt_front - intval[1])))
333
+ if abs(cos_t + 1.) > 1e-5:
334
+ raise RuntimeError(f'No collinear k-point between interval {intval}, so directional derivative cannot be calculate.')
335
+ find_k_intval = True
336
+ break
337
+ elif np.linalg.norm(kpt - intval[1], axis=-1) < 1e-6: # given k-points at interval end
338
+ kpt_back = k_0w_points[k_indx - 1]
339
+ eig_back = eigval_0w[k_indx - 1, n, 1:1 + self.is_spin]
340
+ kpt_front = 2 * kpt - kpt_back # center symmetry
341
+ eig_front = eig_back
342
+ # collinear judge
343
+ cos_t = (((kpt_back - intval[0]) @ (kpt_back - intval[1])) /
344
+ (np.linalg.norm(kpt_back - intval[0]) * np.linalg.norm(kpt_back - intval[1])))
345
+ if abs(cos_t + 1.) > 1e-5:
346
+ raise RuntimeError(f'No collinear k-point between interval {intval}, so directional derivative cannot be calculate.')
347
+ find_k_intval = True
348
+ break
349
+ else: # given k-points within interval
350
+ cos_t = (((kpt - intval[0]) @ (kpt - intval[1])) /
351
+ (np.linalg.norm(kpt - intval[0]) * np.linalg.norm(kpt - intval[1])))
352
+ if abs(cos_t + 1.) < 1e-5:
353
+ kpt_front = k_0w_points[k_indx + 1]
354
+ eig_front = eigval_0w[k_indx + 1, n, 1:1 + self.is_spin]
355
+ kpt_back = k_0w_points[k_indx - 1]
356
+ eig_back = eigval_0w[k_indx - 1, n, 1:1 + self.is_spin]
357
+ find_k_intval = True
358
+ break
359
+ else:
360
+ find_k_intval = False
361
+
362
+ if not find_k_intval:
363
+ raise RuntimeError('Given k-points do not belong any k-path.')
364
+ # interpolation
365
+ # x_ = np.concatenate((kpt_back[None, :], kpt[None, :], kpt_front[None, :]), axis=0)
366
+ # y_ = np.concatenate((eig_back[None, :], eig[None, :], eig_front[None, :]), axis=0)
367
+ # f_interp = interp.interp1d(x_, y_, kind='quadratic', axis=0)
368
+
369
+ # 2nd derivative
370
+ df = np.linalg.norm(kpt - kpt_back)
371
+ db = np.linalg.norm(kpt_front - kpt)
372
+ if (df > 0.02) or (db > 0.02):
373
+ warnings.warn(f'distance among 3 k-points for 2-order central difference quotient are too large ({df, db}), '
374
+ f'thus leading to an inaccurate result.')
375
+ HBAR = 1. # a.u., equal to 1.054571726e-34 J*s
376
+ dev2 = (1 / 27.211) * (eig_front + eig_back - 2 * eig) / (df * db * (1 / 1.889727 ** 2))
377
+ # m_eff = HBAR**2 / dev2 * 2.740322006e-28 # kg, hbar**2/dev2: (6.62606957e-34 J*s)**2/((1.6021766e-19 J)(1e-10 m)**2)
378
+ m_eff = HBAR ** 2 / dev2 # a.u., effective mass relative to static electron mass 9.1093826e-31 kg.
379
+
380
+ return m_eff
381
+
382
+
383
+ class CreateASE:
384
+ r"""
385
+ Create a List[ASE.Atoms] by input crystal information.
386
+
387
+ Parameters:
388
+ verbose: int, control the verboseness of output.
389
+
390
+ Methods:
391
+ create:
392
+ parameters:
393
+ cell_vectors: tensor with shape (batch_size, 3, 3), batch of cell vectors.
394
+ atomic_numbers: tensor with shape (batch_size, n_atom), batch of atomic numbers in each cell.
395
+ atomic_coordinates: tensor with shape (batch_size, n_atom, 3), batch of Cartesian coordinates x,y,z in each cell.
396
+ supercell_index: tensor with shape (3,), the index of cell with respect to the original cell.
397
+ return:
398
+ List[ase.Atoms] with length batch_size
399
+ """
400
+
401
+ def __init__(self, verbose: int = 0) -> None:
402
+ # check model
403
+ if ase is None:
404
+ raise ImportError('`CreateASE` requires package `ase` which could not be imported.')
405
+ self.verbose = verbose
406
+ pass
407
+
408
+ def feat2ase(self, feat: BatchStructures, set_tags: bool = True, n_core=-1):
409
+ r"""
410
+ Convert to ase.Atoms from given BatchStructures.
411
+
412
+ Parameters:
413
+ feat: BatchStructures.
414
+ set_tags: bool, whether to set Atoms.tags which represents the feat.Fixed.
415
+ n_core: number of CPU cores in parallel.
416
+
417
+ Returns:
418
+ List[ase.Atoms], the list of Atoms instances.
419
+ """
420
+ t_st = time.perf_counter()
421
+ feat.generate_atom_list()
422
+ if self.verbose: print('Converting to ASE.Atoms ...')
423
+
424
+ def _base_convert(symb: Sequence, pos: Sequence, cell: Sequence, fix: Sequence,
425
+ pbc: Tuple[bool, bool, bool] | bool = (True, True, True),
426
+ set_tags: bool = True):
427
+ samp = ase.Atoms(symbols=symb, positions=pos, cell=cell, pbc=pbc)
428
+ if set_tags:
429
+ tag = np.sum(fix, axis=1)
430
+ tag = np.where(tag > 0, 1, 0)
431
+ samp.set_tags(tag)
432
+ return samp
433
+
434
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose)
435
+ ase_list = _para(
436
+ jb.delayed(_base_convert)(symb, feat.Coords[i], feat.Cells[i], feat.Fixed[i], True, set_tags) for i, symb in enumerate(feat.Atom_list)
437
+ )
438
+
439
+ if ase_list is None: raise RuntimeError
440
+ if self.verbose: print(f'Done. Total Time: {time.perf_counter() - t_st:<5.4f}')
441
+
442
+ return ase_list
443
+
444
+ def array2ase(self, symb: Sequence, pos: Sequence, cell: Sequence, pbc: Tuple[bool, bool, bool] | bool = (True, True, True),
445
+ set_tags: bool = True, n_core: int = -1):
446
+ r"""
447
+ Convert to ase.Atoms from the given Sequence of symbols, positions, cell vectors and pbc information.
448
+
449
+ Parameters:
450
+ n_core:
451
+ set_tags:
452
+ symb: Sequence[Sequence], the sequence of element lists.
453
+ pos: Sequence[Sequence], the sequence of atom coordinates lists.
454
+ cell: Sequence[Sequence], the sequence of cell vectors lists.
455
+ pbc: bool|Tuple[bool, bool, bool], the direction of periodic boundary condition (x, y, z).
456
+
457
+ Returns:
458
+ List[ase.Atoms], the list of Atoms instances.
459
+ """
460
+ t_st = time.perf_counter()
461
+ if self.verbose: print('Converting to ASE.Atoms ...')
462
+
463
+ def _base_convert(symb: Sequence, pos: Sequence, cell: Sequence, pbc: Tuple[bool, bool, bool] | bool = (True, True, True),
464
+ set_tags: bool = True):
465
+ samp = ase.Atoms(symbols=symb, positions=pos, cell=cell, pbc=pbc)
466
+ if set_tags:
467
+ samp.set_tags(np.ones(len(samp)))
468
+ return samp
469
+
470
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose)
471
+ ase_list = _para(jb.delayed(_base_convert)(_symb, pos[i], cell[i], pbc, set_tags) for i, _symb in enumerate(symb))
472
+
473
+ if ase_list is None: raise RuntimeError
474
+ if self.verbose: print(f'Done. Total Time: {time.perf_counter() - t_st:<5.4f}')
475
+
476
+ return ase_list
477
+
478
+ def feat2ase_dict(self, feat: BatchStructures, set_tags: bool = True, n_core: int = -1):
479
+ r"""
480
+ Convert to ase.Atoms from given BatchStructures.
481
+
482
+ Parameters:
483
+ n_core:
484
+ feat: BatchStructures.
485
+ set_tags: bool, whether set Atoms.tags = np.ones(n_atom) automatically.
486
+
487
+ Returns:
488
+ Dict[samp_id:ase.Atoms], the dict of Atoms instances with keys sample id.
489
+ """
490
+ t_st = time.perf_counter()
491
+ ase_dict = dict()
492
+ feat.generate_atom_list()
493
+ if self.verbose: print('Converting to ASE.Atoms ...')
494
+
495
+ def _sig_conv(i, symb):
496
+ samp = ase.Atoms(symbols=symb, positions=feat.Coords[i], cell=feat.Cells[i], pbc=True)
497
+ if set_tags:
498
+ samp.set_tags(np.ones(len(samp)))
499
+ ase_dict[feat.Sample_ids[i]] = samp
500
+
501
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose, require='sharedmem')
502
+ _para(jb.delayed(_sig_conv)(i, s) for i, s in enumerate(feat.Atom_list)) # type: ignore
503
+
504
+ if self.verbose: print(f'Done. Total Time: {time.perf_counter() - t_st:<5.4f}')
505
+
506
+ return ase_dict
507
+
508
+
509
+ class CreatePygData:
510
+ r"""
511
+ create torch-geometric.data.Data or Batch from various types.
512
+ """
513
+
514
+ def __init__(self, verbose: int = 0) -> None:
515
+ # check module
516
+ if _pyg is None:
517
+ raise ImportError('`CreatePygData` requires package `torch-geometric` which could not be imported.')
518
+ self.verbose = verbose
519
+ pass
520
+
521
+ @staticmethod
522
+ def single_ase2data(atoms):
523
+ """Convert a single atomic structure to a graph.
524
+
525
+ Args:
526
+ atoms (ase.atoms.Atoms): An ASE atoms object.
527
+
528
+ Returns:
529
+ data (torch_geometric.data.Data): A geometric data object with positions, atomic_numbers, tags,
530
+ and optionally, energy, forces, distances, edges, and periodic boundary conditions.
531
+ Optional properties can include by setting r_property=True when constructing the class.
532
+ """
533
+
534
+ # set the atomic numbers, positions, and cell
535
+ atomic_numbers = th.Tensor(atoms.get_atomic_numbers())
536
+ positions = th.Tensor(atoms.get_positions())
537
+ cell = th.Tensor(np.array(atoms.get_cell())).view(1, 3, 3)
538
+ natoms = positions.shape[0]
539
+ # initialized to th.zeros(natoms) if tags missing.
540
+ # https://wiki.fysik.dtu.dk/ase/_modules/ase/atoms.html#Atoms.get_tags
541
+ tags = th.Tensor(atoms.get_tags())
542
+ fixed = th.zeros_like(atomic_numbers)
543
+ pbc = th.from_numpy(atoms.pbc)
544
+ # put the minimum data in th geometric data object
545
+ data = pygData(
546
+ cell=cell,
547
+ pos=positions,
548
+ atomic_numbers=atomic_numbers,
549
+ natoms=natoms,
550
+ tags=tags,
551
+ fixed=fixed,
552
+ pbc=pbc,
553
+ )
554
+ return data
555
+
556
+ def ase2data_list(self, atom_list: List, n_core: int = 1) -> List[pygData]:
557
+ r"""
558
+ Convert a list of ase.Atoms into a pyg.Batch
559
+ """
560
+ if n_core == 1:
561
+ if self.verbose: print('Converting ase Atoms to pyg Batch sequentially...')
562
+ data_list = [self.single_ase2data(_atom) for _atom in atom_list]
563
+ else:
564
+ if self.verbose: print(f'Converting ase Atoms to pyg Batch in parallel with {n_core} cores...')
565
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose)
566
+ data_list: List | None = _para(jb.delayed(self.single_ase2data)(_atom) for _atom in atom_list)
567
+ if data_list is None: raise RuntimeError('Occurred None data.')
568
+
569
+ if self.verbose: print('Done.')
570
+ return data_list
571
+
572
+ def feat2data_list(self, feat: BatchStructures, n_core: int = 1) -> List[pygData]:
573
+ r"""
574
+ Convert BatchStructures into a list of pyg.Data for fair-chem model
575
+ """
576
+ if feat.Atomic_number_list is None: feat.generate_atomic_number_list()
577
+
578
+ def _convert_single(_id, _cell, _coords, _atomic_numbers, _fix):
579
+ cell = th.from_numpy(_cell).view(1, 3, 3).to(th.float32)
580
+ positions = th.from_numpy(_coords).to(th.float32)
581
+ atomic_numbers = th.tensor(_atomic_numbers) # type: ignore
582
+ natoms = len(atomic_numbers)
583
+ tags = th.ones_like(atomic_numbers, dtype=th.float32)
584
+ fixed = th.from_numpy(_fix) #.unsqueeze(0) # fixme
585
+ pbc = th.tensor([[True, True, True]])
586
+ # put the minimum data in th geometric data object
587
+ _data = pygData(
588
+ cell=cell,
589
+ pos=positions,
590
+ atomic_numbers=atomic_numbers,
591
+ natoms=natoms,
592
+ tags=tags,
593
+ fixed=fixed,
594
+ pbc=pbc,
595
+ idx=_id,
596
+ sid=_id
597
+ )
598
+ return _data
599
+
600
+ if n_core == 1:
601
+ if self.verbose: print('Converting BatchStructures to pyg Batch sequentially...')
602
+ data_list: List = [_convert_single(_id,
603
+ feat.Cells[i],
604
+ feat.Coords[i],
605
+ feat.Atomic_number_list[i],
606
+ feat.Fixed[i]) for i, _id in enumerate(feat.Sample_ids)] # type: ignore
607
+ else:
608
+ if self.verbose: print(f'Converting BatchStructures to pyg Batch in parallel with {n_core} cores...')
609
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose)
610
+ data_list = _para(jb.delayed(_convert_single)(_id,
611
+ feat.Cells[i],
612
+ feat.Coords[i],
613
+ feat.Atomic_number_list[i],
614
+ feat.Fixed[i]) for i, _id in enumerate(feat.Sample_ids)) # type: ignore
615
+
616
+ if self.verbose: print('Done.')
617
+ return data_list
618
+
619
+
620
+ class CreateDglData:
621
+ r"""
622
+ create dgl.graph from various types.
623
+ The output DGLGraph has format as follows:
624
+ dgl.heterograph(
625
+ {
626
+ ('atom', 'bond', 'atom'): ([], []),
627
+ ('cell', 'disp', 'cell'): ([], [])
628
+ },
629
+ num_nodes_dict={
630
+ 'atom': n_atom,
631
+ 'cell': 1
632
+ }
633
+ )
634
+ data.nodes['atom'].data['pos']: (n_atom, 3), Atom positions in Cartesian coordinates.
635
+ data.nodes['atom'].data['Z']: (n_atom, ), Atomic numbers.
636
+ data.nodes['cell'].data['cell']: (1, 3, 3), Cell vectors.
637
+ """
638
+
639
+ def __init__(self, verbose: int = 0) -> None:
640
+ # check module
641
+ if dgl is None:
642
+ raise ImportError('`CreateDglData` requires package `dgl` which could not be imported.')
643
+ self.verbose = verbose
644
+ pass
645
+
646
+ @staticmethod
647
+ def single_ase2graph(atoms):
648
+ """
649
+ Convert a single atomic structure to a graph.
650
+
651
+ Args:
652
+ atoms (ase.atoms.Atoms): An ASE atoms object.
653
+
654
+ Returns:
655
+ data (torch_geometric.data.Data): A geometric data object with positions, atomic_numbers, tags,
656
+ and optionally, energy, forces, distances, edges, and periodic boundary conditions.
657
+ Optional properties can include by setting r_property=True when constructing the class.
658
+ """
659
+
660
+ # set the atomic numbers, positions, and cell
661
+ atomic_numbers = th.Tensor(atoms.get_atomic_numbers())
662
+ positions = th.Tensor(atoms.get_positions())
663
+ cell = th.from_numpy(np.array(atoms.get_cell())).view(1, 3, 3)
664
+ n_atoms = positions.shape[0]
665
+ # initialized to th.zeros(natoms) if tags missing.
666
+ # https://wiki.fysik.dtu.dk/ase/_modules/ase/atoms.html#Atoms.get_tags
667
+ # TODO: these other properties might be used in future
668
+ tags = th.Tensor(atoms.get_tags())
669
+ fixed = th.zeros_like(atomic_numbers)
670
+ pbc = th.from_numpy(atoms.pbc)
671
+ # TODO: END
672
+ # put the minimum data in th geometric data object
673
+ data = dgl.heterograph(
674
+ {
675
+ ('atom', 'bond', 'atom'): ([], []),
676
+ ('cell', 'disp', 'cell'): ([], [])
677
+ },
678
+ num_nodes_dict={
679
+ 'atom': n_atoms,
680
+ 'cell': 1
681
+ }
682
+ )
683
+ data.nodes['atom'].data['pos'] = positions
684
+ data.nodes['atom'].data['Z'] = atomic_numbers
685
+ data.nodes['cell'].data['cell'] = cell
686
+ return data
687
+
688
+ def ase2graph_list(self, atom_list: List, n_core: int = 1) -> List[pygData]:
689
+ r"""
690
+ Convert a list of ase.Atoms into a list of dgl.DGLGraph
691
+ """
692
+ if n_core == 1:
693
+ if self.verbose: print('Converting ase.Atoms to dgl.DGLGraph sequentially...')
694
+ data_list = [self.single_ase2graph(_atom) for _atom in atom_list]
695
+ else:
696
+ if self.verbose: print(f'Converting ase Atoms to pyg Batch in parallel with {n_core} cores...')
697
+ _para = jb.Parallel(n_jobs=n_core, verbose=self.verbose)
698
+ data_list: List | None = _para(jb.delayed(self.single_ase2graph)(_atom) for _atom in atom_list)
699
+ if data_list is None: raise RuntimeError('Occurred None data.')
700
+
701
+ if self.verbose: print('Done.')
702
+ return data_list
703
+
704
+ def feat2graph_list(self, feat: BatchStructures, n_core: int = 1) -> List[pygData]:
705
+ r"""
706
+ Convert BatchStructures into a list of pyg.Data for fair-chem model
707
+ """
708
+ if feat.Atomic_number_list is None: feat.generate_atomic_number_list()
709
+
710
+ def _convert_single(_id, _cell, _coords, _atomic_numbers, _fix):
711
+ cell = th.from_numpy(_cell).view(1, 3, 3).to(th.float32)
712
+ positions = th.from_numpy(_coords).to(th.float32)
713
+ atomic_numbers = th.tensor(_atomic_numbers) # type: ignore
714
+ n_atoms = len(atomic_numbers)
715
+ fixed = th.from_numpy(_fix).to(th.float32) # .unsqueeze(0) # fixme
716
+ # put the minimum data in th geometric data object
717
+ _data = dgl.heterograph(
718
+ {
719
+ ('atom', 'bond', 'atom'): ([], []),
720
+ ('cell', 'disp', 'cell'): ([], [])
721
+ },
722
+ num_nodes_dict={
723
+ 'atom': n_atoms,
724
+ 'cell': 1
725
+ }
726
+ )
727
+ _data.nodes['atom'].data['pos'] = positions
728
+ _data.nodes['atom'].data['Z'] = atomic_numbers
729
+ _data.nodes['cell'].data['cell'] = cell
730
+ _data.nodes['cell'].data['idx'] = th.tensor([_id], dtype=th.int64)
731
+ return _data
732
+
733
+ if n_core == 1:
734
+ if self.verbose: print('Converting BatchStructures to dgl.Graph sequentially...')
735
+ data_list: List = [
736
+ _convert_single(
737
+ i,
738
+ feat.Cells[i],
739
+ feat.Coords[i],
740
+ feat.Atomic_number_list[i],
741
+ feat.Fixed[i]
742
+ ) for i, _id in enumerate(feat.Sample_ids)
743
+ ] # type: ignore
744
+ else:
745
+ if self.verbose: print(f'Converting BatchStructures to dgl.Graph in parallel with {n_core} cores...')
746
+ with jb.Parallel(n_jobs=n_core, verbose=self.verbose, backend='threading') as _para:
747
+ data_list = _para(
748
+ jb.delayed(_convert_single)(
749
+ i,
750
+ feat.Cells[i],
751
+ feat.Coords[i],
752
+ feat.Atomic_number_list[i],
753
+ feat.Fixed[i]
754
+ ) for i, _id in enumerate(feat.Sample_ids)
755
+ )
756
+
757
+ if self.verbose: print('Done.')
758
+ return data_list
759
+
760
+
761
+ class BlockedRW:
762
+ """
763
+ Blocked read files and save to a memory-mapping file, or load the memory-mapping file and write to specific files inversely.
764
+ Used to manage big files that memory cannot be loaded at once.
765
+ """
766
+ @staticmethod
767
+ def save(
768
+ files_path: str,
769
+ file_format: Literal['OUTCAR', 'EXTXYZ', 'POSCAR'],
770
+ file_name_list: List[str] | None = None,
771
+ read_configs: Dict | None = None,
772
+ save_path: str = './data',
773
+ chunk_size: int = 32,
774
+ verbose: int = 1
775
+ ):
776
+ """
777
+ Blocked reading files in `files_path`/`file_name_list` by files_reader, and save to `save_path` as memory-mapping files.
778
+ Args:
779
+ files_path: path of files.
780
+ file_format: format of files to read or write.
781
+ file_name_list: list of file names to read. None for all files in given `files_path`.
782
+ read_configs: kwargs of file reader.
783
+ save_path: the path to save memory-mapping files.
784
+ chunk_size: Number of files (NOT Structures) read at a time.
785
+ verbose: verboseness of printing.
786
+
787
+ Returns: None
788
+ """
789
+ if file_format == 'OUTCAR':
790
+ reader = OUTCAR2Feat
791
+ elif file_format == 'EXTXYZ':
792
+ reader = ExtXyz2Feat
793
+ elif file_format == 'POSCAR':
794
+ reader = POSCARs2Feat
795
+ else:
796
+ raise ValueError(f'`file_format {file_format} is invalid.')
797
+
798
+ if read_configs is None: read_configs = dict()
799
+ # split chunks
800
+ n_file = len(file_name_list)
801
+ n_loop = ceil(n_file / chunk_size)
802
+ # write the first chunk file
803
+ f = reader(files_path, verbose)
804
+ f.read(file_name_list[:chunk_size], **read_configs)
805
+ f.save(save_path, 'w')
806
+ del f
807
+ gc.collect()
808
+ for i in range(1, n_loop):
809
+ if verbose > 0:
810
+ print(f'Chunk {i}/{n_loop}, total {chunk_size * i} files have been saved.')
811
+ files = file_name_list[chunk_size * i: chunk_size * (i + 1)]
812
+ f = reader(files_path, verbose)
813
+ f.read(files, **read_configs)
814
+ f.save(save_path, 'a')
815
+ del f
816
+ gc.collect()
817
+
818
+ @staticmethod
819
+ def load(
820
+ load_path: str,
821
+ load_range: Tuple[int, int] | None = None,
822
+ chunk_size: int = 5000,
823
+ verbose: int = 1
824
+ ) -> Generator[BatchStructures, None, None]:
825
+ """
826
+ Blocked load data from given BatchStructure memory-mapping path.
827
+ Args:
828
+ load_path: the path to load memory-mapping.
829
+ load_range: the range of structures to read. `None` for all files in given `load_path`.
830
+ chunk_size: number of structures to read at a time.
831
+ verbose: verboseness of printing.
832
+
833
+ Returns: A generator that yields a BatchStructure with given `chunk_size` each time.
834
+ """
835
+ # read the head file
836
+ with open(os.path.join(load_path, 'head'), 'rb') as f:
837
+ header = pickle.load(f)
838
+ total_size = header['n_batch']
839
+ if load_range is None:
840
+ load_range = (0, total_size)
841
+ elif isinstance(load_range, tuple):
842
+ assert 0 <= load_range[0] <= load_range[1] <= total_size, f'`load_range` must be in (0, {total_size}) range.'
843
+ else:
844
+ raise ValueError(f'Expected `load_range` to be tuple or None, but got {load_range}.')
845
+
846
+ # split chunk
847
+ n_file = load_range[1] - load_range[0]
848
+ n_loop = ceil(n_file / chunk_size)
849
+ # main loop
850
+ for i in range(0, n_loop):
851
+ ptr_st = load_range[0] + i * chunk_size
852
+ ptr_ed = min(ptr_st + chunk_size, load_range[1])
853
+ if verbose > 0:
854
+ print(f'Chunk {i}/{n_loop}, total {chunk_size * i} files have been read.')
855
+ bs = BatchStructures.load_from_file(load_path, (ptr_st, ptr_ed))
856
+ yield bs
857
+
858
+
859
+
860
+ def split_dataset(
861
+ data: BatchStructures,
862
+ ratio: float|List[float] = 0.9,
863
+ save_path: str|List[str]|None = None,
864
+ shuffle: bool = False,
865
+ seed: int = None
866
+ ):
867
+ """
868
+ Splitting the data set into parts with given ratios. It would overwrite if `save_path` already exists.
869
+ Args:
870
+ data: data set with BatchStructure format
871
+ ratio: splitting ratio. If only a float number was given, data would be split into 2 parts with ratio `ratio` and `1 - ratio`.
872
+ The summation of all ratios must be 1.
873
+ save_path: if None, List of split data would be directly returned; otherwise they will be saved as memory-mapping files in given paths.
874
+ The length of `save_path` must be the same as `ratio`.
875
+ shuffle: whether to shuffle data. If not, data will be divided sequentially.
876
+ seed: random seed for shuffle.
877
+
878
+ Returns: List | None
879
+
880
+ """
881
+ # check vars
882
+ if not isinstance(data, BatchStructures):
883
+ raise TypeError('data must be BatchStructures.')
884
+ if isinstance(ratio, float): ratio = [ratio, 1 - ratio]
885
+ if sum(ratio) != 1.:
886
+ raise ValueError(f'Summation of `ratio` must be 1., but got {sum(ratio)}.')
887
+ for _ in ratio:
888
+ if (_ >= 1.) or (_ <= 0.): raise ValueError(f'All values in `ratio` must between (0, 1), but got {_}.')
889
+ if save_path is not None:
890
+ assert len(save_path) == len(ratio), f'`save_path` must have a same length with `ratio`, but got {len(save_path), len(ratio)}.'
891
+ result_list: list[BatchStructures] = list()
892
+ # shuffle
893
+ if shuffle:
894
+ # create a shuffle indices
895
+ indx = list(range(len(data)))
896
+ random.seed(seed)
897
+ random.shuffle(indx)
898
+ # create an inverse indices
899
+ inv_indx = [0] * len(data)
900
+ for j, k in enumerate(indx):
901
+ inv_indx[k] = j
902
+ data.rearrange(indx)
903
+
904
+ n_sample = len(data)
905
+ data_indx_now = 0
906
+ for i, _ in enumerate(ratio[:-1]):
907
+ sub_BS = data[data_indx_now : data_indx_now + floor(n_sample * _)]
908
+ if save_path is not None:
909
+ sub_BS.save(save_path[i])
910
+ else:
911
+ result_list.append(sub_BS)
912
+ data_indx_now += floor(n_sample * _)
913
+ sub_BS = data[data_indx_now:]
914
+ if save_path is not None:
915
+ sub_BS.save(save_path[-1])
916
+ else:
917
+ result_list.append(sub_BS)
918
+
919
+ return result_list if save_path is None else None
920
+
921
+
922
+
923
+