BUCToolkit 1.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
|