lightspeed-series 0.0.1__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.
- lightspeed_series-0.0.1/LICENSE +21 -0
- lightspeed_series-0.0.1/PKG-INFO +79 -0
- lightspeed_series-0.0.1/README.md +51 -0
- lightspeed_series-0.0.1/examples/demo.py +75 -0
- lightspeed_series-0.0.1/pyproject.toml +64 -0
- lightspeed_series-0.0.1/setup.cfg +4 -0
- lightspeed_series-0.0.1/src/lightspeed/__init__.py +43 -0
- lightspeed_series-0.0.1/src/lightspeed/arraysource.py +96 -0
- lightspeed_series-0.0.1/src/lightspeed/cache.py +531 -0
- lightspeed_series-0.0.1/src/lightspeed/cachedir.py +58 -0
- lightspeed_series-0.0.1/src/lightspeed/constants.py +26 -0
- lightspeed_series-0.0.1/src/lightspeed/exceptions.py +18 -0
- lightspeed_series-0.0.1/src/lightspeed/hashing.py +20 -0
- lightspeed_series-0.0.1/src/lightspeed/memo.py +109 -0
- lightspeed_series-0.0.1/src/lightspeed/mpl.py +447 -0
- lightspeed_series-0.0.1/src/lightspeed/py.typed +0 -0
- lightspeed_series-0.0.1/src/lightspeed/pyramid.py +225 -0
- lightspeed_series-0.0.1/src/lightspeed/query.py +260 -0
- lightspeed_series-0.0.1/src/lightspeed/status.py +120 -0
- lightspeed_series-0.0.1/src/lightspeed/timing.py +37 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/PKG-INFO +79 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/SOURCES.txt +34 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/dependency_links.txt +1 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/requires.txt +4 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/scm_file_list.json +31 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/scm_version.json +8 -0
- lightspeed_series-0.0.1/src/lightspeed_series.egg-info/top_level.txt +1 -0
- lightspeed_series-0.0.1/tests/test_cache.py +233 -0
- lightspeed_series-0.0.1/tests/test_cachedir.py +61 -0
- lightspeed_series-0.0.1/tests/test_hashing.py +40 -0
- lightspeed_series-0.0.1/tests/test_locking.py +88 -0
- lightspeed_series-0.0.1/tests/test_memo.py +76 -0
- lightspeed_series-0.0.1/tests/test_mpl.py +337 -0
- lightspeed_series-0.0.1/tests/test_pyramid.py +125 -0
- lightspeed_series-0.0.1/tests/test_query.py +162 -0
- lightspeed_series-0.0.1/uv.lock +2136 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mike Hull
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lightspeed-series
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Fast, zoomable plots of very large series, from min/max level-of-detail pyramids cached in HDF5.
|
|
5
|
+
Author-email: Mike Hull <mikehulluk@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikehulluk/lightspeed
|
|
8
|
+
Project-URL: Source, https://github.com/mikehulluk/lightspeed
|
|
9
|
+
Project-URL: Issues, https://github.com/mikehulluk/lightspeed/issues
|
|
10
|
+
Keywords: time series,large data,plotting,level of detail,hdf5,matplotlib,downsampling
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: filelock>=3.12
|
|
24
|
+
Requires-Dist: h5py>=3.10
|
|
25
|
+
Requires-Dist: matplotlib>=3.8
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# lightspeed
|
|
30
|
+
|
|
31
|
+
Fast, zoomable plots of very large series.
|
|
32
|
+
|
|
33
|
+
A series of tens of millions of samples is too long to draw point by point.
|
|
34
|
+
lightspeed reads it once, in chunks, and reduces it into a pyramid of min/max
|
|
35
|
+
levels of detail, which it caches in an HDF5 file. A plot then asks for the
|
|
36
|
+
x range in view at the resolution the axes can show, and gets back a
|
|
37
|
+
min/max envelope of a few thousand buckets whatever the zoom - and the raw
|
|
38
|
+
samples once you zoom in far enough. Spikes are never averaged away.
|
|
39
|
+
|
|
40
|
+
The cache is keyed by a hash of the source's contents, so a second open of the
|
|
41
|
+
same data is immediate, and several processes can share one cache directory.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pip install lightspeed-series
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The PyPI name is `lightspeed-series`; the import name is `lightspeed`.
|
|
50
|
+
|
|
51
|
+
## Use
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import matplotlib.pyplot as plt
|
|
55
|
+
from lightspeed import SourceSpec, ZoomSync, build_or_get_cache, close_cache
|
|
56
|
+
|
|
57
|
+
# A 1-D dataset in an HDF5 file, sampled at x = 0, 1, 2, ...
|
|
58
|
+
spec = SourceSpec.uniform("signal.h5", "/y", x0=0.0, dx=1.0)
|
|
59
|
+
handle = build_or_get_cache(spec)
|
|
60
|
+
|
|
61
|
+
fig, ax = plt.subplots()
|
|
62
|
+
sync = ZoomSync(ax=ax, handle=handle, spec=spec) # re-queries on pan and zoom
|
|
63
|
+
plt.show()
|
|
64
|
+
|
|
65
|
+
sync.disconnect()
|
|
66
|
+
close_cache(handle)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`examples/demo.py` builds a 20-million-sample source and zooms into it.
|
|
70
|
+
|
|
71
|
+
## Cache directory
|
|
72
|
+
|
|
73
|
+
`build_or_get_cache(spec, cache_dir=...)` uses `cache_dir` if given; otherwise
|
|
74
|
+
`$LIGHTSPEED_CACHE_DIR`, then `%LOCALAPPDATA%\Lightspeed\Cache` on Windows,
|
|
75
|
+
`$XDG_CACHE_HOME/lightspeed`, or `~/.cache/lightspeed`.
|
|
76
|
+
|
|
77
|
+
## Licence
|
|
78
|
+
|
|
79
|
+
MIT.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# lightspeed
|
|
2
|
+
|
|
3
|
+
Fast, zoomable plots of very large series.
|
|
4
|
+
|
|
5
|
+
A series of tens of millions of samples is too long to draw point by point.
|
|
6
|
+
lightspeed reads it once, in chunks, and reduces it into a pyramid of min/max
|
|
7
|
+
levels of detail, which it caches in an HDF5 file. A plot then asks for the
|
|
8
|
+
x range in view at the resolution the axes can show, and gets back a
|
|
9
|
+
min/max envelope of a few thousand buckets whatever the zoom - and the raw
|
|
10
|
+
samples once you zoom in far enough. Spikes are never averaged away.
|
|
11
|
+
|
|
12
|
+
The cache is keyed by a hash of the source's contents, so a second open of the
|
|
13
|
+
same data is immediate, and several processes can share one cache directory.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install lightspeed-series
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The PyPI name is `lightspeed-series`; the import name is `lightspeed`.
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
from lightspeed import SourceSpec, ZoomSync, build_or_get_cache, close_cache
|
|
28
|
+
|
|
29
|
+
# A 1-D dataset in an HDF5 file, sampled at x = 0, 1, 2, ...
|
|
30
|
+
spec = SourceSpec.uniform("signal.h5", "/y", x0=0.0, dx=1.0)
|
|
31
|
+
handle = build_or_get_cache(spec)
|
|
32
|
+
|
|
33
|
+
fig, ax = plt.subplots()
|
|
34
|
+
sync = ZoomSync(ax=ax, handle=handle, spec=spec) # re-queries on pan and zoom
|
|
35
|
+
plt.show()
|
|
36
|
+
|
|
37
|
+
sync.disconnect()
|
|
38
|
+
close_cache(handle)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`examples/demo.py` builds a 20-million-sample source and zooms into it.
|
|
42
|
+
|
|
43
|
+
## Cache directory
|
|
44
|
+
|
|
45
|
+
`build_or_get_cache(spec, cache_dir=...)` uses `cache_dir` if given; otherwise
|
|
46
|
+
`$LIGHTSPEED_CACHE_DIR`, then `%LOCALAPPDATA%\Lightspeed\Cache` on Windows,
|
|
47
|
+
`$XDG_CACHE_HOME/lightspeed`, or `~/.cache/lightspeed`.
|
|
48
|
+
|
|
49
|
+
## Licence
|
|
50
|
+
|
|
51
|
+
MIT.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import h5py
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from lightspeed import SourceSpec, ZoomSync, build_or_get_cache, close_cache
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parent
|
|
12
|
+
|
|
13
|
+
POINT_COUNT = 20_000_000
|
|
14
|
+
CACHE_DIR = ROOT / "_build" / "demo-cache"
|
|
15
|
+
SOURCE_PATH = ROOT / "_build" / "demo-source.h5"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_synthetic_source() -> None:
|
|
19
|
+
SOURCE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
if SOURCE_PATH.exists():
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
print(f"Generating {POINT_COUNT:,} synthetic samples at {SOURCE_PATH}")
|
|
24
|
+
with h5py.File(SOURCE_PATH, "w") as handle:
|
|
25
|
+
dataset = handle.create_dataset("/y", shape=(POINT_COUNT,), dtype=np.float64)
|
|
26
|
+
block = 1_000_000
|
|
27
|
+
rng = np.random.default_rng(0)
|
|
28
|
+
for start in range(0, POINT_COUNT, block):
|
|
29
|
+
end = min(start + block, POINT_COUNT)
|
|
30
|
+
x = np.arange(start, end, dtype=np.float64)
|
|
31
|
+
values = (
|
|
32
|
+
np.sin(x * 0.0003)
|
|
33
|
+
+ 0.3 * np.sin(x * 0.011)
|
|
34
|
+
+ 0.02 * rng.standard_normal(end - start)
|
|
35
|
+
)
|
|
36
|
+
if start <= 3_500_000 < end:
|
|
37
|
+
spike_index = 3_500_000 - start
|
|
38
|
+
values[spike_index] += 6.0
|
|
39
|
+
dataset[start:end] = values
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> None:
|
|
43
|
+
build_synthetic_source()
|
|
44
|
+
|
|
45
|
+
spec = SourceSpec.uniform(SOURCE_PATH, "/y", x0=0.0, dx=1.0)
|
|
46
|
+
|
|
47
|
+
print("Building or reusing cache...")
|
|
48
|
+
handle = build_or_get_cache(spec, cache_dir=CACHE_DIR)
|
|
49
|
+
print(f"content_hash={handle.content_hash} n_levels={handle.n_levels}")
|
|
50
|
+
|
|
51
|
+
fig, ax = plt.subplots(figsize=(10, 4))
|
|
52
|
+
sync = ZoomSync(ax=ax, handle=handle, spec=spec)
|
|
53
|
+
ax.set_title("lightspeed demo: min/max envelope with live re-leveling")
|
|
54
|
+
|
|
55
|
+
print("Simulating pan/zoom...")
|
|
56
|
+
for x_start, x_end in [
|
|
57
|
+
(0, POINT_COUNT - 1),
|
|
58
|
+
(3_000_000, 4_000_000),
|
|
59
|
+
(3_450_000, 3_550_000),
|
|
60
|
+
]:
|
|
61
|
+
ax.set_xlim(x_start, x_end)
|
|
62
|
+
fig.canvas.draw()
|
|
63
|
+
print(f" xlim=({x_start}, {x_end}) -> level={sync._line_min.get_xdata().shape[0]} buckets")
|
|
64
|
+
|
|
65
|
+
output_path = ROOT / "_build" / "demo.png"
|
|
66
|
+
fig.savefig(output_path, dpi=150)
|
|
67
|
+
print(f"Saved {output_path}")
|
|
68
|
+
|
|
69
|
+
sync.disconnect()
|
|
70
|
+
plt.close(fig)
|
|
71
|
+
close_cache(handle)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "setuptools-scm>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# The PyPI name. "lightspeed" is refused as too close to the existing
|
|
7
|
+
# light-speed project, so the distribution is lightspeed-series; the import
|
|
8
|
+
# name stays lightspeed.
|
|
9
|
+
name = "lightspeed-series"
|
|
10
|
+
# The version is the latest python-v* git tag (python-v0.0.1 -> 0.0.1);
|
|
11
|
+
# commits after it get a .devN suffix. The Haskell package is tagged
|
|
12
|
+
# haskell-v* and released on its own. See [tool.setuptools_scm].
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
description = "Fast, zoomable plots of very large series, from min/max level-of-detail pyramids cached in HDF5."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.10"
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = ["LICENSE"]
|
|
19
|
+
authors = [{ name = "Mike Hull", email = "mikehulluk@gmail.com" }]
|
|
20
|
+
keywords = ["time series", "large data", "plotting", "level of detail", "hdf5", "matplotlib", "downsampling"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"filelock>=3.12",
|
|
34
|
+
"h5py>=3.10",
|
|
35
|
+
"matplotlib>=3.8",
|
|
36
|
+
"numpy>=1.26",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = ["pytest>=8.0", "build>=1.2", "twine>=5.0"]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/mikehulluk/lightspeed"
|
|
44
|
+
Source = "https://github.com/mikehulluk/lightspeed"
|
|
45
|
+
Issues = "https://github.com/mikehulluk/lightspeed/issues"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools_scm]
|
|
48
|
+
# The git repository is the directory above this one.
|
|
49
|
+
root = ".."
|
|
50
|
+
fallback_version = "0.0.0"
|
|
51
|
+
# Only python-v* tags count, so a Haskell release does not move this version.
|
|
52
|
+
tag_regex = "^python-v(?P<version>[0-9]+(?:\\.[0-9]+)*(?:[-.+].*)?)$"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools_scm.scm.git]
|
|
55
|
+
describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "python-v*"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.packages.find]
|
|
58
|
+
where = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
lightspeed = ["py.typed"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Fast, zoomable plots of very large series.
|
|
2
|
+
|
|
3
|
+
A series is reduced once into min/max level-of-detail pyramids, cached in
|
|
4
|
+
an HDF5 file, and queried at the resolution the view needs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .arraysource import ArraySource, HdfArraySource, SourceIdentity
|
|
8
|
+
from .cache import CacheHandle, SourceSpec, build_or_get_cache, close_cache, open_existing_cache
|
|
9
|
+
from .cachedir import resolve_cache_dir
|
|
10
|
+
from .constants import DEFAULT_CHUNK_SIZE
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
CacheCorruptError,
|
|
13
|
+
InvalidSourceSpecError,
|
|
14
|
+
NonMonotonicXError,
|
|
15
|
+
LightspeedError,
|
|
16
|
+
UnsupportedQueryError,
|
|
17
|
+
)
|
|
18
|
+
from .mpl import ZoomSync, plot_envelope, update_envelope_artists
|
|
19
|
+
from .query import QueryResult, full_x_range, query_range
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ArraySource",
|
|
23
|
+
"CacheCorruptError",
|
|
24
|
+
"CacheHandle",
|
|
25
|
+
"DEFAULT_CHUNK_SIZE",
|
|
26
|
+
"HdfArraySource",
|
|
27
|
+
"InvalidSourceSpecError",
|
|
28
|
+
"NonMonotonicXError",
|
|
29
|
+
"LightspeedError",
|
|
30
|
+
"QueryResult",
|
|
31
|
+
"SourceIdentity",
|
|
32
|
+
"SourceSpec",
|
|
33
|
+
"UnsupportedQueryError",
|
|
34
|
+
"ZoomSync",
|
|
35
|
+
"build_or_get_cache",
|
|
36
|
+
"close_cache",
|
|
37
|
+
"full_x_range",
|
|
38
|
+
"open_existing_cache",
|
|
39
|
+
"plot_envelope",
|
|
40
|
+
"query_range",
|
|
41
|
+
"resolve_cache_dir",
|
|
42
|
+
"update_envelope_artists",
|
|
43
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterator, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class ArraySource(Protocol):
|
|
13
|
+
"""Disk-backed, random-seek 1-D numeric array data."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def length(self) -> int: ...
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def dtype(self) -> np.dtype: ...
|
|
20
|
+
|
|
21
|
+
def read_range(self, start: int, end: int) -> np.ndarray: ...
|
|
22
|
+
|
|
23
|
+
def iter_blocks(self, block_size: int) -> Iterator[np.ndarray]:
|
|
24
|
+
"""Sequential streaming pass over the whole array, in order."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def close(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SourceIdentity:
|
|
32
|
+
"""Enough information to detect whether an on-disk array has changed."""
|
|
33
|
+
|
|
34
|
+
path: Path
|
|
35
|
+
mtime_ns: int
|
|
36
|
+
size: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HdfArraySource:
|
|
40
|
+
"""Concrete ArraySource backed by one h5py Dataset in one HDF5 file.
|
|
41
|
+
|
|
42
|
+
A 1-D dataset as it stands, or one column of a 2-D (rows x columns)
|
|
43
|
+
dataset. Reading a column pulls each chunk the rows span in full and
|
|
44
|
+
keeps one column of it, so a first pass over a wide dataset costs its
|
|
45
|
+
whole width in I/O; the cache built from it is per column, and later
|
|
46
|
+
passes read from that.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, hdf5_path: Path, dataset_path: str, column: int | None = None) -> None:
|
|
50
|
+
self.hdf5_path = Path(hdf5_path)
|
|
51
|
+
self.dataset_path = dataset_path
|
|
52
|
+
self.column = column
|
|
53
|
+
self._file = h5py.File(self.hdf5_path, "r")
|
|
54
|
+
self._dataset = self._file[dataset_path]
|
|
55
|
+
if column is None:
|
|
56
|
+
if self._dataset.ndim != 1:
|
|
57
|
+
raise ValueError(f"{self.hdf5_path}:{dataset_path} is {self._dataset.ndim}-D; a column must be named")
|
|
58
|
+
else:
|
|
59
|
+
if self._dataset.ndim != 2:
|
|
60
|
+
raise ValueError(f"{self.hdf5_path}:{dataset_path} is {self._dataset.ndim}-D; column applies to a 2-D dataset")
|
|
61
|
+
if not 0 <= column < self._dataset.shape[1]:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"{self.hdf5_path}:{dataset_path} has {self._dataset.shape[1]} columns; no column {column}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def length(self) -> int:
|
|
68
|
+
return int(self._dataset.shape[0])
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def dtype(self) -> np.dtype:
|
|
72
|
+
return self._dataset.dtype
|
|
73
|
+
|
|
74
|
+
def read_range(self, start: int, end: int) -> np.ndarray:
|
|
75
|
+
if self.column is None:
|
|
76
|
+
return np.asarray(self._dataset[start:end])
|
|
77
|
+
return np.asarray(self._dataset[start:end, self.column])
|
|
78
|
+
|
|
79
|
+
def iter_blocks(self, block_size: int) -> Iterator[np.ndarray]:
|
|
80
|
+
length = self.length
|
|
81
|
+
for offset in range(0, length, block_size):
|
|
82
|
+
yield self.read_range(offset, min(offset + block_size, length))
|
|
83
|
+
|
|
84
|
+
def identity(self) -> SourceIdentity:
|
|
85
|
+
resolved = self.hdf5_path.resolve()
|
|
86
|
+
stat = resolved.stat()
|
|
87
|
+
return SourceIdentity(path=resolved, mtime_ns=stat.st_mtime_ns, size=stat.st_size)
|
|
88
|
+
|
|
89
|
+
def close(self) -> None:
|
|
90
|
+
self._file.close()
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> "HdfArraySource":
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
96
|
+
self.close()
|