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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FiberIO plugin for the ComDAS HDF5 container format.
|
|
3
|
+
|
|
4
|
+
The ComDAS HDF5 container format is designed to store patches
|
|
5
|
+
in a codec-agnostic manner
|
|
6
|
+
|
|
7
|
+
Registers the ``COMDAS`` format with DASCore's FiberIO manager. Once
|
|
8
|
+
this module is imported (or the package is pip-installed, via the
|
|
9
|
+
``dascore.fiber_io`` entry point in ``pyproject.toml``), ``dc.spool(path)``
|
|
10
|
+
and ``dc.write(patches, path, file_format="COMDAS")`` work
|
|
11
|
+
like any other DASCore format regardless of which
|
|
12
|
+
:class:`~comdas.codecs.base.Codec` was used to compress each patch.
|
|
13
|
+
|
|
14
|
+
File layout
|
|
15
|
+
-----------
|
|
16
|
+
::
|
|
17
|
+
|
|
18
|
+
/ (root)
|
|
19
|
+
attrs:
|
|
20
|
+
__format__ = "COMDAS"
|
|
21
|
+
__COMDAS_version__ = "1"
|
|
22
|
+
/patches
|
|
23
|
+
/patch_0000
|
|
24
|
+
attrs:
|
|
25
|
+
codec_name (str) -- e.g. "SVD"; looked up in the codec registry
|
|
26
|
+
codec_version (str) -- the codec's own version string
|
|
27
|
+
compression_params (dict) -- parameters used for compression
|
|
28
|
+
original_shape (tuple[int, ...])
|
|
29
|
+
dtype (str) -- original array dtype
|
|
30
|
+
dims (str) -- comma-separated dim names, in order
|
|
31
|
+
attrs_json (str) -- PatchAttrs.model_dump_json()
|
|
32
|
+
<codec-specific datasets/attrs, written by codec.payload_to_group>
|
|
33
|
+
/coords
|
|
34
|
+
<dim_name> -- one dataset per dim, full coord array
|
|
35
|
+
attrs: dtype (str), units (str, empty if none)
|
|
36
|
+
/patch_0001
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
Everything above the codec-specific datasets is written and read by
|
|
40
|
+
this module. Only what's inside a patch's own group (beyond
|
|
41
|
+
``coords``) is delegated to ``codec.payload_to_group`` /
|
|
42
|
+
``codec.payload_from_group``.
|
|
43
|
+
|
|
44
|
+
Patches are appended, not overwritten: calling ``write`` again on an
|
|
45
|
+
existing file adds new ``patch_XXXX`` groups after the existing ones
|
|
46
|
+
(``H5Writer`` opens in ``"a"`` mode).
|
|
47
|
+
|
|
48
|
+
Once ``comdas`` is installed, plain ``dc.spool(path)`` finds and uses this
|
|
49
|
+
format automatically. Writing has one convenience function,
|
|
50
|
+
:func:`write_compressed`.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import numpy as np
|
|
56
|
+
|
|
57
|
+
import dascore as dc
|
|
58
|
+
from dascore.io import FiberIO
|
|
59
|
+
from dascore.utils.hdf5 import H5Reader, H5Writer
|
|
60
|
+
|
|
61
|
+
from comdas.arrays.duck_array import DuckArray
|
|
62
|
+
from comdas.codecs.base import Codec
|
|
63
|
+
|
|
64
|
+
FORMAT_NAME = "COMDAS"
|
|
65
|
+
FORMAT_VERSION = "1"
|
|
66
|
+
_PATCH_GROUP_PREFIX = "patch_"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _patch_group_names(h5file) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
List patch group names in a COMDAS file, in write order.
|
|
72
|
+
|
|
73
|
+
:param h5file: An open ``h5py.File`` for a COMDAS container.
|
|
74
|
+
:returns: Sorted patch group names (empty if the file has none yet).
|
|
75
|
+
:rtype: list[str]
|
|
76
|
+
"""
|
|
77
|
+
if "patches" not in h5file:
|
|
78
|
+
return []
|
|
79
|
+
return sorted(h5file["patches"].keys())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _next_patch_index(h5file) -> int:
|
|
83
|
+
"""
|
|
84
|
+
The next unused ``patch_XXXX`` index for appending to ``h5file``.
|
|
85
|
+
|
|
86
|
+
:param h5file: An open ``h5py.File`` for a COMDAS container.
|
|
87
|
+
:returns: The next patch index to use.
|
|
88
|
+
:rtype: int
|
|
89
|
+
"""
|
|
90
|
+
names = _patch_group_names(h5file)
|
|
91
|
+
if not names:
|
|
92
|
+
return 0
|
|
93
|
+
return int(names[-1][len(_PATCH_GROUP_PREFIX) :]) + 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _dump_coord_array(arr: np.ndarray) -> tuple[np.ndarray, str]:
|
|
97
|
+
"""
|
|
98
|
+
Convert a coordinate array to something h5py can store directly.
|
|
99
|
+
|
|
100
|
+
h5py can't store ``datetime64``/``timedelta64`` directly, so those
|
|
101
|
+
are viewed as ``int64`` for storage; the exact original dtype
|
|
102
|
+
string is returned so :func:`_load_coord_array` can cast back.
|
|
103
|
+
|
|
104
|
+
:param arr: The coordinate array to prepare for storage.
|
|
105
|
+
:type arr: numpy.ndarray
|
|
106
|
+
:returns: A ``(storable_array, dtype_str)`` pair.
|
|
107
|
+
:rtype: tuple[numpy.ndarray, str]
|
|
108
|
+
"""
|
|
109
|
+
dtype_str = str(arr.dtype)
|
|
110
|
+
if np.issubdtype(arr.dtype, np.datetime64) or np.issubdtype(
|
|
111
|
+
arr.dtype, np.timedelta64
|
|
112
|
+
):
|
|
113
|
+
return arr.astype("int64"), dtype_str
|
|
114
|
+
return arr, dtype_str
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _load_coord_array(arr: np.ndarray, dtype_str: str) -> np.ndarray:
|
|
118
|
+
"""
|
|
119
|
+
Invert :func:`_dump_coord_array`.
|
|
120
|
+
|
|
121
|
+
:param arr: The array as read from the HDF5 dataset.
|
|
122
|
+
:type arr: numpy.ndarray
|
|
123
|
+
:param dtype_str: The original dtype string, as stored on write.
|
|
124
|
+
:type dtype_str: str
|
|
125
|
+
:returns: The coordinate array cast back to its original dtype.
|
|
126
|
+
:rtype: numpy.ndarray
|
|
127
|
+
"""
|
|
128
|
+
return arr.astype(dtype_str)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _range_overlaps(requested, actual_min, actual_max) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Whether a ``(lo, hi)`` request overlaps an ``[actual_min, actual_max]`` range.
|
|
134
|
+
|
|
135
|
+
:param requested: A ``(lo, hi)`` tuple (either bound may be
|
|
136
|
+
``None`` for "unbounded"), or ``None`` for "no constraint".
|
|
137
|
+
:param actual_min: The range's actual minimum, or ``None``.
|
|
138
|
+
:param actual_max: The range's actual maximum, or ``None``.
|
|
139
|
+
:returns: True if the two ranges overlap (or either is unbounded).
|
|
140
|
+
:rtype: bool
|
|
141
|
+
"""
|
|
142
|
+
if requested is None:
|
|
143
|
+
return True
|
|
144
|
+
lo, hi = requested
|
|
145
|
+
if lo is not None and actual_max is not None and lo > actual_max:
|
|
146
|
+
return False
|
|
147
|
+
if hi is not None and actual_min is not None and hi < actual_min:
|
|
148
|
+
return False
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _patch_matches_kwargs(attrs: dc.PatchAttrs, kwargs: dict) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Whether a stored patch's coord ranges match DASCore's selection kwargs.
|
|
155
|
+
|
|
156
|
+
:param attrs: The candidate patch's attrs (with a ``coords``
|
|
157
|
+
summary mapping dim name to a min/max-bearing object).
|
|
158
|
+
:type attrs: dascore.PatchAttrs
|
|
159
|
+
:param kwargs: Keyword arguments as received by
|
|
160
|
+
:meth:`ComdasV1.read`; only keys matching a dim name in
|
|
161
|
+
``attrs.coords`` are consulted.
|
|
162
|
+
:returns: True if every dim-range constraint in ``kwargs`` overlaps
|
|
163
|
+
this patch's own range for that dim.
|
|
164
|
+
:rtype: bool
|
|
165
|
+
"""
|
|
166
|
+
for dim, summary in attrs.coords.items():
|
|
167
|
+
if dim not in kwargs or kwargs[dim] is None:
|
|
168
|
+
continue
|
|
169
|
+
if not _range_overlaps(kwargs[dim], summary.min, summary.max):
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _write_patch(
|
|
175
|
+
patches_group, name: str, patch, codec: Codec, encode_kwargs: dict
|
|
176
|
+
) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Compress and write one patch into ``patches_group[name]``.
|
|
179
|
+
|
|
180
|
+
:param patches_group: The open ``/patches`` HDF5 group.
|
|
181
|
+
:type patches_group: h5py.Group
|
|
182
|
+
:param name: The new patch group's name (e.g. ``"patch_0003"``).
|
|
183
|
+
:type name: str
|
|
184
|
+
:param patch: The (uncompressed) patch to write.
|
|
185
|
+
:type patch: dascore.Patch
|
|
186
|
+
:param codec: The codec to compress ``patch.data`` with.
|
|
187
|
+
:type codec: Codec
|
|
188
|
+
:param encode_kwargs: Forwarded to ``codec.encode``.
|
|
189
|
+
:type encode_kwargs: dict
|
|
190
|
+
:raises ValueError: If ``patch.data`` is not 2D (the only shape
|
|
191
|
+
ComDAS codecs currently support).
|
|
192
|
+
"""
|
|
193
|
+
dense = np.asarray(patch.data)
|
|
194
|
+
if dense.ndim != 2:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"ComDAS codecs currently only support 2D patches (e.g. "
|
|
197
|
+
f"channel x time), got shape {dense.shape} for dims {patch.dims}"
|
|
198
|
+
)
|
|
199
|
+
payload = codec.encode(dense, **encode_kwargs)
|
|
200
|
+
|
|
201
|
+
grp = patches_group.create_group(name)
|
|
202
|
+
grp.attrs["codec_name"] = codec.name
|
|
203
|
+
grp.attrs["codec_version"] = codec.version
|
|
204
|
+
grp.attrs["original_shape"] = payload.original_shape
|
|
205
|
+
grp.attrs["dtype"] = str(payload.dtype)
|
|
206
|
+
grp.attrs["dims"] = ",".join(patch.dims)
|
|
207
|
+
grp.attrs["attrs_json"] = patch.attrs.model_dump_json()
|
|
208
|
+
|
|
209
|
+
codec.payload_to_group(payload, grp)
|
|
210
|
+
|
|
211
|
+
coord_grp = grp.create_group("coords")
|
|
212
|
+
for dim in patch.dims:
|
|
213
|
+
arr = patch.coords.get_array(dim)
|
|
214
|
+
storable, dtype_str = _dump_coord_array(arr)
|
|
215
|
+
cds = coord_grp.create_dataset(dim, data=storable)
|
|
216
|
+
cds.attrs["dtype"] = dtype_str
|
|
217
|
+
coord = patch.coords.get_coord(dim)
|
|
218
|
+
units = getattr(coord, "units", None)
|
|
219
|
+
cds.attrs["units"] = str(units) if units is not None else ""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _read_patch(patch_group) -> dc.Patch:
|
|
223
|
+
"""
|
|
224
|
+
Read one patch group back into an ordinary compressed Patch.
|
|
225
|
+
|
|
226
|
+
:param patch_group: An open patch group (e.g. ``f["patches"]["patch_0000"]``).
|
|
227
|
+
:type patch_group: h5py.Group
|
|
228
|
+
:returns: The reconstructed Patch, backed by a
|
|
229
|
+
:class:`~comdas.arrays.duck_array.DuckArray`.
|
|
230
|
+
:rtype: dascore.Patch
|
|
231
|
+
"""
|
|
232
|
+
codec_cls = Codec.get_registered(patch_group.attrs["codec_name"])
|
|
233
|
+
codec = codec_cls()
|
|
234
|
+
original_shape = tuple(patch_group.attrs["original_shape"])
|
|
235
|
+
dtype = np.dtype(patch_group.attrs["dtype"])
|
|
236
|
+
payload = codec.payload_from_group(
|
|
237
|
+
patch_group, original_shape=original_shape, dtype=dtype
|
|
238
|
+
)
|
|
239
|
+
compressed_array = DuckArray(payload, codec)
|
|
240
|
+
|
|
241
|
+
dims = tuple(patch_group.attrs["dims"].split(","))
|
|
242
|
+
coord_grp = patch_group["coords"]
|
|
243
|
+
coords = {}
|
|
244
|
+
for dim in dims:
|
|
245
|
+
ds = coord_grp[dim]
|
|
246
|
+
raw = ds[:]
|
|
247
|
+
dtype_str = ds.attrs.get("dtype", str(raw.dtype))
|
|
248
|
+
coords[dim] = _load_coord_array(raw, dtype_str)
|
|
249
|
+
|
|
250
|
+
attrs = dc.PatchAttrs.model_validate_json(patch_group.attrs["attrs_json"])
|
|
251
|
+
|
|
252
|
+
return dc.Patch(
|
|
253
|
+
data=compressed_array, coords=coords, dims=dims, attrs=attrs
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ComdasV1(FiberIO):
|
|
258
|
+
"""
|
|
259
|
+
FiberIO support for the codec-agnostic ComDAS HDF5 container, v1.
|
|
260
|
+
|
|
261
|
+
Each patch stores which :class:`~comdas.codecs.base.Codec` it was
|
|
262
|
+
compressed with. This means a single file can hold patches
|
|
263
|
+
compressed with different codecs.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
name = FORMAT_NAME
|
|
267
|
+
version = FORMAT_VERSION
|
|
268
|
+
preferred_extensions = ("h5", "hdf5")
|
|
269
|
+
|
|
270
|
+
def get_format(
|
|
271
|
+
self, resource: H5Reader, **kwargs
|
|
272
|
+
) -> tuple[str, str] | bool:
|
|
273
|
+
"""
|
|
274
|
+
Identify a COMDAS file via its root-level magic attrs.
|
|
275
|
+
|
|
276
|
+
:param resource: An open, readable HDF5 file (auto-opened by
|
|
277
|
+
DASCore from a path via the ``H5Reader`` type hint).
|
|
278
|
+
:type resource: h5py.File
|
|
279
|
+
:param kwargs: Unused; accepted for FiberIO API compatibility.
|
|
280
|
+
:returns: ``(format_name, format_version)`` if this file is a
|
|
281
|
+
COMDAS container, else ``False``.
|
|
282
|
+
:rtype: tuple[str, str] or bool
|
|
283
|
+
"""
|
|
284
|
+
fmt = resource.attrs.get("__format__")
|
|
285
|
+
if fmt == FORMAT_NAME:
|
|
286
|
+
version = resource.attrs.get(
|
|
287
|
+
f"__{FORMAT_NAME}_version__", FORMAT_VERSION
|
|
288
|
+
)
|
|
289
|
+
return FORMAT_NAME, str(version)
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def scan(self, resource: H5Reader, **kwargs) -> list[dc.PatchAttrs]:
|
|
293
|
+
"""
|
|
294
|
+
Read every patch's attrs without touching datasets.
|
|
295
|
+
|
|
296
|
+
:param resource: An open, readable COMDAS HDF5 file.
|
|
297
|
+
:type resource: h5py.File
|
|
298
|
+
:param kwargs: Unused; accepted for FiberIO API compatibility.
|
|
299
|
+
:returns: One :class:`dascore.PatchAttrs` per stored patch.
|
|
300
|
+
:rtype: list[dascore.PatchAttrs]
|
|
301
|
+
"""
|
|
302
|
+
out = []
|
|
303
|
+
for name in _patch_group_names(resource):
|
|
304
|
+
grp = resource["patches"][name]
|
|
305
|
+
attrs = dc.PatchAttrs.model_validate_json(grp.attrs["attrs_json"])
|
|
306
|
+
attrs = attrs.update(
|
|
307
|
+
file_format=self.name,
|
|
308
|
+
file_version=self.version,
|
|
309
|
+
path=str(resource.filename),
|
|
310
|
+
)
|
|
311
|
+
out.append(attrs)
|
|
312
|
+
return out
|
|
313
|
+
|
|
314
|
+
def read(self, resource: H5Reader, **kwargs) -> dc.BaseSpool:
|
|
315
|
+
"""
|
|
316
|
+
Read patches.
|
|
317
|
+
|
|
318
|
+
Honors dimension-range kwargs.
|
|
319
|
+
|
|
320
|
+
:param resource: An open, readable COMDAS HDF5 file.
|
|
321
|
+
:type resource: h5py.File
|
|
322
|
+
:param kwargs: Optional dimension-range constraints (e.g.
|
|
323
|
+
``time=(t1, t2)``); any key not matching a coord's dim
|
|
324
|
+
name is ignored.
|
|
325
|
+
:returns: A spool of the matching patches.
|
|
326
|
+
:rtype: dascore.BaseSpool
|
|
327
|
+
"""
|
|
328
|
+
patches = []
|
|
329
|
+
for name in _patch_group_names(resource):
|
|
330
|
+
grp = resource["patches"][name]
|
|
331
|
+
attrs = dc.PatchAttrs.model_validate_json(grp.attrs["attrs_json"])
|
|
332
|
+
if _patch_matches_kwargs(attrs, kwargs):
|
|
333
|
+
patches.append(_read_patch(grp))
|
|
334
|
+
return dc.spool(patches)
|
|
335
|
+
|
|
336
|
+
def write(
|
|
337
|
+
self,
|
|
338
|
+
spool,
|
|
339
|
+
resource: H5Writer,
|
|
340
|
+
*,
|
|
341
|
+
codec: Codec | None = None,
|
|
342
|
+
**encode_kwargs,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""
|
|
345
|
+
Compress and write a Patch/spool of Patches to the container.
|
|
346
|
+
|
|
347
|
+
:param spool: A single Patch, or any iterable of Patches, to
|
|
348
|
+
write.
|
|
349
|
+
:type spool: dascore.Patch or dascore.BaseSpool or Sequence[dascore.Patch]
|
|
350
|
+
:param resource: An open, writable HDF5 file (auto-opened by
|
|
351
|
+
DASCore from a path via the ``H5Writer`` type hint, in
|
|
352
|
+
append mode).
|
|
353
|
+
:type resource: h5py.File
|
|
354
|
+
:param codec: The codec to compress every patch in ``spool``
|
|
355
|
+
with. (e.g. ``SVDCodec(rank=20)``.)
|
|
356
|
+
:type codec: Codec
|
|
357
|
+
:param encode_kwargs: Forwarded to ``codec.encode`` for every
|
|
358
|
+
patch.
|
|
359
|
+
:raises ValueError: If ``codec`` is not given.
|
|
360
|
+
"""
|
|
361
|
+
if codec is None:
|
|
362
|
+
raise ValueError(
|
|
363
|
+
"A `codec` must be provided to write a COMDAS container "
|
|
364
|
+
"(e.g. codec=SVDCodec(rank=20))."
|
|
365
|
+
)
|
|
366
|
+
resource.attrs["__format__"] = FORMAT_NAME
|
|
367
|
+
resource.attrs[f"__{FORMAT_NAME}_version__"] = FORMAT_VERSION
|
|
368
|
+
|
|
369
|
+
if "patches" not in resource:
|
|
370
|
+
resource.create_group("patches")
|
|
371
|
+
patches_group = resource["patches"]
|
|
372
|
+
|
|
373
|
+
patches = [spool] if isinstance(spool, dc.Patch) else list(spool)
|
|
374
|
+
idx = _next_patch_index(resource)
|
|
375
|
+
for patch in patches:
|
|
376
|
+
name = f"{_PATCH_GROUP_PREFIX}{idx:04d}"
|
|
377
|
+
_write_patch(patches_group, name, patch, codec, encode_kwargs)
|
|
378
|
+
idx += 1
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def write_compressed(source, path, codec: Codec, **encode_kwargs) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Compress ``source`` and write it to ``path`` as a COMDAS container.
|
|
384
|
+
|
|
385
|
+
This is the intended way to turn uncompressed DAS data
|
|
386
|
+
into a compressed COMDAS file.
|
|
387
|
+
|
|
388
|
+
:param source: Where the uncompressed data comes from (a file or
|
|
389
|
+
directory path, a single Patch, a sequence of Patches, or an
|
|
390
|
+
existing Spool).
|
|
391
|
+
:type source: str or pathlib.Path or dascore.Patch or dascore.BaseSpool or Sequence[dascore.Patch]
|
|
392
|
+
:param path: Destination path for the COMDAS container. If it
|
|
393
|
+
already exists as a COMDAS file, patches are appended rather
|
|
394
|
+
than overwriting existing ones.
|
|
395
|
+
:type path: str or pathlib.Path
|
|
396
|
+
:param codec: The codec to compress every patch with, e.g.
|
|
397
|
+
``SVDCodec(rank=20)``.
|
|
398
|
+
:type codec: Codec
|
|
399
|
+
:param encode_kwargs: Forwarded to ``codec.encode`` for every
|
|
400
|
+
patch (e.g. ``rank=``/``energy=`` for
|
|
401
|
+
:class:`~comdas.codecs.svd.SVDCodec`, overriding the codec's
|
|
402
|
+
own defaults for this call).
|
|
403
|
+
|
|
404
|
+
:Example:
|
|
405
|
+
|
|
406
|
+
.. code-block:: python
|
|
407
|
+
|
|
408
|
+
import dascore as dc
|
|
409
|
+
from comdas import write_compressed, SVDCodec
|
|
410
|
+
|
|
411
|
+
# from an existing uncompressed file
|
|
412
|
+
write_compressed("raw.h5", "compressed.h5", SVDCodec(rank=20))
|
|
413
|
+
|
|
414
|
+
# from a patch already in memory
|
|
415
|
+
patch = dc.get_example_patch()
|
|
416
|
+
write_compressed(patch, "compressed.h5", SVDCodec(rank=20))
|
|
417
|
+
|
|
418
|
+
# reading back needs nothing ComDAS-specific
|
|
419
|
+
read_back = dc.spool("compressed.h5")
|
|
420
|
+
"""
|
|
421
|
+
spool = dc.spool(source)
|
|
422
|
+
dc.write(
|
|
423
|
+
spool,
|
|
424
|
+
path,
|
|
425
|
+
file_format=FORMAT_NAME,
|
|
426
|
+
file_version=FORMAT_VERSION,
|
|
427
|
+
codec=codec,
|
|
428
|
+
**encode_kwargs,
|
|
429
|
+
)
|
comdas/utils.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for working with compressed patches.
|
|
3
|
+
Includes:
|
|
4
|
+
- :func:`compress_patch` Compress an in-memory DASCore patch's data using a specified codec.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from comdas.arrays.duck_array import DuckArray
|
|
12
|
+
from comdas.codecs.base import Codec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compress_patch(patch, codec: Codec, **encode_kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Return a new ``dascore.Patch`` whose data is compressed with ``codec``.
|
|
18
|
+
|
|
19
|
+
Coords and attrs are unchanged. The returned Patch is an ordinary Patch
|
|
20
|
+
(via ``patch.new``) with the internal data replaced by a
|
|
21
|
+
:class:`~comdas.arrays.duck_array.DuckArray`.
|
|
22
|
+
|
|
23
|
+
:param patch: The uncompressed source patch.
|
|
24
|
+
:type patch: dascore.Patch
|
|
25
|
+
:param codec: The codec to compress ``patch.data`` with.
|
|
26
|
+
:type codec: Codec
|
|
27
|
+
:param encode_kwargs: Forwarded verbatim to ``codec.encode``.
|
|
28
|
+
:returns: A new Patch with the same coords/attrs, backed by a
|
|
29
|
+
:class:`~comdas.arrays.duck_array.DuckArray`.
|
|
30
|
+
:rtype: dascore.Patch
|
|
31
|
+
"""
|
|
32
|
+
dense = np.asarray(patch.data)
|
|
33
|
+
payload = codec.encode(dense, **encode_kwargs)
|
|
34
|
+
compressed_array = DuckArray(payload, codec)
|
|
35
|
+
return patch.new(data=compressed_array)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ComDAS
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DASCORE companion package that adds support for compressed DAS data
|
|
5
|
+
Author-email: Byron-Selvage <bselvage@mines.edu>
|
|
6
|
+
License: LGPL-3.0-or-later
|
|
7
|
+
Keywords: dascore,compression,das,dasdae
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: numpy>=1.24
|
|
12
|
+
Requires-Dist: dascore>=0.1.0
|
|
13
|
+
Requires-Dist: h5py
|
|
14
|
+
Requires-Dist: PyWavelets
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff; extra == "dev"
|
|
18
|
+
Requires-Dist: sphinx; extra == "dev"
|
|
19
|
+
Requires-Dist: furo; extra == "dev"
|
|
20
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "dev"
|
|
21
|
+
Requires-Dist: myst-nb; extra == "dev"
|
|
22
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
23
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# ComDAS
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
ComDAS (Compressed DAS) lets you store and work with compressed distributed acoustic sensing (DAS) data using ordinary [DASCore](https://dascore.org/) tools.
|
|
31
|
+
|
|
32
|
+
With ComDAS you can:
|
|
33
|
+
|
|
34
|
+
- **Shrink DAS data in memory.** Compress a DASCore patch and keep using it like any other.
|
|
35
|
+
- **Save compressed files.** Write patches, spools, or whole directories of raw data to a compressed HDF5 file or files.
|
|
36
|
+
- **Read compressed files with plain DASCore.** Once ComDAS is installed, `dascore.spool()` opens compressed files directly without needing to know how the data was compressed.
|
|
37
|
+
- **Choose your compression level.** Pick a codec and set how much detail to keep, from near-lossless to aggressive compression.
|
|
38
|
+
- **Implement your own algorithm.** Add a new compression method by writing a codec class.
|
|
39
|
+
|
|
40
|
+
> [!NOTE]
|
|
41
|
+
> ComDAS is in active development. The file format and public API may change between releases.
|
|
42
|
+
|
|
43
|
+
## Supported codecs
|
|
44
|
+
|
|
45
|
+
| Codec | Main setting | Reference |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| [SVD](https://byron-selvage.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://byron-selvage.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
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
ComDAS requires Python 3.10 or newer.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install comdas
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
Full documentation is available at [byron-selvage.github.io/ComDAS](https://byron-selvage.github.io/ComDAS/), including:
|
|
61
|
+
|
|
62
|
+
- [Quick start](https://byron-selvage.github.io/ComDAS/quickstart.html): compress a patch, save a compressed file, and read it back
|
|
63
|
+
- [Codecs](https://byron-selvage.github.io/ComDAS/codecs/svd.html): configuration options and examples for each codec
|
|
64
|
+
- [Examples](https://byron-selvage.github.io/ComDAS/examples/compression_comparison.html)
|
|
65
|
+
- [API reference](https://byron-selvage.github.io/ComDAS/api/index.html)
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
ComDAS is licensed under the [GNU Lesser General Public License](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
comdas/__init__.py,sha256=R7M9IFbD5BdOqQGCU1onPjTQNSDMn5tNV8t9kJHlQno,1187
|
|
2
|
+
comdas/utils.py,sha256=A-aeA8VlYB9AmyXnqG1SUOCdmV4PFXjfXgSQRpJvcR4,1203
|
|
3
|
+
comdas/arrays/__init__.py,sha256=X1vsUKtcblCQVpi047nGtXsOW556b4nbacH1bNJ9DnQ,221
|
|
4
|
+
comdas/arrays/duck_array.py,sha256=3BN-xESymPtnEtOSTC2UszUtMbDf_-swLMG9cFSIlnU,10775
|
|
5
|
+
comdas/codecs/__init__.py,sha256=baaEoxKe09LuZ2El_wnqhFxRa7RUs-vhgTlz-DN2v7U,356
|
|
6
|
+
comdas/codecs/base.py,sha256=hNSMc7AqItonSNWp7TVhEwBgCX3rBvd2A2aiub3F5cc,9712
|
|
7
|
+
comdas/codecs/svd.py,sha256=RRWhBs29ELiDEdkR-KYMnBwvw_uB4LZHn6nGnGa5dA8,8656
|
|
8
|
+
comdas/codecs/wavelet.py,sha256=-GjauCly_-dqqxSjBW9JTR_GQterzCc2l-vPgImDPAg,9719
|
|
9
|
+
comdas/io/__init__.py,sha256=aoweeIH-QebldKGyZXNLPn4aQFhZ2CMg627kOSE_ywU,564
|
|
10
|
+
comdas/io/h5_container.py,sha256=Xi5WnNKg9nJrFAhjRkq5GqdDXaba-6OyqrUAiqkXQig,15272
|
|
11
|
+
comdas-0.1.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
12
|
+
comdas-0.1.0.dist-info/METADATA,sha256=jIKvsK5Rl9GDFfX9TXa0AqZogoYOLUJQnoZV3eZdwow,3042
|
|
13
|
+
comdas-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
comdas-0.1.0.dist-info/entry_points.txt,sha256=dHkx7dydw-xH3uJ4SYKbhF6EvxjHGFxrh99I5_DV8xY,64
|
|
15
|
+
comdas-0.1.0.dist-info/top_level.txt,sha256=zkMksyvy9YtJW3mt1ZseiS55h0NbPT84Sbt4B2KrRtE,7
|
|
16
|
+
comdas-0.1.0.dist-info/RECORD,,
|