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,1394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contiguous dumping and reading arrays data by memory mapping.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Copyright (c) 2025.7.4, BUCToolkit.
|
|
6
|
+
# Authors: Pu Pengxin, Song Xin
|
|
7
|
+
# Version: 0.9a
|
|
8
|
+
# File: MemMapTensorsIO.py
|
|
9
|
+
# Environment: Python 3.12
|
|
10
|
+
from typing import Dict, List, Literal, Tuple, ByteString
|
|
11
|
+
from io import BufferedRandom
|
|
12
|
+
import warnings, os, mmap, copy, gc, math
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Optional, Dict, Callable, Any, Literal, Sequence, List
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import torch as th
|
|
18
|
+
from torch import nn
|
|
19
|
+
|
|
20
|
+
from BUCToolkit._version import __version__
|
|
21
|
+
from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
|
|
22
|
+
from BUCToolkit.utils.ElemListReduce import elem_list_reduce
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ArrayDumper:
|
|
26
|
+
"""
|
|
27
|
+
Arrays Dumper for continuing writing arrays to the disk during a contiguous iteration process.
|
|
28
|
+
"""
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
path: str,
|
|
32
|
+
mode: Literal['w', 'x', 'a'] = 'x',
|
|
33
|
+
cache_size: int = 4096,
|
|
34
|
+
head_order: Literal['<', '>'] = '<',
|
|
35
|
+
use_mmap: bool = False,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
path: the file path to save arrays. The File will not open until `self.initialize()` is called.
|
|
41
|
+
mode: the mode of dumper. 'w' for writing & overwriting, 'x' for writing only if file does not exist, 'a' for appending.
|
|
42
|
+
cache_size: the max cache size in kilobytes (kb) to flush to the disk.
|
|
43
|
+
head_order: the head order of the array head information. '<' is the little order, and '>' is the big order.
|
|
44
|
+
use_mmap: whether to use memory map to save arrays.
|
|
45
|
+
"""
|
|
46
|
+
# init vars
|
|
47
|
+
self._dump_file = None # the TextIOWrapper of the file
|
|
48
|
+
self._mmp_f: BufferedRandom|mmap.mmap|None = None # the mmap obj of the file
|
|
49
|
+
self._has_initialized = False
|
|
50
|
+
self._has_started = False
|
|
51
|
+
self._n_arrays = None # number of arrays in each collect step
|
|
52
|
+
self._n_groups = None # number of the array groups.
|
|
53
|
+
self._nbytes_list_to_check = None # check bytes when collect in each step.
|
|
54
|
+
self._count: int|None = None # count the number of steps to collect
|
|
55
|
+
self._cache_size_now: int|None = None
|
|
56
|
+
self._ptr: int|None = None # the current pointer/indices
|
|
57
|
+
self._endptr: int|None = None # the endpoint of the file
|
|
58
|
+
self._current_group_head_position: int|None = None # the start position of the current group head
|
|
59
|
+
self._use_dynamic_steps = False # whether to use dynamic steps. False is to use fixed steps.
|
|
60
|
+
#self.initialize()
|
|
61
|
+
# check input
|
|
62
|
+
self.reset_args(
|
|
63
|
+
path=path,
|
|
64
|
+
mode=mode,
|
|
65
|
+
cache_size=cache_size,
|
|
66
|
+
head_order=head_order,
|
|
67
|
+
use_mmap=use_mmap,
|
|
68
|
+
)
|
|
69
|
+
# test BOM
|
|
70
|
+
if len('a'.encode(self._str_fmt)) != 2:
|
|
71
|
+
raise NotImplementedError(
|
|
72
|
+
f'You may check here whether the encode format {self._str_fmt} is correct. '
|
|
73
|
+
f'Normally it would not be added BOM at the start of bytes, thus resulting 2 bytes, but here string "a" '
|
|
74
|
+
f'is encoded into {len("a".encode(self._str_fmt))} bytes instead.'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def reset_args(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
mode: Literal['w', 'x', 'a'] = 'x',
|
|
81
|
+
cache_size: int = 4096,
|
|
82
|
+
head_order: Literal['<', '>'] = '<',
|
|
83
|
+
use_mmap: bool = False,
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Reset args values BEFORE initializing.
|
|
87
|
+
Returns: None
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
if self._has_initialized:
|
|
91
|
+
warnings.warn(f'The dumper has been initialized already. Resetting args is unavailable.')
|
|
92
|
+
return None
|
|
93
|
+
self.path = str(path)
|
|
94
|
+
self.cache_size = int(cache_size) * 1024 # convert to bytes
|
|
95
|
+
if not self.cache_size > 0: raise ValueError(f"cache_size must be greater than 0, but got {cache_size}")
|
|
96
|
+
self.use_mmap = use_mmap
|
|
97
|
+
if mode in ('w', 'x', 'a'):
|
|
98
|
+
self.mode = mode
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"mode must be 'w', 'x', or 'a', but got {mode}")
|
|
101
|
+
if os.path.isdir(self.path):
|
|
102
|
+
raise IOError(f"The path '{self.path}' has already exist as a directory.")
|
|
103
|
+
if (not os.path.isfile(self.path)) and (self.mode == 'a'):
|
|
104
|
+
# warnings.warn(f'The mode is "a" but file {self.path} does not exist. Hence, mode has been reset to "w".')
|
|
105
|
+
self.mode = 'w'
|
|
106
|
+
|
|
107
|
+
__ORDER1 = {'<': 'utf-16-le', '>': 'utf-16-be'}
|
|
108
|
+
if not head_order in __ORDER1: raise ValueError(f'head_order must be "<" or ">", but got {head_order}.')
|
|
109
|
+
self.head_order = head_order
|
|
110
|
+
self._str_fmt = __ORDER1[head_order]
|
|
111
|
+
__ORDER2: Dict[Literal['<', '>'], Literal['little', 'big']] = {'<': 'little', '>': 'big'}
|
|
112
|
+
self._num_fmt: Literal['little', 'big'] = __ORDER2[head_order]
|
|
113
|
+
|
|
114
|
+
def initialize(self, ):
|
|
115
|
+
r"""
|
|
116
|
+
do initialization before saving arrays, writing the head information.
|
|
117
|
+
Note:
|
|
118
|
+
Use below format as the full file header (in total 16 byte):
|
|
119
|
+
`head_order``magic``version``n_groups`
|
|
120
|
+
char2 char4 2*int1 uint8
|
|
121
|
+
where char2 is one unicode-16 that use 2 bytes, magic is hard coded as "BM" in unicode-16, and version is 2 8-bit-ints.
|
|
122
|
+
`head_order` is Literal['<', '>'], hence one can try both little and big order until one of "<" and ">" is read.
|
|
123
|
+
`n_groups` is the total number of groups in the mmap file. It will be dynamically updated (+1) once `self.start`
|
|
124
|
+
or `start_from_arrays` is called.
|
|
125
|
+
Use below format as the array data header, where int8 (here "8" is 8 bytes) "0" is as the delimiter among shapes:
|
|
126
|
+
`char``n_cycle``n_array``dtype1``shape1[]`0`dtype2``shape2[]`0...`dtype_n``shape_n[]`0`byte_data`...
|
|
127
|
+
HEAD uint8 uint8 char8 [int8, ] 0 char8 [int8, ] 0 ...
|
|
128
|
+
where `char` is the character number indicate the array head information, which is hard coded as "HEAD",
|
|
129
|
+
8 bytes `dtype` are in form of `order``type``length(byte)`, e.g., "<i4" is 32 bit integer in the little order.
|
|
130
|
+
`order` can be "<", "|", or ">"; `type` can be "i" (signed int), "u" (unsigned int), "f" (float), or "c" (complex), they are
|
|
131
|
+
both in unicode-16 (total 4 bytes), and the last number `length` is a 4-byte-int.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
if self._has_initialized:
|
|
135
|
+
warnings.warn('Cannot create a new dumping file because it is already initialized.')
|
|
136
|
+
return
|
|
137
|
+
_open_mode = {'w': 'wb+', 'x': 'xb+', 'a': 'rb+'}.get(self.mode)
|
|
138
|
+
self._n_groups = 0
|
|
139
|
+
if self._dump_file is None:
|
|
140
|
+
self._dump_file = open(self.path, _open_mode)
|
|
141
|
+
else:
|
|
142
|
+
warnings.warn('??? BUG: self is not initialized but there is already an opened dumping file ???')
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if self.mode == 'a':
|
|
146
|
+
if self.use_mmap:
|
|
147
|
+
self._mmp_f = mmap.mmap(self._dump_file.fileno(), 0, access=mmap.ACCESS_WRITE) # allocate 16 bytes to write the head
|
|
148
|
+
else: # simply reference the normal IOWrapper
|
|
149
|
+
self._mmp_f = self._dump_file
|
|
150
|
+
self._mmp_f.seek(0)
|
|
151
|
+
file_head = self._mmp_f.read(16)
|
|
152
|
+
self._parse_head(file_head)
|
|
153
|
+
# jump to the end to append
|
|
154
|
+
self._mmp_f.seek(0, 2)
|
|
155
|
+
self._ptr = self._mmp_f.tell()
|
|
156
|
+
self._endptr = self._mmp_f.tell()
|
|
157
|
+
self._has_initialized = True
|
|
158
|
+
else:
|
|
159
|
+
self._dump_file.truncate(16)
|
|
160
|
+
if self.use_mmap:
|
|
161
|
+
self._mmp_f = mmap.mmap(self._dump_file.fileno(), 0, access=mmap.ACCESS_WRITE) # allocate 16 bytes to write the head
|
|
162
|
+
else: # simply reference the normal IOWrapper
|
|
163
|
+
self._mmp_f = self._dump_file
|
|
164
|
+
self._mmp_f.write(f'{self.head_order}BM'.encode(self._str_fmt))
|
|
165
|
+
_v = __version__.split('.', 2)
|
|
166
|
+
v1 = int(_v[0])
|
|
167
|
+
v2 = int(_v[1])
|
|
168
|
+
if v1 >= 255 or v2 >= 255:
|
|
169
|
+
raise NotImplementedError(f'The version numbers have been reached {_v}. How frightful!')
|
|
170
|
+
__version_bytes = bytearray(2) # It is frightful that version number > 255
|
|
171
|
+
__version_bytes[0:1] = v1.to_bytes(1, byteorder=self._num_fmt)
|
|
172
|
+
__version_bytes[1:2] = v2.to_bytes(1, byteorder=self._num_fmt)
|
|
173
|
+
self._mmp_f.write(__version_bytes) # version: `v1`.`v2`
|
|
174
|
+
self._mmp_f.write(self._n_groups.to_bytes(8, byteorder=self._num_fmt))
|
|
175
|
+
self._has_initialized = True
|
|
176
|
+
self._ptr = 16
|
|
177
|
+
self._endptr = 16
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
raise RuntimeError(f'Failed to initialize dumping file `{self.path}`: {e}')
|
|
181
|
+
|
|
182
|
+
finally:
|
|
183
|
+
self._tmp_close()
|
|
184
|
+
|
|
185
|
+
def _parse_head(self, file_head: bytes):
|
|
186
|
+
"""
|
|
187
|
+
Parse the head of the mmap file for append mode.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
if len(file_head) != 16:
|
|
191
|
+
raise ValueError(f'The file head is not complete.')
|
|
192
|
+
# find the head order
|
|
193
|
+
try_order_le = file_head[:2].decode('utf-16-le')
|
|
194
|
+
try_order_be = file_head[:2].decode('utf-16-be')
|
|
195
|
+
if try_order_le == '<':
|
|
196
|
+
_str_fmt = 'utf-16-le'
|
|
197
|
+
self._num_fmt: Literal['little', 'big'] = 'little'
|
|
198
|
+
elif try_order_be == '>':
|
|
199
|
+
_str_fmt = 'utf-16-be'
|
|
200
|
+
self._num_fmt: Literal['little', 'big'] = 'big'
|
|
201
|
+
else:
|
|
202
|
+
raise ValueError(f'Unexpected endianness: {try_order_le}/{try_order_be}.')
|
|
203
|
+
if _str_fmt != self._str_fmt:
|
|
204
|
+
warnings.warn(f'Inconsistent endianness between input and read file: {self._str_fmt} and {_str_fmt}. ')
|
|
205
|
+
self._str_fmt = _str_fmt
|
|
206
|
+
# check magik
|
|
207
|
+
magik = file_head[2:6].decode(self._str_fmt)
|
|
208
|
+
if magik != "BM":
|
|
209
|
+
raise ValueError(f'Unknown file format: {magik}.')
|
|
210
|
+
# check version
|
|
211
|
+
v1 = int.from_bytes(file_head[6:7], self._num_fmt, signed=False)
|
|
212
|
+
v2 = int.from_bytes(file_head[7:8], self._num_fmt, signed=False)
|
|
213
|
+
_version_now = __version__.split('.', 2)
|
|
214
|
+
v1_now = int(_version_now[0])
|
|
215
|
+
v2_now = int(_version_now[1])
|
|
216
|
+
if v1_now >= 255 or v2_now >= 255:
|
|
217
|
+
raise NotImplementedError(f'The version numbers have been reached {_version_now}. How frightful!')
|
|
218
|
+
if (v1, v2) != (v1_now, v2_now):
|
|
219
|
+
warnings.warn(
|
|
220
|
+
f"The file {self.path} (ver \"{'.'.join((str(v1), str(v2)))}\") is incompatible with "
|
|
221
|
+
f"the current version \"{__version__}\".",
|
|
222
|
+
RuntimeWarning,
|
|
223
|
+
)
|
|
224
|
+
# read the group number
|
|
225
|
+
self._n_groups = int.from_bytes(file_head[8:16], self._num_fmt, signed=False)
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise RuntimeError(f'Failed to parse file header: {e}')
|
|
229
|
+
|
|
230
|
+
def allocate(self, size: int):
|
|
231
|
+
"""
|
|
232
|
+
Append a new space at the end of the mmap file. It will keep the file and mmap opening.
|
|
233
|
+
Args:
|
|
234
|
+
size (int): new space size in bytes.
|
|
235
|
+
Returns: None
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
if not self._has_initialized:
|
|
239
|
+
self.initialize()
|
|
240
|
+
if size <= 0:
|
|
241
|
+
raise ValueError(f'size must be greater than 0, but got {size}.')
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
if self._dump_file.closed:
|
|
245
|
+
self._dump_file = open(self.path, 'rb+')
|
|
246
|
+
|
|
247
|
+
self._endptr += size
|
|
248
|
+
if self.use_mmap:
|
|
249
|
+
if self._mmp_f.closed:
|
|
250
|
+
self._mmp_f = mmap.mmap(self._dump_file.fileno(), 0, access=mmap.ACCESS_WRITE)
|
|
251
|
+
self._mmp_f.resize(self._endptr)
|
|
252
|
+
else:
|
|
253
|
+
self._mmp_f = self._dump_file
|
|
254
|
+
self._mmp_f.truncate(self._endptr)
|
|
255
|
+
self._mmp_f.seek(self._ptr)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self._tmp_close()
|
|
258
|
+
raise RuntimeError(f'Failed to allocate new {size} bytes to `{self.path}`: {e}')
|
|
259
|
+
|
|
260
|
+
def start_from_arrays(self, steps: int, *arrays: np.ndarray, force: bool = False):
|
|
261
|
+
"""
|
|
262
|
+
Write the head information of array from a prototype arrays, allocate the blank space, thus starting a new dumping series.
|
|
263
|
+
Note that arrays input here are only a prototype that will NOT be written to the disk.
|
|
264
|
+
One must call `self.step` method to really write arrays.
|
|
265
|
+
Args:
|
|
266
|
+
steps: the iteration number of arrays, which will be dumped. If `steps` = -1, a dynamic steps will be used.
|
|
267
|
+
*arrays: sequence of arrays as the prototype.
|
|
268
|
+
force: whether to force starting a new dumping series even if there are still blanks at the end of the mmap file.
|
|
269
|
+
|
|
270
|
+
Returns: None
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
# check file status
|
|
274
|
+
if not self._has_initialized:
|
|
275
|
+
self.initialize()
|
|
276
|
+
if self._ptr < self._endptr:
|
|
277
|
+
if force:
|
|
278
|
+
warnings.warn(
|
|
279
|
+
f'There are still {self._endptr - self._ptr} bytes are blank at the end of the mmap file. '
|
|
280
|
+
f'Now these blanks will be dropped.',
|
|
281
|
+
RuntimeWarning
|
|
282
|
+
)
|
|
283
|
+
self.truncate()
|
|
284
|
+
else:
|
|
285
|
+
warnings.warn(
|
|
286
|
+
f'There are still {self._endptr - self._ptr} bytes are blank at the end of the mmap file. '
|
|
287
|
+
f'starting process aborted.',
|
|
288
|
+
RuntimeWarning
|
|
289
|
+
)
|
|
290
|
+
return None
|
|
291
|
+
# check inputs:
|
|
292
|
+
if steps == -1:
|
|
293
|
+
self._use_dynamic_steps = True
|
|
294
|
+
steps = 500
|
|
295
|
+
elif steps <= 0:
|
|
296
|
+
raise ValueError(f'A non-positive steps value {steps} is absurd.')
|
|
297
|
+
else:
|
|
298
|
+
self._use_dynamic_steps = False
|
|
299
|
+
# count head length
|
|
300
|
+
self._nbytes_list_to_check = list()
|
|
301
|
+
self._n_arrays = len(arrays)
|
|
302
|
+
_head_nbytes = 24 # `char``n_cycle``n_array`, 3 * 8
|
|
303
|
+
head_len_list = list()
|
|
304
|
+
arr_type_list: List[Tuple[str, str, int]] = list()
|
|
305
|
+
arr_tol_size = 0
|
|
306
|
+
for i, arr in enumerate(arrays):
|
|
307
|
+
if 0 in arr.shape:
|
|
308
|
+
raise RuntimeError(
|
|
309
|
+
f'Some dimension of the {i}-th array are zero, which means this array is actually empty. '
|
|
310
|
+
f'Dumping such array is MEANINGLESS. Writing is REFUSED.'
|
|
311
|
+
)
|
|
312
|
+
_l = arr.ndim * 8
|
|
313
|
+
_head_nbytes += 8 + _l + 8 # bytes of 'dtype + shape + 0'
|
|
314
|
+
head_len_list.append(_l)
|
|
315
|
+
_dtype = arr.dtype.str # `order``type``len`
|
|
316
|
+
arr_type_list.append((str(_dtype[0]), str(_dtype[1]), int(_dtype[2:])))
|
|
317
|
+
# calc. the arrays size
|
|
318
|
+
self._nbytes_list_to_check.append(arr.nbytes)
|
|
319
|
+
arr_tol_size += arr.nbytes
|
|
320
|
+
|
|
321
|
+
dump_content = bytearray(_head_nbytes)
|
|
322
|
+
dump_content[:8] = 'HEAD'.encode(self._str_fmt)
|
|
323
|
+
dump_content[8:16] = steps.to_bytes(8, self._num_fmt, signed=False) # n_cycle
|
|
324
|
+
dump_content[16:24] = self._n_arrays.to_bytes(8, self._num_fmt, signed=False) # n_arrays
|
|
325
|
+
_ptr = 24
|
|
326
|
+
for i, _shape_len in enumerate(head_len_list):
|
|
327
|
+
# dtype, 8 bytes
|
|
328
|
+
dump_content[_ptr:_ptr + 8] = (
|
|
329
|
+
''.join(arr_type_list[i][:2]).encode(self._str_fmt) + arr_type_list[i][2].to_bytes(4, self._num_fmt, signed=False)
|
|
330
|
+
)
|
|
331
|
+
_ptr += 8
|
|
332
|
+
# shape, n * 8 bytes
|
|
333
|
+
dump_content[_ptr:_ptr + _shape_len] = b''.join(_.to_bytes(8, self._num_fmt, signed=False) for _ in arrays[i].shape)
|
|
334
|
+
_ptr += _shape_len
|
|
335
|
+
# delimiter "0"
|
|
336
|
+
dump_content[_ptr: _ptr + 8] = (0).to_bytes(8, self._num_fmt, signed=False)
|
|
337
|
+
_ptr += 8
|
|
338
|
+
|
|
339
|
+
# write
|
|
340
|
+
self._n_groups += 1
|
|
341
|
+
self.allocate(_head_nbytes + arr_tol_size * steps)
|
|
342
|
+
self._current_group_head_position = self._ptr
|
|
343
|
+
self._mmp_f.write(dump_content)
|
|
344
|
+
# reset the global n_group information:
|
|
345
|
+
# it should be `self._mmp_f[8:16] = self._n_groups.to_bytes(8, self._num_fmt, signed=False)`
|
|
346
|
+
# while _io.BufferedRandom class does not support directly indexed modification,
|
|
347
|
+
# so that uses below ptr operation.
|
|
348
|
+
__tmp_ptr = self._mmp_f.tell()
|
|
349
|
+
self._mmp_f.seek(8)
|
|
350
|
+
self._mmp_f.write(self._n_groups.to_bytes(8, self._num_fmt, signed=False))
|
|
351
|
+
self._mmp_f.seek(__tmp_ptr)
|
|
352
|
+
|
|
353
|
+
self._ptr += _head_nbytes
|
|
354
|
+
self._mmp_f.flush()
|
|
355
|
+
self._has_started = True
|
|
356
|
+
self._count = 0
|
|
357
|
+
self._cache_size_now = 0
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
self.close()
|
|
361
|
+
raise RuntimeError(f'Failed to start in the file `{self.path}`. ERROR: {e}')
|
|
362
|
+
|
|
363
|
+
def start(
|
|
364
|
+
self,
|
|
365
|
+
steps: int,
|
|
366
|
+
dtype_list: List[str],
|
|
367
|
+
shape_list: List[Tuple[int, ...]],
|
|
368
|
+
force: bool = False
|
|
369
|
+
):
|
|
370
|
+
"""
|
|
371
|
+
Write the head information of array, allocate the blank space, thus starting a new dumping series.
|
|
372
|
+
Args:
|
|
373
|
+
steps: the iteration number of arrays, which will be dumped. If `steps` = -1, a dynamic steps will be used.
|
|
374
|
+
dtype_list: list of the arrays' dtypes.
|
|
375
|
+
shape_list: list of the arrays' shapes.
|
|
376
|
+
force: whether to force starting a new dumping series even if there are still blanks at the end of the mmap file.
|
|
377
|
+
|
|
378
|
+
Returns: None
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
# check file status
|
|
382
|
+
if not self._has_initialized:
|
|
383
|
+
self.initialize()
|
|
384
|
+
|
|
385
|
+
if self._ptr < self._endptr:
|
|
386
|
+
if force:
|
|
387
|
+
warnings.warn(
|
|
388
|
+
f'There are still {self._endptr - self._ptr} bytes are blank at the end of the mmap file. '
|
|
389
|
+
f'Now these blanks will be dropped.',
|
|
390
|
+
RuntimeWarning
|
|
391
|
+
)
|
|
392
|
+
self.truncate()
|
|
393
|
+
else:
|
|
394
|
+
warnings.warn(
|
|
395
|
+
f'There are still {self._endptr - self._ptr} bytes are blank at the end of the mmap file. '
|
|
396
|
+
f'starting process aborted.',
|
|
397
|
+
RuntimeWarning
|
|
398
|
+
)
|
|
399
|
+
return None
|
|
400
|
+
# check inputs:
|
|
401
|
+
if steps == -1:
|
|
402
|
+
self._use_dynamic_steps = True
|
|
403
|
+
steps = 500
|
|
404
|
+
elif steps <= 0:
|
|
405
|
+
raise ValueError(f'A non-positive steps value {steps} is absurd.')
|
|
406
|
+
else:
|
|
407
|
+
self._use_dynamic_steps = False
|
|
408
|
+
# count head length
|
|
409
|
+
self._nbytes_list_to_check = list()
|
|
410
|
+
self._n_arrays = len(dtype_list)
|
|
411
|
+
if self._n_arrays != len(shape_list):
|
|
412
|
+
raise ValueError(f'The length of `dtype_list` and `shape_list` must match, but got {self._n_arrays} and {len(shape_list)}.')
|
|
413
|
+
_head_nbytes = 24 # `char``n_cycle``n_array`, 3 * 8
|
|
414
|
+
head_len_list = list()
|
|
415
|
+
arr_type_list: List[Tuple[str, str, int]] = list()
|
|
416
|
+
arr_tol_size = 0
|
|
417
|
+
for i in range(self._n_arrays):
|
|
418
|
+
if 0 in shape_list[i]:
|
|
419
|
+
raise RuntimeError(
|
|
420
|
+
f'Some dimension of the {i}-th array are zero, which means this array is actually empty. '
|
|
421
|
+
f'Dumping such array is MEANINGLESS. Writing is REFUSED.'
|
|
422
|
+
)
|
|
423
|
+
_l = len(shape_list[i]) * 8
|
|
424
|
+
_head_nbytes += 8 + _l + 8 # bytes of 'dtype + shape + 0'
|
|
425
|
+
head_len_list.append(_l)
|
|
426
|
+
_dtype = dtype_list[i] # `order``type``len`
|
|
427
|
+
nbytes = int(_dtype[2:])
|
|
428
|
+
# for Unicode in numpy, each char has 4 bytes rather than 1 byte.
|
|
429
|
+
if str(_dtype[1]) == 'U':
|
|
430
|
+
nbytes *= 4
|
|
431
|
+
arr_type_list.append((str(_dtype[0]), str(_dtype[1]), nbytes))
|
|
432
|
+
# calc. the arrays size
|
|
433
|
+
tol_nbytes = nbytes * math.prod(shape_list[i])
|
|
434
|
+
self._nbytes_list_to_check.append(tol_nbytes)
|
|
435
|
+
arr_tol_size += tol_nbytes
|
|
436
|
+
|
|
437
|
+
dump_content = bytearray(_head_nbytes)
|
|
438
|
+
dump_content[:8] = 'HEAD'.encode(self._str_fmt)
|
|
439
|
+
dump_content[8:16] = steps.to_bytes(8, self._num_fmt, signed=False) # n_cycle
|
|
440
|
+
dump_content[16:24] = self._n_arrays.to_bytes(8, self._num_fmt, signed=False) # n_arrays
|
|
441
|
+
_ptr = 24
|
|
442
|
+
for i, _shape_len in enumerate(head_len_list):
|
|
443
|
+
# dtype, 8 bytes
|
|
444
|
+
dump_content[_ptr:_ptr + 8] = (
|
|
445
|
+
''.join(arr_type_list[i][:2]).encode(self._str_fmt)
|
|
446
|
+
+ arr_type_list[i][2].to_bytes(4, self._num_fmt, signed=False)
|
|
447
|
+
)
|
|
448
|
+
_ptr += 8
|
|
449
|
+
# shape, n * 8 bytes
|
|
450
|
+
dump_content[_ptr:_ptr + _shape_len] = b''.join(_.to_bytes(8, self._num_fmt, signed=False) for _ in shape_list[i])
|
|
451
|
+
_ptr += _shape_len
|
|
452
|
+
# delimiter, "0"
|
|
453
|
+
dump_content[_ptr: _ptr + 8] = (0).to_bytes(8, self._num_fmt, signed=False)
|
|
454
|
+
_ptr += 8
|
|
455
|
+
|
|
456
|
+
# write
|
|
457
|
+
self._n_groups += 1
|
|
458
|
+
self.allocate(_head_nbytes + arr_tol_size * steps)
|
|
459
|
+
self._current_group_head_position = self._ptr
|
|
460
|
+
self._mmp_f.write(dump_content)
|
|
461
|
+
# reset the global n_group information:
|
|
462
|
+
# it should be `self._mmp_f[8:16] = self._n_groups.to_bytes(8, self._num_fmt, signed=False)`
|
|
463
|
+
# while _io.BufferedRandom class does not support directly indexed modification,
|
|
464
|
+
# so that uses below ptr operation.
|
|
465
|
+
__tmp_ptr = self._mmp_f.tell()
|
|
466
|
+
self._mmp_f.seek(8)
|
|
467
|
+
self._mmp_f.write(self._n_groups.to_bytes(8, self._num_fmt, signed=False))
|
|
468
|
+
self._mmp_f.seek(__tmp_ptr)
|
|
469
|
+
|
|
470
|
+
self._ptr += _head_nbytes
|
|
471
|
+
self._mmp_f.flush()
|
|
472
|
+
self._has_started = True
|
|
473
|
+
self._count = 0
|
|
474
|
+
self._cache_size_now = 0
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
self.close()
|
|
478
|
+
raise RuntimeError(f'Failed to start in the file `{self.path}`. ERROR: {e}')
|
|
479
|
+
|
|
480
|
+
def step(
|
|
481
|
+
self,
|
|
482
|
+
*arrays: np.ndarray,
|
|
483
|
+
):
|
|
484
|
+
"""
|
|
485
|
+
Do a collect step that store a list of arrays.
|
|
486
|
+
Args:
|
|
487
|
+
*arrays (np.ndarray): arrays to collect.
|
|
488
|
+
Returns: None
|
|
489
|
+
|
|
490
|
+
"""
|
|
491
|
+
try:
|
|
492
|
+
if not self._has_started:
|
|
493
|
+
raise RuntimeError(f'The dumping series was not started. Please call `self.start(...)/start_from_arrays(...)` first.')
|
|
494
|
+
if self._n_arrays != len(arrays):
|
|
495
|
+
raise RuntimeError(
|
|
496
|
+
f'Inconsistent array number between the record in `self.start` and here input. '
|
|
497
|
+
f'Expected {self._n_arrays}, but got {len(arrays)}.'
|
|
498
|
+
)
|
|
499
|
+
if self._ptr >= self._endptr:
|
|
500
|
+
if not self._use_dynamic_steps:
|
|
501
|
+
raise RuntimeError(f'Data are out of range.')
|
|
502
|
+
else: # extend capacities
|
|
503
|
+
_tol_nbytes = sum(self._nbytes_list_to_check)
|
|
504
|
+
# overwrite n_step in current group
|
|
505
|
+
self._mmp_f.seek(self._current_group_head_position + 16) # the position of n_cycle
|
|
506
|
+
_current_steps = int.from_bytes(self._mmp_f.read(8), self._num_fmt)
|
|
507
|
+
_add_steps = max(1, _current_steps >> 1) # _current_steps * 1.5
|
|
508
|
+
_new_steps = _current_steps + _add_steps # 1.5 times extension
|
|
509
|
+
self._mmp_f.seek(self._current_group_head_position + 16)
|
|
510
|
+
self._mmp_f.write(_new_steps.to_bytes(8, self._num_fmt, signed=False))
|
|
511
|
+
self._mmp_f.seek(self._ptr)
|
|
512
|
+
# allocate
|
|
513
|
+
self.allocate(_add_steps * _tol_nbytes)
|
|
514
|
+
|
|
515
|
+
# main dump
|
|
516
|
+
# TODO: adding multiprocess in future
|
|
517
|
+
for i, arr in enumerate(arrays):
|
|
518
|
+
_nb = arr.nbytes
|
|
519
|
+
if _nb == self._nbytes_list_to_check[i]:
|
|
520
|
+
arr = np.ascontiguousarray(arr)
|
|
521
|
+
self._mmp_f.write(memoryview(arr))
|
|
522
|
+
#self._mmp_f[self._ptr: self._ptr + _nb] = memoryview(arr)
|
|
523
|
+
self._ptr += _nb
|
|
524
|
+
self._cache_size_now += _nb
|
|
525
|
+
#self._mmp_f.seek(self._ptr)
|
|
526
|
+
else:
|
|
527
|
+
raise RuntimeError(
|
|
528
|
+
f'Inconsistent array bytes between the record in `self.start` and here input. '
|
|
529
|
+
f'Expected {self._nbytes_list_to_check[i]}, but got {arr.nbytes}.'
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# dump
|
|
533
|
+
self._count += 1
|
|
534
|
+
if self._cache_size_now >= self.cache_size:
|
|
535
|
+
self._mmp_f.flush()
|
|
536
|
+
self._cache_size_now = 0
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
self.close()
|
|
540
|
+
raise RuntimeError(f'Failed to collect `{self.path}`. ERROR: {e}')
|
|
541
|
+
|
|
542
|
+
def truncate(self):
|
|
543
|
+
"""
|
|
544
|
+
Truncate the blank steps at the end of the file, and exit the current group.
|
|
545
|
+
One must re-start a dumping series to write new data.
|
|
546
|
+
Returns:
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
if not self._has_started:
|
|
550
|
+
raise RuntimeError(f'The dumping series was not started. Please call `self.start(...)/start_from_arrays(...)` first.')
|
|
551
|
+
|
|
552
|
+
if self._dump_file.closed:
|
|
553
|
+
self._dump_file = open(self.path, 'rb+')
|
|
554
|
+
# truncate
|
|
555
|
+
if self._ptr < self._endptr:
|
|
556
|
+
if self.use_mmap:
|
|
557
|
+
if self._mmp_f.closed:
|
|
558
|
+
self._mmp_f = mmap.mmap(self._dump_file.fileno(), 0, access=mmap.ACCESS_WRITE)
|
|
559
|
+
self._mmp_f.resize(self._ptr)
|
|
560
|
+
else:
|
|
561
|
+
self._mmp_f = self._dump_file
|
|
562
|
+
self._mmp_f.truncate(self._ptr)
|
|
563
|
+
self._endptr = self._ptr
|
|
564
|
+
# overwrite n_step in current group
|
|
565
|
+
self._mmp_f.seek(self._current_group_head_position + 8) # the position of n_cycle
|
|
566
|
+
self._mmp_f.write(self._count.to_bytes(8, self._num_fmt, signed=False))
|
|
567
|
+
self._mmp_f.seek(self._ptr)
|
|
568
|
+
self._has_started = False
|
|
569
|
+
self._current_group_head_position = None
|
|
570
|
+
|
|
571
|
+
def dump(self):
|
|
572
|
+
"""
|
|
573
|
+
Manually do a dumping.
|
|
574
|
+
Returns:
|
|
575
|
+
|
|
576
|
+
"""
|
|
577
|
+
if (self._mmp_f is None) or self._mmp_f.closed:
|
|
578
|
+
warnings.warn(f"The dumper does not open yet. Please call `self.start(...)/start_from_arrays(...)` first.")
|
|
579
|
+
else:
|
|
580
|
+
self._mmp_f.flush()
|
|
581
|
+
|
|
582
|
+
def flush(self):
|
|
583
|
+
"""
|
|
584
|
+
Alias for `self.dump`.
|
|
585
|
+
Returns:
|
|
586
|
+
|
|
587
|
+
"""
|
|
588
|
+
self.dump()
|
|
589
|
+
|
|
590
|
+
def _tmp_close(self):
|
|
591
|
+
"""
|
|
592
|
+
temporarily close the mmap file
|
|
593
|
+
Returns:
|
|
594
|
+
|
|
595
|
+
"""
|
|
596
|
+
if (self._dump_file is not None) and (not self._dump_file.closed):
|
|
597
|
+
self._dump_file.close()
|
|
598
|
+
if (self._mmp_f is not None) and (not self._mmp_f.closed):
|
|
599
|
+
self._mmp_f.close()
|
|
600
|
+
self._has_started = False
|
|
601
|
+
self._current_group_head_position = None
|
|
602
|
+
|
|
603
|
+
def close(self):
|
|
604
|
+
"""
|
|
605
|
+
close the file
|
|
606
|
+
Returns:
|
|
607
|
+
|
|
608
|
+
"""
|
|
609
|
+
try:
|
|
610
|
+
if (self._mmp_f is not None) and (not self._mmp_f.closed):
|
|
611
|
+
self._mmp_f.flush()
|
|
612
|
+
if self._has_started:
|
|
613
|
+
self.truncate()
|
|
614
|
+
self._tmp_close()
|
|
615
|
+
#gc.collect()
|
|
616
|
+
self._has_initialized = False
|
|
617
|
+
self._dump_file = None
|
|
618
|
+
self._mmp_f = None
|
|
619
|
+
except Exception as e:
|
|
620
|
+
warnings.warn(f'Failed to close `{self.path}`. ERROR: {e}')
|
|
621
|
+
|
|
622
|
+
@property
|
|
623
|
+
def closed(self):
|
|
624
|
+
_q = (self._mmp_f is None) and (self._dump_file is None) and (self._has_initialized is None)
|
|
625
|
+
return _q
|
|
626
|
+
|
|
627
|
+
def __enter__(self):
|
|
628
|
+
return self
|
|
629
|
+
|
|
630
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
631
|
+
self.close()
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class _ArrayDumperPlaceHolder:
|
|
635
|
+
"""
|
|
636
|
+
A pure placeholder to compatible with path is None,
|
|
637
|
+
It copies all methods of ArrayDumper but did nothing.
|
|
638
|
+
"""
|
|
639
|
+
def __init__(self, path: None, *args, **kwargs) -> None:
|
|
640
|
+
if path is not None:
|
|
641
|
+
raise ValueError(f"This is a placeholder, which only receives path = None, but got {path}.")
|
|
642
|
+
|
|
643
|
+
def reset_args(
|
|
644
|
+
self,
|
|
645
|
+
path: str,
|
|
646
|
+
mode: Literal['w', 'x', 'a'] = 'x',
|
|
647
|
+
cache_size: int = 4096,
|
|
648
|
+
head_order: Literal['<', '>'] = '<',
|
|
649
|
+
use_mmap: bool = False,
|
|
650
|
+
*args, **kwargs
|
|
651
|
+
):
|
|
652
|
+
pass
|
|
653
|
+
|
|
654
|
+
def initialize(self):
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
def allocate(self, *args, **kwargs):
|
|
658
|
+
pass
|
|
659
|
+
|
|
660
|
+
def close(self):
|
|
661
|
+
pass
|
|
662
|
+
|
|
663
|
+
def dump(self):
|
|
664
|
+
pass
|
|
665
|
+
|
|
666
|
+
def start(self, *args, **kwargs):
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
def start_from_arrays(self, *args, **kwargs):
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
def step(self, *args, **kwargs):
|
|
673
|
+
pass
|
|
674
|
+
|
|
675
|
+
def truncate(self, *args, **kwargs):
|
|
676
|
+
pass
|
|
677
|
+
|
|
678
|
+
def __enter__(self):
|
|
679
|
+
return self
|
|
680
|
+
|
|
681
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
682
|
+
pass
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class ArrayDumpReader:
|
|
686
|
+
"""
|
|
687
|
+
Reading the mmap file dumped by class `ArrayDumper`.
|
|
688
|
+
"""
|
|
689
|
+
def __init__(self, path: str):
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
path: the path to the mmap file.
|
|
694
|
+
"""
|
|
695
|
+
self._path = path
|
|
696
|
+
# init vars
|
|
697
|
+
self._ptr = 0
|
|
698
|
+
self._dump_file = None
|
|
699
|
+
self._mmp_f: mmap.mmap|None = None
|
|
700
|
+
# check path
|
|
701
|
+
try:
|
|
702
|
+
with open(self._path, 'rb') as f:
|
|
703
|
+
file_head = f.read(16)
|
|
704
|
+
except FileNotFoundError:
|
|
705
|
+
raise RuntimeError(f'File `{self._path}` was not found.')
|
|
706
|
+
# find the head order
|
|
707
|
+
try_order_le = file_head[:2].decode('utf-16-le')
|
|
708
|
+
try_order_be = file_head[:2].decode('utf-16-be')
|
|
709
|
+
if try_order_le == '<':
|
|
710
|
+
self._str_fmt = 'utf-16-le'
|
|
711
|
+
self._num_fmt: Literal['little', 'big'] = 'little'
|
|
712
|
+
elif try_order_be == '>':
|
|
713
|
+
self._str_fmt = 'utf-16-be'
|
|
714
|
+
self._num_fmt: Literal['little', 'big'] = 'big'
|
|
715
|
+
else:
|
|
716
|
+
raise ValueError(f'Unexpected endianness: {try_order_le}/{try_order_be}.')
|
|
717
|
+
# test BOM
|
|
718
|
+
if len('a'.encode(self._str_fmt)) != 2:
|
|
719
|
+
raise NotImplementedError(
|
|
720
|
+
f'You may check here whether the encode format {self._str_fmt} is correct. '
|
|
721
|
+
f'Normally it would not be added BOM at the start of bytes, thus resulting 2 bytes, but here string "a" '
|
|
722
|
+
f'is encoded into {len('a'.encode(self._str_fmt))} bytes instead.'
|
|
723
|
+
)
|
|
724
|
+
# check magik
|
|
725
|
+
magik = file_head[2:6].decode(self._str_fmt)
|
|
726
|
+
if magik != "BM":
|
|
727
|
+
raise ValueError(f'Unknown file format: {magik}.')
|
|
728
|
+
# check version
|
|
729
|
+
v1 = int.from_bytes(file_head[6:7], self._num_fmt, signed=False)
|
|
730
|
+
v2 = int.from_bytes(file_head[7:8], self._num_fmt, signed=False)
|
|
731
|
+
_version_now = __version__.split('.', 2)
|
|
732
|
+
v1_now = int(_version_now[0])
|
|
733
|
+
v2_now = int(_version_now[1])
|
|
734
|
+
if v1_now >= 255 or v2_now >= 255:
|
|
735
|
+
raise NotImplementedError(f'The version numbers have been reached {_version_now}. How frightful!')
|
|
736
|
+
if (v1, v2) != (v1_now, v2_now):
|
|
737
|
+
warnings.warn(
|
|
738
|
+
f"The file {self._path} (ver \"{'.'.join((str(v1), str(v2)))}\") is incompatible with "
|
|
739
|
+
f"the current version \"{__version__}\".",
|
|
740
|
+
RuntimeWarning,
|
|
741
|
+
)
|
|
742
|
+
# read the group number
|
|
743
|
+
self.n_groups = int.from_bytes(file_head[8:16], self._num_fmt, signed=False)
|
|
744
|
+
# mv ptr
|
|
745
|
+
self._ptr += 16
|
|
746
|
+
|
|
747
|
+
def read(
|
|
748
|
+
self,
|
|
749
|
+
groups: List[int]|slice|int = -1,
|
|
750
|
+
indices: List[int]|slice|int = -1,
|
|
751
|
+
is_copy: bool = True
|
|
752
|
+
) -> Dict[str, List[List[np.ndarray]]]:
|
|
753
|
+
"""
|
|
754
|
+
Read the specific arrays from the mmap file by given groups and indices.
|
|
755
|
+
Args:
|
|
756
|
+
groups: the group number to read. A negative number means read all.
|
|
757
|
+
a single int means read this one group of arrays, and a list of ints means read the ones indexed by the list.
|
|
758
|
+
Note: if `group` in the list form, all element must be unique, and it will be SORTED.
|
|
759
|
+
indices: the indices in each group of the arrays to read. A negative number means read all,
|
|
760
|
+
is_copy: whether to copy the arrays from the mmap file.
|
|
761
|
+
Note: if `is_copy` is False, the mmap file cannot be closed due to the exported pointers used by read arrays.
|
|
762
|
+
One must release all references to the mmap file first to close the memory map file.
|
|
763
|
+
|
|
764
|
+
Returns: Dict[str, List[List[np.ndarray]]], {f'group{i}': List[List[np.ndarray]]}, the outer List is steps, inner List is groups.
|
|
765
|
+
|
|
766
|
+
"""
|
|
767
|
+
if isinstance(groups, int):
|
|
768
|
+
if not groups < self.n_groups: raise ValueError(f'There is only {self.n_groups} groups available, but requested {groups}.')
|
|
769
|
+
groups = range(self.n_groups) if groups < 0 else [groups, ]
|
|
770
|
+
elif isinstance(groups, slice):
|
|
771
|
+
_start, _stop, _step = groups.indices(self.n_groups)
|
|
772
|
+
if not _start >= 0:
|
|
773
|
+
raise ValueError(f'`groups` slice must start from the number greater than or equal to 0, but got {groups.start}.')
|
|
774
|
+
if not _stop <= self.n_groups:
|
|
775
|
+
raise ValueError(f'There is only {self.n_groups} groups available, but requested {groups.stop}.')
|
|
776
|
+
groups = range(_start, _stop, _step)
|
|
777
|
+
elif isinstance(groups, list):
|
|
778
|
+
if not min(groups) >= 0:
|
|
779
|
+
raise ValueError(f'elements in the `groups` must be greater than or equal to 0, but got {min(groups)}.')
|
|
780
|
+
if not max(groups) < self.n_groups:
|
|
781
|
+
raise ValueError(f'There is only {self.n_groups} groups available, but requested {max(groups)}.')
|
|
782
|
+
if len(groups) != len(set(groups)):
|
|
783
|
+
raise ValueError(f'`groups` must have unique elements, but some duplicate elements are found.')
|
|
784
|
+
groups = sorted(groups)
|
|
785
|
+
else:
|
|
786
|
+
raise TypeError(f'`groups` must be a list or int, but got {type(groups)}.')
|
|
787
|
+
|
|
788
|
+
_grp_ptr = 0
|
|
789
|
+
_n_select_groups = len(groups)
|
|
790
|
+
output_arrs = dict()
|
|
791
|
+
for i_grp in range(self.n_groups):
|
|
792
|
+
if _grp_ptr >= _n_select_groups:
|
|
793
|
+
break
|
|
794
|
+
if i_grp == groups[_grp_ptr]:
|
|
795
|
+
output_arrs[f'group{i_grp}'] = self._read_once(indices, is_copy)
|
|
796
|
+
_grp_ptr += 1
|
|
797
|
+
else:
|
|
798
|
+
self._skip_once()
|
|
799
|
+
|
|
800
|
+
if is_copy: # Note: if not copy, exported pointers will exist that the read arrays use them. Hence it cannot be closed.
|
|
801
|
+
self.close()
|
|
802
|
+
|
|
803
|
+
return output_arrs
|
|
804
|
+
|
|
805
|
+
def _skip_once(self):
|
|
806
|
+
"""
|
|
807
|
+
Skip this group data once, instead of reading it.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
|
|
811
|
+
"""
|
|
812
|
+
try:
|
|
813
|
+
n_cycles, n_arrays, dtype_list, shape_list, stride_list = self._parse_arr_head()
|
|
814
|
+
_block_total_stride = sum(stride_list) # total stride (bytes) of this group.
|
|
815
|
+
self._ptr += n_cycles * _block_total_stride # move the ptr to the end of the group
|
|
816
|
+
self._mmp_f.seek(self._ptr)
|
|
817
|
+
|
|
818
|
+
except Exception as e:
|
|
819
|
+
self._tmp_close()
|
|
820
|
+
raise RuntimeError(f'An error occurred while reading file {self._path}. ERROR: {e}.')
|
|
821
|
+
|
|
822
|
+
def _read_once(self, indices: List[int]|slice|int = -1, is_copy: bool = True) -> List[List[np.ndarray]]:
|
|
823
|
+
"""
|
|
824
|
+
Read one time from the beginning of the current group.
|
|
825
|
+
self._ptr must be at the start of the group, and then will be moved to the end of the group.
|
|
826
|
+
Args:
|
|
827
|
+
indices: the indices in each group of the arrays to read. A negative number means read all.
|
|
828
|
+
is_copy: whether to copy the array from mmap file or not.
|
|
829
|
+
|
|
830
|
+
Returns: List[np.ndarray], list of arrays.
|
|
831
|
+
|
|
832
|
+
"""
|
|
833
|
+
try:
|
|
834
|
+
n_cycles, n_arrays, dtype_list, shape_list, stride_list = self._parse_arr_head()
|
|
835
|
+
output_list_arrays = list()
|
|
836
|
+
if isinstance(indices, list):
|
|
837
|
+
if not min(indices) >= 0:
|
|
838
|
+
raise ValueError(f'elements in the `indices` must be greater than or equal to 0, but got {min(indices)}.')
|
|
839
|
+
if not max(indices) < n_cycles:
|
|
840
|
+
raise ValueError(f"indices {max(indices)} is out of range.")
|
|
841
|
+
cycle_list = indices
|
|
842
|
+
elif isinstance(indices, slice):
|
|
843
|
+
_start, _stop, _step = indices.indices(n_cycles)
|
|
844
|
+
if not ((_start >= 0) and (_stop <= n_cycles)):
|
|
845
|
+
raise ValueError(f"indices {indices} is out of range: [{indices.start}, {indices.stop}].")
|
|
846
|
+
cycle_list = range(_start, _stop, _step)
|
|
847
|
+
elif isinstance(indices, int):
|
|
848
|
+
if not indices < n_cycles: raise ValueError(f"indices {indices} is out of range.")
|
|
849
|
+
cycle_list = range(n_cycles) if indices < 0 else [indices, ]
|
|
850
|
+
else:
|
|
851
|
+
raise TypeError(f'Expected indices of List[int], slice, or int, but got {type(indices)}.')
|
|
852
|
+
|
|
853
|
+
_block_total_stride = sum(stride_list) # total stride (bytes) of this group.
|
|
854
|
+
for i_cyc in cycle_list:
|
|
855
|
+
# calculate the ptr position
|
|
856
|
+
_outer_ptr = self._ptr + i_cyc*_block_total_stride
|
|
857
|
+
self._mmp_f.seek(_outer_ptr)
|
|
858
|
+
if is_copy:
|
|
859
|
+
_raw_data = self._mmp_f.read(_block_total_stride)
|
|
860
|
+
# Note: Here can be implemented as multiprocessing.
|
|
861
|
+
_arr_now = list()
|
|
862
|
+
inner_ptr = 0
|
|
863
|
+
for _ist, _stride in enumerate(stride_list):
|
|
864
|
+
_arr = np.frombuffer(_raw_data[inner_ptr:inner_ptr + _stride], dtype=dtype_list[_ist]).reshape(shape_list[_ist])
|
|
865
|
+
_arr_now.append(_arr)
|
|
866
|
+
inner_ptr += _stride
|
|
867
|
+
output_list_arrays.append(_arr_now)
|
|
868
|
+
else:
|
|
869
|
+
_raw_data = memoryview(self._mmp_f)[_outer_ptr:_outer_ptr + _block_total_stride]
|
|
870
|
+
# Note: Here can be implemented as multiprocessing, too.
|
|
871
|
+
_arr_now = list()
|
|
872
|
+
inner_ptr = 0
|
|
873
|
+
for _i, _stride in enumerate(stride_list):
|
|
874
|
+
_arr = np.frombuffer(_raw_data[inner_ptr:inner_ptr + _stride], dtype=dtype_list[_i]).reshape(shape_list[_i])
|
|
875
|
+
_arr_now.append(_arr)
|
|
876
|
+
inner_ptr += _stride
|
|
877
|
+
output_list_arrays.append(_arr_now)
|
|
878
|
+
self._mmp_f.seek(_outer_ptr + _block_total_stride)
|
|
879
|
+
|
|
880
|
+
# move the ptr to the end of the group
|
|
881
|
+
self._ptr += n_cycles * _block_total_stride
|
|
882
|
+
self._mmp_f.seek(self._ptr)
|
|
883
|
+
#if self._ptr != self._mmp_f.tell():
|
|
884
|
+
# raise RuntimeError(f'???? BUG: WHY DOES THE self._ptr MISMATCH ???? Expected {self._mmp_f.tell()} but got {self._ptr}.')
|
|
885
|
+
|
|
886
|
+
return output_list_arrays
|
|
887
|
+
|
|
888
|
+
except Exception as e:
|
|
889
|
+
self._tmp_close()
|
|
890
|
+
raise RuntimeError(f'An error occurred while reading file {self._path}. ERROR: {e}.')
|
|
891
|
+
|
|
892
|
+
def _parse_arr_head(self, ) -> Tuple[int, int, List[str], List[Tuple], List[int]]:
|
|
893
|
+
"""
|
|
894
|
+
Parse the array head information once.
|
|
895
|
+
self._ptr must be at the end of a group.
|
|
896
|
+
Then after calling this method, self._ptr will be moved to the start of the group data.
|
|
897
|
+
|
|
898
|
+
Returns: n_cycles, n_arrays, dtype_list, shape_list, stride_list
|
|
899
|
+
"""
|
|
900
|
+
r"""
|
|
901
|
+
Head information:
|
|
902
|
+
`char``n_cycle``n_array``dtype1``shape1[]`0`dtype2``shape2[]`0...`dtype_n``shape_n[]`0`byte_data`...
|
|
903
|
+
wherein each term occupies 8 bytes.
|
|
904
|
+
"""
|
|
905
|
+
try:
|
|
906
|
+
if (self._dump_file is None) or self._dump_file.closed:
|
|
907
|
+
self._dump_file = open(self._path, 'rb')
|
|
908
|
+
if (self._mmp_f is None) or self._mmp_f.closed:
|
|
909
|
+
self._mmp_f = mmap.mmap(self._dump_file.fileno(), 0, access=mmap.ACCESS_READ)
|
|
910
|
+
|
|
911
|
+
self._mmp_f.seek(self._ptr)
|
|
912
|
+
is_head = self._mmp_f.read(8).decode(self._str_fmt)
|
|
913
|
+
if is_head != 'HEAD':
|
|
914
|
+
raise RuntimeError(f'Could not find head byte in mmap file: {self._path}. This file may be corrupted.')
|
|
915
|
+
n_cycles = int.from_bytes(self._mmp_f.read(8), self._num_fmt, signed=False)
|
|
916
|
+
n_arrays = int.from_bytes(self._mmp_f.read(8), self._num_fmt, signed=False)
|
|
917
|
+
|
|
918
|
+
dtype_list = list()
|
|
919
|
+
shape_list = list()
|
|
920
|
+
stride_list = list()
|
|
921
|
+
for _ in range(n_arrays):
|
|
922
|
+
_dtp_ot = self._mmp_f.read(4).decode(self._str_fmt) # order and type without length
|
|
923
|
+
_dtp_len = int.from_bytes(self._mmp_f.read(4), self._num_fmt, signed=False) # the byte length of each elem in arr.
|
|
924
|
+
_dtype = f'{_dtp_ot}{_dtp_len}'
|
|
925
|
+
# Special for Unicode char in numpy which applied utf-32 of 4 bytes.
|
|
926
|
+
if _dtp_ot[1] == 'U':
|
|
927
|
+
elem_size = _dtp_len * 4
|
|
928
|
+
else:
|
|
929
|
+
elem_size = _dtp_len
|
|
930
|
+
dtype_list.append(_dtype)
|
|
931
|
+
_shape = list()
|
|
932
|
+
while True:
|
|
933
|
+
_sp_num = int.from_bytes(self._mmp_f.read(8), self._num_fmt, signed=False)
|
|
934
|
+
if _sp_num == 0: # reached the delimiter
|
|
935
|
+
break
|
|
936
|
+
_shape.append(_sp_num)
|
|
937
|
+
shape_list.append(tuple(_shape))
|
|
938
|
+
stride_list.append(elem_size * math.prod(_shape))
|
|
939
|
+
self._ptr = self._mmp_f.tell()
|
|
940
|
+
|
|
941
|
+
return n_cycles, n_arrays, dtype_list, shape_list, stride_list
|
|
942
|
+
|
|
943
|
+
except Exception as e:
|
|
944
|
+
self._tmp_close()
|
|
945
|
+
raise RuntimeError(f'An error occurred while reading mmap file: {self._path}: {e}')
|
|
946
|
+
|
|
947
|
+
def _tmp_close(self):
|
|
948
|
+
"""
|
|
949
|
+
temporarily close the mmap file
|
|
950
|
+
Returns:
|
|
951
|
+
|
|
952
|
+
"""
|
|
953
|
+
if (self._dump_file is not None) and (not self._dump_file.closed):
|
|
954
|
+
self._dump_file.close()
|
|
955
|
+
if (self._mmp_f is not None) and (not self._mmp_f.closed):
|
|
956
|
+
self._mmp_f.close()
|
|
957
|
+
|
|
958
|
+
def _raw_close(self):
|
|
959
|
+
"""
|
|
960
|
+
directly close the memmap and gc
|
|
961
|
+
Returns:
|
|
962
|
+
|
|
963
|
+
"""
|
|
964
|
+
self._tmp_close()
|
|
965
|
+
gc.collect()
|
|
966
|
+
self._dump_file = None
|
|
967
|
+
self._mmp_f = None
|
|
968
|
+
|
|
969
|
+
def close(self):
|
|
970
|
+
"""
|
|
971
|
+
Close the memmap and gc with checks.
|
|
972
|
+
Returns:
|
|
973
|
+
|
|
974
|
+
"""
|
|
975
|
+
try:
|
|
976
|
+
self._raw_close()
|
|
977
|
+
except BufferError as bufe:
|
|
978
|
+
warnings.warn(
|
|
979
|
+
f'Failed to close mmap file {self._path}: {bufe}.\n'
|
|
980
|
+
f'YOU MAY RELEASE EVERY REFERENCE FROM THE MEMORY MAPPING FILE, '
|
|
981
|
+
f'AND THEN CLOSE IT MANUALLY AGAIN. '
|
|
982
|
+
f'OTHERWISE, THIS FILE WILL KEEP OPENING.\n'
|
|
983
|
+
f'I HOPE YOU KNOW WHAT YOU ARE DOING!!!'
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
def __enter__(self):
|
|
987
|
+
return self
|
|
988
|
+
|
|
989
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
990
|
+
try:
|
|
991
|
+
self._raw_close()
|
|
992
|
+
except BufferError as bufe:
|
|
993
|
+
warnings.warn(
|
|
994
|
+
f'Failed to close mmap file {self._path}: {bufe}.\n'
|
|
995
|
+
f'YOU MAY RELEASE EVERY REFERENCE FROM THE MEMORY MAPPING FILE, '
|
|
996
|
+
f'AND THEN CLOSE IT MANUALLY AGAIN. '
|
|
997
|
+
f'OTHERWISE, THIS FILE WILL KEEP OPENING.\n'
|
|
998
|
+
f'I HOPE YOU KNOW WHAT YOU ARE DOING!!!'
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def structures_io_dumper(path: str|None, mode: Literal['w', 'x', 'a'] = 'x', disable: bool = False):
|
|
1003
|
+
"""
|
|
1004
|
+
Auxiliary function for structure IO. It will be added into Batch* methods as a general dumper.
|
|
1005
|
+
if `path` is None, or `disable` is True,
|
|
1006
|
+
a placeholder which contains all methods of ArrayDumper but does nothing when called will be assigned.
|
|
1007
|
+
"""
|
|
1008
|
+
if (not disable) and (path is not None):
|
|
1009
|
+
dumper = ArrayDumper(path, mode=mode, cache_size=4096, use_mmap=False)
|
|
1010
|
+
else:
|
|
1011
|
+
dumper = _ArrayDumperPlaceHolder(path)
|
|
1012
|
+
|
|
1013
|
+
return dumper
|
|
1014
|
+
|
|
1015
|
+
def read_md_traj(
|
|
1016
|
+
path,
|
|
1017
|
+
indices: List[int]|slice|int = -1,
|
|
1018
|
+
is_copy: bool = True
|
|
1019
|
+
):
|
|
1020
|
+
"""
|
|
1021
|
+
A specialized reader for dump files generated by MD.
|
|
1022
|
+
For BaseMD class, the information is as follows
|
|
1023
|
+
with denoting shape [n_batch, n_atom, n_dim] (regular batch) or [1, sumNi, n_atom] (irregular batch) as "sX":
|
|
1024
|
+
group 1: 1-step
|
|
1025
|
+
batch_indices_tensor[n_batch, ] (optional, exists for irregular batches)
|
|
1026
|
+
cell_vec[n_batch, 3, 3] / [1], if No cell_vec input, it will be set to [0] (shape: [1, ]).
|
|
1027
|
+
atomic_numbers[n]
|
|
1028
|
+
fixed_mask[sX]
|
|
1029
|
+
group 2: n_time_steps step
|
|
1030
|
+
E[n_batch, ] (energy)
|
|
1031
|
+
X[sX] (coordinates)
|
|
1032
|
+
V[sX] (velocities)
|
|
1033
|
+
F[sX] (forces)
|
|
1034
|
+
Args:
|
|
1035
|
+
path: the path to the dump file.
|
|
1036
|
+
indices: the indices in each group of the arrays to read. A negative number means read all.
|
|
1037
|
+
is_copy: whether to copy the arrays from the mmap file.
|
|
1038
|
+
Note: if `is_copy` is False, the mmap file cannot be closed due to the exported pointers used by read arrays.
|
|
1039
|
+
One must release all references to the mmap file first to close the memory map file.
|
|
1040
|
+
Returns:
|
|
1041
|
+
BatchStructures
|
|
1042
|
+
|
|
1043
|
+
"""
|
|
1044
|
+
reader = ArrayDumpReader(path)
|
|
1045
|
+
raw_results = reader.read(groups=-1, indices=indices, is_copy=is_copy)
|
|
1046
|
+
n_grp = len(raw_results)
|
|
1047
|
+
if n_grp % 2 != 0:
|
|
1048
|
+
raise EOFError(f"Molecular dynamics file must contain even number of groups, but got {n_grp}.")
|
|
1049
|
+
|
|
1050
|
+
smp_ids = list()
|
|
1051
|
+
cell_list = list()
|
|
1052
|
+
element_list = list()
|
|
1053
|
+
numbers_list = list()
|
|
1054
|
+
coo_t_list = list()
|
|
1055
|
+
coo_list = list()
|
|
1056
|
+
veloc_list = list()
|
|
1057
|
+
fixed_list = list()
|
|
1058
|
+
energy_list = list()
|
|
1059
|
+
force_list = list()
|
|
1060
|
+
|
|
1061
|
+
for i in range(0, n_grp, 2):
|
|
1062
|
+
i_head = i
|
|
1063
|
+
i_content = i + 1
|
|
1064
|
+
i_cyc = i_head // 2
|
|
1065
|
+
if len(raw_results[f'group{i_head}'][0]) == 3: # no irregular batch_indices
|
|
1066
|
+
batch_indices_tensor = None
|
|
1067
|
+
(
|
|
1068
|
+
cell_vec,
|
|
1069
|
+
atomic_numbers,
|
|
1070
|
+
fixed_mask
|
|
1071
|
+
) = raw_results[f'group{i_head}'][0]
|
|
1072
|
+
n_batch = len(cell_vec)
|
|
1073
|
+
_elements = list()
|
|
1074
|
+
_numbers = list()
|
|
1075
|
+
_id_per_frame = list()
|
|
1076
|
+
for ii, _atml in enumerate(atomic_numbers):
|
|
1077
|
+
elements, _, numbers = elem_list_reduce(_atml)
|
|
1078
|
+
_elements.append(elements)
|
|
1079
|
+
_numbers.append(numbers)
|
|
1080
|
+
_id_per_frame.append(ii)
|
|
1081
|
+
_fixed = [_ for _ in fixed_mask]
|
|
1082
|
+
_cells = [_ for _ in cell_vec]
|
|
1083
|
+
kk = 0
|
|
1084
|
+
n_cyc = len(raw_results[f'group{i_content}'])
|
|
1085
|
+
cell_list.extend(_cells * n_cyc)
|
|
1086
|
+
element_list.extend(_elements * n_cyc)
|
|
1087
|
+
numbers_list.extend(_numbers * n_cyc)
|
|
1088
|
+
coo_t_list.extend(['C'] * (n_batch * n_cyc))
|
|
1089
|
+
fixed_list.extend(_fixed * n_cyc)
|
|
1090
|
+
for en, x, v, f in raw_results[f'group{i_content}']:
|
|
1091
|
+
_x = [_ for _ in x]
|
|
1092
|
+
_f = [_ for _ in f]
|
|
1093
|
+
_v = [_ for _ in v]
|
|
1094
|
+
smp_ids.extend([f'samp{i_cyc}_struc{_}_step{kk}' for _ in _id_per_frame])
|
|
1095
|
+
coo_list.extend(_x)
|
|
1096
|
+
veloc_list.extend(_v)
|
|
1097
|
+
energy_list.extend(en.tolist())
|
|
1098
|
+
force_list.extend(_f)
|
|
1099
|
+
kk += 1
|
|
1100
|
+
|
|
1101
|
+
elif len(raw_results[f'group{i_head}'][0]) == 4: # irregular situation
|
|
1102
|
+
(
|
|
1103
|
+
batch_indices_tensor,
|
|
1104
|
+
cell_vec,
|
|
1105
|
+
atomic_numbers,
|
|
1106
|
+
fixed_mask
|
|
1107
|
+
) = raw_results[f'group{i_head}'][0]
|
|
1108
|
+
n_batch = len(batch_indices_tensor)
|
|
1109
|
+
_split_indices = np.cumsum(batch_indices_tensor)[:-1]
|
|
1110
|
+
_cells = [_ for _ in cell_vec]
|
|
1111
|
+
_tol_atm_list = np.split(atomic_numbers[0], _split_indices, axis=0)
|
|
1112
|
+
_elements = list()
|
|
1113
|
+
_numbers = list()
|
|
1114
|
+
_id_per_frame = list()
|
|
1115
|
+
for ii, _atml in enumerate(_tol_atm_list):
|
|
1116
|
+
elements, _, numbers = elem_list_reduce(_atml)
|
|
1117
|
+
_elements.append(elements)
|
|
1118
|
+
_numbers.append(numbers)
|
|
1119
|
+
_id_per_frame.append(ii)
|
|
1120
|
+
_fixed = np.split(fixed_mask[0], _split_indices, axis=0)
|
|
1121
|
+
# main data
|
|
1122
|
+
kk = 0
|
|
1123
|
+
n_cyc = len(raw_results[f'group{i_content}'])
|
|
1124
|
+
cell_list.extend(_cells * n_cyc)
|
|
1125
|
+
element_list.extend(_elements * n_cyc)
|
|
1126
|
+
numbers_list.extend(_numbers * n_cyc)
|
|
1127
|
+
coo_t_list.extend(['C'] * (n_batch * n_cyc))
|
|
1128
|
+
fixed_list.extend(_fixed * n_cyc)
|
|
1129
|
+
for en, x, v, f in raw_results[f'group{i_content}']:
|
|
1130
|
+
_x = np.split(x[0], _split_indices, axis=0)
|
|
1131
|
+
_f = np.split(f[0], _split_indices, axis=0)
|
|
1132
|
+
_v = np.split(v[0], _split_indices, axis=0)
|
|
1133
|
+
smp_ids.extend([f'samp{i_cyc}_struc{_}_step{kk}' for _ in _id_per_frame])
|
|
1134
|
+
coo_list.extend(_x)
|
|
1135
|
+
veloc_list.extend(_v)
|
|
1136
|
+
energy_list.extend(en.tolist())
|
|
1137
|
+
force_list.extend(_f)
|
|
1138
|
+
kk += 1
|
|
1139
|
+
else:
|
|
1140
|
+
raise ValueError(f"Invalid file format: {path}. It may be not a MD dump file.")
|
|
1141
|
+
|
|
1142
|
+
bs = BatchStructures()
|
|
1143
|
+
bs.append_from_lists(
|
|
1144
|
+
smp_ids,
|
|
1145
|
+
cell_list,
|
|
1146
|
+
element_list,
|
|
1147
|
+
numbers_list,
|
|
1148
|
+
coo_t_list,
|
|
1149
|
+
coo_list,
|
|
1150
|
+
fixed_list,
|
|
1151
|
+
energy_list,
|
|
1152
|
+
force_list,
|
|
1153
|
+
veloc_list
|
|
1154
|
+
)
|
|
1155
|
+
bs._check_id()
|
|
1156
|
+
bs._check_len()
|
|
1157
|
+
|
|
1158
|
+
return bs
|
|
1159
|
+
|
|
1160
|
+
def read_mc_traj(
|
|
1161
|
+
path,
|
|
1162
|
+
indices: List[int]|slice|int = -1,
|
|
1163
|
+
is_copy: bool = True
|
|
1164
|
+
):
|
|
1165
|
+
"""
|
|
1166
|
+
A specialized reader for dump files generated by Monte Carlo.
|
|
1167
|
+
For BaseMC class, the information is as follows
|
|
1168
|
+
with denoting shape [n_batch, n_atom, n_dim] (regular batch) or [1, sumNi, n_atom] (irregular batch) as "sX":
|
|
1169
|
+
group 1: 1-step
|
|
1170
|
+
batch_indices_tensor[n_batch, ] (optional, exists for irregular batches)
|
|
1171
|
+
cell_vec[n_batch, 3, 3] / [1], if No cell_vec input, it will be set to [0] (shape: [1, ]).
|
|
1172
|
+
atomic_numbers[n]
|
|
1173
|
+
fixed_mask[sX]
|
|
1174
|
+
group 2: n_time_steps step
|
|
1175
|
+
E[n_batch, ] (energy)
|
|
1176
|
+
X[sX] (coordinates)
|
|
1177
|
+
Args:
|
|
1178
|
+
path: the path to the dump file.
|
|
1179
|
+
indices: the indices in each group of the arrays to read. A negative number means read all.
|
|
1180
|
+
is_copy: whether to copy the arrays from the mmap file.
|
|
1181
|
+
Note: if `is_copy` is False, the mmap file cannot be closed due to the exported pointers used by read arrays.
|
|
1182
|
+
One must release all references to the mmap file first to close the memory map file.
|
|
1183
|
+
Returns:
|
|
1184
|
+
BatchStructures
|
|
1185
|
+
|
|
1186
|
+
"""
|
|
1187
|
+
reader = ArrayDumpReader(path)
|
|
1188
|
+
raw_results = reader.read(groups=-1, indices=indices, is_copy=is_copy)
|
|
1189
|
+
n_grp = len(raw_results)
|
|
1190
|
+
if n_grp % 2 != 0:
|
|
1191
|
+
raise EOFError(f"Molecular dynamics file must contain even number of groups, but got {n_grp}.")
|
|
1192
|
+
|
|
1193
|
+
smp_ids = list()
|
|
1194
|
+
cell_list = list()
|
|
1195
|
+
element_list = list()
|
|
1196
|
+
numbers_list = list()
|
|
1197
|
+
coo_t_list = list()
|
|
1198
|
+
coo_list = list()
|
|
1199
|
+
fixed_list = list()
|
|
1200
|
+
energy_list = list()
|
|
1201
|
+
|
|
1202
|
+
for i in range(0, n_grp, 2):
|
|
1203
|
+
i_head = i
|
|
1204
|
+
i_content = i + 1
|
|
1205
|
+
i_cyc = i_head // 2
|
|
1206
|
+
if len(raw_results[f'group{i_head}'][0]) == 3: # no irregular batch_indices
|
|
1207
|
+
batch_indices_tensor = None
|
|
1208
|
+
(
|
|
1209
|
+
cell_vec,
|
|
1210
|
+
atomic_numbers,
|
|
1211
|
+
fixed_mask
|
|
1212
|
+
) = raw_results[f'group{i_head}'][0]
|
|
1213
|
+
n_batch = len(cell_vec)
|
|
1214
|
+
_elements = list()
|
|
1215
|
+
_numbers = list()
|
|
1216
|
+
_id_per_frame = list()
|
|
1217
|
+
for ii, _atml in enumerate(atomic_numbers):
|
|
1218
|
+
elements, _, numbers = elem_list_reduce(_atml)
|
|
1219
|
+
_elements.append(elements)
|
|
1220
|
+
_numbers.append(numbers)
|
|
1221
|
+
_id_per_frame.append(ii)
|
|
1222
|
+
_fixed = [_ for _ in fixed_mask]
|
|
1223
|
+
_cells = [_ for _ in cell_vec]
|
|
1224
|
+
kk = 0
|
|
1225
|
+
n_cyc = len(raw_results[f'group{i_content}'])
|
|
1226
|
+
cell_list.extend(_cells * n_cyc)
|
|
1227
|
+
element_list.extend(_elements * n_cyc)
|
|
1228
|
+
numbers_list.extend(_numbers * n_cyc)
|
|
1229
|
+
coo_t_list.extend(['C'] * (n_batch * n_cyc))
|
|
1230
|
+
fixed_list.extend(_fixed * n_cyc)
|
|
1231
|
+
for en, x in raw_results[f'group{i_content}']:
|
|
1232
|
+
_x = [_ for _ in x]
|
|
1233
|
+
smp_ids.extend([f'samp{i_cyc}_struc{_}_step{kk}' for _ in _id_per_frame])
|
|
1234
|
+
coo_list.extend(_x)
|
|
1235
|
+
energy_list.extend(en.tolist())
|
|
1236
|
+
kk += 1
|
|
1237
|
+
|
|
1238
|
+
elif len(raw_results[f'group{i_head}'][0]) == 4: # irregular situation
|
|
1239
|
+
(
|
|
1240
|
+
batch_indices_tensor,
|
|
1241
|
+
cell_vec,
|
|
1242
|
+
atomic_numbers,
|
|
1243
|
+
fixed_mask
|
|
1244
|
+
) = raw_results[f'group{i_head}'][0]
|
|
1245
|
+
n_batch = len(batch_indices_tensor)
|
|
1246
|
+
_split_indices = np.cumsum(batch_indices_tensor)[:-1]
|
|
1247
|
+
_cells = [_ for _ in cell_vec]
|
|
1248
|
+
_tol_atm_list = np.split(atomic_numbers[0], _split_indices, axis=0)
|
|
1249
|
+
_elements = list()
|
|
1250
|
+
_numbers = list()
|
|
1251
|
+
_id_per_frame = list()
|
|
1252
|
+
for ii, _atml in enumerate(_tol_atm_list):
|
|
1253
|
+
elements, _, numbers = elem_list_reduce(_atml)
|
|
1254
|
+
_elements.append(elements)
|
|
1255
|
+
_numbers.append(numbers)
|
|
1256
|
+
_id_per_frame.append(ii)
|
|
1257
|
+
_fixed = np.split(fixed_mask[0], _split_indices, axis=0)
|
|
1258
|
+
# main data
|
|
1259
|
+
kk = 0
|
|
1260
|
+
n_cyc = len(raw_results[f'group{i_content}'])
|
|
1261
|
+
cell_list.extend(_cells * n_cyc)
|
|
1262
|
+
element_list.extend(_elements * n_cyc)
|
|
1263
|
+
numbers_list.extend(_numbers * n_cyc)
|
|
1264
|
+
coo_t_list.extend(['C'] * (n_batch * n_cyc))
|
|
1265
|
+
fixed_list.extend(_fixed * n_cyc)
|
|
1266
|
+
for en, x in raw_results[f'group{i_content}']:
|
|
1267
|
+
_x = np.split(x[0], _split_indices, axis=0)
|
|
1268
|
+
smp_ids.extend([f'samp{i_cyc}_struc{_}_step{kk}' for _ in _id_per_frame])
|
|
1269
|
+
coo_list.extend(_x)
|
|
1270
|
+
energy_list.extend(en.tolist())
|
|
1271
|
+
kk += 1
|
|
1272
|
+
else:
|
|
1273
|
+
raise ValueError(f"Invalid file format: {path}. It may be not a MD dump file.")
|
|
1274
|
+
|
|
1275
|
+
bs = BatchStructures()
|
|
1276
|
+
bs.append_from_lists(
|
|
1277
|
+
smp_ids,
|
|
1278
|
+
cell_list,
|
|
1279
|
+
element_list,
|
|
1280
|
+
numbers_list,
|
|
1281
|
+
coo_t_list,
|
|
1282
|
+
coo_list,
|
|
1283
|
+
fixed_list,
|
|
1284
|
+
energy_list,
|
|
1285
|
+
)
|
|
1286
|
+
bs._check_id()
|
|
1287
|
+
bs._check_len()
|
|
1288
|
+
|
|
1289
|
+
return bs
|
|
1290
|
+
|
|
1291
|
+
def read_opt_structures(
|
|
1292
|
+
path,
|
|
1293
|
+
indices: List[int]|slice|int = -1,
|
|
1294
|
+
is_copy: bool = True
|
|
1295
|
+
):
|
|
1296
|
+
"""
|
|
1297
|
+
A specialized reader for dump files generated by Structure Optimization.
|
|
1298
|
+
For `StructureOptimization` class, the information is as follows
|
|
1299
|
+
with denoting shape [1, sumNi, n_atom] (irregular batch) as "sX":
|
|
1300
|
+
group 1: 1-step
|
|
1301
|
+
batch_indices[n_batch, ]
|
|
1302
|
+
idx[n_batch, ], dtype='<U128', the name of structures.
|
|
1303
|
+
cells[n_batch, 3, 3]
|
|
1304
|
+
elements[sumNi]
|
|
1305
|
+
pos[sX]
|
|
1306
|
+
fixations[sX]
|
|
1307
|
+
energies[sumNi]
|
|
1308
|
+
forces[sX]
|
|
1309
|
+
The coordinates type is forever 'Cartesian'.
|
|
1310
|
+
|
|
1311
|
+
Args:
|
|
1312
|
+
path: the path to the dump file.
|
|
1313
|
+
indices: the indices in each group of the arrays to read. A negative number means read all.
|
|
1314
|
+
is_copy: whether to copy the arrays from the mmap file.
|
|
1315
|
+
Note: if `is_copy` is False, the mmap file cannot be closed due to the exported pointers used by read arrays.
|
|
1316
|
+
One must release all references to the mmap file first to close the memory map file.
|
|
1317
|
+
Returns:
|
|
1318
|
+
BatchStructures
|
|
1319
|
+
|
|
1320
|
+
"""
|
|
1321
|
+
reader = ArrayDumpReader(path)
|
|
1322
|
+
raw_results = reader.read(groups=-1, indices=indices, is_copy=is_copy)
|
|
1323
|
+
n_grp = len(raw_results)
|
|
1324
|
+
|
|
1325
|
+
smp_ids = list()
|
|
1326
|
+
cell_list = list()
|
|
1327
|
+
element_list = list()
|
|
1328
|
+
numbers_list = list()
|
|
1329
|
+
coo_t_list = list()
|
|
1330
|
+
coo_list = list()
|
|
1331
|
+
fixed_list = list()
|
|
1332
|
+
energy_list = list()
|
|
1333
|
+
force_list = list()
|
|
1334
|
+
|
|
1335
|
+
for i in range(n_grp):
|
|
1336
|
+
if len(raw_results[f'group{i}'][0]) != 8: # irregular situation
|
|
1337
|
+
raise ValueError(f"Invalid file format: {path}. It may be not a Structure Optimization dumped file.")
|
|
1338
|
+
(
|
|
1339
|
+
batch_indices,
|
|
1340
|
+
idx,
|
|
1341
|
+
cells,
|
|
1342
|
+
elements,
|
|
1343
|
+
pos,
|
|
1344
|
+
fixations,
|
|
1345
|
+
energies,
|
|
1346
|
+
forces
|
|
1347
|
+
) = raw_results[f'group{i}'][0]
|
|
1348
|
+
|
|
1349
|
+
n_batch = len(batch_indices)
|
|
1350
|
+
_split_indices = np.cumsum(batch_indices)[:-1]
|
|
1351
|
+
_cells = [_ for _ in cells]
|
|
1352
|
+
_tol_atm_list = np.split(elements, _split_indices, axis=0)
|
|
1353
|
+
_elements = list()
|
|
1354
|
+
_numbers = list()
|
|
1355
|
+
_id_per_frame = list()
|
|
1356
|
+
for ii, _atml in enumerate(_tol_atm_list):
|
|
1357
|
+
elements, _, numbers = elem_list_reduce(_atml)
|
|
1358
|
+
_elements.append(elements)
|
|
1359
|
+
_numbers.append(numbers)
|
|
1360
|
+
_id_per_frame.append(ii)
|
|
1361
|
+
_fixed = np.split(fixations, _split_indices, axis=0)
|
|
1362
|
+
# main data
|
|
1363
|
+
kk = 0
|
|
1364
|
+
n_cyc = len(raw_results[f'group{i}'])
|
|
1365
|
+
if n_cyc != 1:
|
|
1366
|
+
raise RuntimeError(f'??? BUG: why is not the cycle number of structure optimization 1, but {n_cyc} cycles? Report us please! ???')
|
|
1367
|
+
cell_list.extend(_cells)
|
|
1368
|
+
element_list.extend(_elements)
|
|
1369
|
+
numbers_list.extend(_numbers)
|
|
1370
|
+
coo_t_list.extend(['C'] * n_batch)
|
|
1371
|
+
fixed_list.extend(_fixed)
|
|
1372
|
+
_x = np.split(pos, _split_indices, axis=0)
|
|
1373
|
+
_f = np.split(forces, _split_indices, axis=0)
|
|
1374
|
+
smp_ids.extend(idx.tolist())
|
|
1375
|
+
coo_list.extend(_x)
|
|
1376
|
+
energy_list.extend(energies.tolist())
|
|
1377
|
+
force_list.extend(_f)
|
|
1378
|
+
|
|
1379
|
+
bs = BatchStructures()
|
|
1380
|
+
bs.append_from_lists(
|
|
1381
|
+
smp_ids,
|
|
1382
|
+
cell_list,
|
|
1383
|
+
element_list,
|
|
1384
|
+
numbers_list,
|
|
1385
|
+
coo_t_list,
|
|
1386
|
+
coo_list,
|
|
1387
|
+
fixed_list,
|
|
1388
|
+
energy_list,
|
|
1389
|
+
force_list,
|
|
1390
|
+
)
|
|
1391
|
+
bs._check_id()
|
|
1392
|
+
bs._check_len()
|
|
1393
|
+
|
|
1394
|
+
return bs
|