micress-micpy 0.3.2b2__py3-none-any.whl → 0.4.0a1__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.
- micpy/MicressBinaryReader copy.py +130 -0
- micpy/MicressBinaryReader.py +263 -0
- micpy/__init__.py +0 -3
- micpy/bin copy.py +865 -0
- micpy/bin.py +535 -560
- micpy/paraview_plugin.py +221 -0
- micpy/paraview_plugin_stub.py +1 -0
- micpy/version.py +1 -1
- micpy/vtk.py +60 -0
- {micress_micpy-0.3.2b2.dist-info → micress_micpy-0.4.0a1.dist-info}/METADATA +16 -3
- micress_micpy-0.4.0a1.dist-info/RECORD +18 -0
- {micress_micpy-0.3.2b2.dist-info → micress_micpy-0.4.0a1.dist-info}/WHEEL +1 -1
- micress_micpy-0.3.2b2.dist-info/RECORD +0 -12
- {micress_micpy-0.3.2b2.dist-info → micress_micpy-0.4.0a1.dist-info/licenses}/LICENSE +0 -0
- {micress_micpy-0.3.2b2.dist-info → micress_micpy-0.4.0a1.dist-info}/top_level.txt +0 -0
micpy/bin.py
CHANGED
|
@@ -1,61 +1,79 @@
|
|
|
1
1
|
"""The `micpy.bin` module provides methods to read and write binary files."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
3
|
+
import builtins
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from time import time as t
|
|
7
|
+
from typing import IO, List, Optional, Tuple, Union, overload
|
|
8
|
+
import os
|
|
5
9
|
|
|
6
10
|
import gzip
|
|
7
|
-
import
|
|
8
|
-
import sys
|
|
9
|
-
import zlib
|
|
11
|
+
import rapidgzip
|
|
10
12
|
|
|
11
13
|
import numpy as np
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
try:
|
|
16
|
+
import matplotlib
|
|
17
|
+
from matplotlib.axes import Axes
|
|
18
|
+
from matplotlib.figure import Figure
|
|
19
|
+
from matplotlib.colorbar import Colorbar
|
|
20
|
+
from matplotlib import pyplot
|
|
21
|
+
|
|
22
|
+
HAS_MATPLOTLIB = True
|
|
23
|
+
|
|
24
|
+
def register_colormaps():
|
|
25
|
+
colors = [
|
|
26
|
+
"#1102d8",
|
|
27
|
+
"#3007ba",
|
|
28
|
+
"#500b9d",
|
|
29
|
+
"#6f0e81",
|
|
30
|
+
"#8d1364",
|
|
31
|
+
"#ac1748",
|
|
32
|
+
"#cb1b2b",
|
|
33
|
+
"#ea1e0f",
|
|
34
|
+
"#f83605",
|
|
35
|
+
"#fa600f",
|
|
36
|
+
"#fb8817",
|
|
37
|
+
"#fdb120",
|
|
38
|
+
"#ffda29",
|
|
39
|
+
"#ffed4d",
|
|
40
|
+
"#fff380",
|
|
41
|
+
"#fffbb4",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
create = matplotlib.colors.LinearSegmentedColormap.from_list
|
|
45
|
+
|
|
46
|
+
colormap = create(name="micpy", colors=colors, N=1024)
|
|
47
|
+
colormap_r = create(name="micpy_r", colors=colors[::-1], N=1024)
|
|
48
|
+
|
|
49
|
+
register = matplotlib.colormaps.register
|
|
50
|
+
register(colormap)
|
|
51
|
+
register(colormap_r)
|
|
52
|
+
|
|
53
|
+
if not HAS_MATPLOTLIB:
|
|
54
|
+
raise ImportError("Matplotlib is not installed.")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
register_colormaps()
|
|
58
|
+
except ValueError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
except ImportError:
|
|
62
|
+
HAS_MATPLOTLIB = False
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from vtk import vtkImageData, vtkXMLImageDataWriter
|
|
66
|
+
from vtkmodules.util.numpy_support import numpy_to_vtk
|
|
67
|
+
HAS_VTK = True
|
|
68
|
+
except ImportError:
|
|
69
|
+
HAS_VTK = False
|
|
40
70
|
|
|
41
|
-
if decompressobj:
|
|
42
|
-
decompressobj = decompressobj.copy()
|
|
43
|
-
else:
|
|
44
|
-
decompressobj = (
|
|
45
|
-
zlib.decompressobj(zlib.MAX_WBITS | 32) if compressed else None
|
|
46
|
-
)
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if data == b"":
|
|
51
|
-
break
|
|
72
|
+
from micpy import geo
|
|
73
|
+
from micpy import utils
|
|
52
74
|
|
|
53
|
-
if compressed:
|
|
54
|
-
prev_decompressobj = decompressobj
|
|
55
|
-
decompressobj = prev_decompressobj.copy()
|
|
56
|
-
data = decompressobj.decompress(data)
|
|
57
75
|
|
|
58
|
-
|
|
76
|
+
__all__ = ["open", "read", "Field", "Series", "File"]
|
|
59
77
|
|
|
60
78
|
|
|
61
79
|
class Footer:
|
|
@@ -102,161 +120,24 @@ class Header:
|
|
|
102
120
|
).tobytes()
|
|
103
121
|
|
|
104
122
|
@staticmethod
|
|
105
|
-
def read(
|
|
123
|
+
def read(file: IO[bytes]) -> "Header":
|
|
106
124
|
"""Read the header of a binary file."""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
data = file.read(Header.SIZE)
|
|
112
|
-
return Header.from_bytes(data)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@dataclass
|
|
116
|
-
class Position:
|
|
117
|
-
"""A field position in a binary file."""
|
|
118
|
-
|
|
119
|
-
id: int
|
|
120
|
-
time: float
|
|
121
|
-
chunk_id: int
|
|
122
|
-
chunk_offset: int
|
|
123
|
-
decompressobj: zlib.decompressobj
|
|
124
|
-
chunk_size: int
|
|
125
|
-
field_size: int
|
|
126
|
-
file: IO[bytes]
|
|
127
|
-
|
|
125
|
+
file.seek(0)
|
|
126
|
+
data = file.read(Header.SIZE)
|
|
127
|
+
return Header.from_bytes(data)
|
|
128
|
+
|
|
128
129
|
@staticmethod
|
|
129
|
-
def
|
|
130
|
-
file
|
|
131
|
-
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
position.decompressobj is not None,
|
|
136
|
-
position.chunk_size,
|
|
137
|
-
position.field_size,
|
|
138
|
-
position.id,
|
|
139
|
-
position.chunk_id[0],
|
|
140
|
-
position.chunk_offset[0],
|
|
141
|
-
position.decompressobj.copy() if position.decompressobj else None,
|
|
142
|
-
position.chunk_id[0] * position.chunk_size,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
header = Header.read(file.name, compressed)
|
|
146
|
-
return (file, compressed, chunk_size, header.field_size, 0, 0, 0, None, 0)
|
|
147
|
-
|
|
148
|
-
@staticmethod
|
|
149
|
-
def _process_buffer(chunk_buffer: bytes, field_buffer: bytes, field_size: int):
|
|
150
|
-
required_size = field_size - len(field_buffer)
|
|
151
|
-
required_buffer = chunk_buffer[:required_size]
|
|
152
|
-
field_buffer += required_buffer
|
|
153
|
-
chunk_buffer = chunk_buffer[required_size:]
|
|
154
|
-
return field_buffer, chunk_buffer, len(required_buffer)
|
|
155
|
-
|
|
156
|
-
@staticmethod
|
|
157
|
-
def iterate(
|
|
158
|
-
file: IO[bytes] = None,
|
|
159
|
-
compressed: bool = True,
|
|
160
|
-
chunk_size: int = Chunk.DEFAULT_SIZE,
|
|
161
|
-
position: "Position" = None,
|
|
162
|
-
) -> Generator["Position", None, None]:
|
|
163
|
-
"""Yield positions of fields in a binary file."""
|
|
164
|
-
|
|
165
|
-
# File: [0100110001110101011010110110000101110011]
|
|
166
|
-
# Chunks: [ 0 | 1 | 2 | 3 | 4 ]
|
|
167
|
-
# Fields: [0 |1 |2 |3 |4 |5 ]
|
|
168
|
-
|
|
169
|
-
(
|
|
170
|
-
file,
|
|
171
|
-
compressed,
|
|
172
|
-
chunk_size,
|
|
173
|
-
field_size,
|
|
174
|
-
field_id,
|
|
175
|
-
chunk_id,
|
|
176
|
-
chunk_offset,
|
|
177
|
-
decompressobj,
|
|
178
|
-
offset,
|
|
179
|
-
) = Position._iterate_params(file, position, compressed, chunk_size)
|
|
180
|
-
|
|
181
|
-
field_buffer = b""
|
|
182
|
-
prev_chunk_id = chunk_id
|
|
183
|
-
prev_chunk_offset = chunk_offset
|
|
184
|
-
|
|
185
|
-
for chunk in Chunk.iterate(
|
|
186
|
-
file,
|
|
187
|
-
chunk_size=chunk_size,
|
|
188
|
-
compressed=compressed,
|
|
189
|
-
offset=offset,
|
|
190
|
-
decompressobj=decompressobj,
|
|
191
|
-
):
|
|
192
|
-
chunk_buffer = chunk.data
|
|
193
|
-
|
|
194
|
-
if chunk_offset:
|
|
195
|
-
chunk_buffer = chunk_buffer[chunk_offset:]
|
|
196
|
-
|
|
197
|
-
while chunk_buffer:
|
|
198
|
-
if len(field_buffer) == 0:
|
|
199
|
-
decompressobj = chunk.decompressobj
|
|
200
|
-
prev_chunk_id = chunk_id
|
|
201
|
-
prev_chunk_offset = chunk_offset
|
|
202
|
-
|
|
203
|
-
(field_buffer, chunk_buffer, consumed) = Position._process_buffer(
|
|
204
|
-
chunk_buffer, field_buffer, field_size
|
|
205
|
-
)
|
|
206
|
-
chunk_offset += consumed
|
|
207
|
-
|
|
208
|
-
if len(field_buffer) == field_size:
|
|
209
|
-
if not (position and position.id == field_id):
|
|
210
|
-
yield Position(
|
|
211
|
-
id=field_id,
|
|
212
|
-
time=Header.from_bytes(field_buffer).time,
|
|
213
|
-
chunk_id=(prev_chunk_id, chunk_id),
|
|
214
|
-
chunk_offset=(prev_chunk_offset, chunk_offset),
|
|
215
|
-
chunk_size=chunk_size,
|
|
216
|
-
field_size=field_size,
|
|
217
|
-
decompressobj=decompressobj,
|
|
218
|
-
file=file,
|
|
219
|
-
)
|
|
220
|
-
field_id += 1
|
|
221
|
-
field_buffer = b""
|
|
222
|
-
|
|
223
|
-
chunk_id += 1
|
|
224
|
-
chunk_offset = 0
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
class Index(List[Position]):
|
|
228
|
-
"""An index of fields in a binary file."""
|
|
229
|
-
|
|
230
|
-
@staticmethod
|
|
231
|
-
def from_file(
|
|
232
|
-
file: IO[bytes],
|
|
233
|
-
verbose: bool = True,
|
|
234
|
-
chunk_size: int = Chunk.DEFAULT_SIZE,
|
|
235
|
-
compressed: bool = True,
|
|
236
|
-
position: Position = None,
|
|
237
|
-
):
|
|
238
|
-
"""Build an index from a binary file."""
|
|
239
|
-
iterator = Position.iterate(
|
|
240
|
-
file, chunk_size=chunk_size, compressed=compressed, position=position
|
|
241
|
-
)
|
|
130
|
+
def read_at(file: IO[bytes], offset: int) -> "Header":
|
|
131
|
+
file.seek(offset)
|
|
132
|
+
data = file.read(Header.SIZE)
|
|
133
|
+
if len(data) < Header.SIZE:
|
|
134
|
+
raise EOFError("Unexpected end of file")
|
|
135
|
+
return Header.from_bytes(data)
|
|
242
136
|
|
|
243
|
-
if verbose:
|
|
244
|
-
iterator = utils.progress_indicator(
|
|
245
|
-
iterator, description="Indexing", unit="Field"
|
|
246
|
-
)
|
|
247
137
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def from_filename(
|
|
252
|
-
filename: str,
|
|
253
|
-
verbose: bool = True,
|
|
254
|
-
chunk_size: int = Chunk.DEFAULT_SIZE,
|
|
255
|
-
compressed: bool = True,
|
|
256
|
-
):
|
|
257
|
-
"""Build an index from a binary file."""
|
|
258
|
-
file = open(filename, "rb")
|
|
259
|
-
return Index.from_file(file, verbose, chunk_size, compressed)
|
|
138
|
+
def get_field_count(self, file_size: int) -> int:
|
|
139
|
+
"""Get the number of fields in the file."""
|
|
140
|
+
return file_size // self.field_size
|
|
260
141
|
|
|
261
142
|
|
|
262
143
|
class Field(np.ndarray):
|
|
@@ -290,8 +171,6 @@ class Field(np.ndarray):
|
|
|
290
171
|
|
|
291
172
|
data = data[start:end]
|
|
292
173
|
data = np.frombuffer(data, count=count, dtype="float32")
|
|
293
|
-
if np.all(np.isclose(data, data.astype("int32"))):
|
|
294
|
-
data = data.astype("int32")
|
|
295
174
|
|
|
296
175
|
if shape is not None:
|
|
297
176
|
data = data.reshape(shape)
|
|
@@ -321,36 +200,42 @@ class Field(np.ndarray):
|
|
|
321
200
|
return 3
|
|
322
201
|
|
|
323
202
|
@staticmethod
|
|
324
|
-
def read(
|
|
203
|
+
def read(
|
|
204
|
+
file: IO[bytes],
|
|
205
|
+
field_index: int,
|
|
206
|
+
field_size: int,
|
|
207
|
+
shape=None,
|
|
208
|
+
spacing=None,
|
|
209
|
+
) -> "Field":
|
|
325
210
|
"""Read a field from a binary file."""
|
|
326
|
-
file_offset = position.chunk_id[0] * position.chunk_size
|
|
327
|
-
position.file.seek(file_offset)
|
|
328
|
-
|
|
329
|
-
decompressobj = (
|
|
330
|
-
position.decompressobj.copy() if position.decompressobj else None
|
|
331
|
-
)
|
|
332
211
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if len(field_buffer) >= position.field_size:
|
|
349
|
-
break
|
|
212
|
+
if field_index < 0:
|
|
213
|
+
start = t()
|
|
214
|
+
_debug("Indexing file...")
|
|
215
|
+
file.seek(0, 2)
|
|
216
|
+
file_size = file.tell()
|
|
217
|
+
field_count = file_size // field_size
|
|
218
|
+
field_index += field_count
|
|
219
|
+
_debug(f"Index completed in {t() - start:.2f} seconds.")
|
|
220
|
+
|
|
221
|
+
start = t()
|
|
222
|
+
_debug(f"Seeking to field index {field_index}...")
|
|
223
|
+
offset = field_size * field_index
|
|
224
|
+
file.seek(offset)
|
|
225
|
+
_debug(f"Seek completed in {t() - start:.2f} seconds.")
|
|
350
226
|
|
|
351
|
-
|
|
227
|
+
start = t()
|
|
228
|
+
_debug(f"Reading data for field index {field_index}...")
|
|
229
|
+
field_data = file.read(field_size)
|
|
230
|
+
if len(field_data) < field_size:
|
|
231
|
+
raise EOFError("Unexpected end of file")
|
|
232
|
+
_debug(f"Read completed in {t() - start:.2f} seconds.")
|
|
352
233
|
|
|
353
|
-
|
|
234
|
+
start = t()
|
|
235
|
+
_debug(f"Creating Field object for field index {field_index}...")
|
|
236
|
+
field = Field.from_bytes(field_data, shape=shape, spacing=spacing)
|
|
237
|
+
_debug(f"Field object created in {t() - start:.2f} seconds.")
|
|
238
|
+
return field
|
|
354
239
|
|
|
355
240
|
def to_file(self, file: IO[bytes], geometry: bool = True):
|
|
356
241
|
"""Write the field to a binary file.
|
|
@@ -387,6 +272,220 @@ class Field(np.ndarray):
|
|
|
387
272
|
with file_open(filename, "wb") as file:
|
|
388
273
|
self.to_file(file, geometry)
|
|
389
274
|
|
|
275
|
+
def plot(
|
|
276
|
+
self,
|
|
277
|
+
axis: str = "y",
|
|
278
|
+
index: int = 0,
|
|
279
|
+
title: Optional[str] = None,
|
|
280
|
+
xlabel: Optional[str] = None,
|
|
281
|
+
ylabel: Optional[str] = None,
|
|
282
|
+
figsize: Optional[Tuple[float, float]] = None,
|
|
283
|
+
dpi: Optional[int] = None,
|
|
284
|
+
aspect: str = "equal",
|
|
285
|
+
ax: "Axes" = None,
|
|
286
|
+
cax: "Axes" = None,
|
|
287
|
+
vmin: Optional[float] = None,
|
|
288
|
+
vmax: Optional[float] = None,
|
|
289
|
+
cmap: str = "micpy",
|
|
290
|
+
alpha: float = 1.0,
|
|
291
|
+
interpolation: str = "none",
|
|
292
|
+
extent: Optional[Tuple[float, float, float, float]] = None,
|
|
293
|
+
) -> Tuple["Figure", "Axes", "Colorbar"]:
|
|
294
|
+
"""Plot a slice of the field using Matplotlib.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
axis (str, optional): Axis to plot. Possible values are `x`, `y`, and `z`.
|
|
298
|
+
Defaults to `y`.
|
|
299
|
+
index (int, optional): Index of the slice. Defaults to `0`.
|
|
300
|
+
title (str, optional): Title of the plot. Defaults to `None`.
|
|
301
|
+
xlabel (str, optional): Label of the x-axis. Defaults to `None`.
|
|
302
|
+
ylabel (str, optional): Label of the y-axis. Defaults to `None`.
|
|
303
|
+
figsize (Tuple[float, float], optional): Figure size. Defaults to `None`.
|
|
304
|
+
dpi (int, optional): Figure DPI. Defaults to `None`.
|
|
305
|
+
aspect (str, optional): Aspect ratio. Defaults to `equal`.
|
|
306
|
+
ax (Axes, optional): Axes of the plot. Defaults to `None`.
|
|
307
|
+
cax (Axes, optional): Axes of the color bar. Defaults to `None`.
|
|
308
|
+
vmin (float, optional): Minimum value of the color bar. Defaults to `None`.
|
|
309
|
+
vmax (float, optional): Maximum value of the color bar. Defaults to `None`.
|
|
310
|
+
cmap (str, optional): Colormap. Defaults to `micpy`.
|
|
311
|
+
alpha (float, optional): Transparency of the plot. Defaults to `1.0`.
|
|
312
|
+
interpolation (str, optional): Interpolation method. Defaults to `none`.
|
|
313
|
+
extent (Tuple[float, float, float, float], optional): Extent of the plot.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Matplotlib figure, axes, and color bar.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if not HAS_MATPLOTLIB:
|
|
320
|
+
raise ImportError("Matplotlib is not installed.")
|
|
321
|
+
|
|
322
|
+
if self.ndim != 3:
|
|
323
|
+
raise ValueError("'field' must be a 3D array")
|
|
324
|
+
|
|
325
|
+
if axis == "z":
|
|
326
|
+
x, y = "x", "y"
|
|
327
|
+
slice_2d = self[index, :, :]
|
|
328
|
+
elif axis == "y":
|
|
329
|
+
x, y = "x", "z"
|
|
330
|
+
slice_2d = self[:, index, :]
|
|
331
|
+
elif axis == "x":
|
|
332
|
+
x, y = "y", "z"
|
|
333
|
+
slice_2d = self[:, :, index]
|
|
334
|
+
else:
|
|
335
|
+
raise ValueError("'axis' must be 'x', 'y', or 'z'")
|
|
336
|
+
|
|
337
|
+
fig, ax = (
|
|
338
|
+
pyplot.subplots(figsize=figsize, dpi=dpi)
|
|
339
|
+
if ax is None
|
|
340
|
+
else (ax.get_figure(), ax)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if title is not None:
|
|
344
|
+
ax.set_title(title)
|
|
345
|
+
else:
|
|
346
|
+
ax.set_title(f"t={np.round(self.time, 7)}s")
|
|
347
|
+
if xlabel is not None:
|
|
348
|
+
ax.set_xlabel(xlabel)
|
|
349
|
+
else:
|
|
350
|
+
ax.set_xlabel(x)
|
|
351
|
+
if ylabel is not None:
|
|
352
|
+
ax.set_ylabel(ylabel)
|
|
353
|
+
else:
|
|
354
|
+
ax.set_ylabel(y)
|
|
355
|
+
if aspect is not None:
|
|
356
|
+
ax.set_aspect(aspect)
|
|
357
|
+
ax.set_frame_on(False)
|
|
358
|
+
|
|
359
|
+
image = ax.imshow(
|
|
360
|
+
slice_2d,
|
|
361
|
+
cmap=cmap,
|
|
362
|
+
vmin=vmin,
|
|
363
|
+
vmax=vmax,
|
|
364
|
+
interpolation=interpolation,
|
|
365
|
+
alpha=alpha,
|
|
366
|
+
extent=extent,
|
|
367
|
+
origin="lower",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
cbar = pyplot.colorbar(image, ax=ax, cax=cax)
|
|
371
|
+
cbar.locator = matplotlib.ticker.MaxNLocator(
|
|
372
|
+
integer=np.issubdtype(slice_2d.dtype, np.integer)
|
|
373
|
+
)
|
|
374
|
+
cbar.outline.set_visible(False)
|
|
375
|
+
cbar.update_ticks()
|
|
376
|
+
|
|
377
|
+
return fig, ax, cbar
|
|
378
|
+
|
|
379
|
+
def as_vti(
|
|
380
|
+
self,
|
|
381
|
+
name: str = "values",
|
|
382
|
+
point_data: bool = False,
|
|
383
|
+
) -> "vtkImageData":
|
|
384
|
+
"""Convert the field to a VTK ImageData object.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
name (str, optional): Name of the data array. Defaults to `values`.
|
|
388
|
+
point_data (bool, optional): `True` if data should be stored as PointData,
|
|
389
|
+
`False` if data should be stored as CellData. Defaults to `False`.
|
|
390
|
+
Returns:
|
|
391
|
+
VTK ImageData object.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
if not HAS_VTK:
|
|
395
|
+
raise ImportError("VTK is not installed.")
|
|
396
|
+
|
|
397
|
+
cells = np.asarray(self)
|
|
398
|
+
if cells.ndim != 3:
|
|
399
|
+
raise ValueError("field must be 3D (nz, ny, nx)")
|
|
400
|
+
|
|
401
|
+
nz, ny, nx = cells.shape
|
|
402
|
+
dz, dy, dx = self.spacing
|
|
403
|
+
|
|
404
|
+
def cell_to_point_average(c: np.ndarray) -> np.ndarray:
|
|
405
|
+
nz_, ny_, nx_ = c.shape
|
|
406
|
+
acc = np.zeros((nz_ + 1, ny_ + 1, nx_ + 1),
|
|
407
|
+
dtype=np.result_type(c.dtype, np.float32))
|
|
408
|
+
w = np.zeros((nz_ + 1, ny_ + 1, nx_ + 1), dtype=np.float32)
|
|
409
|
+
|
|
410
|
+
acc[0:nz_, 0:ny_, 0:nx_ ] += c
|
|
411
|
+
acc[0:nz_, 0:ny_, 1:nx_+1] += c
|
|
412
|
+
acc[0:nz_, 1:ny_+1, 0:nx_ ] += c
|
|
413
|
+
acc[0:nz_, 1:ny_+1, 1:nx_+1] += c
|
|
414
|
+
acc[1:nz_+1, 0:ny_, 0:nx_ ] += c
|
|
415
|
+
acc[1:nz_+1, 0:ny_, 1:nx_+1] += c
|
|
416
|
+
acc[1:nz_+1, 1:ny_+1, 0:nx_ ] += c
|
|
417
|
+
acc[1:nz_+1, 1:ny_+1, 1:nx_+1] += c
|
|
418
|
+
|
|
419
|
+
w[0:nz_, 0:ny_, 0:nx_ ] += 1
|
|
420
|
+
w[0:nz_, 0:ny_, 1:nx_+1] += 1
|
|
421
|
+
w[0:nz_, 1:ny_+1, 0:nx_ ] += 1
|
|
422
|
+
w[0:nz_, 1:ny_+1, 1:nx_+1] += 1
|
|
423
|
+
w[1:nz_+1, 0:ny_, 0:nx_ ] += 1
|
|
424
|
+
w[1:nz_+1, 0:ny_, 1:nx_+1] += 1
|
|
425
|
+
w[1:nz_+1, 1:ny_+1, 0:nx_ ] += 1
|
|
426
|
+
w[1:nz_+1, 1:ny_+1, 1:nx_+1] += 1
|
|
427
|
+
|
|
428
|
+
return acc / w
|
|
429
|
+
|
|
430
|
+
if point_data:
|
|
431
|
+
data = cell_to_point_average(cells)
|
|
432
|
+
else:
|
|
433
|
+
data = cells
|
|
434
|
+
|
|
435
|
+
data_c = np.ascontiguousarray(data)
|
|
436
|
+
flat = data_c.ravel(order="C")
|
|
437
|
+
|
|
438
|
+
image = vtkImageData()
|
|
439
|
+
image.SetOrigin(0.0, 0.0, 0.0)
|
|
440
|
+
image.SetSpacing(float(dx), float(dy), float(dz))
|
|
441
|
+
image.SetDimensions(nx + 1, ny + 1, nz + 1)
|
|
442
|
+
|
|
443
|
+
vtk_arr = numpy_to_vtk(flat, deep=False)
|
|
444
|
+
vtk_arr.SetName(name)
|
|
445
|
+
|
|
446
|
+
if point_data:
|
|
447
|
+
image.GetPointData().SetScalars(vtk_arr)
|
|
448
|
+
else:
|
|
449
|
+
image.GetCellData().SetScalars(vtk_arr)
|
|
450
|
+
|
|
451
|
+
image._numpy_pin = data_c
|
|
452
|
+
|
|
453
|
+
return image
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def save_vti(
|
|
457
|
+
self,
|
|
458
|
+
filename: str,
|
|
459
|
+
name: str = "values",
|
|
460
|
+
point_data: bool = False,
|
|
461
|
+
) -> str:
|
|
462
|
+
"""Save the field as a VTK ImageData file.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
filename (str): Filename of the VTK ImageData file.
|
|
466
|
+
name (str, optional): Name of the data array. Defaults to `values`.
|
|
467
|
+
point_data (bool, optional): `True` if data should be stored as PointData
|
|
468
|
+
`False` if data should be stored as CellData. Defaults to `False`.
|
|
469
|
+
Returns:
|
|
470
|
+
Filename of the VTK ImageData file.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
image = self.as_vti(name=name, point_data=point_data)
|
|
474
|
+
|
|
475
|
+
filename = str(Path(filename).with_suffix(".vti"))
|
|
476
|
+
|
|
477
|
+
writer = vtkXMLImageDataWriter()
|
|
478
|
+
writer.SetFileName(filename)
|
|
479
|
+
|
|
480
|
+
writer.SetInputData(image)
|
|
481
|
+
writer.SetDataModeToAppended()
|
|
482
|
+
writer.EncodeAppendedDataOff()
|
|
483
|
+
|
|
484
|
+
ok = writer.Write()
|
|
485
|
+
if ok != 1:
|
|
486
|
+
raise OSError(f"Failed to write {filename}")
|
|
487
|
+
|
|
488
|
+
return filename
|
|
390
489
|
|
|
391
490
|
class Series(np.ndarray):
|
|
392
491
|
def __new__(cls, fields: List[Field]):
|
|
@@ -403,16 +502,7 @@ class Series(np.ndarray):
|
|
|
403
502
|
self.times = getattr(obj, "times", None)
|
|
404
503
|
self.spacings = getattr(obj, "spacings", None)
|
|
405
504
|
|
|
406
|
-
def
|
|
407
|
-
"""Iterate over fields in the series.
|
|
408
|
-
|
|
409
|
-
Yields:
|
|
410
|
-
Field.
|
|
411
|
-
"""
|
|
412
|
-
for item, time, spacing in zip(self, self.times, self.spacings):
|
|
413
|
-
yield Field(item, time, spacing)
|
|
414
|
-
|
|
415
|
-
def get_field(self, index: int) -> Field:
|
|
505
|
+
def field(self, index: int) -> Field:
|
|
416
506
|
"""Get a field from the series.
|
|
417
507
|
|
|
418
508
|
Args:
|
|
@@ -423,22 +513,22 @@ class Series(np.ndarray):
|
|
|
423
513
|
"""
|
|
424
514
|
return Field(self[index], self.times[index], self.spacings[index])
|
|
425
515
|
|
|
426
|
-
def
|
|
516
|
+
def series(self, key: Union[int, slice, list]) -> "Series":
|
|
427
517
|
"""Get a series of fields.
|
|
428
518
|
|
|
429
519
|
Args:
|
|
430
|
-
key (Union[int, slice, list]): Key to list of field
|
|
431
|
-
list of field
|
|
520
|
+
key (Union[int, slice, list]): Key to list of field indices, a slice object, or a
|
|
521
|
+
list of field indices.
|
|
432
522
|
|
|
433
523
|
Returns:
|
|
434
524
|
Series of fields.
|
|
435
525
|
"""
|
|
436
526
|
if isinstance(key, int):
|
|
437
|
-
return Series([self.
|
|
527
|
+
return Series([self.field(key)])
|
|
438
528
|
if isinstance(key, slice):
|
|
439
|
-
return Series([self.
|
|
529
|
+
return Series([self.field(i) for i in range(*key.indices(len(self)))])
|
|
440
530
|
if isinstance(key, list):
|
|
441
|
-
return Series([self.
|
|
531
|
+
return Series([self.field(i) for i in key])
|
|
442
532
|
raise TypeError("Invalid argument type")
|
|
443
533
|
|
|
444
534
|
def write(self, filename: str, compressed: bool = True, geometry: bool = True):
|
|
@@ -454,55 +544,84 @@ class Series(np.ndarray):
|
|
|
454
544
|
|
|
455
545
|
file_open = gzip.open if compressed else open
|
|
456
546
|
with file_open(filename, "wb") as file:
|
|
457
|
-
for
|
|
458
|
-
|
|
547
|
+
for item, time, spacing in zip(self, self.times, self.spacings):
|
|
548
|
+
Field(item, time, spacing).to_file(file, geometry)
|
|
459
549
|
geometry = False
|
|
460
550
|
|
|
461
551
|
|
|
552
|
+
DEBUG = False
|
|
553
|
+
WARN = True
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _debug(*args):
|
|
557
|
+
# TODO: Use logging module
|
|
558
|
+
if DEBUG:
|
|
559
|
+
print("Debug:", *args)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _warn(*args):
|
|
563
|
+
# TODO: Use logging module
|
|
564
|
+
if WARN:
|
|
565
|
+
print("Warning:", *args)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _index_filename(filename: str) -> Path:
|
|
569
|
+
return Path(f"{filename}.idx")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _try_import_index(file, index: str):
|
|
573
|
+
path = Path(index)
|
|
574
|
+
if not path.is_file():
|
|
575
|
+
_debug("Index file not found.")
|
|
576
|
+
return
|
|
577
|
+
if not os.access(path, os.R_OK):
|
|
578
|
+
_debug("Index file is not readable.")
|
|
579
|
+
return
|
|
580
|
+
try:
|
|
581
|
+
file.import_index(str(path))
|
|
582
|
+
_debug("Index file imported.")
|
|
583
|
+
except ValueError:
|
|
584
|
+
_debug("Index file is invalid.")
|
|
585
|
+
return
|
|
586
|
+
except (PermissionError, FileNotFoundError, IsADirectoryError, OSError):
|
|
587
|
+
_debug("Failed to import index file.")
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _try_export_index(file, index: str):
|
|
592
|
+
path = Path(index)
|
|
593
|
+
|
|
594
|
+
if path.exists() and not path.is_file():
|
|
595
|
+
_warn("Index path is not a file.")
|
|
596
|
+
return
|
|
597
|
+
if not os.access(path.parent, os.W_OK):
|
|
598
|
+
_warn("Index file is not writable.")
|
|
599
|
+
return
|
|
600
|
+
try:
|
|
601
|
+
file.export_index(str(path))
|
|
602
|
+
_debug("Index file exported.")
|
|
603
|
+
except (PermissionError, IsADirectoryError, FileNotFoundError, OSError):
|
|
604
|
+
_warn("Failed to export index file.")
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
|
|
462
608
|
class File:
|
|
463
609
|
"""A binary file."""
|
|
464
610
|
|
|
465
611
|
def __init__(
|
|
466
612
|
self,
|
|
467
613
|
filename: str,
|
|
468
|
-
|
|
469
|
-
verbose: bool = True,
|
|
614
|
+
threads: int = None,
|
|
470
615
|
):
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
Args:
|
|
474
|
-
filename (str): File name.
|
|
475
|
-
chunk_size (int, optional): Chunk size in bytes. Defaults to `8388608`
|
|
476
|
-
(8 MiB).
|
|
477
|
-
verbose (bool, optional): Verbose output. Defaults to `True`.
|
|
478
|
-
|
|
479
|
-
Raises:
|
|
480
|
-
`FileNotFoundError`: If file is not found.
|
|
481
|
-
"""
|
|
482
|
-
if not os.path.isfile(filename):
|
|
483
|
-
raise FileNotFoundError(f"File not found: {filename}")
|
|
484
|
-
|
|
485
|
-
self._filename: str = filename
|
|
486
|
-
self._chunk_size: int = chunk_size
|
|
487
|
-
self._verbose: bool = verbose
|
|
616
|
+
self.shape: Tuple[int, int, int] = None
|
|
617
|
+
self.spacing: Tuple[float, float, float] = None
|
|
488
618
|
|
|
619
|
+
self._filename = filename
|
|
620
|
+
self._threads = threads if threads is not None else min(4, os.cpu_count())
|
|
489
621
|
self._file: IO[bytes] = None
|
|
490
622
|
self._compressed: bool = None
|
|
491
|
-
self.
|
|
492
|
-
self.
|
|
493
|
-
self._index: Index = None
|
|
494
|
-
|
|
495
|
-
self.shape: np.ndarray[(3,), np.int32] = None
|
|
496
|
-
self.spacing: np.ndarray[(3,), np.float32] = None
|
|
497
|
-
|
|
498
|
-
try:
|
|
499
|
-
self.find_geometry()
|
|
500
|
-
except (geo.GeometryFileNotFoundError, geo.MultipleGeometryFilesError):
|
|
501
|
-
if self._verbose:
|
|
502
|
-
self._warn("Caution: A geometry file was not found.")
|
|
503
|
-
|
|
504
|
-
def __getitem__(self, key: Union[int, slice, list, Callable[[Field], bool]]):
|
|
505
|
-
return self.read(key)
|
|
623
|
+
self._indexed: bool = False
|
|
624
|
+
self._header: Header = None
|
|
506
625
|
|
|
507
626
|
def __enter__(self):
|
|
508
627
|
return self.open()
|
|
@@ -510,328 +629,184 @@ class File:
|
|
|
510
629
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
511
630
|
self.close()
|
|
512
631
|
|
|
513
|
-
def __iter__(self):
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
632
|
+
def __iter__(self) -> Iterator[Field]:
|
|
633
|
+
index = 0
|
|
634
|
+
while True:
|
|
635
|
+
try:
|
|
636
|
+
yield self._read_field(index)
|
|
637
|
+
index += 1
|
|
638
|
+
except (IndexError, EOFError):
|
|
639
|
+
break
|
|
519
640
|
|
|
520
|
-
def
|
|
521
|
-
|
|
522
|
-
print(*args, file=sys.stderr)
|
|
641
|
+
def open(self) -> "File":
|
|
642
|
+
"""Open the file.
|
|
523
643
|
|
|
524
|
-
|
|
525
|
-
|
|
644
|
+
Returns:
|
|
645
|
+
File: The opened binary file.
|
|
646
|
+
"""
|
|
526
647
|
self._compressed = utils.is_compressed(self._filename)
|
|
527
648
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
649
|
+
if self._compressed:
|
|
650
|
+
self._file = rapidgzip.open(self._filename, self._threads)
|
|
651
|
+
start = t()
|
|
652
|
+
_debug("Importing index file...")
|
|
653
|
+
_try_import_index(self._file, _index_filename(self._filename))
|
|
654
|
+
_debug(f"Index file imported in {t() - start:.2f} seconds.")
|
|
655
|
+
else:
|
|
656
|
+
self._file = builtins.open(self._filename, "rb")
|
|
532
657
|
|
|
533
|
-
|
|
534
|
-
self._file = None
|
|
535
|
-
self._compressed = None
|
|
536
|
-
self._created = None
|
|
537
|
-
self._modified = None
|
|
538
|
-
self._index = None
|
|
658
|
+
self._header = Header.read(self._file)
|
|
539
659
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
660
|
+
try:
|
|
661
|
+
geometry = geo.read(geo.find(self._filename), type=geo.Type.BASIC)
|
|
662
|
+
self.shape = self.shape or geometry["shape"][::-1]
|
|
663
|
+
self.spacing = self.spacing or geometry["spacing"][::-1]
|
|
664
|
+
except (geo.GeometryFileNotFoundError, geo.MultipleGeometryFilesError):
|
|
665
|
+
_warn("Caution: A geometry file was not found.")
|
|
543
666
|
|
|
544
|
-
def open(self):
|
|
545
|
-
"""Open the file."""
|
|
546
|
-
if not self._file:
|
|
547
|
-
self.create_index()
|
|
548
667
|
return self
|
|
549
|
-
|
|
550
|
-
def
|
|
551
|
-
"""
|
|
552
|
-
self._close_file()
|
|
553
|
-
|
|
554
|
-
def index(self):
|
|
555
|
-
"""Get the index of the file."""
|
|
556
|
-
if self._created != os.path.getctime(self._filename):
|
|
557
|
-
self.create_index()
|
|
558
|
-
elif self._modified != os.path.getmtime(self._filename):
|
|
559
|
-
self.update_index()
|
|
560
|
-
return self._index
|
|
561
|
-
|
|
562
|
-
def create_index(self):
|
|
563
|
-
"""Create an index of the file."""
|
|
564
|
-
self._close_file()
|
|
565
|
-
self._open_file()
|
|
566
|
-
self._update_timestamps()
|
|
567
|
-
self._index = Index.from_file(
|
|
568
|
-
self._file,
|
|
569
|
-
verbose=self._verbose,
|
|
570
|
-
chunk_size=self._chunk_size,
|
|
571
|
-
compressed=self._compressed,
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
def update_index(self):
|
|
575
|
-
"""Update the index of the file."""
|
|
576
|
-
self._update_timestamps()
|
|
577
|
-
index = Index.from_file(
|
|
578
|
-
self._file,
|
|
579
|
-
verbose=self._verbose,
|
|
580
|
-
chunk_size=self._chunk_size,
|
|
581
|
-
compressed=self._compressed,
|
|
582
|
-
position=self._index[-1],
|
|
583
|
-
)
|
|
584
|
-
self._index.extend(index)
|
|
585
|
-
|
|
586
|
-
def times(self) -> List[float]:
|
|
587
|
-
"""Get the times of the fields in the file.
|
|
668
|
+
|
|
669
|
+
def times(self) -> list[float]:
|
|
670
|
+
"""Get the time steps from the binary file.
|
|
588
671
|
|
|
589
672
|
Returns:
|
|
590
|
-
List of
|
|
673
|
+
list[float]: List of time steps.
|
|
591
674
|
"""
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def set_geometry(
|
|
595
|
-
self, shape: Tuple[int, int, int], spacing: Tuple[float, float, float]
|
|
596
|
-
):
|
|
597
|
-
"""Set the geometry.
|
|
675
|
+
if not self._file:
|
|
676
|
+
self.open()
|
|
598
677
|
|
|
599
|
-
|
|
600
|
-
shape (Tuple[int, int, int]): Shape of the geometry (z, y, x).
|
|
601
|
-
spacing (Tuple[float, float, float]): Spacing of the geometry (dz, dy, dx) in μm.
|
|
602
|
-
"""
|
|
678
|
+
field_size = self._header.field_size
|
|
603
679
|
|
|
604
|
-
|
|
605
|
-
self.
|
|
680
|
+
cur = self._file.tell()
|
|
681
|
+
self._file.seek(0, 2)
|
|
682
|
+
file_size = self._file.tell()
|
|
683
|
+
field_count = file_size // field_size
|
|
684
|
+
self._file.seek(cur)
|
|
606
685
|
|
|
607
|
-
|
|
686
|
+
times: list[float] = []
|
|
608
687
|
|
|
609
|
-
|
|
610
|
-
|
|
688
|
+
for i in range(int(field_count)):
|
|
689
|
+
hdr = Header.read_at(self._file, i * field_size)
|
|
690
|
+
times.append(hdr.time)
|
|
611
691
|
|
|
612
|
-
|
|
613
|
-
filename (str): Filename of a geometry file.
|
|
614
|
-
compressed (bool, optional): `True` if file is compressed, `False`
|
|
615
|
-
otherwise. Defaults to `None` (auto).
|
|
616
|
-
"""
|
|
617
|
-
if compressed is None:
|
|
618
|
-
compressed = utils.is_compressed(filename)
|
|
692
|
+
return times
|
|
619
693
|
|
|
620
|
-
|
|
694
|
+
def _read_field(self, key: int) -> Field:
|
|
695
|
+
if not self._file:
|
|
696
|
+
self.open()
|
|
697
|
+
if isinstance(key, int):
|
|
698
|
+
return Field.read(
|
|
699
|
+
file=self._file,
|
|
700
|
+
field_index=key,
|
|
701
|
+
field_size=Header.read(self._file).field_size,
|
|
702
|
+
shape=self.shape,
|
|
703
|
+
spacing=self.spacing,
|
|
704
|
+
)
|
|
705
|
+
raise TypeError("Invalid argument type")
|
|
621
706
|
|
|
622
|
-
|
|
623
|
-
|
|
707
|
+
def _read_series(self, key: list[int] = None) -> Series:
|
|
708
|
+
if key is None:
|
|
709
|
+
fields: list[Field] = list(self)
|
|
710
|
+
return Series(fields)
|
|
711
|
+
if isinstance(key, list):
|
|
712
|
+
return Series([self._read_field(i) for i in key])
|
|
713
|
+
raise TypeError("Invalid argument type")
|
|
624
714
|
|
|
625
|
-
|
|
715
|
+
@overload
|
|
716
|
+
def read(self, key: int, threads: int = None) -> Field: ...
|
|
717
|
+
@overload
|
|
718
|
+
def read(self, key: list[int], threads: int = None) -> Series: ...
|
|
719
|
+
@overload
|
|
720
|
+
def read(self, key: None = None, threads: int = None) -> Series: ...
|
|
626
721
|
|
|
627
|
-
def
|
|
628
|
-
"""
|
|
722
|
+
def read(self, key: Union[int, list[int]] = None, threads: int = None) -> Union[Field, Series]:
|
|
723
|
+
"""Read a field or series from a binary file.
|
|
629
724
|
|
|
630
725
|
Args:
|
|
631
|
-
|
|
632
|
-
Defaults to `None
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
`GeometryFileNotFoundError`: If no geometry file is found.
|
|
636
|
-
`MultipleGeometryFilesError`: If multiple geometry files are found.
|
|
637
|
-
"""
|
|
638
|
-
filename = geo.find(self._filename)
|
|
639
|
-
|
|
640
|
-
self.read_geometry(filename, compressed=compressed)
|
|
641
|
-
|
|
642
|
-
def print_geometry(self):
|
|
643
|
-
"""Get a string representation of the geometry."""
|
|
644
|
-
|
|
645
|
-
if self.shape is None or self.spacing is None:
|
|
646
|
-
self._info("Geometry: None")
|
|
647
|
-
return
|
|
648
|
-
|
|
649
|
-
dimensions = Field.dimensions(self.shape)
|
|
650
|
-
cells = self.shape
|
|
651
|
-
spacing = 1e4 * np.round(self.spacing.astype(float), 7)
|
|
652
|
-
size = cells * spacing
|
|
653
|
-
|
|
654
|
-
self._info(f"Geometry: {dimensions}-Dimensional Grid")
|
|
655
|
-
self._info(f"Grid Size [μm]: {tuple(size)}")
|
|
656
|
-
self._info(f"Grid Shape (Cell Count): {tuple(cells)}")
|
|
657
|
-
self._info(f"Grid Spacing (Cell Size) [μm]: {tuple(spacing)}")
|
|
658
|
-
|
|
659
|
-
def iterate(self) -> Generator[Field, None, None]:
|
|
660
|
-
"""Iterate over fields in the file.
|
|
661
|
-
|
|
726
|
+
key (Union[int, list[int]], optional): Key to a field index or a list of field
|
|
727
|
+
indices. Defaults to `None`, which reads all fields.
|
|
728
|
+
threads (int, optional): Number of threads to use for reading compressed files.
|
|
729
|
+
Defaults to `None`, which uses up to 4 threads.
|
|
662
730
|
Returns:
|
|
663
|
-
|
|
731
|
+
Union[Field, Series]: Field or series of fields.
|
|
664
732
|
"""
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
733
|
+
if key is None:
|
|
734
|
+
return self._read_series()
|
|
735
|
+
if isinstance(key, int):
|
|
736
|
+
return self._read_field(key)
|
|
737
|
+
if isinstance(key, list):
|
|
738
|
+
return self._read_series(key)
|
|
739
|
+
raise TypeError("Invalid argument type")
|
|
670
740
|
|
|
671
|
-
|
|
672
|
-
|
|
741
|
+
def close(self):
|
|
742
|
+
"""Close the file."""
|
|
743
|
+
if not self._file:
|
|
744
|
+
return
|
|
673
745
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
746
|
+
if self._compressed:
|
|
747
|
+
start = t()
|
|
748
|
+
_debug("Exporting index file...")
|
|
749
|
+
_try_export_index(self._file, _index_filename(self._filename))
|
|
750
|
+
_debug(f"Index file exported in {t() - start:.2f} seconds.")
|
|
678
751
|
|
|
679
|
-
|
|
680
|
-
|
|
752
|
+
self._file.close()
|
|
753
|
+
self._file = None
|
|
681
754
|
|
|
682
|
-
def read(
|
|
683
|
-
self, key: Optional[Union[int, slice, list, Callable[[Field], bool]]] = None
|
|
684
|
-
) -> Series:
|
|
685
|
-
"""Read a series of fields from the file.
|
|
686
755
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
field IDs, a slice object, a list of field IDs, or a function that filters
|
|
690
|
-
fields. Defaults to `None`.
|
|
756
|
+
def open(filename: str, threads: int = None) -> File:
|
|
757
|
+
"""Open a binary file.
|
|
691
758
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
759
|
+
Args:
|
|
760
|
+
filename (str): Filename of the binary file.
|
|
761
|
+
threads (int, optional): Number of threads to use for reading compressed files.
|
|
762
|
+
Defaults to `None`, which uses up to 4 threads.
|
|
763
|
+
Returns:
|
|
764
|
+
File: Binary file.
|
|
765
|
+
"""
|
|
766
|
+
return File(filename, threads).open()
|
|
695
767
|
|
|
696
|
-
def iterable(iterable):
|
|
697
|
-
if self._verbose:
|
|
698
|
-
return utils.progress_indicator(
|
|
699
|
-
iterable, description="Reading", unit="Field"
|
|
700
|
-
)
|
|
701
|
-
return iterable
|
|
702
768
|
|
|
703
|
-
|
|
769
|
+
def times(filename: str, threads: int = None) -> list[float]:
|
|
770
|
+
"""Get the time steps from a binary file.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
filename (str): Filename of the binary file.
|
|
774
|
+
threads (int, optional): Number of threads to use for reading compressed files.
|
|
775
|
+
Defaults to `None`, which uses up to 4 threads.
|
|
776
|
+
Returns:
|
|
777
|
+
list[float]: List of time steps.
|
|
778
|
+
"""
|
|
704
779
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
elif isinstance(key, int):
|
|
708
|
-
fields = [self.read_field(key)]
|
|
709
|
-
elif isinstance(key, slice):
|
|
710
|
-
indices = range(*key.indices(len(self._index)))
|
|
711
|
-
fields = [self.read_field(i) for i in iterable(indices)]
|
|
712
|
-
elif isinstance(key, list):
|
|
713
|
-
fields = [self.read_field(i) for i in iterable(key)]
|
|
714
|
-
elif isinstance(key, Callable):
|
|
715
|
-
fields = [field for field in iterable(self.iterate()) if key(field)]
|
|
716
|
-
else:
|
|
717
|
-
raise TypeError("Invalid argument type")
|
|
718
|
-
return Series(fields)
|
|
780
|
+
with open(filename, threads) as file:
|
|
781
|
+
return file.times()
|
|
719
782
|
|
|
783
|
+
@overload
|
|
784
|
+
def read(filename: str, key: int, threads: int = None) -> Field: ...
|
|
785
|
+
@overload
|
|
786
|
+
def read(filename: str, key: list[int], threads: int = None) -> Series: ...
|
|
787
|
+
@overload
|
|
788
|
+
def read(filename: str, key: None = None, threads: int = None) -> Series: ...
|
|
720
789
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
"""Arguments for plotting a field.
|
|
790
|
+
def read(filename: str, key: Union[int, list[int]] = None, threads: int = None) -> Union[Field, Series]:
|
|
791
|
+
"""Read a field or series from a binary file.
|
|
724
792
|
|
|
725
793
|
Args:
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
cax (matplotlib.Axes, optional): Axes of the color bar. Defaults to `None`.
|
|
734
|
-
vmin (float, optional): Minimum value of the color bar. Defaults to `None`.
|
|
735
|
-
vmax (float, optional): Maximum value of the color bar. Defaults to `None`.
|
|
736
|
-
cmap (str, optional): Colormap. Defaults to `micpy`.
|
|
737
|
-
alpha (float, optional): Transparency of the plot. Defaults to `1.0`.
|
|
738
|
-
interpolation (str, optional): Interpolation method. Defaults to `none`.
|
|
794
|
+
filename (str): Filename of the binary file.
|
|
795
|
+
key (Union[int, list[int]], optional): Key to a field index or a list of field
|
|
796
|
+
indices. Defaults to `None`, which reads all fields.
|
|
797
|
+
threads (int, optional): Number of threads to use for reading compressed files.
|
|
798
|
+
Defaults to `None`, which uses up to 4 threads.
|
|
799
|
+
Returns:
|
|
800
|
+
Union[Field, Series]: Field or series of fields.
|
|
739
801
|
"""
|
|
802
|
+
with open(filename, threads) as file:
|
|
803
|
+
return file.read(key)
|
|
740
804
|
|
|
741
|
-
title: Optional[str] = None
|
|
742
|
-
xlabel: Optional[str] = None
|
|
743
|
-
ylabel: Optional[str] = None
|
|
744
|
-
figsize: Optional[Tuple[float, float]] = None
|
|
745
|
-
dpi: Optional[int] = None
|
|
746
|
-
aspect: str = "equal"
|
|
747
|
-
ax: "matplotlib.Axes" = None
|
|
748
|
-
cax: "matplotlib.Axes" = None
|
|
749
|
-
vmin: Optional[float] = None
|
|
750
|
-
vmax: Optional[float] = None
|
|
751
|
-
cmap: str = "micpy"
|
|
752
|
-
alpha: float = 1.0
|
|
753
|
-
interpolation: str = "none"
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
def plot(
|
|
757
|
-
field: Field,
|
|
758
|
-
axis: str = "y",
|
|
759
|
-
index: int = 0,
|
|
760
|
-
args: Optional[PlotArgs] = None,
|
|
761
|
-
) -> Tuple["matplotlib.Figure", "matplotlib.Axes"]:
|
|
762
|
-
"""Plot a slice of the field.
|
|
763
805
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
axis (str, optional): Axis to plot. Possible values are `x`, `y`, and `z`.
|
|
767
|
-
Defaults to `y`.
|
|
768
|
-
index (int, optional): Index of the slice. Defaults to `0`.
|
|
769
|
-
args (PlotArgs, optional): Arguments for plotting. Defaults to `None`.
|
|
806
|
+
def pv_plugin_path() -> str:
|
|
807
|
+
"""Get the path to the ParaView plugin directory for MicPy.
|
|
770
808
|
|
|
771
809
|
Returns:
|
|
772
|
-
|
|
810
|
+
str: Path to the ParaView plugin directory for MicPy.
|
|
773
811
|
"""
|
|
774
|
-
|
|
775
|
-
if matplotlib is None:
|
|
776
|
-
raise ImportError("matplotlib is not installed")
|
|
777
|
-
|
|
778
|
-
if axis not in ["x", "y", "z"]:
|
|
779
|
-
raise ValueError("Invalid axis")
|
|
780
|
-
|
|
781
|
-
if field.ndim != 3:
|
|
782
|
-
raise ValueError("Invalid field shape")
|
|
783
|
-
|
|
784
|
-
if args is None:
|
|
785
|
-
args = PlotArgs()
|
|
786
|
-
|
|
787
|
-
if axis == "z":
|
|
788
|
-
x, y = "x", "y"
|
|
789
|
-
slice_2d = field[index, :, :]
|
|
790
|
-
elif axis == "y":
|
|
791
|
-
x, y = "x", "z"
|
|
792
|
-
slice_2d = field[:, index, :]
|
|
793
|
-
elif axis == "x":
|
|
794
|
-
x, y = "y", "z"
|
|
795
|
-
slice_2d = field[:, :, index]
|
|
796
|
-
|
|
797
|
-
fig, ax = (
|
|
798
|
-
pyplot.subplots(figsize=args.figsize, dpi=args.dpi)
|
|
799
|
-
if args.ax is None
|
|
800
|
-
else (args.ax.get_figure(), args.ax)
|
|
801
|
-
)
|
|
802
|
-
|
|
803
|
-
if args.title is not None:
|
|
804
|
-
ax.set_title(args.title)
|
|
805
|
-
else:
|
|
806
|
-
if isinstance(field, Field):
|
|
807
|
-
ax.set_title(f"t={np.round(field.time, 7)}s")
|
|
808
|
-
if args.xlabel is not None:
|
|
809
|
-
ax.set_xlabel(args.xlabel)
|
|
810
|
-
else:
|
|
811
|
-
ax.set_xlabel(x)
|
|
812
|
-
if args.ylabel is not None:
|
|
813
|
-
ax.set_ylabel(args.ylabel)
|
|
814
|
-
else:
|
|
815
|
-
ax.set_ylabel(y)
|
|
816
|
-
if args.aspect is not None:
|
|
817
|
-
ax.set_aspect(args.aspect)
|
|
818
|
-
ax.set_frame_on(False)
|
|
819
|
-
|
|
820
|
-
image = ax.imshow(
|
|
821
|
-
slice_2d,
|
|
822
|
-
cmap=args.cmap,
|
|
823
|
-
vmin=args.vmin,
|
|
824
|
-
vmax=args.vmax,
|
|
825
|
-
interpolation=args.interpolation,
|
|
826
|
-
alpha=args.alpha,
|
|
827
|
-
origin="lower",
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
bar = pyplot.colorbar(image, ax=ax, cax=args.cax)
|
|
831
|
-
bar.locator = matplotlib.ticker.MaxNLocator(
|
|
832
|
-
integer=np.issubdtype(slice_2d.dtype, np.integer)
|
|
833
|
-
)
|
|
834
|
-
bar.outline.set_visible(False)
|
|
835
|
-
bar.update_ticks()
|
|
836
|
-
|
|
837
|
-
return fig, ax, bar
|
|
812
|
+
return str(Path(__file__).parent / "paraview")
|