BUCToolkit 1.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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