legend-pydataobj 1.0.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.
- legend_pydataobj-1.0.0.dist-info/LICENSE +674 -0
- legend_pydataobj-1.0.0.dist-info/METADATA +63 -0
- legend_pydataobj-1.0.0.dist-info/RECORD +26 -0
- legend_pydataobj-1.0.0.dist-info/WHEEL +5 -0
- legend_pydataobj-1.0.0.dist-info/top_level.txt +1 -0
- lgdo/__init__.py +75 -0
- lgdo/_version.py +4 -0
- lgdo/compression/__init__.py +36 -0
- lgdo/compression/base.py +29 -0
- lgdo/compression/generic.py +77 -0
- lgdo/compression/radware.py +579 -0
- lgdo/compression/utils.py +34 -0
- lgdo/compression/varlen.py +449 -0
- lgdo/lgdo_utils.py +196 -0
- lgdo/lh5_store.py +1711 -0
- lgdo/types/__init__.py +30 -0
- lgdo/types/array.py +140 -0
- lgdo/types/arrayofequalsizedarrays.py +133 -0
- lgdo/types/encoded.py +390 -0
- lgdo/types/fixedsizearray.py +43 -0
- lgdo/types/lgdo.py +51 -0
- lgdo/types/scalar.py +59 -0
- lgdo/types/struct.py +108 -0
- lgdo/types/table.py +349 -0
- lgdo/types/vectorofvectors.py +627 -0
- lgdo/types/waveform_table.py +264 -0
@@ -0,0 +1,449 @@
|
|
1
|
+
"""Variable-length code compression algorithms."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from dataclasses import dataclass
|
7
|
+
|
8
|
+
import numba
|
9
|
+
import numpy as np
|
10
|
+
from numpy import int32, ubyte, uint32
|
11
|
+
from numpy.typing import NDArray
|
12
|
+
|
13
|
+
from .. import types as lgdo
|
14
|
+
from .base import WaveformCodec
|
15
|
+
|
16
|
+
log = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass(frozen=True)
|
20
|
+
class ULEB128ZigZagDiff(WaveformCodec):
|
21
|
+
"""ZigZag [#WikiZZ]_ encoding followed by Unsigned Little Endian Base 128 (ULEB128) [#WikiULEB128]_ encoding of array differences.
|
22
|
+
|
23
|
+
.. [#WikiZZ] https://wikipedia.org/wiki/Variable-length_quantity#Zigzag_encoding
|
24
|
+
.. [#WikiULEB128] https://wikipedia.org/wiki/LEB128#Unsigned_LEB128
|
25
|
+
"""
|
26
|
+
|
27
|
+
codec: str = "uleb128_zigzag_diff"
|
28
|
+
|
29
|
+
|
30
|
+
def encode(
|
31
|
+
sig_in: NDArray | lgdo.VectorOfVectors | lgdo.ArrayOfEqualSizedArrays,
|
32
|
+
sig_out: NDArray[ubyte] = None,
|
33
|
+
) -> (NDArray[ubyte], NDArray[uint32]) | lgdo.VectorOfEncodedVectors:
|
34
|
+
"""Compress digital signal(s) with a variable-length encoding of its derivative.
|
35
|
+
|
36
|
+
Wraps :func:`uleb128_zigzag_diff_array_encode` and adds support for encoding
|
37
|
+
LGDOs.
|
38
|
+
|
39
|
+
Note
|
40
|
+
----
|
41
|
+
If `sig_in` is a NumPy array, no resizing of `sig_out` is performed. Not
|
42
|
+
even of the internally allocated one.
|
43
|
+
|
44
|
+
Because of the current implementation, providing a pre-allocated
|
45
|
+
:class:`.VectorOfEncodedVectors` as `sig_out` is not possible.
|
46
|
+
|
47
|
+
Parameters
|
48
|
+
----------
|
49
|
+
sig_in
|
50
|
+
array(s) holding the input signal(s).
|
51
|
+
sig_out
|
52
|
+
pre-allocated unsigned 8-bit integer array(s) for the compressed
|
53
|
+
signal(s). If not provided, a new one will be allocated.
|
54
|
+
|
55
|
+
Returns
|
56
|
+
-------
|
57
|
+
sig_out, nbytes
|
58
|
+
given pre-allocated `sig_out` structure or new structure of unsigned
|
59
|
+
8-bit integers, plus the number of bytes (length) of the encoded
|
60
|
+
signal. If `sig_in` is an :class:`.LGDO`, only a newly allocated
|
61
|
+
:class:`.VectorOfEncodedVectors` is returned.
|
62
|
+
|
63
|
+
See Also
|
64
|
+
--------
|
65
|
+
uleb128_zigzag_diff_array_encode
|
66
|
+
"""
|
67
|
+
if isinstance(sig_in, np.ndarray):
|
68
|
+
s = sig_in.shape
|
69
|
+
if len(sig_in) == 0:
|
70
|
+
return np.empty(s[:-1] + (0,), dtype=ubyte), np.empty(0, dtype=uint32)
|
71
|
+
|
72
|
+
if sig_out is None:
|
73
|
+
# the encoded signal is an array of bytes
|
74
|
+
# pre-allocate ubyte (uint8) array with a generous (but safe) size
|
75
|
+
max_b = int(np.ceil(np.iinfo(sig_in.dtype).bits / 16) * 5)
|
76
|
+
# expand last dimension
|
77
|
+
sig_out = np.empty(s[:-1] + (s[-1] * max_b,), dtype=ubyte)
|
78
|
+
|
79
|
+
if sig_out.dtype != ubyte:
|
80
|
+
raise ValueError("sig_out must be of type ubyte")
|
81
|
+
|
82
|
+
# nbytes has one dimension less (the last one)
|
83
|
+
nbytes = np.empty(s[:-1], dtype=uint32)
|
84
|
+
|
85
|
+
uleb128_zigzag_diff_array_encode(sig_in, sig_out, nbytes)
|
86
|
+
|
87
|
+
# return without resizing
|
88
|
+
return sig_out, nbytes
|
89
|
+
|
90
|
+
elif isinstance(sig_in, lgdo.VectorOfVectors):
|
91
|
+
if sig_out:
|
92
|
+
log.warning(
|
93
|
+
"a pre-allocated VectorOfEncodedVectors was given "
|
94
|
+
"to hold an encoded ArrayOfEqualSizedArrays. "
|
95
|
+
"This is not supported at the moment, so a new one "
|
96
|
+
"will be allocated to replace it"
|
97
|
+
)
|
98
|
+
# convert VectorOfVectors to ArrayOfEqualSizedArrays so it can be
|
99
|
+
# directly passed to the low-level encoding routine
|
100
|
+
sig_out_nda, nbytes = encode(sig_in.to_aoesa())
|
101
|
+
|
102
|
+
# build the encoded LGDO
|
103
|
+
encoded_data = lgdo.ArrayOfEqualSizedArrays(nda=sig_out_nda).to_vov(
|
104
|
+
cumulative_length=np.cumsum(nbytes, dtype=uint32)
|
105
|
+
)
|
106
|
+
# decoded_size is an array, compute it by diff'ing the original VOV
|
107
|
+
decoded_size = np.diff(sig_in.cumulative_length, prepend=uint32(0))
|
108
|
+
|
109
|
+
sig_out = lgdo.VectorOfEncodedVectors(encoded_data, decoded_size)
|
110
|
+
|
111
|
+
return sig_out
|
112
|
+
|
113
|
+
elif isinstance(sig_in, lgdo.ArrayOfEqualSizedArrays):
|
114
|
+
if sig_out:
|
115
|
+
log.warning(
|
116
|
+
"a pre-allocated VectorOfEncodedVectors was given "
|
117
|
+
"to hold an encoded ArrayOfEqualSizedArrays. "
|
118
|
+
"This is not supported at the moment, so a new one "
|
119
|
+
"will be allocated to replace it"
|
120
|
+
)
|
121
|
+
|
122
|
+
# encode the internal numpy array
|
123
|
+
sig_out_nda, nbytes = encode(sig_in.nda)
|
124
|
+
|
125
|
+
# build the encoded LGDO
|
126
|
+
encoded_data = lgdo.ArrayOfEqualSizedArrays(nda=sig_out_nda).to_vov(
|
127
|
+
cumulative_length=np.cumsum(nbytes, dtype=uint32)
|
128
|
+
)
|
129
|
+
sig_out = lgdo.ArrayOfEncodedEqualSizedArrays(
|
130
|
+
encoded_data, decoded_size=sig_in.nda.shape[1]
|
131
|
+
)
|
132
|
+
|
133
|
+
return sig_out
|
134
|
+
|
135
|
+
elif isinstance(sig_in, lgdo.Array):
|
136
|
+
# encode the internal numpy array
|
137
|
+
sig_out_nda, nbytes = encode(sig_in.nda, sig_out)
|
138
|
+
return lgdo.Array(sig_out_nda), nbytes
|
139
|
+
|
140
|
+
else:
|
141
|
+
raise ValueError(f"unsupported input signal type ({type(sig_in)})")
|
142
|
+
|
143
|
+
|
144
|
+
def decode(
|
145
|
+
sig_in: (NDArray[ubyte], NDArray[uint32]) | lgdo.VectorOfEncodedVectors,
|
146
|
+
sig_out: NDArray | lgdo.VectorOfVectors | lgdo.ArrayOfEqualSizedArrays = None,
|
147
|
+
) -> NDArray | lgdo.VectorOfVectors | lgdo.ArrayOfEqualSizedArrays:
|
148
|
+
"""Deompress digital signal(s) with a variable-length encoding of its derivative.
|
149
|
+
|
150
|
+
Wraps :func:`uleb128_zigzag_diff_array_decode` and adds support for decoding
|
151
|
+
LGDOs.
|
152
|
+
|
153
|
+
Note
|
154
|
+
----
|
155
|
+
If `sig_in` is a NumPy array, no resizing (along the last dimension) of
|
156
|
+
`sig_out` to its actual length is performed. Not even of the internally
|
157
|
+
allocated one. If a pre-allocated :class:`.ArrayOfEqualSizedArrays` is
|
158
|
+
provided, it won't be resized too. The internally allocated
|
159
|
+
:class:`.ArrayOfEqualSizedArrays` `sig_out` has instead always the correct
|
160
|
+
size.
|
161
|
+
|
162
|
+
Because of the current implementation, providing a pre-allocated
|
163
|
+
:class:`.VectorOfVectors` as `sig_out` is not possible.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
sig_in
|
168
|
+
array(s) holding the input, compressed signal(s). Output of
|
169
|
+
:func:`.encode`.
|
170
|
+
sig_out
|
171
|
+
pre-allocated array(s) for the decompressed signal(s). If not
|
172
|
+
provided, will allocate a 32-bit integer array(s) structure.
|
173
|
+
|
174
|
+
Returns
|
175
|
+
-------
|
176
|
+
sig_out
|
177
|
+
given pre-allocated structure or new structure of 32-bit integers.
|
178
|
+
|
179
|
+
See Also
|
180
|
+
--------
|
181
|
+
uleb128_zigzag_diff_array_decode
|
182
|
+
"""
|
183
|
+
# expect the output of encode()
|
184
|
+
if isinstance(sig_in, tuple):
|
185
|
+
if sig_out is None:
|
186
|
+
# allocate output array of the same shape (generous)
|
187
|
+
sig_out = np.empty_like(sig_in[0], dtype=int32)
|
188
|
+
|
189
|
+
# siglen has one dimension less (the last)
|
190
|
+
s = sig_in[0].shape
|
191
|
+
siglen = np.empty(s[:-1], dtype=uint32)
|
192
|
+
|
193
|
+
if len(sig_in[0]) == 0:
|
194
|
+
return sig_out, siglen
|
195
|
+
|
196
|
+
# call low-level routine
|
197
|
+
uleb128_zigzag_diff_array_decode(sig_in[0], sig_in[1], sig_out, siglen)
|
198
|
+
|
199
|
+
return sig_out, siglen
|
200
|
+
|
201
|
+
elif isinstance(sig_in, lgdo.ArrayOfEncodedEqualSizedArrays):
|
202
|
+
if not sig_out:
|
203
|
+
# initialize output structure with decoded_size
|
204
|
+
sig_out = lgdo.ArrayOfEqualSizedArrays(
|
205
|
+
dims=(1, 1),
|
206
|
+
shape=(len(sig_in), sig_in.decoded_size.value),
|
207
|
+
dtype=int32,
|
208
|
+
attrs=sig_in.getattrs(),
|
209
|
+
)
|
210
|
+
|
211
|
+
siglen = np.empty(len(sig_in), dtype=uint32)
|
212
|
+
# save original encoded vector lengths
|
213
|
+
nbytes = np.diff(sig_in.encoded_data.cumulative_length.nda, prepend=uint32(0))
|
214
|
+
|
215
|
+
if len(sig_in) == 0:
|
216
|
+
return sig_out
|
217
|
+
|
218
|
+
# convert vector of vectors to array of equal sized arrays
|
219
|
+
# can now decode on the 2D matrix together with number of bytes to read per row
|
220
|
+
_, siglen = decode(
|
221
|
+
(sig_in.encoded_data.to_aoesa(preserve_dtype=True).nda, nbytes), sig_out.nda
|
222
|
+
)
|
223
|
+
|
224
|
+
# sanity check
|
225
|
+
assert np.all(sig_in.decoded_size.value == siglen)
|
226
|
+
|
227
|
+
return sig_out
|
228
|
+
|
229
|
+
elif isinstance(sig_in, lgdo.VectorOfEncodedVectors):
|
230
|
+
if sig_out:
|
231
|
+
log.warning(
|
232
|
+
"a pre-allocated VectorOfVectors was given "
|
233
|
+
"to hold an encoded VectorOfVectors. "
|
234
|
+
"This is not supported at the moment, so a new one "
|
235
|
+
"will be allocated to replace it"
|
236
|
+
)
|
237
|
+
|
238
|
+
siglen = np.empty(len(sig_in), dtype=uint32)
|
239
|
+
# save original encoded vector lengths
|
240
|
+
nbytes = np.diff(sig_in.encoded_data.cumulative_length.nda, prepend=uint32(0))
|
241
|
+
|
242
|
+
# convert vector of vectors to array of equal sized arrays
|
243
|
+
# can now decode on the 2D matrix together with number of bytes to read per row
|
244
|
+
sig_out, siglen = decode(
|
245
|
+
(sig_in.encoded_data.to_aoesa(preserve_dtype=True).nda, nbytes)
|
246
|
+
)
|
247
|
+
|
248
|
+
# sanity check
|
249
|
+
assert np.array_equal(sig_in.decoded_size, siglen)
|
250
|
+
|
251
|
+
# converto to VOV before returning
|
252
|
+
return sig_out.to_vov(np.cumsum(siglen, dtype=uint32))
|
253
|
+
|
254
|
+
else:
|
255
|
+
raise ValueError("unsupported input signal type")
|
256
|
+
|
257
|
+
|
258
|
+
@numba.vectorize(
|
259
|
+
["uint64(int64)", "uint32(int32)", "uint16(int16)"],
|
260
|
+
nopython=True,
|
261
|
+
)
|
262
|
+
def zigzag_encode(x: int | NDArray[int]) -> int | NDArray[int]:
|
263
|
+
"""ZigZag-encode [#WikiZZ]_ signed integer numbers."""
|
264
|
+
return (x >> 31) ^ (x << 1)
|
265
|
+
|
266
|
+
|
267
|
+
@numba.vectorize(
|
268
|
+
["int64(uint64)", "int32(uint32)", "int16(uint16)"],
|
269
|
+
nopython=True,
|
270
|
+
)
|
271
|
+
def zigzag_decode(x: int | NDArray[int]) -> int | NDArray[int]:
|
272
|
+
"""ZigZag-decode [#WikiZZ]_ signed integer numbers."""
|
273
|
+
return (x >> 1) ^ -(x & 1)
|
274
|
+
|
275
|
+
|
276
|
+
@numba.jit(["uint32(int64, byte[:])"], nopython=True)
|
277
|
+
def uleb128_encode(x: int, encx: NDArray[ubyte]) -> int:
|
278
|
+
"""Compute a variable-length representation of an unsigned integer.
|
279
|
+
|
280
|
+
Implements the Unsigned Little Endian Base-128 encoding [#WikiULEB128]_.
|
281
|
+
Only positive numbers are expected, as no *two’s complement* is applied.
|
282
|
+
|
283
|
+
Parameters
|
284
|
+
----------
|
285
|
+
x
|
286
|
+
the number to be encoded.
|
287
|
+
encx
|
288
|
+
the encoded varint as a NumPy array of bytes.
|
289
|
+
|
290
|
+
Returns
|
291
|
+
-------
|
292
|
+
nbytes
|
293
|
+
size of varint in bytes
|
294
|
+
"""
|
295
|
+
i = 0
|
296
|
+
bits = x & 0x7F
|
297
|
+
x >>= 7
|
298
|
+
while x:
|
299
|
+
encx[i] = 0x80 | bits
|
300
|
+
bits = x & 0x7F
|
301
|
+
i += 1
|
302
|
+
x >>= 7
|
303
|
+
|
304
|
+
encx[i] = bits
|
305
|
+
# return size of varint in bytes
|
306
|
+
return i + 1
|
307
|
+
|
308
|
+
|
309
|
+
@numba.jit(["UniTuple(uint32, 2)(byte[:])"], nopython=True)
|
310
|
+
def uleb128_decode(encx: NDArray[ubyte]) -> (int, int):
|
311
|
+
"""Decode a variable-length integer into an unsigned integer.
|
312
|
+
|
313
|
+
Implements the Unsigned Little Endian Base-128 decoding [#WikiULEB128]_.
|
314
|
+
Only encoded positive numbers are expected, as no *two’s complement* is
|
315
|
+
applied.
|
316
|
+
|
317
|
+
Parameters
|
318
|
+
----------
|
319
|
+
encx
|
320
|
+
the encoded varint as a NumPy array of bytes.
|
321
|
+
|
322
|
+
Returns
|
323
|
+
-------
|
324
|
+
x, nread
|
325
|
+
the decoded value and the number of bytes read from the input array.
|
326
|
+
"""
|
327
|
+
if len(encx) <= 0:
|
328
|
+
raise ValueError("input bytes array is empty")
|
329
|
+
|
330
|
+
x = pos = uint32(0)
|
331
|
+
for b in encx:
|
332
|
+
x = x | ((b & 0x7F) << pos)
|
333
|
+
if (b & 0x80) == 0:
|
334
|
+
return (x, int(pos / 7 + 1))
|
335
|
+
else:
|
336
|
+
pos += 7
|
337
|
+
|
338
|
+
if pos >= 64:
|
339
|
+
raise OverflowError("overflow during decoding of varint encoded number")
|
340
|
+
|
341
|
+
raise RuntimeError("malformed varint")
|
342
|
+
|
343
|
+
|
344
|
+
@numba.guvectorize(
|
345
|
+
[
|
346
|
+
"void(uint16[:], byte[:], uint32[:])",
|
347
|
+
"void(uint32[:], byte[:], uint32[:])",
|
348
|
+
"void(uint64[:], byte[:], uint32[:])",
|
349
|
+
"void(int16[:], byte[:], uint32[:])",
|
350
|
+
"void(int32[:], byte[:], uint32[:])",
|
351
|
+
"void(int64[:], byte[:], uint32[:])",
|
352
|
+
],
|
353
|
+
"(n),(m),()",
|
354
|
+
nopython=True,
|
355
|
+
)
|
356
|
+
def uleb128_zigzag_diff_array_encode(
|
357
|
+
sig_in: NDArray[int], sig_out: NDArray[ubyte], nbytes: int
|
358
|
+
) -> None:
|
359
|
+
"""Encode an array of integer numbers.
|
360
|
+
|
361
|
+
The algorithm computes the derivative (prepending 0 first) of `sig_in`,
|
362
|
+
maps it to positive numbers by applying :func:`zigzag_encode` and finally
|
363
|
+
computes its variable-length binary representation with
|
364
|
+
:func:`uleb128_encode`.
|
365
|
+
|
366
|
+
The encoded data is stored in `sig_out` as an array of bytes. The number of
|
367
|
+
bytes written is stored in `nbytes`. The actual encoded data can therefore
|
368
|
+
be found in ``sig_out[:nbytes]``.
|
369
|
+
|
370
|
+
Parameters
|
371
|
+
----------
|
372
|
+
sig_in
|
373
|
+
the input array of integers.
|
374
|
+
sig_out
|
375
|
+
pre-allocated bytes array for the output encoded data.
|
376
|
+
nbytes
|
377
|
+
pre-allocated output array holding the number of bytes written (stored
|
378
|
+
in the first index).
|
379
|
+
|
380
|
+
See Also
|
381
|
+
--------
|
382
|
+
.uleb128_zigzag_diff_array_decode
|
383
|
+
"""
|
384
|
+
pos = uint32(0)
|
385
|
+
last = int32(0)
|
386
|
+
for s in sig_in:
|
387
|
+
zzdiff = zigzag_encode(int32(s - last))
|
388
|
+
pos += uleb128_encode(zzdiff, sig_out[pos:])
|
389
|
+
last = s
|
390
|
+
|
391
|
+
nbytes[0] = pos
|
392
|
+
|
393
|
+
|
394
|
+
@numba.guvectorize(
|
395
|
+
[
|
396
|
+
"void(byte[:], uint32[:], uint16[:], uint32[:])",
|
397
|
+
"void(byte[:], uint32[:], uint32[:], uint32[:])",
|
398
|
+
"void(byte[:], uint32[:], uint64[:], uint32[:])",
|
399
|
+
"void(byte[:], uint32[:], int16[:], uint32[:])",
|
400
|
+
"void(byte[:], uint32[:], int32[:], uint32[:])",
|
401
|
+
"void(byte[:], uint32[:], int64[:], uint32[:])",
|
402
|
+
],
|
403
|
+
"(n),(),(m),()",
|
404
|
+
nopython=True,
|
405
|
+
)
|
406
|
+
def uleb128_zigzag_diff_array_decode(
|
407
|
+
sig_in: NDArray[ubyte],
|
408
|
+
nbytes: int,
|
409
|
+
sig_out: NDArray[int],
|
410
|
+
siglen: int,
|
411
|
+
) -> None:
|
412
|
+
"""Decode an array of variable-length integers.
|
413
|
+
|
414
|
+
The algorithm inverts :func:`.uleb128_zigzag_diff_array_encode` by decoding
|
415
|
+
the variable-length binary data in `sig_in` with :func:`uleb128_decode`,
|
416
|
+
then reconstructing the original signal derivative with
|
417
|
+
:func:`zigzag_decode` and finally computing its cumulative (i.e. the
|
418
|
+
original signal).
|
419
|
+
|
420
|
+
Parameters
|
421
|
+
----------
|
422
|
+
sig_in
|
423
|
+
the array of bytes encoding the variable-length integers.
|
424
|
+
nbytes
|
425
|
+
the number of bytes to read from `sig_in` (stored in the first index of
|
426
|
+
this array).
|
427
|
+
sig_out
|
428
|
+
pre-allocated array for the output decoded signal.
|
429
|
+
siglen
|
430
|
+
the length of the decoded signal, (stored in the first index of this
|
431
|
+
array).
|
432
|
+
|
433
|
+
See Also
|
434
|
+
--------
|
435
|
+
.uleb128_zigzag_diff_array_encode
|
436
|
+
"""
|
437
|
+
if len(sig_in) <= 0:
|
438
|
+
raise ValueError("input bytes array is empty")
|
439
|
+
|
440
|
+
_nbytes = min(nbytes[0], len(sig_in))
|
441
|
+
pos = i = uint32(0)
|
442
|
+
last = int32(0)
|
443
|
+
while pos < _nbytes:
|
444
|
+
x, nread = uleb128_decode(sig_in[pos:])
|
445
|
+
sig_out[i] = last = zigzag_decode(x) + last
|
446
|
+
i += 1
|
447
|
+
pos += nread
|
448
|
+
|
449
|
+
siglen[0] = i
|
lgdo/lgdo_utils.py
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
"""Implements utilities for LEGEND Data Objects."""
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import glob
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import string
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
|
11
|
+
from . import types as lgdo
|
12
|
+
|
13
|
+
log = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def get_element_type(obj: object) -> str:
|
17
|
+
"""Get the LGDO element type of a scalar or array.
|
18
|
+
|
19
|
+
For use in LGDO datatype attributes.
|
20
|
+
|
21
|
+
Parameters
|
22
|
+
----------
|
23
|
+
obj
|
24
|
+
if a ``str``, will automatically return ``string`` if the object has
|
25
|
+
a :class:`numpy.dtype`, that will be used for determining the element
|
26
|
+
type otherwise will attempt to case the type of the object to a
|
27
|
+
:class:`numpy.dtype`.
|
28
|
+
|
29
|
+
Returns
|
30
|
+
-------
|
31
|
+
element_type
|
32
|
+
A string stating the determined element type of the object.
|
33
|
+
"""
|
34
|
+
|
35
|
+
# special handling for strings
|
36
|
+
if isinstance(obj, str):
|
37
|
+
return "string"
|
38
|
+
|
39
|
+
# the rest use dtypes
|
40
|
+
dt = obj.dtype if hasattr(obj, "dtype") else np.dtype(type(obj))
|
41
|
+
kind = dt.kind
|
42
|
+
|
43
|
+
if kind == "b":
|
44
|
+
return "bool"
|
45
|
+
if kind == "V":
|
46
|
+
return "blob"
|
47
|
+
if kind in ["i", "u", "f"]:
|
48
|
+
return "real"
|
49
|
+
if kind == "c":
|
50
|
+
return "complex"
|
51
|
+
if kind in ["S", "U"]:
|
52
|
+
return "string"
|
53
|
+
|
54
|
+
# couldn't figure it out
|
55
|
+
raise ValueError(
|
56
|
+
"cannot determine lgdo element_type for object of type", type(obj).__name__
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
def copy(obj: lgdo.LGDO, dtype: np.dtype = None) -> lgdo.LGDO:
|
61
|
+
"""Return a copy of an LGDO.
|
62
|
+
|
63
|
+
Parameters
|
64
|
+
----------
|
65
|
+
obj
|
66
|
+
the LGDO to be copied.
|
67
|
+
dtype
|
68
|
+
NumPy dtype to be used for the copied object.
|
69
|
+
|
70
|
+
"""
|
71
|
+
if dtype is None:
|
72
|
+
dtype = obj.dtype
|
73
|
+
|
74
|
+
if isinstance(obj, lgdo.Array):
|
75
|
+
return lgdo.Array(
|
76
|
+
np.array(obj.nda, dtype=dtype, copy=True), attrs=dict(obj.attrs)
|
77
|
+
)
|
78
|
+
|
79
|
+
if isinstance(obj, lgdo.VectorOfVectors):
|
80
|
+
return lgdo.VectorOfVectors(
|
81
|
+
flattened_data=copy(obj.flattened_data, dtype=dtype),
|
82
|
+
cumulative_length=copy(obj.cumulative_length),
|
83
|
+
attrs=dict(obj.attrs),
|
84
|
+
)
|
85
|
+
|
86
|
+
else:
|
87
|
+
raise ValueError(f"copy of {type(obj)} not supported")
|
88
|
+
|
89
|
+
|
90
|
+
def parse_datatype(datatype: str) -> tuple[str, tuple[int, ...], str | list[str]]:
|
91
|
+
"""Parse datatype string and return type, dimensions and elements.
|
92
|
+
|
93
|
+
Parameters
|
94
|
+
----------
|
95
|
+
datatype
|
96
|
+
a LGDO-formatted datatype string.
|
97
|
+
|
98
|
+
Returns
|
99
|
+
-------
|
100
|
+
element_type
|
101
|
+
the datatype name dims if not ``None``, a tuple of dimensions for the
|
102
|
+
LGDO. Note this is not the same as the NumPy shape of the underlying
|
103
|
+
data object. See the LGDO specification for more information. Also see
|
104
|
+
:class:`~.types.ArrayOfEqualSizedArrays` and
|
105
|
+
:meth:`.lh5_store.LH5Store.read_object` for example code elements for
|
106
|
+
numeric objects, the element type for struct-like objects, the list of
|
107
|
+
fields in the struct.
|
108
|
+
"""
|
109
|
+
if "{" not in datatype:
|
110
|
+
return "scalar", None, datatype
|
111
|
+
|
112
|
+
# for other datatypes, need to parse the datatype string
|
113
|
+
from parse import parse
|
114
|
+
|
115
|
+
datatype, element_description = parse("{}{{{}}}", datatype)
|
116
|
+
if datatype.endswith(">"):
|
117
|
+
datatype, dims = parse("{}<{}>", datatype)
|
118
|
+
dims = [int(i) for i in dims.split(",")]
|
119
|
+
return datatype, tuple(dims), element_description
|
120
|
+
else:
|
121
|
+
return datatype, None, element_description.split(",")
|
122
|
+
|
123
|
+
|
124
|
+
def expand_vars(expr: str, substitute: dict[str, str] = None) -> str:
|
125
|
+
"""Expand (environment) variables.
|
126
|
+
|
127
|
+
Note
|
128
|
+
----
|
129
|
+
Malformed variable names and references to non-existing variables are left
|
130
|
+
unchanged.
|
131
|
+
|
132
|
+
Parameters
|
133
|
+
----------
|
134
|
+
expr
|
135
|
+
string expression, which may include (environment) variables prefixed by
|
136
|
+
``$``.
|
137
|
+
substitute
|
138
|
+
use this dictionary to substitute variables. Environment variables take
|
139
|
+
precedence.
|
140
|
+
"""
|
141
|
+
if substitute is None:
|
142
|
+
substitute = {}
|
143
|
+
|
144
|
+
# expand env variables first
|
145
|
+
# then try using provided mapping
|
146
|
+
return string.Template(os.path.expandvars(expr)).safe_substitute(substitute)
|
147
|
+
|
148
|
+
|
149
|
+
def expand_path(
|
150
|
+
path: str,
|
151
|
+
substitute: dict[str, str] = None,
|
152
|
+
list: bool = False,
|
153
|
+
base_path: str = None,
|
154
|
+
) -> str | list:
|
155
|
+
"""Expand (environment) variables and wildcards to return absolute paths.
|
156
|
+
|
157
|
+
Parameters
|
158
|
+
----------
|
159
|
+
path
|
160
|
+
name of path, which may include environment variables and wildcards.
|
161
|
+
list
|
162
|
+
if ``True``, return a list. If ``False``, return a string; if ``False``
|
163
|
+
and a unique file is not found, raise an exception.
|
164
|
+
substitute
|
165
|
+
use this dictionary to substitute variables. Environment variables take
|
166
|
+
precedence.
|
167
|
+
base_path
|
168
|
+
name of base path. Returned paths will be relative to base.
|
169
|
+
|
170
|
+
Returns
|
171
|
+
-------
|
172
|
+
path or list of paths
|
173
|
+
Unique absolute path, or list of all absolute paths
|
174
|
+
"""
|
175
|
+
if base_path is not None and base_path != "":
|
176
|
+
base_path = os.path.expanduser(os.path.expandvars(base_path))
|
177
|
+
path = os.path.join(base_path, path)
|
178
|
+
|
179
|
+
# first expand variables
|
180
|
+
_path = expand_vars(path, substitute)
|
181
|
+
|
182
|
+
# then expand wildcards
|
183
|
+
paths = glob.glob(os.path.expanduser(_path))
|
184
|
+
|
185
|
+
if base_path is not None and base_path != "":
|
186
|
+
paths = [os.path.relpath(p, base_path) for p in paths]
|
187
|
+
|
188
|
+
if not list:
|
189
|
+
if len(paths) == 0:
|
190
|
+
raise FileNotFoundError(f"could not find path matching {path}")
|
191
|
+
elif len(paths) > 1:
|
192
|
+
raise FileNotFoundError(f"found multiple paths matching {path}")
|
193
|
+
else:
|
194
|
+
return paths[0]
|
195
|
+
else:
|
196
|
+
return paths
|