ComDAS 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → comdas-0.2.0}/.github/workflows/publish.yml +1 -0
- {comdas-0.1.0 → comdas-0.2.0}/.github/workflows/testing.yml +1 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/PKG-INFO +8 -8
- {comdas-0.1.0 → comdas-0.2.0}/PKG-INFO +8 -8
- {comdas-0.1.0 → comdas-0.2.0}/README.md +7 -7
- {comdas-0.1.0 → comdas-0.2.0}/comdas/arrays/duck_array.py +157 -76
- {comdas-0.1.0 → comdas-0.2.0}/comdas/codecs/base.py +44 -3
- {comdas-0.1.0 → comdas-0.2.0}/comdas/codecs/svd.py +19 -10
- {comdas-0.1.0 → comdas-0.2.0}/comdas/codecs/wavelet.py +23 -5
- {comdas-0.1.0 → comdas-0.2.0}/comdas/io/h5_container.py +61 -13
- {comdas-0.1.0 → comdas-0.2.0}/pyproject.toml +1 -1
- {comdas-0.1.0 → comdas-0.2.0}/tests/test_codecs.py +92 -0
- {comdas-0.1.0 → comdas-0.2.0}/tests/test_duck_array.py +148 -0
- comdas-0.2.0/tests/test_h5_container.py +241 -0
- comdas-0.1.0/tests/test_h5_container.py +0 -129
- {comdas-0.1.0 → comdas-0.2.0}/.github/workflows/docs.yml +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/.gitignore +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/SOURCES.txt +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/dependency_links.txt +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/entry_points.txt +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/requires.txt +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/ComDAS.egg-info/top_level.txt +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/LICENSE +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/comdas/__init__.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/comdas/arrays/__init__.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/comdas/codecs/__init__.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/comdas/io/__init__.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/comdas/utils.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/Makefile +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/api/index.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/codecs/add_a_codec.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/codecs/svd.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/codecs/wavelet.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/conf.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/examples/compression_comparison.ipynb +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/index.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/installation.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/make.bat +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/docs/quickstart.rst +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/setup.cfg +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/tests/__init__.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/tests/_dummy_codec.py +0 -0
- {comdas-0.1.0 → comdas-0.2.0}/tests/test_codec_base.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ComDAS
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: DASCORE companion package that adds support for compressed DAS data
|
|
5
5
|
Author-email: Byron-Selvage <bselvage@mines.edu>
|
|
6
6
|
License: LGPL-3.0-or-later
|
|
@@ -44,8 +44,8 @@ With ComDAS you can:
|
|
|
44
44
|
|
|
45
45
|
| Codec | Main setting | Reference |
|
|
46
46
|
| --- | --- | --- |
|
|
47
|
-
| [SVD](https://
|
|
48
|
-
| [Wavelet](https://
|
|
47
|
+
| [SVD](https://dasdae.github.io/ComDAS/codecs/svd.html) | `rank` or retained `energy` | [NumPy SVD](https://numpy.org/doc/1.22/reference/generated/numpy.linalg.svd.html) |
|
|
48
|
+
| [Wavelet](https://dasdae.github.io/ComDAS/codecs/wavelet.html) | `keep_fraction` or `threshold` | [PyWavelets](https://doi.org/10.21105/joss.01237); [DeVore et al. (1992)](https://doi.org/10.1109/18.119733) |
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
@@ -57,12 +57,12 @@ pip install comdas
|
|
|
57
57
|
|
|
58
58
|
## Documentation
|
|
59
59
|
|
|
60
|
-
Full documentation is available at [
|
|
60
|
+
Full documentation is available at [dasdae.github.io/ComDAS](https://dasdae.github.io/ComDAS/), including:
|
|
61
61
|
|
|
62
|
-
- [Quick start](https://
|
|
63
|
-
- [Codecs](https://
|
|
64
|
-
- [Examples](https://
|
|
65
|
-
- [API reference](https://
|
|
62
|
+
- [Quick start](https://dasdae.github.io/ComDAS/quickstart.html): compress a patch, save a compressed file, and read it back
|
|
63
|
+
- [Codecs](https://dasdae.github.io/ComDAS/codecs/svd.html): configuration options and examples for each codec
|
|
64
|
+
- [Examples](https://dasdae.github.io/ComDAS/examples/compression_comparison.html)
|
|
65
|
+
- [API reference](https://dasdae.github.io/ComDAS/api/index.html)
|
|
66
66
|
|
|
67
67
|
## License
|
|
68
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ComDAS
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: DASCORE companion package that adds support for compressed DAS data
|
|
5
5
|
Author-email: Byron-Selvage <bselvage@mines.edu>
|
|
6
6
|
License: LGPL-3.0-or-later
|
|
@@ -44,8 +44,8 @@ With ComDAS you can:
|
|
|
44
44
|
|
|
45
45
|
| Codec | Main setting | Reference |
|
|
46
46
|
| --- | --- | --- |
|
|
47
|
-
| [SVD](https://
|
|
48
|
-
| [Wavelet](https://
|
|
47
|
+
| [SVD](https://dasdae.github.io/ComDAS/codecs/svd.html) | `rank` or retained `energy` | [NumPy SVD](https://numpy.org/doc/1.22/reference/generated/numpy.linalg.svd.html) |
|
|
48
|
+
| [Wavelet](https://dasdae.github.io/ComDAS/codecs/wavelet.html) | `keep_fraction` or `threshold` | [PyWavelets](https://doi.org/10.21105/joss.01237); [DeVore et al. (1992)](https://doi.org/10.1109/18.119733) |
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
@@ -57,12 +57,12 @@ pip install comdas
|
|
|
57
57
|
|
|
58
58
|
## Documentation
|
|
59
59
|
|
|
60
|
-
Full documentation is available at [
|
|
60
|
+
Full documentation is available at [dasdae.github.io/ComDAS](https://dasdae.github.io/ComDAS/), including:
|
|
61
61
|
|
|
62
|
-
- [Quick start](https://
|
|
63
|
-
- [Codecs](https://
|
|
64
|
-
- [Examples](https://
|
|
65
|
-
- [API reference](https://
|
|
62
|
+
- [Quick start](https://dasdae.github.io/ComDAS/quickstart.html): compress a patch, save a compressed file, and read it back
|
|
63
|
+
- [Codecs](https://dasdae.github.io/ComDAS/codecs/svd.html): configuration options and examples for each codec
|
|
64
|
+
- [Examples](https://dasdae.github.io/ComDAS/examples/compression_comparison.html)
|
|
65
|
+
- [API reference](https://dasdae.github.io/ComDAS/api/index.html)
|
|
66
66
|
|
|
67
67
|
## License
|
|
68
68
|
|
|
@@ -19,8 +19,8 @@ With ComDAS you can:
|
|
|
19
19
|
|
|
20
20
|
| Codec | Main setting | Reference |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| [SVD](https://
|
|
23
|
-
| [Wavelet](https://
|
|
22
|
+
| [SVD](https://dasdae.github.io/ComDAS/codecs/svd.html) | `rank` or retained `energy` | [NumPy SVD](https://numpy.org/doc/1.22/reference/generated/numpy.linalg.svd.html) |
|
|
23
|
+
| [Wavelet](https://dasdae.github.io/ComDAS/codecs/wavelet.html) | `keep_fraction` or `threshold` | [PyWavelets](https://doi.org/10.21105/joss.01237); [DeVore et al. (1992)](https://doi.org/10.1109/18.119733) |
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
@@ -32,12 +32,12 @@ pip install comdas
|
|
|
32
32
|
|
|
33
33
|
## Documentation
|
|
34
34
|
|
|
35
|
-
Full documentation is available at [
|
|
35
|
+
Full documentation is available at [dasdae.github.io/ComDAS](https://dasdae.github.io/ComDAS/), including:
|
|
36
36
|
|
|
37
|
-
- [Quick start](https://
|
|
38
|
-
- [Codecs](https://
|
|
39
|
-
- [Examples](https://
|
|
40
|
-
- [API reference](https://
|
|
37
|
+
- [Quick start](https://dasdae.github.io/ComDAS/quickstart.html): compress a patch, save a compressed file, and read it back
|
|
38
|
+
- [Codecs](https://dasdae.github.io/ComDAS/codecs/svd.html): configuration options and examples for each codec
|
|
39
|
+
- [Examples](https://dasdae.github.io/ComDAS/examples/compression_comparison.html)
|
|
40
|
+
- [API reference](https://dasdae.github.io/ComDAS/api/index.html)
|
|
41
41
|
|
|
42
42
|
## License
|
|
43
43
|
|
|
@@ -6,13 +6,80 @@ Patch data.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import itertools
|
|
10
|
-
|
|
11
9
|
import numpy as np
|
|
12
10
|
|
|
13
11
|
from comdas.codecs.base import Codec, CompressedPayload
|
|
14
12
|
|
|
15
13
|
|
|
14
|
+
def _materialize(obj):
|
|
15
|
+
"""
|
|
16
|
+
Replace every :class:`DuckArray` in ``obj`` with a dense NumPy array,
|
|
17
|
+
recursing into lists, tuples, and dicts (e.g. the list passed to
|
|
18
|
+
``np.concatenate``).
|
|
19
|
+
|
|
20
|
+
:param obj: An argument (or container of arguments) to a NumPy call.
|
|
21
|
+
:returns: ``obj`` with DuckArrays materialized.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(obj, DuckArray):
|
|
24
|
+
return np.asarray(obj)
|
|
25
|
+
if type(obj) is list:
|
|
26
|
+
return [_materialize(o) for o in obj]
|
|
27
|
+
if type(obj) is tuple:
|
|
28
|
+
return tuple(_materialize(o) for o in obj)
|
|
29
|
+
if type(obj) is dict:
|
|
30
|
+
return {k: _materialize(v) for k, v in obj.items()}
|
|
31
|
+
return obj
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _densify_out(kwargs: dict) -> tuple[dict, list]:
|
|
35
|
+
"""
|
|
36
|
+
Swap any DuckArray in an ``out=`` kwarg for a dense temporary buffer.
|
|
37
|
+
|
|
38
|
+
The buffer starts as a copy of the DuckArray's current values, so
|
|
39
|
+
elements a ``where=`` mask leaves untouched are preserved.
|
|
40
|
+
|
|
41
|
+
:param kwargs: Keyword arguments to a NumPy function/ufunc.
|
|
42
|
+
:returns: ``(new_kwargs, pairs)`` where ``pairs`` is a list of
|
|
43
|
+
``(duck_array, dense_buffer)`` to pass to :func:`_write_back_out`.
|
|
44
|
+
"""
|
|
45
|
+
out = kwargs.get("out")
|
|
46
|
+
if out is None:
|
|
47
|
+
return kwargs, []
|
|
48
|
+
is_tuple = isinstance(out, tuple)
|
|
49
|
+
pairs = []
|
|
50
|
+
new_out = []
|
|
51
|
+
for o in out if is_tuple else (out,):
|
|
52
|
+
if isinstance(o, DuckArray):
|
|
53
|
+
dense = np.asarray(o)
|
|
54
|
+
pairs.append((o, dense))
|
|
55
|
+
new_out.append(dense)
|
|
56
|
+
else:
|
|
57
|
+
new_out.append(o)
|
|
58
|
+
new_kwargs = dict(kwargs)
|
|
59
|
+
new_kwargs["out"] = tuple(new_out) if is_tuple else new_out[0]
|
|
60
|
+
return new_kwargs, pairs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _write_back_out(result, pairs: list):
|
|
64
|
+
"""
|
|
65
|
+
Fold dense ``out`` buffers back into their DuckArrays (re-encoding
|
|
66
|
+
them) and return those DuckArrays in place of the buffers.
|
|
67
|
+
|
|
68
|
+
:param result: The value returned by the NumPy call.
|
|
69
|
+
:param pairs: As returned by :func:`_densify_out`.
|
|
70
|
+
:returns: ``result``, with dense buffers replaced by their DuckArrays.
|
|
71
|
+
"""
|
|
72
|
+
if not pairs:
|
|
73
|
+
return result
|
|
74
|
+
swap = {}
|
|
75
|
+
for duck, dense in pairs:
|
|
76
|
+
duck.consolidate(dense=dense)
|
|
77
|
+
swap[id(dense)] = duck
|
|
78
|
+
if isinstance(result, tuple):
|
|
79
|
+
return tuple(swap.get(id(r), r) for r in result)
|
|
80
|
+
return swap.get(id(result), result)
|
|
81
|
+
|
|
82
|
+
|
|
16
83
|
class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
17
84
|
"""
|
|
18
85
|
A NumPy-like duck array backed by a compressed payload and codec defining
|
|
@@ -131,8 +198,13 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
131
198
|
|
|
132
199
|
def __array_function__(self, func, types, args, kwargs):
|
|
133
200
|
"""
|
|
134
|
-
Apply a numpy function to this array. Decompresses
|
|
135
|
-
|
|
201
|
+
Apply a numpy function to this array. Decompresses any DuckArrays
|
|
202
|
+
among the arguments (including inside lists/tuples/dicts, e.g. for
|
|
203
|
+
``np.concatenate``) before applying the NumPy function.
|
|
204
|
+
|
|
205
|
+
If a DuckArray is given as ``out=``, the result is computed into a
|
|
206
|
+
dense buffer and then re-encoded into that DuckArray (see
|
|
207
|
+
:meth:`consolidate`), which is returned in place of the buffer.
|
|
136
208
|
|
|
137
209
|
NOTE: Some codecs support efficient operations directly
|
|
138
210
|
on the compressed representation. We should provide a way
|
|
@@ -148,13 +220,19 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
148
220
|
:param kwargs: Keyword arguments to ``func``.
|
|
149
221
|
:returns: The result of calling ``func`` on materialized arrays.
|
|
150
222
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
223
|
+
kwargs, out_pairs = _densify_out(kwargs)
|
|
224
|
+
result = func(*_materialize(args), **_materialize(kwargs))
|
|
225
|
+
return _write_back_out(result, out_pairs)
|
|
153
226
|
|
|
154
227
|
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
155
228
|
"""
|
|
156
229
|
Materializes the array then applies the ufunc.
|
|
157
230
|
|
|
231
|
+
DuckArrays given as ``out=`` (including via in-place operators such
|
|
232
|
+
as ``+=``) or as the target of ``ufunc.at`` are updated by computing
|
|
233
|
+
into a dense buffer and re-encoding it (see :meth:`consolidate`).
|
|
234
|
+
Re-encoding with a lossy codec can introduce reconstruction error.
|
|
235
|
+
|
|
158
236
|
NOTE: We should explore per-codec optimizations.
|
|
159
237
|
|
|
160
238
|
:param ufunc: The NumPy ufunc being dispatched.
|
|
@@ -163,10 +241,20 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
163
241
|
:param kwargs: Keyword arguments to the ufunc.
|
|
164
242
|
:returns: The result of calling the ufunc on materialized arrays.
|
|
165
243
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
244
|
+
kwargs, out_pairs = _densify_out(kwargs)
|
|
245
|
+
|
|
246
|
+
# ufunc.at modifies its first input in place.
|
|
247
|
+
if method == "at" and isinstance(inputs[0], DuckArray):
|
|
248
|
+
target = inputs[0]
|
|
249
|
+
dense = np.asarray(target)
|
|
250
|
+
ufunc.at(dense, *_materialize(inputs[1:]), **_materialize(kwargs))
|
|
251
|
+
target.consolidate(dense=dense)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
result = getattr(ufunc, method)(
|
|
255
|
+
*_materialize(inputs), **_materialize(kwargs)
|
|
256
|
+
)
|
|
257
|
+
return _write_back_out(result, out_pairs)
|
|
170
258
|
|
|
171
259
|
def __repr__(self) -> str:
|
|
172
260
|
ratio = self._codec.compression_ratio(self._payload)
|
|
@@ -176,41 +264,25 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
176
264
|
f"overlay={self.overlay_size})"
|
|
177
265
|
)
|
|
178
266
|
|
|
179
|
-
def
|
|
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]]
|
|
267
|
+
def _selected_positions(self, key) -> tuple[np.ndarray, ...]:
|
|
186
268
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _normalize_key(key, ndim: int) -> tuple:
|
|
269
|
+
Find the physical position of every element selected by ``key``.
|
|
270
|
+
|
|
271
|
+
NumPy itself applies ``key`` (to zero-copy broadcast views of
|
|
272
|
+
per-axis index grids), so the result follows NumPy's indexing
|
|
273
|
+
semantics: negative indices, bounds checks, slices,
|
|
274
|
+
broadcast/paired advanced indices, ellipsis, newaxis, and boolean
|
|
275
|
+
masks.
|
|
276
|
+
|
|
277
|
+
:param key: A NumPy-style index key.
|
|
278
|
+
:returns: One integer array per axis, each with the shape of
|
|
279
|
+
``dense[key]``, giving the coordinates of each
|
|
280
|
+
selected element.
|
|
281
|
+
:rtype: tuple[numpy.ndarray, ...]
|
|
282
|
+
:raises IndexError: If ``key`` is invalid for this array's shape.
|
|
202
283
|
"""
|
|
203
|
-
|
|
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))
|
|
284
|
+
grids = np.indices(self.shape, sparse=True)
|
|
285
|
+
return tuple(np.broadcast_to(g, self.shape)[key] for g in grids)
|
|
214
286
|
|
|
215
287
|
def __getitem__(self, key):
|
|
216
288
|
"""
|
|
@@ -223,28 +295,24 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
223
295
|
if not self._overlay:
|
|
224
296
|
return self._codec.decode_partial(self._payload, key)
|
|
225
297
|
|
|
226
|
-
|
|
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
|
-
]
|
|
298
|
+
positions = self._selected_positions(key)
|
|
233
299
|
base = np.array(
|
|
234
300
|
self._codec.decode_partial(self._payload, key), copy=True
|
|
235
301
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
302
|
+
|
|
303
|
+
# Match pending overlay writes to the selected elements by flat
|
|
304
|
+
# physical index.
|
|
305
|
+
selected = np.ravel_multi_index(positions, self.shape)
|
|
306
|
+
overlay_idx = np.ravel_multi_index(
|
|
307
|
+
tuple(np.array(list(self._overlay)).T), self.shape
|
|
308
|
+
)
|
|
309
|
+
overlay_vals = np.array(list(self._overlay.values()), dtype=self.dtype)
|
|
310
|
+
hit = np.isin(selected, overlay_idx)
|
|
311
|
+
if hit.any():
|
|
312
|
+
order = np.argsort(overlay_idx)
|
|
313
|
+
loc = np.searchsorted(overlay_idx, selected[hit], sorter=order)
|
|
314
|
+
base[hit] = overlay_vals[order[loc]]
|
|
315
|
+
return base[()] if base.ndim == 0 else base
|
|
248
316
|
|
|
249
317
|
def __setitem__(self, key, value) -> None:
|
|
250
318
|
"""
|
|
@@ -277,10 +345,9 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
277
345
|
except NotImplementedError:
|
|
278
346
|
pass
|
|
279
347
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
size *= len(a)
|
|
348
|
+
positions = self._selected_positions(key)
|
|
349
|
+
target_shape = positions[0].shape
|
|
350
|
+
size = positions[0].size
|
|
284
351
|
|
|
285
352
|
if (
|
|
286
353
|
size > self._max_pending_writes
|
|
@@ -291,10 +358,21 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
291
358
|
self.consolidate(dense=dense)
|
|
292
359
|
return
|
|
293
360
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
361
|
+
values = np.asarray(value).astype(self.dtype, copy=False)
|
|
362
|
+
# Like NumPy assignment, allow extra leading length-1 dims on value.
|
|
363
|
+
while values.ndim > len(target_shape) and values.shape[0] == 1:
|
|
364
|
+
values = values[0]
|
|
365
|
+
try:
|
|
366
|
+
values = np.broadcast_to(values, target_shape)
|
|
367
|
+
except ValueError:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"could not broadcast input array from shape "
|
|
370
|
+
f"{np.shape(value)} into shape {target_shape}"
|
|
371
|
+
) from None
|
|
372
|
+
|
|
373
|
+
flat_positions = [p.ravel() for p in positions]
|
|
374
|
+
for idx, val in zip(zip(*flat_positions), values.ravel()):
|
|
375
|
+
self._overlay[tuple(int(i) for i in idx)] = val
|
|
298
376
|
|
|
299
377
|
def consolidate(
|
|
300
378
|
self,
|
|
@@ -305,19 +383,22 @@ class DuckArray(np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
305
383
|
"""
|
|
306
384
|
Fold the write overlay into a freshly-compressed payload.
|
|
307
385
|
|
|
308
|
-
By default, re-encodes using whatever compression
|
|
309
|
-
current payload already used.
|
|
386
|
+
By default, re-encodes using whatever compression parameters the
|
|
387
|
+
current payload already used. Any ``encode_kwargs`` given override
|
|
388
|
+
those (see :meth:`~comdas.codecs.base.Codec.merge_compression_params`).
|
|
310
389
|
|
|
311
390
|
:param dense: Precomputed dense array to encode, if the
|
|
312
391
|
caller already has one.
|
|
313
392
|
:type dense: numpy.ndarray or None
|
|
314
|
-
:param encode_kwargs:
|
|
315
|
-
|
|
393
|
+
:param encode_kwargs: Codec-specific kwargs forwarded to
|
|
394
|
+
``codec.encode``, overriding the current payload's parameters.
|
|
316
395
|
"""
|
|
317
396
|
if dense is None:
|
|
318
397
|
dense = self.__array__()
|
|
319
398
|
|
|
320
|
-
|
|
399
|
+
params = self._codec.merge_compression_params(
|
|
400
|
+
self._payload.compression_params, encode_kwargs
|
|
401
|
+
)
|
|
321
402
|
|
|
322
|
-
self._payload = self._codec.encode(dense, **
|
|
403
|
+
self._payload = self._codec.encode(dense, **params)
|
|
323
404
|
self._overlay.clear()
|
|
@@ -70,11 +70,17 @@ class Codec(ABC):
|
|
|
70
70
|
:cvar max_pending_writes: Maximum number of pending writes before automatic consolidation.
|
|
71
71
|
This is only used by codecs that do not implement :meth:`partial_write`.
|
|
72
72
|
:vartype max_pending_writes: int
|
|
73
|
+
:cvar exclusive_params: Names of mutually exclusive ``encode`` parameters
|
|
74
|
+
that select the compression mode (e.g. ``("rank", "energy")``). When
|
|
75
|
+
a caller overrides one of them, the others are dropped from the
|
|
76
|
+
payload's stored parameters. See :meth:`merge_compression_params`.
|
|
77
|
+
:vartype exclusive_params: tuple[str, ...]
|
|
73
78
|
"""
|
|
74
79
|
|
|
75
80
|
name: str = ""
|
|
76
81
|
version: str = "1"
|
|
77
82
|
max_pending_writes: int = 10_000
|
|
83
|
+
exclusive_params: tuple[str, ...] = ()
|
|
78
84
|
|
|
79
85
|
_registry: dict[str, type[Codec]] = {}
|
|
80
86
|
|
|
@@ -211,20 +217,55 @@ class Codec(ABC):
|
|
|
211
217
|
:rtype: int
|
|
212
218
|
"""
|
|
213
219
|
|
|
220
|
+
def merge_compression_params(self, stored: dict, overrides: dict) -> dict:
|
|
221
|
+
"""
|
|
222
|
+
Combine a payload's stored compression parameters with explicit
|
|
223
|
+
overrides, for re-encoding (e.g. in
|
|
224
|
+
:meth:`~comdas.arrays.duck_array.DuckArray.consolidate`).
|
|
225
|
+
|
|
226
|
+
Overrides win. If an override selects one of the mutually exclusive
|
|
227
|
+
:attr:`exclusive_params`, the other exclusive params are dropped
|
|
228
|
+
from ``stored`` so the old mode doesn't conflict with the new one.
|
|
229
|
+
Conflicts *within* ``overrides`` are left for :meth:`encode` to
|
|
230
|
+
reject.
|
|
231
|
+
|
|
232
|
+
:param stored: The payload's ``compression_params``.
|
|
233
|
+
:type stored: dict
|
|
234
|
+
:param overrides: Explicit keyword arguments from the caller.
|
|
235
|
+
:type overrides: dict
|
|
236
|
+
:returns: The parameters to pass to :meth:`encode`.
|
|
237
|
+
:rtype: dict
|
|
238
|
+
"""
|
|
239
|
+
merged = dict(stored)
|
|
240
|
+
if any(k in self.exclusive_params for k in overrides):
|
|
241
|
+
for k in self.exclusive_params:
|
|
242
|
+
merged.pop(k, None)
|
|
243
|
+
merged.update(overrides)
|
|
244
|
+
return merged
|
|
245
|
+
|
|
214
246
|
def compression_ratio(self, payload: CompressedPayload) -> float:
|
|
215
247
|
"""
|
|
216
|
-
Helper to determine the compression ratio of the payload
|
|
248
|
+
Helper to determine the compression ratio of the payload's stored
|
|
249
|
+
arrays.
|
|
250
|
+
|
|
251
|
+
This only accounts for the payload's arrays, not metadata or HDF5
|
|
252
|
+
container overhead, so it is not the on-disk file compression ratio.
|
|
217
253
|
|
|
218
254
|
:param payload: A payload previously produced by :meth:`encode`.
|
|
219
255
|
:type payload: CompressedPayload
|
|
220
|
-
:returns: Original bytes divided by compressed bytes.
|
|
256
|
+
:returns: Original bytes divided by compressed bytes. ``inf`` if the
|
|
257
|
+
payload stores no bytes for a non-empty array, and ``nan`` if the
|
|
258
|
+
original array is empty too.
|
|
221
259
|
:rtype: float
|
|
222
260
|
"""
|
|
223
261
|
original_bytes = (
|
|
224
262
|
int(np.prod(payload.original_shape))
|
|
225
263
|
* np.dtype(payload.dtype).itemsize
|
|
226
264
|
)
|
|
227
|
-
|
|
265
|
+
compressed_bytes = self.compressed_size_bytes(payload)
|
|
266
|
+
if compressed_bytes == 0:
|
|
267
|
+
return float("inf") if original_bytes > 0 else float("nan")
|
|
268
|
+
return original_bytes / compressed_bytes
|
|
228
269
|
|
|
229
270
|
@abstractmethod
|
|
230
271
|
def payload_to_group(self, payload: CompressedPayload, group) -> None:
|
|
@@ -78,6 +78,7 @@ class SVDCodec(Codec):
|
|
|
78
78
|
|
|
79
79
|
# Writes are expensive with SVD
|
|
80
80
|
max_pending_writes = 10_000
|
|
81
|
+
exclusive_params = ("rank", "energy")
|
|
81
82
|
|
|
82
83
|
def __init__(self, rank: int | None = None, energy: float | None = None):
|
|
83
84
|
if rank is not None and energy is not None:
|
|
@@ -166,26 +167,34 @@ class SVDCodec(Codec):
|
|
|
166
167
|
|
|
167
168
|
:param payload: A payload previously produced by :meth:`encode`.
|
|
168
169
|
:type payload: SVDPayload
|
|
169
|
-
:param key:
|
|
170
|
+
:param key: A NumPy-style index. Up to two ints/slices take the
|
|
171
|
+
fast path; anything else falls back to a full decode.
|
|
170
172
|
:returns: The requested subset of the reconstructed array.
|
|
171
173
|
:rtype: numpy.ndarray
|
|
174
|
+
:raises IndexError: If ``key`` is invalid for a 2D array (e.g. too
|
|
175
|
+
many indices or out of bounds), as with NumPy.
|
|
172
176
|
"""
|
|
173
177
|
if not isinstance(key, tuple):
|
|
174
178
|
key = (key,)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
|
|
180
|
+
# Validate the key against the array shape using NumPy's rules.
|
|
181
|
+
np.broadcast_to(np.empty((), dtype=bool), payload.original_shape)[key]
|
|
182
|
+
|
|
183
|
+
def _is_simple(k) -> bool:
|
|
184
|
+
return isinstance(k, (int, np.integer, slice)) and not isinstance(
|
|
185
|
+
k, (bool, np.bool_)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# If using complex indexing (anything more than up to two simple
|
|
189
|
+
# ints/slices, e.g. arrays, ellipsis, newaxis) fall back to full decode
|
|
190
|
+
if len(key) > 2 or not all(_is_simple(k) for k in key):
|
|
184
191
|
warn(
|
|
185
192
|
"Only simple int/slice indexing is supported for partial decoding. Fully decoding entire array and then applying the index."
|
|
186
193
|
)
|
|
187
194
|
return self.decode(payload)[key]
|
|
188
195
|
|
|
196
|
+
key = key + (slice(None),) * (2 - len(key))
|
|
197
|
+
row_key, col_key = key
|
|
189
198
|
row_is_int = isinstance(row_key, (int, np.integer))
|
|
190
199
|
col_is_int = isinstance(col_key, (int, np.integer))
|
|
191
200
|
|
|
@@ -27,12 +27,16 @@ class WaveletPayload(CompressedPayload):
|
|
|
27
27
|
:vartype indices: numpy.ndarray
|
|
28
28
|
:ivar values: Values of retained coefficients.
|
|
29
29
|
:vartype values: numpy.ndarray
|
|
30
|
+
:ivar threshold: The absolute threshold used to select coefficients, if
|
|
31
|
+
encoded in threshold mode. ``None`` if encoded with ``keep_fraction``.
|
|
32
|
+
:vartype threshold: float or None
|
|
30
33
|
"""
|
|
31
34
|
|
|
32
35
|
wavelet: str
|
|
33
36
|
mode: str
|
|
34
37
|
indices: np.ndarray
|
|
35
38
|
values: np.ndarray
|
|
39
|
+
threshold: float | None = None
|
|
36
40
|
|
|
37
41
|
@property
|
|
38
42
|
def coeff_shapes(self) -> list:
|
|
@@ -63,13 +67,18 @@ class WaveletPayload(CompressedPayload):
|
|
|
63
67
|
"""
|
|
64
68
|
The compression parameters used for this payload.
|
|
65
69
|
|
|
70
|
+
Threshold-encoded payloads report their ``threshold``, so re-encoding
|
|
71
|
+
applies the same rule (a derived ``keep_fraction`` could be 0, which
|
|
72
|
+
isn't a valid input). Otherwise the retained ``keep_fraction``.
|
|
73
|
+
|
|
66
74
|
:rtype: dict
|
|
67
75
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
params = {"wavelet": self.wavelet, "mode": self.mode}
|
|
77
|
+
if self.threshold is not None:
|
|
78
|
+
params["threshold"] = self.threshold
|
|
79
|
+
else:
|
|
80
|
+
params["keep_fraction"] = self.values.size / self.n_coeffs
|
|
81
|
+
return params
|
|
73
82
|
|
|
74
83
|
|
|
75
84
|
class WaveletCodec(Codec):
|
|
@@ -98,6 +107,7 @@ class WaveletCodec(Codec):
|
|
|
98
107
|
"""
|
|
99
108
|
|
|
100
109
|
name = "WAVELET"
|
|
110
|
+
exclusive_params = ("keep_fraction", "threshold")
|
|
101
111
|
version = "1"
|
|
102
112
|
|
|
103
113
|
def __init__(
|
|
@@ -191,6 +201,7 @@ class WaveletCodec(Codec):
|
|
|
191
201
|
mode=mode,
|
|
192
202
|
indices=indices.astype(index_dtype),
|
|
193
203
|
values=np.ascontiguousarray(flat[indices]),
|
|
204
|
+
threshold=float(threshold) if threshold is not None else None,
|
|
194
205
|
)
|
|
195
206
|
|
|
196
207
|
def decode(self, payload: WaveletPayload) -> np.ndarray:
|
|
@@ -244,6 +255,8 @@ class WaveletCodec(Codec):
|
|
|
244
255
|
group.create_dataset("values", data=payload.values)
|
|
245
256
|
group.attrs["wavelet"] = payload.wavelet
|
|
246
257
|
group.attrs["mode"] = payload.mode
|
|
258
|
+
if payload.threshold is not None:
|
|
259
|
+
group.attrs["threshold"] = payload.threshold
|
|
247
260
|
|
|
248
261
|
def payload_from_group(
|
|
249
262
|
self, group, *, original_shape: tuple[int, ...], dtype: np.dtype
|
|
@@ -270,4 +283,9 @@ class WaveletCodec(Codec):
|
|
|
270
283
|
mode=str(group.attrs["mode"]),
|
|
271
284
|
indices=group["indices"][:],
|
|
272
285
|
values=group["values"][:],
|
|
286
|
+
threshold=(
|
|
287
|
+
float(group.attrs["threshold"])
|
|
288
|
+
if "threshold" in group.attrs
|
|
289
|
+
else None
|
|
290
|
+
),
|
|
273
291
|
)
|