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/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ComDAS: compressed DAS patches for DASCore.
|
|
3
|
+
|
|
4
|
+
ComDAS patches work identically to normal ``dascore.Patch`` objects,
|
|
5
|
+
but the data they hold is stored compressed in memory
|
|
6
|
+
(:class:`~comdas.arrays.duck_array.DuckArray`) and
|
|
7
|
+
decompressed on access. Compression is controlled via
|
|
8
|
+
the :class:`~comdas.codecs.base.Codec` interface. Both the in-memory
|
|
9
|
+
array and the on-disk ``COMDAS`` container format
|
|
10
|
+
(:mod:`comdas.io.h5_container`) are codec-agnostic, working with any
|
|
11
|
+
registered codec by name rather than hardcoding a specific one.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from comdas.codecs.base import Codec, CompressedPayload
|
|
15
|
+
from comdas.codecs.svd import SVDCodec, SVDPayload
|
|
16
|
+
from comdas.codecs.wavelet import WaveletCodec, WaveletPayload
|
|
17
|
+
from comdas.arrays.duck_array import DuckArray
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Codec",
|
|
21
|
+
"CompressedPayload",
|
|
22
|
+
"SVDCodec",
|
|
23
|
+
"SVDPayload",
|
|
24
|
+
"WaveletCodec",
|
|
25
|
+
"WaveletPayload",
|
|
26
|
+
"DuckArray",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from comdas.utils import compress_patch # requires dascore installed
|
|
31
|
+
from comdas.io import write_compressed # noqa: F401 -- also self-registers the COMDAS FiberIO plugin
|
|
32
|
+
|
|
33
|
+
__all__ += ["compress_patch", "write_compressed"]
|
|
34
|
+
except ImportError: # pragma: no cover
|
|
35
|
+
pass
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implements a NumPy-like `duck array
|
|
3
|
+
<https://docs.xarray.dev/en/stable/user-guide/duckarrays.html>`_ for compressed
|
|
4
|
+
Patch data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import itertools
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from comdas.codecs.base import Codec, CompressedPayload
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
17
|
+
"""
|
|
18
|
+
A NumPy-like duck array backed by a compressed payload and codec defining
|
|
19
|
+
how the data is compressed and decompressed.
|
|
20
|
+
|
|
21
|
+
Reads are performed via ``codec.decode_partial``. Writes go into a
|
|
22
|
+
sparse overlay dict rather than immediately forcing a complete recompression
|
|
23
|
+
of the underlying compressed payload. :meth:`consolidate` can be called to
|
|
24
|
+
merge the overlay into the compressed payload. :meth:`consolidate` will be
|
|
25
|
+
called automatically when the number of pending writes exceeds the codec's
|
|
26
|
+
specified maximum number of pending writes.
|
|
27
|
+
|
|
28
|
+
If the specified codec implements partial writes efficiently, the codec's
|
|
29
|
+
partial writes function will always be used to update the compressed
|
|
30
|
+
payload without accumulation.
|
|
31
|
+
|
|
32
|
+
:param payload: The compressed payload backing this duck array.
|
|
33
|
+
:type payload: CompressedPayload
|
|
34
|
+
:param codec: The codec defining how the data is compressed and decompressed.
|
|
35
|
+
:type codec: Codec
|
|
36
|
+
:param max_pending_writes: Maximum number of pending writes before automatic
|
|
37
|
+
consolidation.
|
|
38
|
+
:type max_pending_writes: int or None
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
payload: CompressedPayload,
|
|
44
|
+
codec: Codec,
|
|
45
|
+
*,
|
|
46
|
+
max_pending_writes: int | None = None,
|
|
47
|
+
):
|
|
48
|
+
self._payload = payload
|
|
49
|
+
self._codec = codec
|
|
50
|
+
self._overlay: dict[tuple[int, ...], object] = {}
|
|
51
|
+
self._max_pending_writes = (
|
|
52
|
+
max_pending_writes
|
|
53
|
+
if max_pending_writes is not None
|
|
54
|
+
else codec.max_pending_writes
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Array basics
|
|
58
|
+
@property
|
|
59
|
+
def shape(self) -> tuple[int, ...]:
|
|
60
|
+
"""
|
|
61
|
+
Get the shape of the decompressed/original array.
|
|
62
|
+
|
|
63
|
+
:rtype: tuple[int, ...]
|
|
64
|
+
"""
|
|
65
|
+
return self._payload.original_shape
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def dtype(self) -> np.dtype:
|
|
69
|
+
"""
|
|
70
|
+
Get the data type of the decompressed/original array.
|
|
71
|
+
|
|
72
|
+
:rtype: numpy.dtype
|
|
73
|
+
"""
|
|
74
|
+
return self._payload.dtype
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def ndim(self) -> int:
|
|
78
|
+
"""
|
|
79
|
+
Get the number of dimensions of the decompressed/original array.
|
|
80
|
+
|
|
81
|
+
:rtype: int
|
|
82
|
+
"""
|
|
83
|
+
return len(self.shape)
|
|
84
|
+
|
|
85
|
+
def __len__(self) -> int:
|
|
86
|
+
return self.shape[0]
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def payload(self) -> CompressedPayload:
|
|
90
|
+
"""
|
|
91
|
+
Get the current compressed payload backing this array.
|
|
92
|
+
|
|
93
|
+
:rtype: CompressedPayload
|
|
94
|
+
"""
|
|
95
|
+
return self._payload
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def codec(self) -> Codec:
|
|
99
|
+
"""
|
|
100
|
+
Get the codec used to encode/decode :attr:`payload`.
|
|
101
|
+
|
|
102
|
+
:rtype: Codec
|
|
103
|
+
"""
|
|
104
|
+
return self._codec
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def overlay_size(self) -> int:
|
|
108
|
+
"""
|
|
109
|
+
Get the number of elements currently held in the write overlay.
|
|
110
|
+
|
|
111
|
+
:rtype: int
|
|
112
|
+
"""
|
|
113
|
+
return len(self._overlay)
|
|
114
|
+
|
|
115
|
+
def __array__(self, dtype=None) -> np.ndarray:
|
|
116
|
+
"""
|
|
117
|
+
Fully materialize this array as a dense NumPy array.
|
|
118
|
+
|
|
119
|
+
:param dtype: Optional dtype to cast the result to.
|
|
120
|
+
:type dtype: numpy.dtype or None
|
|
121
|
+
:returns: The decompressed array, with any pending overlay
|
|
122
|
+
writes applied.
|
|
123
|
+
:rtype: numpy.ndarray
|
|
124
|
+
"""
|
|
125
|
+
out = self._codec.decode(self._payload).copy()
|
|
126
|
+
for idx, val in self._overlay.items():
|
|
127
|
+
out[idx] = val
|
|
128
|
+
if dtype is not None:
|
|
129
|
+
out = out.astype(dtype, copy=False)
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
def __array_function__(self, func, types, args, kwargs):
|
|
133
|
+
"""
|
|
134
|
+
Apply a numpy function to this array. Decompresses the
|
|
135
|
+
array before applying the NumPy function.
|
|
136
|
+
|
|
137
|
+
NOTE: Some codecs support efficient operations directly
|
|
138
|
+
on the compressed representation. We should provide a way
|
|
139
|
+
to implement these in the future.
|
|
140
|
+
|
|
141
|
+
NOTE: Applying a chain of functions will decompress the array
|
|
142
|
+
at each step. We should optimize chained operations to avoid
|
|
143
|
+
this.
|
|
144
|
+
|
|
145
|
+
:param func: The NumPy function being dispatched.
|
|
146
|
+
:param types: Types of the arguments involved.
|
|
147
|
+
:param args: Positional arguments to ``func``.
|
|
148
|
+
:param kwargs: Keyword arguments to ``func``.
|
|
149
|
+
:returns: The result of calling ``func`` on materialized arrays.
|
|
150
|
+
"""
|
|
151
|
+
args = [np.asarray(a) if isinstance(a, DuckArray) else a for a in args]
|
|
152
|
+
return func(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
155
|
+
"""
|
|
156
|
+
Materializes the array then applies the ufunc.
|
|
157
|
+
|
|
158
|
+
NOTE: We should explore per-codec optimizations.
|
|
159
|
+
|
|
160
|
+
:param ufunc: The NumPy ufunc being dispatched.
|
|
161
|
+
:param method: The ufunc method being called (e.g. ``"__call__"``).
|
|
162
|
+
:param inputs: Positional inputs to the ufunc.
|
|
163
|
+
:param kwargs: Keyword arguments to the ufunc.
|
|
164
|
+
:returns: The result of calling the ufunc on materialized arrays.
|
|
165
|
+
"""
|
|
166
|
+
inputs = [
|
|
167
|
+
np.asarray(i) if isinstance(i, DuckArray) else i for i in inputs
|
|
168
|
+
]
|
|
169
|
+
return getattr(ufunc, method)(*inputs, **kwargs)
|
|
170
|
+
|
|
171
|
+
def __repr__(self) -> str:
|
|
172
|
+
ratio = self._codec.compression_ratio(self._payload)
|
|
173
|
+
return (
|
|
174
|
+
f"DuckArray(shape={self.shape}, dtype={self.dtype}, "
|
|
175
|
+
f"codec={type(self._codec).__name__}, ratio={ratio:.1f}x, "
|
|
176
|
+
f"overlay={self.overlay_size})"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _key_to_index_lists(self, key) -> list[list[int]]:
|
|
180
|
+
"""
|
|
181
|
+
Normalize a NumPy-style key into per-axis integer index lists.
|
|
182
|
+
|
|
183
|
+
:param key: A NumPy-style index/slice key.
|
|
184
|
+
:returns: One list of concrete integer indices per axis.
|
|
185
|
+
:rtype: list[list[int]]
|
|
186
|
+
"""
|
|
187
|
+
if not isinstance(key, tuple):
|
|
188
|
+
key = (key,)
|
|
189
|
+
key = key + (slice(None),) * (self.ndim - len(key))
|
|
190
|
+
axes: list[list[int]] = []
|
|
191
|
+
for k, dim in zip(key, self.shape):
|
|
192
|
+
if isinstance(k, (int, np.integer)):
|
|
193
|
+
axes.append([int(k)])
|
|
194
|
+
elif isinstance(k, slice):
|
|
195
|
+
axes.append(list(range(*k.indices(dim))))
|
|
196
|
+
else:
|
|
197
|
+
axes.append(np.atleast_1d(np.asarray(k)).tolist())
|
|
198
|
+
return axes
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _normalize_key(key, ndim: int) -> tuple:
|
|
202
|
+
"""
|
|
203
|
+
Pad ``key`` with trailing full slices to length ``ndim``.
|
|
204
|
+
|
|
205
|
+
:param key: A NumPy-style index/slice key.
|
|
206
|
+
:param ndim: Number of dimensions to pad to.
|
|
207
|
+
:type ndim: int
|
|
208
|
+
:returns: The normalized, tuple-form key.
|
|
209
|
+
:rtype: tuple
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(key, tuple):
|
|
212
|
+
key = (key,)
|
|
213
|
+
return key + (slice(None),) * (ndim - len(key))
|
|
214
|
+
|
|
215
|
+
def __getitem__(self, key):
|
|
216
|
+
"""
|
|
217
|
+
Read values from the array.
|
|
218
|
+
|
|
219
|
+
:param key: A NumPy-style index/slice key.
|
|
220
|
+
:returns: The requested subset of the array.
|
|
221
|
+
:rtype: numpy.ndarray or scalar
|
|
222
|
+
"""
|
|
223
|
+
if not self._overlay:
|
|
224
|
+
return self._codec.decode_partial(self._payload, key)
|
|
225
|
+
|
|
226
|
+
axes = self._key_to_index_lists(key)
|
|
227
|
+
axes_sets = [set(a) for a in axes]
|
|
228
|
+
touched = [
|
|
229
|
+
(idx, val)
|
|
230
|
+
for idx, val in self._overlay.items()
|
|
231
|
+
if all(idx[d] in axes_sets[d] for d in range(self.ndim))
|
|
232
|
+
]
|
|
233
|
+
base = np.array(
|
|
234
|
+
self._codec.decode_partial(self._payload, key), copy=True
|
|
235
|
+
)
|
|
236
|
+
if not touched:
|
|
237
|
+
return base
|
|
238
|
+
|
|
239
|
+
norm_key = self._normalize_key(key, self.ndim)
|
|
240
|
+
for idx, val in touched:
|
|
241
|
+
local = tuple(
|
|
242
|
+
axes[d].index(idx[d])
|
|
243
|
+
for d, k in enumerate(norm_key)
|
|
244
|
+
if not isinstance(k, (int, np.integer))
|
|
245
|
+
)
|
|
246
|
+
base[local] = val
|
|
247
|
+
return base
|
|
248
|
+
|
|
249
|
+
def __setitem__(self, key, value) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Write a value. Attempts to not decompress the entire array if possible.
|
|
252
|
+
|
|
253
|
+
Three strategies are tried, in order:
|
|
254
|
+
|
|
255
|
+
1. If the codec implements :meth:`~comdas.codecs.base.Codec.partial_write`,
|
|
256
|
+
it's used directly. In this case, the payload is patched in place.
|
|
257
|
+
A codec may raise :class:`NotImplementedError` for a particular ``key``
|
|
258
|
+
it can't handle (e.g. it only supports single elements), in
|
|
259
|
+
this case it goes to strategy 2 for that call.
|
|
260
|
+
2. Otherwise, small writes land in a sparse overlay dict,
|
|
261
|
+
applied on read and folded in by :meth:`consolidate`.
|
|
262
|
+
3. A write that touches more than
|
|
263
|
+
:attr:`~comdas.codecs.base.Codec.max_pending_writes`
|
|
264
|
+
elements or would push the overlay size past it auto-consolidates
|
|
265
|
+
instead.
|
|
266
|
+
|
|
267
|
+
:param key: A NumPy-style index/slice key.
|
|
268
|
+
:param value: The value(s) to write; broadcast against the
|
|
269
|
+
shape implied by ``key``, matching normal NumPy assignment.
|
|
270
|
+
"""
|
|
271
|
+
if self._codec.supports_partial_write():
|
|
272
|
+
try:
|
|
273
|
+
self._payload = self._codec.partial_write(
|
|
274
|
+
self._payload, key, value
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
except NotImplementedError:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
axes = self._key_to_index_lists(key)
|
|
281
|
+
size = 1
|
|
282
|
+
for a in axes:
|
|
283
|
+
size *= len(a)
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
size > self._max_pending_writes
|
|
287
|
+
or (self.overlay_size + size) > self._max_pending_writes
|
|
288
|
+
):
|
|
289
|
+
dense = self.__array__()
|
|
290
|
+
dense[key] = value
|
|
291
|
+
self.consolidate(dense=dense)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
indices = list(itertools.product(*axes))
|
|
295
|
+
values = np.broadcast_to(value, tuple(len(a) for a in axes)).ravel()
|
|
296
|
+
for idx, val in zip(indices, values):
|
|
297
|
+
self._overlay[idx] = val
|
|
298
|
+
|
|
299
|
+
def consolidate(
|
|
300
|
+
self,
|
|
301
|
+
*,
|
|
302
|
+
dense: np.ndarray | None = None,
|
|
303
|
+
**encode_kwargs,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Fold the write overlay into a freshly-compressed payload.
|
|
307
|
+
|
|
308
|
+
By default, re-encodes using whatever compression level the
|
|
309
|
+
current payload already used.
|
|
310
|
+
|
|
311
|
+
:param dense: Precomputed dense array to encode, if the
|
|
312
|
+
caller already has one.
|
|
313
|
+
:type dense: numpy.ndarray or None
|
|
314
|
+
:param encode_kwargs: Additional codec-specific kwargs
|
|
315
|
+
forwarded to ``codec.encode``.
|
|
316
|
+
"""
|
|
317
|
+
if dense is None:
|
|
318
|
+
dense = self.__array__()
|
|
319
|
+
|
|
320
|
+
encode_kwargs = self._payload.compression_params
|
|
321
|
+
|
|
322
|
+
self._payload = self._codec.encode(dense, **encode_kwargs)
|
|
323
|
+
self._overlay.clear()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Compression codec interfaces and implementations."""
|
|
2
|
+
|
|
3
|
+
from comdas.codecs.base import Codec, CompressedPayload
|
|
4
|
+
from comdas.codecs.svd import SVDCodec, SVDPayload
|
|
5
|
+
from comdas.codecs.wavelet import WaveletCodec, WaveletPayload
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Codec",
|
|
9
|
+
"CompressedPayload",
|
|
10
|
+
"SVDCodec",
|
|
11
|
+
"SVDPayload",
|
|
12
|
+
"WaveletCodec",
|
|
13
|
+
"WaveletPayload",
|
|
14
|
+
]
|
comdas/codecs/base.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for compression codecs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CompressedPayload:
|
|
15
|
+
"""
|
|
16
|
+
Base container for a codec's compressed payload (representation of an array).
|
|
17
|
+
|
|
18
|
+
Codecs should subclass this to hold the arrays/parameters the method requires.
|
|
19
|
+
|
|
20
|
+
:ivar original_shape: The shape of the array before compression.
|
|
21
|
+
:vartype original_shape: tuple[int, ...]
|
|
22
|
+
:ivar dtype: The data type of the array before compression.
|
|
23
|
+
:vartype dtype: numpy.dtype
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
original_shape: tuple[int, ...]
|
|
27
|
+
dtype: np.dtype
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def compression_params(self) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
The parameters used for compression.
|
|
33
|
+
|
|
34
|
+
The default implementation returns an empty dict. Codecs with
|
|
35
|
+
parameters relevant to re-encoding should override this.
|
|
36
|
+
|
|
37
|
+
:rtype: dict
|
|
38
|
+
"""
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Codec(ABC):
|
|
43
|
+
"""
|
|
44
|
+
Base class for all codecs.
|
|
45
|
+
|
|
46
|
+
A codec is responsible for compressing and decompressing arrays. It knows
|
|
47
|
+
how to turn a dense NumPy array into a :class:`CompressedPayload` and back,
|
|
48
|
+
and how to (de)serialize that payload to/from an HDF5 file.
|
|
49
|
+
|
|
50
|
+
Subclasses are auto-registered by name on class creation. This allows
|
|
51
|
+
the reader to automatically recognize and use the appropriate codec by name,
|
|
52
|
+
without having to import every codec.
|
|
53
|
+
|
|
54
|
+
Most codecs do not support random access writes and algorithms can be
|
|
55
|
+
inefficient at performing many small writes, so writes to the payload are
|
|
56
|
+
buffered until max_pending_writes is reached. Once this value is exceeded,
|
|
57
|
+
the entire array will be decompressed, the writes applied, and the
|
|
58
|
+
array recompressed. By default, this value is set to 10,000.
|
|
59
|
+
|
|
60
|
+
If a codec supports random access writes efficiently, it should
|
|
61
|
+
implement the :meth:`partial_write` method. If this method is
|
|
62
|
+
implemented, it will always be immediately used for applying writes,
|
|
63
|
+
bypassing max_pending_writes entirely.
|
|
64
|
+
|
|
65
|
+
:cvar name: Unique, human-readable name for the codec.
|
|
66
|
+
:vartype name: str
|
|
67
|
+
:cvar version: Codec format version, bumped if the codec's payload
|
|
68
|
+
structure changes in a way that is not backward-compatible.
|
|
69
|
+
:vartype version: str
|
|
70
|
+
:cvar max_pending_writes: Maximum number of pending writes before automatic consolidation.
|
|
71
|
+
This is only used by codecs that do not implement :meth:`partial_write`.
|
|
72
|
+
:vartype max_pending_writes: int
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: str = ""
|
|
76
|
+
version: str = "1"
|
|
77
|
+
max_pending_writes: int = 10_000
|
|
78
|
+
|
|
79
|
+
_registry: dict[str, type[Codec]] = {}
|
|
80
|
+
|
|
81
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Register ``cls`` in the registry under ``cls.name``.
|
|
84
|
+
|
|
85
|
+
:param kwargs: Forwarded to :meth:`object.__init_subclass__`.
|
|
86
|
+
:raises ValueError: If a concrete subclass doesn't set a
|
|
87
|
+
non-empty ``name``.
|
|
88
|
+
"""
|
|
89
|
+
super().__init_subclass__(**kwargs)
|
|
90
|
+
if getattr(cls, "__abstractmethods__", None):
|
|
91
|
+
return
|
|
92
|
+
if not cls.name:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"{cls.__name__} must define a `name` for codec registration."
|
|
95
|
+
)
|
|
96
|
+
Codec._registry[cls.name] = cls
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def get_registered(cls, name: str) -> type[Codec]:
|
|
100
|
+
"""
|
|
101
|
+
Look up a codec class by name.
|
|
102
|
+
|
|
103
|
+
:param name: The codec's ``name`` attribute.
|
|
104
|
+
:type name: str
|
|
105
|
+
:returns: The codec class registered under ``name``.
|
|
106
|
+
:rtype: type[Codec]
|
|
107
|
+
:raises KeyError: If no codec is registered under ``name``.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
return cls._registry[name]
|
|
111
|
+
except KeyError as err:
|
|
112
|
+
raise KeyError(
|
|
113
|
+
f"No codec registered under name {name!r}. Known codecs: {sorted(cls._registry)}"
|
|
114
|
+
) from err
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def encode(self, array: np.ndarray, **kwargs) -> CompressedPayload:
|
|
118
|
+
"""
|
|
119
|
+
Compress ``array`` into a :class:`CompressedPayload`.
|
|
120
|
+
|
|
121
|
+
:param array: The dense array to compress.
|
|
122
|
+
:type array: numpy.ndarray
|
|
123
|
+
:param kwargs: Codec-specific compression parameters.
|
|
124
|
+
:returns: The compressed payload.
|
|
125
|
+
:rtype: CompressedPayload
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def decode(self, payload: CompressedPayload) -> np.ndarray:
|
|
130
|
+
"""
|
|
131
|
+
Fully decompress the payload.
|
|
132
|
+
|
|
133
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
134
|
+
:type payload: CompressedPayload
|
|
135
|
+
:returns: The decompressed array.
|
|
136
|
+
:rtype: numpy.ndarray
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def decode_partial(self, payload: CompressedPayload, key) -> np.ndarray:
|
|
140
|
+
"""
|
|
141
|
+
Partially decompress the payload, reconstructing only the elements
|
|
142
|
+
addressed by ``key``.
|
|
143
|
+
|
|
144
|
+
The default implementation uses a full decode. Codecs that support
|
|
145
|
+
partial decompression or random access read/write should overwrite
|
|
146
|
+
this method.
|
|
147
|
+
|
|
148
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
149
|
+
:type payload: CompressedPayload
|
|
150
|
+
:param key: A NumPy-style index/slice key.
|
|
151
|
+
:returns: The requested subset of the reconstructed array.
|
|
152
|
+
:rtype: numpy.ndarray
|
|
153
|
+
"""
|
|
154
|
+
return self.decode(payload)[key]
|
|
155
|
+
|
|
156
|
+
def partial_write(
|
|
157
|
+
self, payload: CompressedPayload, key, value
|
|
158
|
+
) -> CompressedPayload:
|
|
159
|
+
"""
|
|
160
|
+
Write ``value`` into the compressed representation at ``key``,
|
|
161
|
+
without a full decode -> update -> re-encode round trip.
|
|
162
|
+
|
|
163
|
+
This is an optional method, most codecs do not support random access
|
|
164
|
+
writes. The default implementation always raises a
|
|
165
|
+
:class:`NotImplementedError`, which
|
|
166
|
+
:class:`~comdas.arrays.duck_array.DuckArray`
|
|
167
|
+
interprets as "this codec or write attempt doesn't
|
|
168
|
+
support true partial writes" and falls back to its overlay +
|
|
169
|
+
:attr:`default_overlay_flush_threshold` strategy instead.
|
|
170
|
+
|
|
171
|
+
A codec should implement this for the cases it can handle
|
|
172
|
+
cheaply and raise :class:`NotImplementedError` for cases it can't.
|
|
173
|
+
The fallback applies per call, not just
|
|
174
|
+
per codec. See :meth:`supports_partial_write`.
|
|
175
|
+
|
|
176
|
+
Implementations must apply exactly the same assignment
|
|
177
|
+
semantics as ``dense_array[key] = value`` would on the fully
|
|
178
|
+
decompressed array, including NumPy's broadcasting rules.
|
|
179
|
+
|
|
180
|
+
:param payload: The payload before the write.
|
|
181
|
+
:type payload: CompressedPayload
|
|
182
|
+
:param key: A NumPy-style index/slice key.
|
|
183
|
+
:param value: The value(s) to write, broadcast against the
|
|
184
|
+
shape implied by ``key`` exactly as plain NumPy assignment
|
|
185
|
+
would.
|
|
186
|
+
:returns: The payload reflecting the write.
|
|
187
|
+
:rtype: CompressedPayload
|
|
188
|
+
:raises NotImplementedError: Always, unless overridden.
|
|
189
|
+
"""
|
|
190
|
+
raise NotImplementedError(
|
|
191
|
+
f"{type(self).__name__} does not support partial_write()."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def supports_partial_write(self) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Whether this codec has overridden :meth:`partial_write`.
|
|
197
|
+
|
|
198
|
+
:returns: True if :meth:`partial_write` is overridden.
|
|
199
|
+
:rtype: bool
|
|
200
|
+
"""
|
|
201
|
+
return type(self).partial_write is not Codec.partial_write
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def compressed_size_bytes(self, payload: CompressedPayload) -> int:
|
|
205
|
+
"""
|
|
206
|
+
Get the total size, in bytes, of everything stored in ``payload``.
|
|
207
|
+
|
|
208
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
209
|
+
:type payload: CompressedPayload
|
|
210
|
+
:returns: Total size in bytes of the payload's stored arrays.
|
|
211
|
+
:rtype: int
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def compression_ratio(self, payload: CompressedPayload) -> float:
|
|
215
|
+
"""
|
|
216
|
+
Helper to determine the compression ratio of the payload.
|
|
217
|
+
|
|
218
|
+
:param payload: A payload previously produced by :meth:`encode`.
|
|
219
|
+
:type payload: CompressedPayload
|
|
220
|
+
:returns: Original bytes divided by compressed bytes.
|
|
221
|
+
:rtype: float
|
|
222
|
+
"""
|
|
223
|
+
original_bytes = (
|
|
224
|
+
int(np.prod(payload.original_shape))
|
|
225
|
+
* np.dtype(payload.dtype).itemsize
|
|
226
|
+
)
|
|
227
|
+
return original_bytes / self.compressed_size_bytes(payload)
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def payload_to_group(self, payload: CompressedPayload, group) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Write this codec's own arrays/params into an open HDF5 group.
|
|
233
|
+
|
|
234
|
+
Only codec-specific data belongs here. The general ``COMDAS``
|
|
235
|
+
container already writes ``original_shape``, ``dtype``,
|
|
236
|
+
``codec_name``, and ``codec_version`` at the container level
|
|
237
|
+
before calling this. Don't duplicate those fields.
|
|
238
|
+
|
|
239
|
+
:param payload: The payload to serialize.
|
|
240
|
+
:type payload: CompressedPayload
|
|
241
|
+
:param group: An open, writable ``h5py.Group`` dedicated to
|
|
242
|
+
this one patch.
|
|
243
|
+
:type group: h5py.Group
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
@abstractmethod
|
|
247
|
+
def payload_from_group(
|
|
248
|
+
self, group, *, original_shape: tuple[int, ...], dtype: np.dtype
|
|
249
|
+
) -> CompressedPayload:
|
|
250
|
+
"""
|
|
251
|
+
Reconstruct this codec's payload from an HDF5 group.
|
|
252
|
+
|
|
253
|
+
:param group: An open, readable ``h5py.Group`` previously
|
|
254
|
+
written by :meth:`payload_to_group`.
|
|
255
|
+
:type group: h5py.Group
|
|
256
|
+
:param original_shape: The array shape, as already read from
|
|
257
|
+
the container's own (codec-independent) attributes.
|
|
258
|
+
:type original_shape: tuple[int, ...]
|
|
259
|
+
:param dtype: The array dtype, as already read from the
|
|
260
|
+
container's own (codec-independent) attributes.
|
|
261
|
+
:type dtype: numpy.dtype
|
|
262
|
+
:returns: The reconstructed payload.
|
|
263
|
+
:rtype: CompressedPayload
|
|
264
|
+
"""
|