wavesimpro 0.9.0__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.
- wavesimpro/__init__.py +98 -0
- wavesimpro/__main__.py +29 -0
- wavesimpro/binary_utils.py +936 -0
- wavesimpro/client.py +1475 -0
- wavesimpro/config.py +123 -0
- wavesimpro/exceptions.py +40 -0
- wavesimpro/execute.py +461 -0
- wavesimpro/json_helper.py +755 -0
- wavesimpro/py.typed +4 -0
- wavesimpro/setup.py +162 -0
- wavesimpro/simulate.py +162 -0
- wavesimpro/validate.py +251 -0
- wavesimpro-0.9.0.dist-info/METADATA +21 -0
- wavesimpro-0.9.0.dist-info/RECORD +16 -0
- wavesimpro-0.9.0.dist-info/WHEEL +5 -0
- wavesimpro-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Binary data utilities for uploading/downloading numpy arrays
|
|
3
|
+
|
|
4
|
+
This module provides utilities to convert numpy arrays to/from binary files
|
|
5
|
+
for use with VoxelsModel primitives and CustomField sources.
|
|
6
|
+
|
|
7
|
+
Also provides RMON (Rayfos MONitor) format reader/writer for self-describing
|
|
8
|
+
monitor binary files.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import gzip
|
|
12
|
+
import struct
|
|
13
|
+
import numpy as np
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Tuple, Union, Optional, Dict, Any
|
|
16
|
+
import tempfile
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# RMON format constants
|
|
22
|
+
RMON_MAGIC = b'RMON'
|
|
23
|
+
RMON_VERSION = 3
|
|
24
|
+
RMON_FIXED_HEADER_SIZE = 64
|
|
25
|
+
|
|
26
|
+
# RMON data type codes
|
|
27
|
+
RMON_DATATYPE_COMPLEX64 = 0
|
|
28
|
+
RMON_DATATYPE_FLOAT32 = 1
|
|
29
|
+
RMON_DATATYPE_FLOAT64 = 2
|
|
30
|
+
RMON_DATATYPE_COMPLEX128 = 3
|
|
31
|
+
|
|
32
|
+
# RMON monitor type codes
|
|
33
|
+
RMON_MONITOR_PERMITTIVITY = 0
|
|
34
|
+
RMON_MONITOR_FIELD = 1
|
|
35
|
+
RMON_MONITOR_FLUX = 2
|
|
36
|
+
RMON_MONITOR_DIFFRACTION = 3
|
|
37
|
+
|
|
38
|
+
# RMON plane codes
|
|
39
|
+
RMON_PLANE_XY = 0
|
|
40
|
+
RMON_PLANE_YZ = 1
|
|
41
|
+
RMON_PLANE_XZ = 2
|
|
42
|
+
RMON_PLANE_VOLUME = 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VoxelDataFormat:
|
|
46
|
+
"""Binary data format specifications"""
|
|
47
|
+
FLOAT32 = "Float32"
|
|
48
|
+
FLOAT64 = "Float64"
|
|
49
|
+
COMPLEX64 = "Complex64"
|
|
50
|
+
COMPLEX128 = "Complex128"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def upload_array_as_binary(
|
|
54
|
+
client,
|
|
55
|
+
project_id: str,
|
|
56
|
+
version_id: str,
|
|
57
|
+
data_array: np.ndarray,
|
|
58
|
+
filename: str,
|
|
59
|
+
data_type: str = "float32",
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Upload a numpy array as a binary file
|
|
63
|
+
|
|
64
|
+
This function converts a numpy array to a binary file and uploads it
|
|
65
|
+
to a project version. Useful for uploading refractive index distributions
|
|
66
|
+
or field data for custom primitives and sources.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
client: RayfosClient instance
|
|
70
|
+
project_id: Project UUID
|
|
71
|
+
version_id: Version UUID
|
|
72
|
+
data_array: Numpy array of any dimensions
|
|
73
|
+
filename: Desired filename for upload
|
|
74
|
+
data_type: Data type - 'float32', 'float64', 'complex64', 'complex128'
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
dict: Upload result with keys:
|
|
78
|
+
- fileId or id or blobName: File identifier
|
|
79
|
+
- dataFormat: VoxelDataFormat string
|
|
80
|
+
- dimensions: Shape of the uploaded array
|
|
81
|
+
- originalDataType: The data_type parameter
|
|
82
|
+
|
|
83
|
+
Binary File Format:
|
|
84
|
+
Real data: Values written sequentially in C-order (row-major)
|
|
85
|
+
Complex data: Values written as interleaved [real1, imag1, real2, imag2, ...]
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
# Upload refractive index array
|
|
89
|
+
import numpy as np
|
|
90
|
+
w = np.ones((50, 40, 200), dtype=np.float32)
|
|
91
|
+
file_info = upload_array_as_binary(client, proj_id, ver_id, w, 'ri_data.bin', 'float32')
|
|
92
|
+
|
|
93
|
+
# Upload complex field data
|
|
94
|
+
Es = np.random.rand(50, 40, 1) + 1j * np.random.rand(50, 40, 1)
|
|
95
|
+
file_info = upload_array_as_binary(client, proj_id, ver_id, Es, 'source_field.bin', 'complex64')
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Validate inputs
|
|
99
|
+
if not isinstance(data_array, np.ndarray):
|
|
100
|
+
raise TypeError(f"data_array must be a numpy array, got {type(data_array)}")
|
|
101
|
+
|
|
102
|
+
if data_array.size == 0:
|
|
103
|
+
raise ValueError("data_array cannot be empty")
|
|
104
|
+
|
|
105
|
+
# Store original dimensions
|
|
106
|
+
original_shape = data_array.shape
|
|
107
|
+
|
|
108
|
+
# Determine data format and prepare data
|
|
109
|
+
data_type_lower = data_type.lower()
|
|
110
|
+
|
|
111
|
+
if data_type_lower == 'float32':
|
|
112
|
+
format_str = VoxelDataFormat.FLOAT32
|
|
113
|
+
data = data_array.astype(np.float32).flatten(order='C')
|
|
114
|
+
dtype_write = np.float32
|
|
115
|
+
|
|
116
|
+
elif data_type_lower == 'float64':
|
|
117
|
+
format_str = VoxelDataFormat.FLOAT64
|
|
118
|
+
data = data_array.astype(np.float64).flatten(order='C')
|
|
119
|
+
dtype_write = np.float64
|
|
120
|
+
|
|
121
|
+
elif data_type_lower == 'complex64':
|
|
122
|
+
format_str = VoxelDataFormat.COMPLEX64
|
|
123
|
+
# Interleave real and imaginary parts
|
|
124
|
+
data_flat = data_array.astype(np.complex64).flatten(order='C')
|
|
125
|
+
data = np.empty(data_flat.size * 2, dtype=np.float32)
|
|
126
|
+
data[0::2] = data_flat.real
|
|
127
|
+
data[1::2] = data_flat.imag
|
|
128
|
+
dtype_write = np.float32
|
|
129
|
+
|
|
130
|
+
elif data_type_lower == 'complex128':
|
|
131
|
+
format_str = VoxelDataFormat.COMPLEX128
|
|
132
|
+
# Interleave real and imaginary parts
|
|
133
|
+
data_flat = data_array.astype(np.complex128).flatten(order='C')
|
|
134
|
+
data = np.empty(data_flat.size * 2, dtype=np.float64)
|
|
135
|
+
data[0::2] = data_flat.real
|
|
136
|
+
data[1::2] = data_flat.imag
|
|
137
|
+
dtype_write = np.float64
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Unknown data type: {data_type}. "
|
|
142
|
+
f"Valid options: 'float32', 'float64', 'complex64', 'complex128'"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Create temporary file
|
|
146
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.bin', delete=False) as tmp_file:
|
|
147
|
+
temp_path = tmp_file.name
|
|
148
|
+
|
|
149
|
+
# Write header (20 bytes total):
|
|
150
|
+
# - Version (int32, 4 bytes) = 1
|
|
151
|
+
# - DataTypeCode (int32, 4 bytes) = format index
|
|
152
|
+
# - Nx (int32, 4 bytes)
|
|
153
|
+
# - Ny (int32, 4 bytes)
|
|
154
|
+
# - Nz (int32, 4 bytes)
|
|
155
|
+
import struct
|
|
156
|
+
|
|
157
|
+
version = 1
|
|
158
|
+
# Map data format to code: 0=Complex64, 1=Float32, 2=Float64, 3=Complex128
|
|
159
|
+
format_code_map = {
|
|
160
|
+
VoxelDataFormat.FLOAT32: 1,
|
|
161
|
+
VoxelDataFormat.FLOAT64: 2,
|
|
162
|
+
VoxelDataFormat.COMPLEX64: 0,
|
|
163
|
+
VoxelDataFormat.COMPLEX128: 3
|
|
164
|
+
}
|
|
165
|
+
data_type_code = format_code_map.get(format_str, 0)
|
|
166
|
+
|
|
167
|
+
# Pad dimensions to 3D if needed
|
|
168
|
+
shape_3d = list(original_shape)
|
|
169
|
+
while len(shape_3d) < 3:
|
|
170
|
+
shape_3d.append(1)
|
|
171
|
+
nx, ny, nz = shape_3d[0], shape_3d[1], shape_3d[2]
|
|
172
|
+
|
|
173
|
+
# Write header
|
|
174
|
+
header = struct.pack('<5i', version, data_type_code, nx, ny, nz)
|
|
175
|
+
tmp_file.write(header)
|
|
176
|
+
|
|
177
|
+
# Write data
|
|
178
|
+
data.tofile(tmp_file)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Upload file
|
|
182
|
+
file_info = client.upload_file(project_id, version_id, temp_path, 'input')
|
|
183
|
+
|
|
184
|
+
# Add metadata
|
|
185
|
+
file_info['dataFormat'] = format_str
|
|
186
|
+
file_info['dimensions'] = list(original_shape)
|
|
187
|
+
file_info['originalDataType'] = data_type
|
|
188
|
+
|
|
189
|
+
logger.info(
|
|
190
|
+
f"✓ Uploaded array as {format_str} ({filename}): {original_shape}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return file_info
|
|
194
|
+
|
|
195
|
+
finally:
|
|
196
|
+
# Clean up temporary file
|
|
197
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_binary_field(
|
|
201
|
+
file_path_or_data: Union[str, Path, bytes],
|
|
202
|
+
dimensions: Optional[Tuple[int, ...]] = None,
|
|
203
|
+
data_format: str = 'Complex64'
|
|
204
|
+
) -> np.ndarray:
|
|
205
|
+
"""
|
|
206
|
+
Read binary field data from wavesim output (file or memory)
|
|
207
|
+
|
|
208
|
+
This function reads binary field data files produced by wavesim simulations
|
|
209
|
+
and reconstructs the numpy array with proper dimensions. Supports reading from
|
|
210
|
+
both file paths and in-memory bytes data.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
file_path_or_data: Path to binary file or bytes data
|
|
214
|
+
dimensions: Array dimensions (nx, ny, nz) or (nx, ny, nz, n_components)
|
|
215
|
+
data_format: Data format - 'Float32', 'Float64', 'Complex64', 'Complex128'
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
np.ndarray: Field data with specified dimensions
|
|
219
|
+
|
|
220
|
+
Binary File Format:
|
|
221
|
+
Version 1 Header (20 bytes):
|
|
222
|
+
- Version (int32): 1
|
|
223
|
+
- DataTypeCode (int32): 0=Complex64, 1=Float32, 2=Float64, 3=Complex128
|
|
224
|
+
- Nx, Ny, Nz (3x int32)
|
|
225
|
+
Version 2 Header (24 bytes):
|
|
226
|
+
- Version (int32): 2
|
|
227
|
+
- DataTypeCode (int32): 0=Complex64, 1=Float32, 2=Float64, 3=Complex128
|
|
228
|
+
- nPol (int32): Number of polarization components (1=scalar, 3=vectorial)
|
|
229
|
+
- Nx, Ny, Nz (3x int32)
|
|
230
|
+
Data:
|
|
231
|
+
- Real data: Values stored sequentially
|
|
232
|
+
- Complex data: Interleaved [real1, imag1, real2, imag2, ...]
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
# Read from file
|
|
236
|
+
E = read_binary_field('output.bin', (50, 40, 200), 'Complex64')
|
|
237
|
+
|
|
238
|
+
# Read from memory
|
|
239
|
+
data_bytes = client.download_file_data(project_id, version_id, file_id)
|
|
240
|
+
E = read_binary_field(data_bytes, (50, 40, 200), 'Complex64')
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
# Handle input: file path or bytes
|
|
244
|
+
if isinstance(file_path_or_data, bytes):
|
|
245
|
+
# Read from memory
|
|
246
|
+
file_data = file_path_or_data
|
|
247
|
+
file_size = len(file_data)
|
|
248
|
+
else:
|
|
249
|
+
# Read from file
|
|
250
|
+
file_path = Path(file_path_or_data)
|
|
251
|
+
if not file_path.exists():
|
|
252
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
253
|
+
file_size = file_path.stat().st_size
|
|
254
|
+
with open(file_path, 'rb') as f:
|
|
255
|
+
file_data = f.read()
|
|
256
|
+
|
|
257
|
+
# Validate inputs
|
|
258
|
+
if not dimensions:
|
|
259
|
+
raise ValueError("dimensions must be a non-empty tuple")
|
|
260
|
+
|
|
261
|
+
# Try to read header
|
|
262
|
+
# Version 1: 20 bytes (5 int32s: version, dataType, nx, ny, nz)
|
|
263
|
+
# Version 2: 24 bytes (6 int32s: version, dataType, nPol, nx, ny, nz)
|
|
264
|
+
has_header = file_size >= 20
|
|
265
|
+
version = 2 # Default to version 2 (solver output)
|
|
266
|
+
n_pol_from_header = 1
|
|
267
|
+
header_dims = None
|
|
268
|
+
|
|
269
|
+
if has_header:
|
|
270
|
+
try:
|
|
271
|
+
# Read first int32 to check version
|
|
272
|
+
ver_check = struct.unpack('<i', file_data[:4])[0]
|
|
273
|
+
|
|
274
|
+
if ver_check == 1:
|
|
275
|
+
# Version 1: 20-byte header [version, dataType, nx, ny, nz]
|
|
276
|
+
header = struct.unpack('<5i', file_data[:20])
|
|
277
|
+
version, data_type_code, nx_file, ny_file, nz_file = header
|
|
278
|
+
n_pol_from_header = 1
|
|
279
|
+
header_dims = (nx_file, ny_file, nz_file)
|
|
280
|
+
logger.info(f"Found binary header (version 1): dims=[{nx_file}, {ny_file}, {nz_file}]")
|
|
281
|
+
file_data = file_data[20:]
|
|
282
|
+
file_size -= 20
|
|
283
|
+
elif ver_check == 2:
|
|
284
|
+
# Version 2: 24-byte header [version, dataType, nPol, nx, ny, nz]
|
|
285
|
+
header = struct.unpack('<6i', file_data[:24])
|
|
286
|
+
version, data_type_code, n_pol_from_header, nx_file, ny_file, nz_file = header
|
|
287
|
+
if n_pol_from_header > 1:
|
|
288
|
+
header_dims = (n_pol_from_header, nx_file, ny_file, nz_file)
|
|
289
|
+
else:
|
|
290
|
+
header_dims = (nx_file, ny_file, nz_file)
|
|
291
|
+
logger.info(f"Found binary header (version 2): nPol={n_pol_from_header}, dims=[{nx_file}, {ny_file}, {nz_file}]")
|
|
292
|
+
file_data = file_data[24:]
|
|
293
|
+
file_size -= 24
|
|
294
|
+
else:
|
|
295
|
+
# Not a valid header, treat as raw data (version 2)
|
|
296
|
+
logger.info("No valid header found, assuming version 2 (raw data)")
|
|
297
|
+
version = 2
|
|
298
|
+
except struct.error:
|
|
299
|
+
# Failed to unpack header, treat as raw data
|
|
300
|
+
logger.info("Failed to read header, assuming version 2 (raw data)")
|
|
301
|
+
version = 2
|
|
302
|
+
else:
|
|
303
|
+
logger.info("File too small for header, assuming version 2 (raw data)")
|
|
304
|
+
version = 2
|
|
305
|
+
|
|
306
|
+
# Use header dimensions if caller didn't provide them
|
|
307
|
+
if dimensions is None or len(dimensions) == 0:
|
|
308
|
+
if header_dims is not None:
|
|
309
|
+
dimensions = header_dims
|
|
310
|
+
logger.info(f"Auto-detected dimensions from header: {dimensions}")
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError("No dimensions provided and no valid header found to auto-detect")
|
|
313
|
+
|
|
314
|
+
# Determine precision and whether data is complex
|
|
315
|
+
if data_format == 'Float32':
|
|
316
|
+
dtype_read = np.float32
|
|
317
|
+
is_complex = False
|
|
318
|
+
bytes_per_value = 4
|
|
319
|
+
|
|
320
|
+
elif data_format == 'Float64':
|
|
321
|
+
dtype_read = np.float64
|
|
322
|
+
is_complex = False
|
|
323
|
+
bytes_per_value = 8
|
|
324
|
+
|
|
325
|
+
elif data_format == 'Complex64':
|
|
326
|
+
dtype_read = np.float32
|
|
327
|
+
is_complex = True
|
|
328
|
+
bytes_per_value = 8 # 4 bytes real + 4 bytes imag
|
|
329
|
+
|
|
330
|
+
elif data_format == 'Complex128':
|
|
331
|
+
dtype_read = np.float64
|
|
332
|
+
is_complex = True
|
|
333
|
+
bytes_per_value = 16 # 8 bytes real + 8 bytes imag
|
|
334
|
+
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"Unknown data format: {data_format}. "
|
|
338
|
+
f"Valid options: 'Float32', 'Float64', 'Complex64', 'Complex128'"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Calculate expected number of elements
|
|
342
|
+
total_elements = np.prod(dimensions)
|
|
343
|
+
expected_size = total_elements * bytes_per_value
|
|
344
|
+
|
|
345
|
+
if file_size != expected_size:
|
|
346
|
+
logger.warning(
|
|
347
|
+
f"File size mismatch!\n"
|
|
348
|
+
f" Expected: {expected_size} bytes ({total_elements} elements × {bytes_per_value} bytes/element)\n"
|
|
349
|
+
f" Actual: {file_size} bytes\n"
|
|
350
|
+
f" Attempting to read anyway..."
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Read data
|
|
354
|
+
if is_complex:
|
|
355
|
+
# Read interleaved complex data
|
|
356
|
+
data = np.frombuffer(file_data, dtype=dtype_read)
|
|
357
|
+
|
|
358
|
+
if data.size < total_elements * 2:
|
|
359
|
+
logger.warning(
|
|
360
|
+
f"Read fewer elements than expected: {data.size} / {total_elements * 2}"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# De-interleave: [r1, i1, r2, i2, ...] -> [r1, r2, ...] + 1j*[i1, i2, ...]
|
|
364
|
+
real_part = data[0::2]
|
|
365
|
+
imag_part = data[1::2]
|
|
366
|
+
field_data = real_part + 1j * imag_part
|
|
367
|
+
|
|
368
|
+
else:
|
|
369
|
+
# Read real data
|
|
370
|
+
field_data = np.frombuffer(file_data, dtype=dtype_read)
|
|
371
|
+
|
|
372
|
+
if field_data.size < total_elements:
|
|
373
|
+
logger.warning(
|
|
374
|
+
f"Read fewer elements than expected: {field_data.size} / {total_elements}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Handle dimension permutation based on version
|
|
378
|
+
if version == 1:
|
|
379
|
+
# Version 1: Data was permuted before writing, need to reverse
|
|
380
|
+
reversed_dims = dimensions[::-1]
|
|
381
|
+
field_data = field_data.reshape(reversed_dims, order='C')
|
|
382
|
+
# Reverse permutation
|
|
383
|
+
perm_order = tuple(range(len(dimensions) - 1, -1, -1))
|
|
384
|
+
field_data = np.transpose(field_data, perm_order)
|
|
385
|
+
else:
|
|
386
|
+
# Version 2: Data is already in correct order
|
|
387
|
+
field_data = field_data.reshape(dimensions, order='C')
|
|
388
|
+
|
|
389
|
+
logger.info(
|
|
390
|
+
f"✓ Read {data_format} field data (version {version}): {field_data.shape} ({file_size} bytes)"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return field_data
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def write_array_to_binary(
|
|
397
|
+
data_array: np.ndarray,
|
|
398
|
+
file_path: Union[str, Path],
|
|
399
|
+
data_type: str = "float32"
|
|
400
|
+
) -> Path:
|
|
401
|
+
"""
|
|
402
|
+
Write a numpy array to a binary file
|
|
403
|
+
|
|
404
|
+
Helper function to create binary files locally before uploading.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
data_array: Numpy array to write
|
|
408
|
+
file_path: Path where to save the binary file
|
|
409
|
+
data_type: Data type - 'float32', 'float64', 'complex64', 'complex128'
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Path: Path to the created file
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
import numpy as np
|
|
416
|
+
w = np.ones((50, 40, 200), dtype=np.float32)
|
|
417
|
+
file_path = write_array_to_binary(w, 'ri_data.bin', 'float32')
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
file_path = Path(file_path)
|
|
421
|
+
|
|
422
|
+
# Determine data format and prepare data
|
|
423
|
+
data_type_lower = data_type.lower()
|
|
424
|
+
|
|
425
|
+
if data_type_lower == 'float32':
|
|
426
|
+
data = data_array.astype(np.float32).flatten(order='C')
|
|
427
|
+
|
|
428
|
+
elif data_type_lower == 'float64':
|
|
429
|
+
data = data_array.astype(np.float64).flatten(order='C')
|
|
430
|
+
|
|
431
|
+
elif data_type_lower == 'complex64':
|
|
432
|
+
# Interleave real and imaginary parts
|
|
433
|
+
data_flat = data_array.astype(np.complex64).flatten(order='C')
|
|
434
|
+
data = np.empty(data_flat.size * 2, dtype=np.float32)
|
|
435
|
+
data[0::2] = data_flat.real
|
|
436
|
+
data[1::2] = data_flat.imag
|
|
437
|
+
|
|
438
|
+
elif data_type_lower == 'complex128':
|
|
439
|
+
# Interleave real and imaginary parts
|
|
440
|
+
data_flat = data_array.astype(np.complex128).flatten(order='C')
|
|
441
|
+
data = np.empty(data_flat.size * 2, dtype=np.float64)
|
|
442
|
+
data[0::2] = data_flat.real
|
|
443
|
+
data[1::2] = data_flat.imag
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
f"Unknown data type: {data_type}. "
|
|
448
|
+
f"Valid options: 'float32', 'float64', 'complex64', 'complex128'"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Write to file
|
|
452
|
+
data.tofile(file_path)
|
|
453
|
+
|
|
454
|
+
logger.info(f"✓ Wrote {data_array.shape} array to {file_path} ({file_path.stat().st_size} bytes)")
|
|
455
|
+
|
|
456
|
+
return file_path
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def write_monitor_binary(
|
|
460
|
+
file_path: Union[str, Path],
|
|
461
|
+
data: np.ndarray,
|
|
462
|
+
name: str = "",
|
|
463
|
+
monitor_type: int = RMON_MONITOR_FIELD,
|
|
464
|
+
plane: int = RMON_PLANE_XY,
|
|
465
|
+
field_mask: int = 0,
|
|
466
|
+
voxel_size_um: float = 0.0,
|
|
467
|
+
origin_um: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
468
|
+
wavelength_um: float = 0.0,
|
|
469
|
+
) -> Path:
|
|
470
|
+
"""
|
|
471
|
+
Write a numpy array as an RMON (Rayfos MONitor) self-describing binary file.
|
|
472
|
+
|
|
473
|
+
The RMON format contains a full metadata header (monitor type, plane, voxel size,
|
|
474
|
+
world-space origin, wavelength, etc.) so the file can be interpreted without
|
|
475
|
+
external context.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
file_path: Output file path
|
|
479
|
+
data: Numpy array of shape (nx, ny, nz) or (nComponents, nx, ny, nz).
|
|
480
|
+
Complex arrays are stored as interleaved real/imag pairs.
|
|
481
|
+
name: Monitor name (stored in header)
|
|
482
|
+
monitor_type: RMON_MONITOR_* constant (0=Permittivity, 1=Field, 2=Flux, 3=Diffraction)
|
|
483
|
+
plane: RMON_PLANE_* constant (0=XY, 1=YZ, 2=XZ, 3=Volume)
|
|
484
|
+
field_mask: Bitmask of recorded field components (bit0=Ex, bit1=Ey, etc.)
|
|
485
|
+
voxel_size_um: Voxel size in micrometers
|
|
486
|
+
origin_um: World-space origin (x, y, z) in micrometers
|
|
487
|
+
wavelength_um: Simulation wavelength in micrometers
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Path to the created file
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
import numpy as np
|
|
494
|
+
E = np.random.rand(50, 40, 1) + 1j * np.random.rand(50, 40, 1)
|
|
495
|
+
write_monitor_binary('monitor_Field_0.bin', E, name='Field_0',
|
|
496
|
+
voxel_size_um=0.05, origin_um=(-1.25, -1.0, 0.0))
|
|
497
|
+
"""
|
|
498
|
+
file_path = Path(file_path)
|
|
499
|
+
|
|
500
|
+
# Determine shape and components
|
|
501
|
+
if data.ndim == 4:
|
|
502
|
+
n_components, nx, ny, nz = data.shape
|
|
503
|
+
elif data.ndim == 3:
|
|
504
|
+
n_components = 1
|
|
505
|
+
nx, ny, nz = data.shape
|
|
506
|
+
data = data[np.newaxis, ...] # Add component dimension
|
|
507
|
+
else:
|
|
508
|
+
raise ValueError(f"data must be 3D or 4D, got {data.ndim}D")
|
|
509
|
+
|
|
510
|
+
# Determine data type
|
|
511
|
+
if np.iscomplexobj(data):
|
|
512
|
+
data_type_code = RMON_DATATYPE_COMPLEX64
|
|
513
|
+
write_dtype = np.float32
|
|
514
|
+
else:
|
|
515
|
+
data_type_code = RMON_DATATYPE_FLOAT32
|
|
516
|
+
write_dtype = np.float32
|
|
517
|
+
|
|
518
|
+
# Encode name
|
|
519
|
+
name_bytes = name.encode('utf-8')
|
|
520
|
+
name_length = len(name_bytes)
|
|
521
|
+
|
|
522
|
+
# Calculate total header size (aligned to 4 bytes)
|
|
523
|
+
variable_size = 4 + name_length # NameLength + Name
|
|
524
|
+
total_header = RMON_FIXED_HEADER_SIZE + variable_size
|
|
525
|
+
total_header = (total_header + 3) & ~3 # Align to 4 bytes
|
|
526
|
+
padding = total_header - (RMON_FIXED_HEADER_SIZE + variable_size)
|
|
527
|
+
|
|
528
|
+
with open(file_path, 'wb') as f:
|
|
529
|
+
# Fixed header (64 bytes)
|
|
530
|
+
f.write(RMON_MAGIC) # 0-3: Magic
|
|
531
|
+
f.write(struct.pack('<i', RMON_VERSION)) # 4-7: Version
|
|
532
|
+
f.write(struct.pack('<i', total_header)) # 8-11: HeaderSize
|
|
533
|
+
f.write(struct.pack('<i', data_type_code)) # 12-15: DataType
|
|
534
|
+
f.write(struct.pack('<i', monitor_type)) # 16-19: MonitorType
|
|
535
|
+
f.write(struct.pack('<i', plane)) # 20-23: Plane
|
|
536
|
+
f.write(struct.pack('<i', field_mask)) # 24-27: FieldMask
|
|
537
|
+
f.write(struct.pack('<i', n_components)) # 28-31: nComponents
|
|
538
|
+
f.write(struct.pack('<i', nx)) # 32-35: ShapeX
|
|
539
|
+
f.write(struct.pack('<i', ny)) # 36-39: ShapeY
|
|
540
|
+
f.write(struct.pack('<i', nz)) # 40-43: ShapeZ
|
|
541
|
+
f.write(struct.pack('<f', voxel_size_um)) # 44-47: VoxelSize_um
|
|
542
|
+
f.write(struct.pack('<f', origin_um[0])) # 48-51: OriginX_um
|
|
543
|
+
f.write(struct.pack('<f', origin_um[1])) # 52-55: OriginY_um
|
|
544
|
+
f.write(struct.pack('<f', origin_um[2])) # 56-59: OriginZ_um
|
|
545
|
+
f.write(struct.pack('<f', wavelength_um)) # 60-63: Wavelength_um
|
|
546
|
+
|
|
547
|
+
# Variable header
|
|
548
|
+
f.write(struct.pack('<i', name_length)) # 64-67: NameLength
|
|
549
|
+
f.write(name_bytes) # 68+: Name
|
|
550
|
+
f.write(b'\x00' * padding) # Padding
|
|
551
|
+
|
|
552
|
+
# Data section: component-major, then X -> Y -> Z
|
|
553
|
+
for comp in range(n_components):
|
|
554
|
+
comp_data = data[comp]
|
|
555
|
+
if np.iscomplexobj(comp_data):
|
|
556
|
+
# Interleave real/imag
|
|
557
|
+
flat = comp_data.astype(np.complex64).flatten(order='C')
|
|
558
|
+
interleaved = np.empty(flat.size * 2, dtype=np.float32)
|
|
559
|
+
interleaved[0::2] = flat.real
|
|
560
|
+
interleaved[1::2] = flat.imag
|
|
561
|
+
interleaved.tofile(f)
|
|
562
|
+
else:
|
|
563
|
+
comp_data.astype(write_dtype).flatten(order='C').tofile(f)
|
|
564
|
+
|
|
565
|
+
file_size = file_path.stat().st_size
|
|
566
|
+
logger.info(
|
|
567
|
+
f"✓ Wrote RMON file: {file_path} ({nx}×{ny}×{nz}, {n_components} comp, {file_size} bytes)"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return file_path
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def read_monitor_binary(
|
|
574
|
+
file_path_or_data: Union[str, Path, bytes],
|
|
575
|
+
) -> Tuple[Dict[str, Any], np.ndarray]:
|
|
576
|
+
"""
|
|
577
|
+
Read an RMON (Rayfos MONitor) binary file.
|
|
578
|
+
|
|
579
|
+
Auto-detects RMON format by checking for "RMON" magic bytes. If the data
|
|
580
|
+
is not RMON format, raises ValueError.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
file_path_or_data: Path to binary file or bytes data
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Tuple of (header_dict, data_array):
|
|
587
|
+
- header_dict: Dictionary with all RMON header fields
|
|
588
|
+
- data_array: Numpy array of shape (nComponents, nx, ny, nz).
|
|
589
|
+
Complex data is returned as complex64 dtype.
|
|
590
|
+
|
|
591
|
+
Example:
|
|
592
|
+
header, data = read_monitor_binary('monitor_Field_0.bin')
|
|
593
|
+
print(f"Monitor: {header['name']}, Shape: {data.shape}")
|
|
594
|
+
magnitude = np.abs(data[0]) # First component magnitude
|
|
595
|
+
"""
|
|
596
|
+
# Read data
|
|
597
|
+
if isinstance(file_path_or_data, bytes):
|
|
598
|
+
file_data = file_path_or_data
|
|
599
|
+
else:
|
|
600
|
+
file_path = Path(file_path_or_data)
|
|
601
|
+
if not file_path.exists():
|
|
602
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
603
|
+
with open(file_path, 'rb') as f:
|
|
604
|
+
file_data = f.read()
|
|
605
|
+
|
|
606
|
+
# Check magic bytes
|
|
607
|
+
if len(file_data) < RMON_FIXED_HEADER_SIZE:
|
|
608
|
+
raise ValueError("Data too small for RMON header")
|
|
609
|
+
|
|
610
|
+
if file_data[:4] != RMON_MAGIC:
|
|
611
|
+
raise ValueError(
|
|
612
|
+
f"Not an RMON file: magic bytes are {file_data[:4]!r}, expected {RMON_MAGIC!r}"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Parse fixed header
|
|
616
|
+
(version,) = struct.unpack_from('<i', file_data, 4)
|
|
617
|
+
if version != 3:
|
|
618
|
+
raise ValueError(f"Unsupported RMON version: {version}. Expected 3.")
|
|
619
|
+
|
|
620
|
+
(header_size,) = struct.unpack_from('<i', file_data, 8)
|
|
621
|
+
(data_type_code,) = struct.unpack_from('<i', file_data, 12)
|
|
622
|
+
(monitor_type,) = struct.unpack_from('<i', file_data, 16)
|
|
623
|
+
(plane,) = struct.unpack_from('<i', file_data, 20)
|
|
624
|
+
(field_mask,) = struct.unpack_from('<i', file_data, 24)
|
|
625
|
+
(n_components,) = struct.unpack_from('<i', file_data, 28)
|
|
626
|
+
(nx,) = struct.unpack_from('<i', file_data, 32)
|
|
627
|
+
(ny,) = struct.unpack_from('<i', file_data, 36)
|
|
628
|
+
(nz,) = struct.unpack_from('<i', file_data, 40)
|
|
629
|
+
(voxel_size_um,) = struct.unpack_from('<f', file_data, 44)
|
|
630
|
+
(origin_x_um,) = struct.unpack_from('<f', file_data, 48)
|
|
631
|
+
(origin_y_um,) = struct.unpack_from('<f', file_data, 52)
|
|
632
|
+
(origin_z_um,) = struct.unpack_from('<f', file_data, 56)
|
|
633
|
+
(wavelength_um,) = struct.unpack_from('<f', file_data, 60)
|
|
634
|
+
|
|
635
|
+
# Parse variable header (name)
|
|
636
|
+
name = ""
|
|
637
|
+
if header_size > 68:
|
|
638
|
+
(name_length,) = struct.unpack_from('<i', file_data, 64)
|
|
639
|
+
if 0 < name_length < 1024:
|
|
640
|
+
name = file_data[68:68 + name_length].decode('utf-8', errors='replace')
|
|
641
|
+
|
|
642
|
+
monitor_type_names = {0: 'Permittivity', 1: 'Field', 2: 'Flux', 3: 'Diffraction'}
|
|
643
|
+
plane_names = {0: 'XY', 1: 'YZ', 2: 'XZ', 3: 'Volume'}
|
|
644
|
+
|
|
645
|
+
header = {
|
|
646
|
+
'version': version,
|
|
647
|
+
'header_size': header_size,
|
|
648
|
+
'data_type': data_type_code,
|
|
649
|
+
'monitor_type': monitor_type,
|
|
650
|
+
'monitor_type_name': monitor_type_names.get(monitor_type, f'unknown({monitor_type})'),
|
|
651
|
+
'plane': plane,
|
|
652
|
+
'plane_name': plane_names.get(plane, f'unknown({plane})'),
|
|
653
|
+
'field_mask': field_mask,
|
|
654
|
+
'n_components': n_components,
|
|
655
|
+
'shape': (nx, ny, nz),
|
|
656
|
+
'voxel_size_um': voxel_size_um,
|
|
657
|
+
'origin_um': (origin_x_um, origin_y_um, origin_z_um),
|
|
658
|
+
'wavelength_um': wavelength_um,
|
|
659
|
+
'name': name,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# Parse data section
|
|
663
|
+
raw_data = file_data[header_size:]
|
|
664
|
+
is_complex = data_type_code in (RMON_DATATYPE_COMPLEX64, RMON_DATATYPE_COMPLEX128)
|
|
665
|
+
|
|
666
|
+
total_voxels = n_components * nx * ny * nz
|
|
667
|
+
|
|
668
|
+
if is_complex:
|
|
669
|
+
if data_type_code == RMON_DATATYPE_COMPLEX64:
|
|
670
|
+
floats = np.frombuffer(raw_data, dtype=np.float32)
|
|
671
|
+
else:
|
|
672
|
+
floats = np.frombuffer(raw_data, dtype=np.float64)
|
|
673
|
+
|
|
674
|
+
real_part = floats[0::2]
|
|
675
|
+
imag_part = floats[1::2]
|
|
676
|
+
flat_complex = real_part + 1j * imag_part
|
|
677
|
+
data = flat_complex[:total_voxels].reshape((n_components, nx, ny, nz), order='C')
|
|
678
|
+
if data_type_code == RMON_DATATYPE_COMPLEX64:
|
|
679
|
+
data = data.astype(np.complex64)
|
|
680
|
+
else:
|
|
681
|
+
if data_type_code == RMON_DATATYPE_FLOAT32:
|
|
682
|
+
flat = np.frombuffer(raw_data, dtype=np.float32)
|
|
683
|
+
else:
|
|
684
|
+
flat = np.frombuffer(raw_data, dtype=np.float64)
|
|
685
|
+
data = flat[:total_voxels].reshape((n_components, nx, ny, nz), order='C')
|
|
686
|
+
|
|
687
|
+
logger.info(
|
|
688
|
+
f"✓ Read RMON file: name='{name}', type={header['monitor_type_name']}, "
|
|
689
|
+
f"shape=({nx},{ny},{nz}), {n_components} comp"
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return header, data
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def is_rmon_file(file_path_or_data: Union[str, Path, bytes]) -> bool:
|
|
696
|
+
"""
|
|
697
|
+
Check if a file or bytes data is in RMON format by checking magic bytes.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
file_path_or_data: Path to file or bytes data
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
True if the data starts with RMON magic bytes
|
|
704
|
+
"""
|
|
705
|
+
if isinstance(file_path_or_data, bytes):
|
|
706
|
+
return len(file_path_or_data) >= 4 and file_path_or_data[:4] == RMON_MAGIC
|
|
707
|
+
|
|
708
|
+
file_path = Path(file_path_or_data)
|
|
709
|
+
if not file_path.exists() or file_path.stat().st_size < 4:
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
with open(file_path, 'rb') as f:
|
|
713
|
+
magic = f.read(4)
|
|
714
|
+
return magic == RMON_MAGIC
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _prepare_binary_data(
|
|
718
|
+
data_array: np.ndarray,
|
|
719
|
+
data_type: str,
|
|
720
|
+
header_version: int,
|
|
721
|
+
) -> Tuple[bytes, str, list]:
|
|
722
|
+
"""
|
|
723
|
+
Prepare binary data with header for upload.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
data_array: Numpy array
|
|
727
|
+
data_type: 'float32', 'float64', 'complex64', 'complex128'
|
|
728
|
+
header_version: 1 for source fields, 2 for RI/permittivity
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Tuple of (binary_data_bytes, format_string, shape_3d_list)
|
|
732
|
+
"""
|
|
733
|
+
if not isinstance(data_array, np.ndarray):
|
|
734
|
+
raise TypeError(f"data_array must be a numpy array, got {type(data_array)}")
|
|
735
|
+
if data_array.size == 0:
|
|
736
|
+
raise ValueError("data_array cannot be empty")
|
|
737
|
+
|
|
738
|
+
original_shape = data_array.shape
|
|
739
|
+
shape_3d = list(original_shape)
|
|
740
|
+
while len(shape_3d) < 3:
|
|
741
|
+
shape_3d.append(1)
|
|
742
|
+
nx, ny, nz = shape_3d[0], shape_3d[1], shape_3d[2]
|
|
743
|
+
|
|
744
|
+
data_type_lower = data_type.lower()
|
|
745
|
+
|
|
746
|
+
format_code_map = {
|
|
747
|
+
'float32': (VoxelDataFormat.FLOAT32, 1),
|
|
748
|
+
'float64': (VoxelDataFormat.FLOAT64, 2),
|
|
749
|
+
'complex64': (VoxelDataFormat.COMPLEX64, 0),
|
|
750
|
+
'complex128': (VoxelDataFormat.COMPLEX128, 3),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if data_type_lower not in format_code_map:
|
|
754
|
+
raise ValueError(
|
|
755
|
+
f"Unknown data type: {data_type}. "
|
|
756
|
+
f"Valid options: 'float32', 'float64', 'complex64', 'complex128'"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
format_str, data_type_code = format_code_map[data_type_lower]
|
|
760
|
+
is_complex = data_type_lower in ('complex64', 'complex128')
|
|
761
|
+
|
|
762
|
+
# Permute and flatten based on header version
|
|
763
|
+
if header_version == 2:
|
|
764
|
+
# Version 2 (RI/permittivity): XYZ order, Z-fastest
|
|
765
|
+
# C-order flatten of (nx, ny, nz) gives Z-fastest directly — no transpose needed.
|
|
766
|
+
# (The old transpose + flatten('C') produced X-fastest, which was wrong.)
|
|
767
|
+
flat = data_array.reshape(nx, ny, nz).flatten(order='C')
|
|
768
|
+
else:
|
|
769
|
+
# Version 1 (source fields): C-order flatten (z-slowest, x-fastest)
|
|
770
|
+
reshaped = data_array.reshape(nx, ny, nz)
|
|
771
|
+
flat = np.empty(nx * ny * nz, dtype=data_array.dtype)
|
|
772
|
+
idx = 0
|
|
773
|
+
for iz in range(nz):
|
|
774
|
+
for iy in range(ny):
|
|
775
|
+
for ix in range(nx):
|
|
776
|
+
flat[idx] = reshaped[ix, iy, iz]
|
|
777
|
+
idx += 1
|
|
778
|
+
|
|
779
|
+
# Convert to interleaved real/imag for complex types
|
|
780
|
+
if is_complex:
|
|
781
|
+
if data_type_lower == 'complex64':
|
|
782
|
+
flat_c = flat.astype(np.complex64)
|
|
783
|
+
write_dtype = np.float32
|
|
784
|
+
else:
|
|
785
|
+
flat_c = flat.astype(np.complex128)
|
|
786
|
+
write_dtype = np.float64
|
|
787
|
+
interleaved = np.empty(flat_c.size * 2, dtype=write_dtype)
|
|
788
|
+
interleaved[0::2] = flat_c.real
|
|
789
|
+
interleaved[1::2] = flat_c.imag
|
|
790
|
+
data_bytes_payload = interleaved.tobytes()
|
|
791
|
+
else:
|
|
792
|
+
if data_type_lower == 'float32':
|
|
793
|
+
data_bytes_payload = flat.astype(np.float32).tobytes()
|
|
794
|
+
else:
|
|
795
|
+
data_bytes_payload = flat.astype(np.float64).tobytes()
|
|
796
|
+
|
|
797
|
+
# Build header
|
|
798
|
+
header = struct.pack('<5i', header_version, data_type_code, nx, ny, nz)
|
|
799
|
+
|
|
800
|
+
return header + data_bytes_payload, format_str, [nx, ny, nz]
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def upload_source_field_as_binary(
|
|
804
|
+
client,
|
|
805
|
+
project_id: str,
|
|
806
|
+
version_id: str,
|
|
807
|
+
data_array: np.ndarray,
|
|
808
|
+
filename: str,
|
|
809
|
+
data_type: str = "complex64",
|
|
810
|
+
) -> dict:
|
|
811
|
+
"""
|
|
812
|
+
Upload a source field array as a binary file with Version 1 header.
|
|
813
|
+
|
|
814
|
+
Source fields use Version 1 header format with C-order data layout
|
|
815
|
+
(z-slowest, x-fastest), matching MATLAB's uploadSourceFieldAsBinary.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
client: RayfosClient instance
|
|
819
|
+
project_id: Project UUID
|
|
820
|
+
version_id: Version UUID
|
|
821
|
+
data_array: Numpy array (typically complex)
|
|
822
|
+
filename: Desired filename for upload
|
|
823
|
+
data_type: 'float32', 'float64', 'complex64', 'complex128'
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
dict: Upload result with fileId/id/blobName, dataFormat, dimensions
|
|
827
|
+
"""
|
|
828
|
+
binary_data, format_str, dims = _prepare_binary_data(data_array, data_type, header_version=1)
|
|
829
|
+
|
|
830
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.bin', delete=False) as tmp:
|
|
831
|
+
temp_path = tmp.name
|
|
832
|
+
tmp.write(binary_data)
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
file_info = client.upload_file(project_id, version_id, temp_path, 'input')
|
|
836
|
+
file_info['dataFormat'] = format_str
|
|
837
|
+
file_info['dimensions'] = dims
|
|
838
|
+
file_info['originalDataType'] = data_type
|
|
839
|
+
logger.info(f"Uploaded source field as {format_str} ({filename}): {dims}")
|
|
840
|
+
return file_info
|
|
841
|
+
finally:
|
|
842
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def upload_array_as_binary_zipped(
|
|
846
|
+
client,
|
|
847
|
+
project_id: str,
|
|
848
|
+
version_id: str,
|
|
849
|
+
data_array: np.ndarray,
|
|
850
|
+
filename: str,
|
|
851
|
+
data_type: str = "float32",
|
|
852
|
+
) -> dict:
|
|
853
|
+
"""
|
|
854
|
+
Upload a numpy array as a gzip-compressed binary file with Version 2 header.
|
|
855
|
+
|
|
856
|
+
Used for refractive index / permittivity data. Version 2 header with
|
|
857
|
+
permuted data order (z-fastest after permutation), matching MATLAB's
|
|
858
|
+
uploadArrayAsBinaryZipped.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
client: RayfosClient instance
|
|
862
|
+
project_id: Project UUID
|
|
863
|
+
version_id: Version UUID
|
|
864
|
+
data_array: Numpy array
|
|
865
|
+
filename: Desired filename (without .gz)
|
|
866
|
+
data_type: 'float32', 'float64', 'complex64', 'complex128'
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
dict: Upload result with fileId, dataFormat, dimensions, isCompressed
|
|
870
|
+
"""
|
|
871
|
+
binary_data, format_str, dims = _prepare_binary_data(data_array, data_type, header_version=2)
|
|
872
|
+
|
|
873
|
+
gz_filename = filename + '.gz'
|
|
874
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.bin.gz', delete=False) as tmp:
|
|
875
|
+
temp_path = tmp.name
|
|
876
|
+
tmp.write(gzip.compress(binary_data))
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
file_info = client.upload_file(project_id, version_id, temp_path, 'input')
|
|
880
|
+
file_info['dataFormat'] = format_str
|
|
881
|
+
file_info['dimensions'] = dims
|
|
882
|
+
file_info['originalDataType'] = data_type
|
|
883
|
+
file_info['isCompressed'] = True
|
|
884
|
+
file_info['compressionType'] = 'gzip'
|
|
885
|
+
file_info['uncompressedSize'] = len(binary_data)
|
|
886
|
+
logger.info(f"Uploaded compressed array as {format_str} ({gz_filename}): {dims}")
|
|
887
|
+
return file_info
|
|
888
|
+
finally:
|
|
889
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def upload_source_field_as_binary_zipped(
|
|
893
|
+
client,
|
|
894
|
+
project_id: str,
|
|
895
|
+
version_id: str,
|
|
896
|
+
data_array: np.ndarray,
|
|
897
|
+
filename: str,
|
|
898
|
+
data_type: str = "complex64",
|
|
899
|
+
) -> dict:
|
|
900
|
+
"""
|
|
901
|
+
Upload a source field as a gzip-compressed binary file with Version 1 header.
|
|
902
|
+
|
|
903
|
+
Combines the source field binary format (Version 1 header, C-order data)
|
|
904
|
+
with gzip compression, matching MATLAB's uploadSourceFieldAsBinaryZipped.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
client: RayfosClient instance
|
|
908
|
+
project_id: Project UUID
|
|
909
|
+
version_id: Version UUID
|
|
910
|
+
data_array: Numpy array (typically complex)
|
|
911
|
+
filename: Desired filename (without .gz)
|
|
912
|
+
data_type: 'float32', 'float64', 'complex64', 'complex128'
|
|
913
|
+
|
|
914
|
+
Returns:
|
|
915
|
+
dict: Upload result with fileId, dataFormat, dimensions, isCompressed
|
|
916
|
+
"""
|
|
917
|
+
binary_data, format_str, dims = _prepare_binary_data(data_array, data_type, header_version=1)
|
|
918
|
+
|
|
919
|
+
gz_filename = filename + '.gz'
|
|
920
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.bin.gz', delete=False) as tmp:
|
|
921
|
+
temp_path = tmp.name
|
|
922
|
+
tmp.write(gzip.compress(binary_data))
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
file_info = client.upload_file(project_id, version_id, temp_path, 'input')
|
|
926
|
+
file_info['dataFormat'] = format_str
|
|
927
|
+
file_info['dimensions'] = dims
|
|
928
|
+
file_info['originalDataType'] = data_type
|
|
929
|
+
file_info['isCompressed'] = True
|
|
930
|
+
file_info['compressionType'] = 'gzip'
|
|
931
|
+
file_info['uncompressedSize'] = len(binary_data)
|
|
932
|
+
logger.info(f"Uploaded compressed source field as {format_str} ({gz_filename}): {dims}")
|
|
933
|
+
return file_info
|
|
934
|
+
finally:
|
|
935
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
936
|
+
|