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,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]
|