-
Notifications
You must be signed in to change notification settings - Fork 1
/
_binary_readers_writers.py
417 lines (295 loc) · 12.9 KB
/
_binary_readers_writers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
"""
pywopwop - https://github.com/fchirono/pywopwop
Collection of convenience routines to parse and create PSU-WOPWOP input
files version 1.0.
--> PSU-WOPWOP file readers and writers
Author:
Fabio Casagrande Hirono
Nov 2022
"""
import numpy as np
import struct
from pywopwop._consts_and_dicts import ENDIANNESS, VALUE_LENGTH, IS_SIGNED
# #############################################################################
# %% PSU-WOPWOP initial file check for 'magic number' and endianness
# #############################################################################
def initial_check(filename):
"""
Check the first 4 bytes of a file for the 'magic number' and return the
file endianness. If the 'magic number' is not found, the file is probably
not a PSU-WOPWOP file and an error is raised.
"""
endianness_flag = 'little'
# read first four bytes to check for 'magic number' 42 and endianness
with open(filename, 'rb') as file:
bytes_data = file.read(4)
# if data is 42 in little endian, continue
if bytes_data == b'*\x00\x00\x00':
print('Magic number is correct - file {} is little endian\n'.format(filename))
# if data is 42 in little endian, change flag and continue
elif bytes_data == b'\x00\x00\x00*':
endianness_flag = 'big'
print('Magic number is correct - file {} is big endian\n'.format(filename))
# if magic number is incorrect, it's probably not PSU-WOPWOP file!
else:
raise ValueError('Magic number is incorrect - file {} is probably not a PSU-WOPWOP patch file v1.0!'.format(filename))
return endianness_flag
# #############################################################################
# %% PSU-WOPWOP Plot3D-like block readers and writers
# #############################################################################
def read_block(bytes_data, start_index, num_dims, iMax, jMax):
"""
Reads a block of data in PLOT3D-like format from a binary file.
The block of data is a (num_dims, iMax, jMax)-shaped array of float32
values. It can be a block of 3D data (x,y,z) if 'num_dims=3', 2D
data (x,y) if 'num_dims=2', or one-dimensional data (x) if 'num_dims=1'.
Parameters
----------
bytes_data : file object
File object obtained from calling 'open(filename)'.
start_index : int
Index indicating initial position of XYZ block within binary file.
num_dims : int
Number of dimensions to be read - e.g. '3' for XYZ, '2' for XY, or
'1' for surface pressure data
iMax : int
First dimension of the data block
jMax : int
Second dimension of the data block
Returns
-------
block_data : (num_dims, iMax, jMax) array_like
Numpy array containing the block data using float32 numbers.
next_index : int
Index for the next data field in the binary file.
Notes
-----
The data in the file is assumed to be organized in Fortran order:
*************************************************************************
-Dim 0 (i.e X):
X(i=0, j=0), X(i=1, j=0), ... X(i=iMax, j=0)
X(i=0, j=1), ... ... X(i=iMax, j=1)
... ...
X(i=0, j=jMax) ... X(i=iMax, j=jMax)
*************************************************************************
-Dim 1 (i.e. Y):
Y(i=0, j=0), Y(i=1, j=0), ... Y(i=iMax, j=0)
Y(i=1, j=0), ...
...etc...
*************************************************************************
"""
# read surface pressures - i.e. (iMax, jMax)-shaped
if num_dims == 1:
block_data = np.zeros((iMax, jMax), dtype=np.float32)
for j in range(jMax):
for i in range(iMax):
# fields are (i,j) order, hence j*iMax + i
current_index = (start_index
+ ((j*iMax + i)*VALUE_LENGTH))
block_data[i, j], _ = read_float(bytes_data, current_index)
# *************************************************************************
# read XYZ or XY - i.e. (num_dims, iMax, jMax)-shaped
else:
block_data = np.zeros((num_dims, iMax, jMax), dtype=np.float32)
for n in range(num_dims):
for j in range(jMax):
for i in range(iMax):
# fields are (i,j) order, hence j*iMax + i
current_index = (start_index
+ ((n*iMax*jMax + j*iMax + i)
* VALUE_LENGTH))
block_data[n, i, j], _ = read_float(bytes_data, current_index)
# *************************************************************************
# increase start index by ('value_length' bytes * num_dims coords * iMax * jMax)
next_index = start_index + VALUE_LENGTH*(num_dims*iMax*jMax)
# *************************************************************************
return block_data, next_index
# #############################################################################
def write_block(file, block_data):
"""
Writes a block of data in PLOT3D-like format to a binary file.
The block of data is a (num_dims, iMax, jMax)-shaped array of float32
values, and the write order is Fortran (column-major).
Parameters
----------
file : file object
File object obtained from calling 'open(some_filename)'.
block_data : (iMax, jMax) or (num_dims, iMax, jMax) array_like
Numpy array containing the data to be written. Data can be integer
or floating-point.
Returns
-------
None
Notes
-----
See documentation for 'pywopwop.write_binary' function for more info on
supported data types.
"""
# if block_data is (iMax, jMax)-shaped, reshape to have 3 dims
if block_data.ndim == 2:
block_data = block_data[np.newaxis, :, :]
num_dims, iMax, jMax = block_data.shape
# write block data
for n in range(num_dims):
for j in range(jMax):
for i in range(iMax):
write_binary(file, block_data[n, i, j])
# #############################################################################
def read_IBLANKblock(bytes_data, start_index, iMax, jMax):
"""
Reads a block of IBLANK data in PLOT3D format from a binary file.
The block of data is a (iMax, jMax)-shaped array of int32 values.
Parameters
----------
bytes_data : file object
File object obtained from calling 'open(filename)'.
start_index : int
Index indicating initial position of XYZ block within binary file.
iMax : int
First dimension of the data block
jMax : int
Second dimension of the data block
Returns
-------
IBLANK_data : (iMax, jMax) array_like
Numpy array containing IBLANK data as int32 numbers.
next_index : int
Index for the next data field in the binary file.
Notes
-----
The data in the file is assumed to be organized as:
**************************************************************************
X(i=0, j=0), X(i=1, j=0), ... X(i=iMax, j=0)
X(i=0, j=1), ... ... X(i=iMax, j=1)
... ...
X(i=0, j=jMax) ... X(i=iMax, j=jMax)
**************************************************************************
"""
IBLANK_data = np.zeros((iMax, jMax), dtype=np.int32)
for j in range(jMax):
for i in range(iMax):
# fields are (i,j) order, hence j*iMax + i
current_index = (start_index
+ (j*iMax + i)*VALUE_LENGTH)
IBLANK_data[i, j] = read_int(bytes_data, current_index)
# increase start index by ('value_length' bytes * iMax * jMax)
next_index = start_index + VALUE_LENGTH*(iMax*jMax)
return IBLANK_data, next_index
# #############################################################################
# %% PSU-WOPWOP string reader / writer
# #############################################################################
def write_string(file, string, max_length):
"""
Writes a ASCII-compatible string to an open binary file object, up to a
maximum length. If string is shorter than 'max_length', pad with spaces.
"""
# check string is ASCII compatible
ascii_error = 'String is not ASCII compatible!'
assert string[:max_length].isascii(), ascii_error
# check string has length 'max_length', pad with spaces otherwise
if len(string) < max_length:
string += (max_length-len(string))*' '
file.write(string[:max_length].encode('ascii'))
# #############################################################################
def read_string(obj_name, start_index, len_string):
"""
Reads strings of arbitrary length from binary file object.
"""
mystring = ''
for i in range(len_string):
mystring += chr(read_int(obj_name, start_index + i, 1))
return mystring
# #############################################################################
# %% PSU-WOPWOP int and float reader / writer
# #############################################################################
def read_int(obj_name, start_index, n_bytes=VALUE_LENGTH,
endianness_flag=ENDIANNESS):
"""
Reads one integer value from an open file and returns the unpacked value.
Parameters
----------
obj_name : bytes_object
Object containing binary data, such as an open file object.
start_index : int
Starting index of value to be read.
n_bytes : {1, 2, 4, 8}
Size of the integer to be read, in bytes. The default is the constant
'VALUE_LENGTH' = 4.
endianness_flag : {'little', 'big'}, optional
String indicating the byte endinaness to be used. The default is
the constant 'ENDIANNESS' = 'little'.
Returns
-------
out : int
Integer value unpacked from file data.
"""
n_bytes_dict = {1:'b', 2:'h', 4:'i', 8:'q'}
endianness_dict = {'little':'<', 'big':'>'}
return struct.unpack(endianness_dict[endianness_flag] + n_bytes_dict[n_bytes],
obj_name[start_index:start_index + n_bytes])[0]
# #############################################################################
def read_float(obj_name, start_index, n_bytes=VALUE_LENGTH,
endianness_flag=ENDIANNESS):
"""
Reads one float value from an open file, and returns the unpacked value and
the starting index of the next value.
Parameters
----------
obj_name : bytes_object
Object containing binary data, such as an open file object.
start_index : int
Starting index of value to be read.
n_bytes : {4, 8}
Size of the float to be read, in bytes.
endianness_flag : {'little', 'big'}, optional
String indicating the byte endinaness to be used. The default is
'little'.
Returns
-------
out : float
Float value unpacked from file data.
next_index : int
Starting index of the next value to be read, in bytes.
"""
n_bytes_dict = {4:'f', 8:'d'}
endianness_dict = {'little':'<', 'big':'>'}
next_index = start_index + n_bytes
float_value = struct.unpack(endianness_dict[endianness_flag]
+ n_bytes_dict[n_bytes],
obj_name[start_index : next_index])[0]
return float_value, next_index
# #############################################################################
def write_binary(file, data, length=VALUE_LENGTH,
endianness_flag=ENDIANNESS, is_signed=IS_SIGNED):
"""
Writes one value of data to an open binary file.
Parameters
----------
file : file object
File object obtained from calling 'open(filename)'.
data : int or float
Value to be written to file. Must be int or float.
length : {1, 2, 4, 8}, optional
Byte length of the value to be written. The default is 4 bytes.
endianness_flag : {'little', 'big'}, optional
String indicating the byte endinaness to be used. The default is
'little'.
is_signed : boolean, optional
Flag indicating whether the values are signed (True) or unsigned
(False). The default is True.
Returns
-------
None.
Notes
-----
Data value to be written must be integer or floating point.
Floating point data can only accept lengths of 4 and 8 bytes.
"""
if type(data) is int:
file.write(data.to_bytes(length, endianness_flag, signed=is_signed))
# if data is python float or numpy float, write to file as float
elif (isinstance(data, (float, np.floating))):
endianness = {'little':'<', 'big':'>'}
floatlen = {4:'f', 8:'d'}
format_string = endianness[endianness_flag] + floatlen[length]
file.write(struct.pack(format_string, data))