ComDAS 0.1.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.
- comdas/__init__.py +35 -0
- comdas/arrays/__init__.py +9 -0
- comdas/arrays/duck_array.py +323 -0
- comdas/codecs/__init__.py +14 -0
- comdas/codecs/base.py +264 -0
- comdas/codecs/svd.py +258 -0
- comdas/codecs/wavelet.py +273 -0
- comdas/io/__init__.py +18 -0
- comdas/io/h5_container.py +429 -0
- comdas/utils.py +35 -0
- comdas-0.1.0.dist-info/METADATA +69 -0
- comdas-0.1.0.dist-info/RECORD +16 -0
- comdas-0.1.0.dist-info/WHEEL +5 -0
- comdas-0.1.0.dist-info/entry_points.txt +2 -0
- comdas-0.1.0.dist-info/licenses/LICENSE +165 -0
- comdas-0.1.0.dist-info/top_level.txt +1 -0
comdas/codecs/svd.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Truncated Singular Value Decomposition (SVD) codec for 2D arrays.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from comdas.codecs.base import Codec, CompressedPayload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SVDPayload(CompressedPayload):
|
|
17
|
+
"""
|
|
18
|
+
Truncated-SVD representation of a 2D array:
|
|
19
|
+
``A ~= (U * s) @ Vt``.
|
|
20
|
+
|
|
21
|
+
:ivar U: Left singular vectors, shape ``(m, k)``.
|
|
22
|
+
:vartype U: numpy.ndarray
|
|
23
|
+
:ivar s: Singular values, shape ``(k,)``.
|
|
24
|
+
:vartype s: numpy.ndarray
|
|
25
|
+
:ivar Vt: Right singular vectors (transposed), shape ``(k, n)``.
|
|
26
|
+
:vartype Vt: numpy.ndarray
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
U: np.ndarray
|
|
30
|
+
s: np.ndarray
|
|
31
|
+
Vt: np.ndarray
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def rank(self) -> int:
|
|
35
|
+
"""
|
|
36
|
+
The truncation rank ``k`` used on this payload.
|
|
37
|
+
|
|
38
|
+
:rtype: int
|
|
39
|
+
"""
|
|
40
|
+
return self.s.shape[0]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def compression_params(self) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
The compression parameters used for this payload.
|
|
46
|
+
|
|
47
|
+
:rtype: dict
|
|
48
|
+
"""
|
|
49
|
+
return {
|
|
50
|
+
"rank": self.rank,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SVDCodec(Codec):
|
|
55
|
+
"""
|
|
56
|
+
Truncated-Singular Value Decomposition (SVD) codec for 2D arrays.
|
|
57
|
+
|
|
58
|
+
This codec compresses a 2D array by computing its truncated singular value
|
|
59
|
+
decomposition (SVD) and storing the left singular vectors, singular
|
|
60
|
+
values, and right singular vectors (transposed) as a :class:`SVDPayload`.
|
|
61
|
+
|
|
62
|
+
Truncation rank can be chosen either as a fixed integer ``rank`` or
|
|
63
|
+
as a retained energy fraction ``energy`` in ``(0,1]``. Exactly one option
|
|
64
|
+
must be specified, either as a codec-level default or per-call parameter.
|
|
65
|
+
|
|
66
|
+
SVD does not implement partial writes efficiently, so the full array is
|
|
67
|
+
re-encoded every 10,000 pending writes (or on demand).
|
|
68
|
+
|
|
69
|
+
:param rank: Default truncation rank ``k`` to use if not specified per-call.
|
|
70
|
+
:type rank: int | None
|
|
71
|
+
:param energy: Default retained energy fraction to use if not specified per-call.
|
|
72
|
+
:type energy: float | None
|
|
73
|
+
:raises ValueError: If both of ``rank`` and ``energy`` are specified.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name = "SVD"
|
|
77
|
+
version = "1"
|
|
78
|
+
|
|
79
|
+
# Writes are expensive with SVD
|
|
80
|
+
max_pending_writes = 10_000
|
|
81
|
+
|
|
82
|
+
def __init__(self, rank: int | None = None, energy: float | None = None):
|
|
83
|
+
if rank is not None and energy is not None:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"Provide at most one of rank / energy to act as codec default."
|
|
86
|
+
)
|
|
87
|
+
self.default_rank = rank
|
|
88
|
+
self.default_energy = energy
|
|
89
|
+
|
|
90
|
+
def encode(
|
|
91
|
+
self,
|
|
92
|
+
array: np.ndarray,
|
|
93
|
+
*,
|
|
94
|
+
rank: int | None = None,
|
|
95
|
+
energy: float | None = None,
|
|
96
|
+
**kwargs,
|
|
97
|
+
) -> SVDPayload:
|
|
98
|
+
"""
|
|
99
|
+
Compress a 2D array via truncated SVD.
|
|
100
|
+
|
|
101
|
+
:param array: The dense 2D array to compress.
|
|
102
|
+
:type array: numpy.ndarray
|
|
103
|
+
:param rank: Fixed truncation rank. Overrides
|
|
104
|
+
``self.default_rank`` for this call.
|
|
105
|
+
:type rank: int or None
|
|
106
|
+
:param energy: Retained energy fraction in ``(0, 1]``.
|
|
107
|
+
Overrides ``self.default_energy`` for this call.
|
|
108
|
+
:type energy: float or None
|
|
109
|
+
:param kwargs: Accepted and ignored for compatibility.
|
|
110
|
+
:returns: The truncated-SVD payload.
|
|
111
|
+
:rtype: SVDPayload
|
|
112
|
+
:raises ValueError: If ``array`` is not 2D, or if both
|
|
113
|
+
``rank`` and ``energy`` are specified.
|
|
114
|
+
"""
|
|
115
|
+
array = np.asarray(array)
|
|
116
|
+
if array.ndim != 2:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"SVDCodec only supports 2D arrays, got shape {array.shape}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if rank is not None and energy is not None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Specify only one of `rank` or `energy` per call or use the codec default."
|
|
124
|
+
)
|
|
125
|
+
if rank is None and energy is None:
|
|
126
|
+
rank = self.default_rank
|
|
127
|
+
energy = self.default_energy
|
|
128
|
+
if (rank is None) == (energy is None):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"Exactly one of `rank` or `energy` must be specified, "
|
|
131
|
+
"either here or as a codec default."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
U, s, Vt = np.linalg.svd(array, full_matrices=False)
|
|
135
|
+
|
|
136
|
+
if rank is None:
|
|
137
|
+
cumulative = np.cumsum(s**2)
|
|
138
|
+
cumulative /= cumulative[-1]
|
|
139
|
+
rank = int(np.searchsorted(cumulative, energy) + 1)
|
|
140
|
+
rank = min(rank, s.shape[0])
|
|
141
|
+
rank = max(1, min(rank, s.shape[0]))
|
|
142
|
+
|
|
143
|
+
return SVDPayload(
|
|
144
|
+
original_shape=array.shape,
|
|
145
|
+
dtype=array.dtype,
|
|
146
|
+
U=np.ascontiguousarray(U[:, :rank]),
|
|
147
|
+
s=np.ascontiguousarray(s[:rank]),
|
|
148
|
+
Vt=np.ascontiguousarray(Vt[:rank, :]),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def decode(self, payload: SVDPayload) -> np.ndarray:
|
|
152
|
+
"""
|
|
153
|
+
Fully reconstruct the dense array as ``(U * s) @ Vt``.
|
|
154
|
+
|
|
155
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
156
|
+
:type payload: SVDPayload
|
|
157
|
+
:returns: The reconstructed dense array.
|
|
158
|
+
:rtype: numpy.ndarray
|
|
159
|
+
"""
|
|
160
|
+
out = (payload.U * payload.s) @ payload.Vt
|
|
161
|
+
return out.astype(payload.dtype, copy=False)
|
|
162
|
+
|
|
163
|
+
def decode_partial(self, payload: SVDPayload, key) -> np.ndarray:
|
|
164
|
+
"""
|
|
165
|
+
Reconstruct only the requested rows/cols of ``payload``.
|
|
166
|
+
|
|
167
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
168
|
+
:type payload: SVDPayload
|
|
169
|
+
:param key: An int/slice index in the NumPy-style.
|
|
170
|
+
:returns: The requested subset of the reconstructed array.
|
|
171
|
+
:rtype: numpy.ndarray
|
|
172
|
+
"""
|
|
173
|
+
if not isinstance(key, tuple):
|
|
174
|
+
key = (key,)
|
|
175
|
+
key = key + (slice(None),) * (2 - len(key))
|
|
176
|
+
row_key, col_key = key[0], key[1]
|
|
177
|
+
|
|
178
|
+
# If using complex indexing (anything more than a simple int or slice)
|
|
179
|
+
# Fall back to full decode
|
|
180
|
+
if not (
|
|
181
|
+
isinstance(row_key, (int, np.integer, slice))
|
|
182
|
+
and isinstance(col_key, (int, np.integer, slice))
|
|
183
|
+
):
|
|
184
|
+
warn(
|
|
185
|
+
"Only simple int/slice indexing is supported for partial decoding. Fully decoding entire array and then applying the index."
|
|
186
|
+
)
|
|
187
|
+
return self.decode(payload)[key]
|
|
188
|
+
|
|
189
|
+
row_is_int = isinstance(row_key, (int, np.integer))
|
|
190
|
+
col_is_int = isinstance(col_key, (int, np.integer))
|
|
191
|
+
|
|
192
|
+
U_sub = payload.U[row_key, :]
|
|
193
|
+
Vt_sub = payload.Vt[:, col_key]
|
|
194
|
+
if row_is_int:
|
|
195
|
+
U_sub = U_sub[np.newaxis, :]
|
|
196
|
+
if col_is_int:
|
|
197
|
+
Vt_sub = Vt_sub[:, np.newaxis]
|
|
198
|
+
|
|
199
|
+
out = (U_sub * payload.s) @ Vt_sub
|
|
200
|
+
out = out.astype(payload.dtype, copy=False)
|
|
201
|
+
|
|
202
|
+
if row_is_int:
|
|
203
|
+
out = out[0]
|
|
204
|
+
if col_is_int:
|
|
205
|
+
out = out[..., 0]
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
def compressed_size_bytes(self, payload: SVDPayload) -> int:
|
|
209
|
+
"""
|
|
210
|
+
Get the total number of bytes used by ``U``, ``s``, and ``Vt``.
|
|
211
|
+
|
|
212
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
213
|
+
:type payload: SVDPayload
|
|
214
|
+
:returns: Combined byte size of the three stored arrays.
|
|
215
|
+
:rtype: int
|
|
216
|
+
"""
|
|
217
|
+
return payload.U.nbytes + payload.s.nbytes + payload.Vt.nbytes
|
|
218
|
+
|
|
219
|
+
def payload_to_group(self, payload: SVDPayload, group) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Write ``U``, ``s``, ``Vt``, and ``rank`` into an open ``h5py.Group``.
|
|
222
|
+
|
|
223
|
+
:param payload: The payload to serialize.
|
|
224
|
+
:type payload: SVDPayload
|
|
225
|
+
:param group: An open, writable ``h5py.Group`` dedicated to
|
|
226
|
+
this one patch.
|
|
227
|
+
:type group: h5py.Group
|
|
228
|
+
"""
|
|
229
|
+
group.create_dataset("U", data=payload.U)
|
|
230
|
+
group.create_dataset("s", data=payload.s)
|
|
231
|
+
group.create_dataset("Vt", data=payload.Vt)
|
|
232
|
+
group.attrs["rank"] = payload.rank
|
|
233
|
+
|
|
234
|
+
def payload_from_group(
|
|
235
|
+
self, group, *, original_shape: tuple[int, ...], dtype: np.dtype
|
|
236
|
+
) -> SVDPayload:
|
|
237
|
+
"""
|
|
238
|
+
Construct an :class:`SVDPayload` from an HDF5 group.
|
|
239
|
+
|
|
240
|
+
:param group: An open, readable ``h5py.Group`` previously
|
|
241
|
+
written by :meth:`payload_to_group`.
|
|
242
|
+
:type group: h5py.Group
|
|
243
|
+
:param original_shape: The array shape, as read from the
|
|
244
|
+
container's own attributes.
|
|
245
|
+
:type original_shape: tuple[int, ...]
|
|
246
|
+
:param dtype: The array dtype, as read from the container's
|
|
247
|
+
own attributes.
|
|
248
|
+
:type dtype: numpy.dtype
|
|
249
|
+
:returns: The reconstructed payload.
|
|
250
|
+
:rtype: SVDPayload
|
|
251
|
+
"""
|
|
252
|
+
return SVDPayload(
|
|
253
|
+
original_shape=original_shape,
|
|
254
|
+
dtype=dtype,
|
|
255
|
+
U=group["U"][:],
|
|
256
|
+
s=group["s"][:],
|
|
257
|
+
Vt=group["Vt"][:],
|
|
258
|
+
)
|
comdas/codecs/wavelet.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wavelet compression via PyWavelets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pywt
|
|
11
|
+
|
|
12
|
+
from comdas.codecs.base import Codec, CompressedPayload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class WaveletPayload(CompressedPayload):
|
|
17
|
+
"""
|
|
18
|
+
Thresholded wavelet-coefficient representation of an array.
|
|
19
|
+
|
|
20
|
+
Retained coefficients are stored as ``(indices, values)`` pairs.
|
|
21
|
+
|
|
22
|
+
:ivar wavelet: Name of the PyWavelets wavelet.
|
|
23
|
+
:vartype wavelet: str
|
|
24
|
+
:ivar mode: PyWavelets signal extension mode.
|
|
25
|
+
:vartype mode: str
|
|
26
|
+
:ivar indices: Flat indices of retained coefficients.
|
|
27
|
+
:vartype indices: numpy.ndarray
|
|
28
|
+
:ivar values: Values of retained coefficients.
|
|
29
|
+
:vartype values: numpy.ndarray
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
wavelet: str
|
|
33
|
+
mode: str
|
|
34
|
+
indices: np.ndarray
|
|
35
|
+
values: np.ndarray
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def coeff_shapes(self) -> list:
|
|
39
|
+
"""
|
|
40
|
+
Get the coefficient shapes of the decomposition before thresholding.
|
|
41
|
+
|
|
42
|
+
:rtype: list
|
|
43
|
+
"""
|
|
44
|
+
return pywt.wavedecn_shapes(
|
|
45
|
+
self.original_shape, self.wavelet, mode=self.mode
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def n_coeffs(self) -> int:
|
|
50
|
+
"""
|
|
51
|
+
Total number of coefficients in the full decomposition without thresholding.
|
|
52
|
+
|
|
53
|
+
:rtype: int
|
|
54
|
+
"""
|
|
55
|
+
shapes = self.coeff_shapes
|
|
56
|
+
total = int(np.prod(shapes[0]))
|
|
57
|
+
for details in shapes[1:]:
|
|
58
|
+
total += sum(int(np.prod(s)) for s in details.values())
|
|
59
|
+
return total
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def compression_params(self) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
The compression parameters used for this payload.
|
|
65
|
+
|
|
66
|
+
:rtype: dict
|
|
67
|
+
"""
|
|
68
|
+
return {
|
|
69
|
+
"wavelet": self.wavelet,
|
|
70
|
+
"mode": self.mode,
|
|
71
|
+
"keep_fraction": self.values.size / self.n_coeffs,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WaveletCodec(Codec):
|
|
76
|
+
"""
|
|
77
|
+
Wavelet-thresholding codec.
|
|
78
|
+
|
|
79
|
+
The array is decomposed with a multilevel n-dimensional discrete wavelet
|
|
80
|
+
transform (:func:`pywt.wavedecn`) at the maximum useful level for the
|
|
81
|
+
array shape, and only the largest-magnitude coefficients are retained.
|
|
82
|
+
|
|
83
|
+
Coefficients to keep can be chosen either as a fraction ``keep_fraction`` in
|
|
84
|
+
``(0, 1]`` of all coefficients, or as an absolute magnitude
|
|
85
|
+
``threshold`` where coefficients with ``|c| > threshold`` are kept. Exactly
|
|
86
|
+
one option must be specified, either as a codec-level default or
|
|
87
|
+
per-call parameter.
|
|
88
|
+
|
|
89
|
+
:param wavelet: PyWavelets wavelet name. (default: ``"db4"``)
|
|
90
|
+
:type wavelet: str
|
|
91
|
+
:param mode: PyWavelets signal extension mode. (default: ``"symmetric"``)
|
|
92
|
+
:type mode: str
|
|
93
|
+
:param keep_fraction: Default fraction of coefficients to retain.
|
|
94
|
+
:type keep_fraction: float | None
|
|
95
|
+
:param threshold: Default absolute coefficient magnitude threshold.
|
|
96
|
+
:type threshold: float | None
|
|
97
|
+
:raises ValueError: If both of ``keep_fraction`` and ``threshold`` are specified.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
name = "WAVELET"
|
|
101
|
+
version = "1"
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
wavelet: str = "db4",
|
|
106
|
+
mode: str = "symmetric",
|
|
107
|
+
keep_fraction: float | None = None,
|
|
108
|
+
threshold: float | None = None,
|
|
109
|
+
):
|
|
110
|
+
if keep_fraction is not None and threshold is not None:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Provide at most one of keep_fraction / threshold to act as codec default."
|
|
113
|
+
)
|
|
114
|
+
self.default_wavelet = wavelet
|
|
115
|
+
self.default_mode = mode
|
|
116
|
+
self.default_keep_fraction = keep_fraction
|
|
117
|
+
self.default_threshold = threshold
|
|
118
|
+
|
|
119
|
+
def encode(
|
|
120
|
+
self,
|
|
121
|
+
array: np.ndarray,
|
|
122
|
+
*,
|
|
123
|
+
wavelet: str | None = None,
|
|
124
|
+
mode: str | None = None,
|
|
125
|
+
keep_fraction: float | None = None,
|
|
126
|
+
threshold: float | None = None,
|
|
127
|
+
**kwargs,
|
|
128
|
+
) -> WaveletPayload:
|
|
129
|
+
"""
|
|
130
|
+
Compress an array via wavelet coefficient thresholding.
|
|
131
|
+
|
|
132
|
+
:param array: The dense array to compress.
|
|
133
|
+
:type array: numpy.ndarray
|
|
134
|
+
:param wavelet: Overrides ``self.default_wavelet`` for this call.
|
|
135
|
+
:type wavelet: str or None
|
|
136
|
+
:param mode: Overrides ``self.default_mode`` for this call.
|
|
137
|
+
:type mode: str or None
|
|
138
|
+
:param keep_fraction: Fraction of coefficients to retain, in ``(0, 1]``.
|
|
139
|
+
Overrides ``self.default_keep_fraction`` for this call.
|
|
140
|
+
:type keep_fraction: float or None
|
|
141
|
+
:param threshold: Absolute coefficient magnitude threshold.
|
|
142
|
+
Overrides ``self.default_threshold`` for this call.
|
|
143
|
+
:type threshold: float or None
|
|
144
|
+
:param kwargs: Accepted and ignored for compatibility.
|
|
145
|
+
:returns: The sparse wavelet payload.
|
|
146
|
+
:rtype: WaveletPayload
|
|
147
|
+
:raises ValueError: If both or neither of ``keep_fraction`` and
|
|
148
|
+
``threshold`` are specified, or either is out of range.
|
|
149
|
+
"""
|
|
150
|
+
array = np.asarray(array)
|
|
151
|
+
wavelet = wavelet if wavelet is not None else self.default_wavelet
|
|
152
|
+
mode = mode if mode is not None else self.default_mode
|
|
153
|
+
|
|
154
|
+
if keep_fraction is not None and threshold is not None:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
"Specify only one of `keep_fraction` or `threshold` per call or use the codec default."
|
|
157
|
+
)
|
|
158
|
+
if keep_fraction is None and threshold is None:
|
|
159
|
+
keep_fraction = self.default_keep_fraction
|
|
160
|
+
threshold = self.default_threshold
|
|
161
|
+
if (keep_fraction is None) == (threshold is None):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"Exactly one of `keep_fraction` or `threshold` must be specified, "
|
|
164
|
+
"either here or as a codec default."
|
|
165
|
+
)
|
|
166
|
+
if keep_fraction is not None and not 0 < keep_fraction <= 1:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"`keep_fraction` must be in (0, 1], got {keep_fraction}"
|
|
169
|
+
)
|
|
170
|
+
if threshold is not None and threshold < 0:
|
|
171
|
+
raise ValueError(f"`threshold` must be >= 0, got {threshold}")
|
|
172
|
+
|
|
173
|
+
coeffs = pywt.wavedecn(array, wavelet, mode=mode)
|
|
174
|
+
flat = pywt.ravel_coeffs(coeffs)[0]
|
|
175
|
+
magnitude = np.abs(flat)
|
|
176
|
+
|
|
177
|
+
if keep_fraction is not None:
|
|
178
|
+
k = max(1, min(flat.size, int(round(keep_fraction * flat.size))))
|
|
179
|
+
indices = np.argpartition(magnitude, -k)[-k:]
|
|
180
|
+
indices.sort()
|
|
181
|
+
else:
|
|
182
|
+
indices = np.flatnonzero(magnitude > threshold)
|
|
183
|
+
|
|
184
|
+
index_dtype = (
|
|
185
|
+
np.uint32 if flat.size <= np.iinfo(np.uint32).max else np.uint64
|
|
186
|
+
)
|
|
187
|
+
return WaveletPayload(
|
|
188
|
+
original_shape=array.shape,
|
|
189
|
+
dtype=array.dtype,
|
|
190
|
+
wavelet=wavelet,
|
|
191
|
+
mode=mode,
|
|
192
|
+
indices=indices.astype(index_dtype),
|
|
193
|
+
values=np.ascontiguousarray(flat[indices]),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def decode(self, payload: WaveletPayload) -> np.ndarray:
|
|
197
|
+
"""
|
|
198
|
+
Reconstruct the dense array via the inverse wavelet transform.
|
|
199
|
+
|
|
200
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
201
|
+
:type payload: WaveletPayload
|
|
202
|
+
:returns: The reconstructed dense array.
|
|
203
|
+
:rtype: numpy.ndarray
|
|
204
|
+
"""
|
|
205
|
+
shapes = payload.coeff_shapes
|
|
206
|
+
template = [np.zeros(shapes[0])] + [
|
|
207
|
+
{key: np.zeros(shape) for key, shape in details.items()}
|
|
208
|
+
for details in shapes[1:]
|
|
209
|
+
]
|
|
210
|
+
_, slices, coeff_shapes = pywt.ravel_coeffs(template)
|
|
211
|
+
flat = np.zeros(payload.n_coeffs, dtype=payload.values.dtype)
|
|
212
|
+
flat[payload.indices] = payload.values
|
|
213
|
+
|
|
214
|
+
coeffs = pywt.unravel_coeffs(
|
|
215
|
+
flat, slices, coeff_shapes, output_format="wavedecn"
|
|
216
|
+
)
|
|
217
|
+
out = pywt.waverecn(coeffs, payload.wavelet, mode=payload.mode)
|
|
218
|
+
out = out[tuple(slice(0, n) for n in payload.original_shape)]
|
|
219
|
+
return out.astype(payload.dtype, copy=False)
|
|
220
|
+
|
|
221
|
+
def compressed_size_bytes(self, payload: WaveletPayload) -> int:
|
|
222
|
+
"""
|
|
223
|
+
Get payload size.
|
|
224
|
+
|
|
225
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
226
|
+
:type payload: WaveletPayload
|
|
227
|
+
:returns: Combined byte size of the stored arrays.
|
|
228
|
+
:rtype: int
|
|
229
|
+
"""
|
|
230
|
+
return payload.indices.nbytes + payload.values.nbytes
|
|
231
|
+
|
|
232
|
+
def payload_to_group(self, payload: WaveletPayload, group) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Write ``indices``, ``values``, and transform parameters into an
|
|
235
|
+
open ``h5py.Group``.
|
|
236
|
+
|
|
237
|
+
:param payload: The payload to serialize.
|
|
238
|
+
:type payload: WaveletPayload
|
|
239
|
+
:param group: An open, writable ``h5py.Group`` dedicated to
|
|
240
|
+
this one patch.
|
|
241
|
+
:type group: h5py.Group
|
|
242
|
+
"""
|
|
243
|
+
group.create_dataset("indices", data=payload.indices)
|
|
244
|
+
group.create_dataset("values", data=payload.values)
|
|
245
|
+
group.attrs["wavelet"] = payload.wavelet
|
|
246
|
+
group.attrs["mode"] = payload.mode
|
|
247
|
+
|
|
248
|
+
def payload_from_group(
|
|
249
|
+
self, group, *, original_shape: tuple[int, ...], dtype: np.dtype
|
|
250
|
+
) -> WaveletPayload:
|
|
251
|
+
"""
|
|
252
|
+
Construct a :class:`WaveletPayload` from an HDF5 group.
|
|
253
|
+
|
|
254
|
+
:param group: An open, readable ``h5py.Group`` previously
|
|
255
|
+
written by :meth:`payload_to_group`.
|
|
256
|
+
:type group: h5py.Group
|
|
257
|
+
:param original_shape: The array shape, as read from the
|
|
258
|
+
container's own attributes.
|
|
259
|
+
:type original_shape: tuple[int, ...]
|
|
260
|
+
:param dtype: The array dtype, as read from the container's
|
|
261
|
+
own attributes.
|
|
262
|
+
:type dtype: numpy.dtype
|
|
263
|
+
:returns: The reconstructed payload.
|
|
264
|
+
:rtype: WaveletPayload
|
|
265
|
+
"""
|
|
266
|
+
return WaveletPayload(
|
|
267
|
+
original_shape=original_shape,
|
|
268
|
+
dtype=dtype,
|
|
269
|
+
wavelet=str(group.attrs["wavelet"]),
|
|
270
|
+
mode=str(group.attrs["mode"]),
|
|
271
|
+
indices=group["indices"][:],
|
|
272
|
+
values=group["values"][:],
|
|
273
|
+
)
|
comdas/io/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
I/O support for ComDAS patches.
|
|
3
|
+
|
|
4
|
+
Importing this subpackage registers the ``COMDAS`` HDF5 container
|
|
5
|
+
format (:class:`~comdas.io.h5_container.ComdasV1`) with DASCore's
|
|
6
|
+
FiberIO manager, so ``dc.spool(path)`` / ``dc.read(...)`` / ``dc.write(...)``
|
|
7
|
+
work with it directly. :func:`~comdas.io.h5_container.write_compressed`
|
|
8
|
+
is a small convenience wrapper for the write side.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from comdas.io.h5_container import (
|
|
12
|
+
ComdasV1,
|
|
13
|
+
FORMAT_NAME,
|
|
14
|
+
FORMAT_VERSION,
|
|
15
|
+
write_compressed,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["ComdasV1", "FORMAT_NAME", "FORMAT_VERSION", "write_compressed"]
|