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.
Files changed (36) hide show
  1. lightspeed_series-0.0.1/LICENSE +21 -0
  2. lightspeed_series-0.0.1/PKG-INFO +79 -0
  3. lightspeed_series-0.0.1/README.md +51 -0
  4. lightspeed_series-0.0.1/examples/demo.py +75 -0
  5. lightspeed_series-0.0.1/pyproject.toml +64 -0
  6. lightspeed_series-0.0.1/setup.cfg +4 -0
  7. lightspeed_series-0.0.1/src/lightspeed/__init__.py +43 -0
  8. lightspeed_series-0.0.1/src/lightspeed/arraysource.py +96 -0
  9. lightspeed_series-0.0.1/src/lightspeed/cache.py +531 -0
  10. lightspeed_series-0.0.1/src/lightspeed/cachedir.py +58 -0
  11. lightspeed_series-0.0.1/src/lightspeed/constants.py +26 -0
  12. lightspeed_series-0.0.1/src/lightspeed/exceptions.py +18 -0
  13. lightspeed_series-0.0.1/src/lightspeed/hashing.py +20 -0
  14. lightspeed_series-0.0.1/src/lightspeed/memo.py +109 -0
  15. lightspeed_series-0.0.1/src/lightspeed/mpl.py +447 -0
  16. lightspeed_series-0.0.1/src/lightspeed/py.typed +0 -0
  17. lightspeed_series-0.0.1/src/lightspeed/pyramid.py +225 -0
  18. lightspeed_series-0.0.1/src/lightspeed/query.py +260 -0
  19. lightspeed_series-0.0.1/src/lightspeed/status.py +120 -0
  20. lightspeed_series-0.0.1/src/lightspeed/timing.py +37 -0
  21. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/PKG-INFO +79 -0
  22. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/SOURCES.txt +34 -0
  23. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/dependency_links.txt +1 -0
  24. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/requires.txt +4 -0
  25. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/scm_file_list.json +31 -0
  26. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/scm_version.json +8 -0
  27. lightspeed_series-0.0.1/src/lightspeed_series.egg-info/top_level.txt +1 -0
  28. lightspeed_series-0.0.1/tests/test_cache.py +233 -0
  29. lightspeed_series-0.0.1/tests/test_cachedir.py +61 -0
  30. lightspeed_series-0.0.1/tests/test_hashing.py +40 -0
  31. lightspeed_series-0.0.1/tests/test_locking.py +88 -0
  32. lightspeed_series-0.0.1/tests/test_memo.py +76 -0
  33. lightspeed_series-0.0.1/tests/test_mpl.py +337 -0
  34. lightspeed_series-0.0.1/tests/test_pyramid.py +125 -0
  35. lightspeed_series-0.0.1/tests/test_query.py +162 -0
  36. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()