wdm-transform 0.1.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 (34) hide show
  1. wdm_transform-0.1.1/CHANGELOG.md +15 -0
  2. wdm_transform-0.1.1/MANIFEST.in +5 -0
  3. wdm_transform-0.1.1/PKG-INFO +67 -0
  4. wdm_transform-0.1.1/README.md +29 -0
  5. wdm_transform-0.1.1/pyproject.toml +121 -0
  6. wdm_transform-0.1.1/setup.cfg +4 -0
  7. wdm_transform-0.1.1/src/wdm_transform/__init__.py +13 -0
  8. wdm_transform-0.1.1/src/wdm_transform/backends/__init__.py +62 -0
  9. wdm_transform-0.1.1/src/wdm_transform/backends/base.py +17 -0
  10. wdm_transform-0.1.1/src/wdm_transform/backends/cupy_backend.py +15 -0
  11. wdm_transform-0.1.1/src/wdm_transform/backends/jax_backend.py +17 -0
  12. wdm_transform-0.1.1/src/wdm_transform/backends/numpy_backend.py +8 -0
  13. wdm_transform-0.1.1/src/wdm_transform/benchmarking.py +284 -0
  14. wdm_transform-0.1.1/src/wdm_transform/datatypes/__init__.py +4 -0
  15. wdm_transform-0.1.1/src/wdm_transform/datatypes/series.py +123 -0
  16. wdm_transform-0.1.1/src/wdm_transform/datatypes/wdm.py +206 -0
  17. wdm_transform-0.1.1/src/wdm_transform/freqseries.py +3 -0
  18. wdm_transform-0.1.1/src/wdm_transform/plotting.py +302 -0
  19. wdm_transform-0.1.1/src/wdm_transform/timeseries.py +3 -0
  20. wdm_transform-0.1.1/src/wdm_transform/transforms/__init__.py +78 -0
  21. wdm_transform-0.1.1/src/wdm_transform/transforms/jax_backend.py +384 -0
  22. wdm_transform-0.1.1/src/wdm_transform/transforms/xp_backend.py +377 -0
  23. wdm_transform-0.1.1/src/wdm_transform/wdm.py +3 -0
  24. wdm_transform-0.1.1/src/wdm_transform/windows.py +164 -0
  25. wdm_transform-0.1.1/src/wdm_transform.egg-info/PKG-INFO +67 -0
  26. wdm_transform-0.1.1/src/wdm_transform.egg-info/SOURCES.txt +32 -0
  27. wdm_transform-0.1.1/src/wdm_transform.egg-info/dependency_links.txt +1 -0
  28. wdm_transform-0.1.1/src/wdm_transform.egg-info/requires.txt +21 -0
  29. wdm_transform-0.1.1/src/wdm_transform.egg-info/top_level.txt +1 -0
  30. wdm_transform-0.1.1/tests/conftest.py +62 -0
  31. wdm_transform-0.1.1/tests/test_api_helpers.py +61 -0
  32. wdm_transform-0.1.1/tests/test_docs.py +82 -0
  33. wdm_transform-0.1.1/tests/test_roundtrip.py +199 -0
  34. wdm_transform-0.1.1/tests/test_validation.py +113 -0
@@ -0,0 +1,15 @@
1
+ ## v0.1.1 (2026-03-30)
2
+
3
+ ### fix
4
+
5
+ - update api overview
6
+
7
+ ## v0.1.0 (2026-03-30)
8
+
9
+ ### add
10
+
11
+ - prep for v0.1.0 release
12
+
13
+ ### fix
14
+
15
+ - workflows were rekt
@@ -0,0 +1,5 @@
1
+ prune .github
2
+ prune docs
3
+ prune site
4
+ exclude .gitignore
5
+ exclude uv.lock
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: wdm-transform
3
+ Version: 0.1.1
4
+ Summary: Object-oriented WDM transforms for sampled one-dimensional signals.
5
+ Project-URL: Homepage, https://github.com/pywavelet/wdm_transform
6
+ Project-URL: Repository, https://github.com/pywavelet/wdm_transform
7
+ Project-URL: Issues, https://github.com/pywavelet/wdm_transform/issues
8
+ Keywords: wdm,wavelets,signal-processing,time-series
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: numpy>=1.26
21
+ Requires-Dist: matplotlib>=3.8
22
+ Requires-Dist: scipy>=1.13
23
+ Provides-Extra: jax
24
+ Requires-Dist: jax[cpu]>=0.4.30; extra == "jax"
25
+ Provides-Extra: dev
26
+ Requires-Dist: black>=24.8; extra == "dev"
27
+ Requires-Dist: commitizen>=3.29; extra == "dev"
28
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.6; extra == "dev"
32
+ Provides-Extra: docs
33
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
34
+ Requires-Dist: mkdocs-jupyter>=0.25; extra == "docs"
35
+ Requires-Dist: mkdocs-material>=9.6; extra == "docs"
36
+ Requires-Dist: mkdocstrings>=1.0; extra == "docs"
37
+ Requires-Dist: mkdocstrings-python>=2.0; extra == "docs"
38
+
39
+ # wdm-transform
40
+
41
+ `wdm-transform` is a small object-oriented package for Wilson-Daubechies-Meyer transforms on
42
+ sampled one-dimensional signals.
43
+
44
+ ![wdm-transform demo](https://raw.githubusercontent.com/pywavelet/wdm_transform/main/docs/_static/demo.gif)
45
+
46
+ The public API centers on three objects:
47
+
48
+ - `TimeSeries`
49
+ - `FrequencySeries`
50
+ - `WDM`
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install wdm-transform
56
+ ```
57
+
58
+ Optional JAX support:
59
+
60
+ ```bash
61
+ pip install "wdm-transform[jax]"
62
+ ```
63
+
64
+ Documentation and source live at:
65
+
66
+ - <https://github.com/pywavelet/wdm_transform>
67
+ - <https://github.com/pywavelet/wdm_transform/tree/main/docs>
@@ -0,0 +1,29 @@
1
+ # wdm-transform
2
+
3
+ `wdm-transform` is a small object-oriented package for Wilson-Daubechies-Meyer transforms on
4
+ sampled one-dimensional signals.
5
+
6
+ ![wdm-transform demo](https://raw.githubusercontent.com/pywavelet/wdm_transform/main/docs/_static/demo.gif)
7
+
8
+ The public API centers on three objects:
9
+
10
+ - `TimeSeries`
11
+ - `FrequencySeries`
12
+ - `WDM`
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install wdm-transform
18
+ ```
19
+
20
+ Optional JAX support:
21
+
22
+ ```bash
23
+ pip install "wdm-transform[jax]"
24
+ ```
25
+
26
+ Documentation and source live at:
27
+
28
+ - <https://github.com/pywavelet/wdm_transform>
29
+ - <https://github.com/pywavelet/wdm_transform/tree/main/docs>
@@ -0,0 +1,121 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80", "setuptools-scm>=8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wdm-transform"
7
+ dynamic = ["version"]
8
+ description = "Object-oriented WDM transforms for sampled one-dimensional signals."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ keywords = ["wdm", "wavelets", "signal-processing", "time-series"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Science/Research",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Scientific/Engineering",
21
+ "Topic :: Scientific/Engineering :: Mathematics",
22
+ ]
23
+ dependencies = [
24
+ "numpy>=1.26",
25
+ "matplotlib>=3.8",
26
+ "scipy>=1.13",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/pywavelet/wdm_transform"
31
+ Repository = "https://github.com/pywavelet/wdm_transform"
32
+ Issues = "https://github.com/pywavelet/wdm_transform/issues"
33
+
34
+ [project.optional-dependencies]
35
+ jax = [
36
+ "jax[cpu]>=0.4.30",
37
+ ]
38
+ dev = [
39
+ "black>=24.8",
40
+ "commitizen>=3.29",
41
+ "pre-commit>=3.7",
42
+ "pytest>=8.0",
43
+ "pytest-cov>=5.0",
44
+ "ruff>=0.6",
45
+ ]
46
+ docs = [
47
+ "mkdocs>=1.6",
48
+ "mkdocs-jupyter>=0.25",
49
+ "mkdocs-material>=9.6",
50
+ "mkdocstrings>=1.0",
51
+ "mkdocstrings-python>=2.0",
52
+ ]
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+
57
+ [tool.setuptools.packages.find]
58
+ where = ["src"]
59
+
60
+ [tool.setuptools_scm]
61
+ local_scheme = "no-local-version"
62
+ tag_regex = "^v(?P<version>.+)$"
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ addopts = [
67
+ "--cov=wdm_transform",
68
+ "--cov-report=term-missing",
69
+ "--cov-report=xml",
70
+ ]
71
+
72
+ [tool.coverage.run]
73
+ branch = true
74
+ source = ["wdm_transform"]
75
+ relative_files = true
76
+ omit = ["*/benchmarking.py", "*/backends/cupy_backend.py"]
77
+
78
+ [tool.coverage.report]
79
+ show_missing = true
80
+ skip_empty = true
81
+ exclude_lines = [
82
+ "pragma: no cover",
83
+ ]
84
+
85
+ [tool.black]
86
+ line-length = 88
87
+ target-version = ["py310"]
88
+
89
+ [tool.ruff]
90
+ line-length = 88
91
+ target-version = "py310"
92
+
93
+ [tool.ruff.lint]
94
+ select = ["E", "F", "I", "B"]
95
+
96
+ [tool.commitizen]
97
+ name = "cz_customize"
98
+ version_provider = "scm"
99
+ tag_format = "v$version"
100
+ version_scheme = "pep440"
101
+ update_changelog_on_bump = true
102
+
103
+ [tool.commitizen.customize]
104
+ schema = "<type>: <message>"
105
+ schema_pattern = "^(feat|add|fix):\\s.+"
106
+ example = "feat: add inverse transform helper"
107
+ commit_parser = "^(?P<change_type>feat|add|fix):\\s(?P<message>.*)$"
108
+ changelog_pattern = "^(feat|add|fix):"
109
+ bump_pattern = "^(feat|add|fix)"
110
+ change_type_order = ["feat", "add", "fix"]
111
+ info = """
112
+ Use one of:
113
+ - feat: new user-facing feature (minor bump)
114
+ - add: additive improvement (minor bump)
115
+ - fix: bug fix (patch bump)
116
+ """
117
+
118
+ [tool.commitizen.customize.bump_map]
119
+ feat = "MINOR"
120
+ add = "MINOR"
121
+ fix = "PATCH"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """Public package exports for the WDM transform scaffold."""
2
+
3
+ from .backends import Backend, get_backend, register_backend
4
+ from .datatypes import FrequencySeries, TimeSeries, WDM
5
+
6
+ __all__ = [
7
+ "Backend",
8
+ "FrequencySeries",
9
+ "TimeSeries",
10
+ "WDM",
11
+ "get_backend",
12
+ "register_backend",
13
+ ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from importlib import import_module
5
+ from typing import Any
6
+
7
+ from .base import Backend
8
+ from .numpy_backend import NUMPY_BACKEND
9
+
10
+ _BACKENDS: dict[str, Backend] = {
11
+ NUMPY_BACKEND.name: NUMPY_BACKEND,
12
+ }
13
+
14
+ _BUILTIN_BACKEND_LOADERS = {
15
+ "jax": ("wdm_transform.backends.jax_backend", "load_jax_backend"),
16
+ "cupy": ("wdm_transform.backends.cupy_backend", "load_cupy_backend"),
17
+ }
18
+
19
+
20
+ def register_backend(name: str, xp: Any, fft: Any) -> Backend:
21
+ """Register a backend by name and return the resulting backend object."""
22
+ backend = Backend(name=name, xp=xp, fft=fft)
23
+ _BACKENDS[name] = backend
24
+ return backend
25
+
26
+
27
+ def _load_builtin_backend(name: str) -> Backend:
28
+ try:
29
+ module_name, loader_name = _BUILTIN_BACKEND_LOADERS[name]
30
+ except KeyError as exc:
31
+ raise ValueError(f"Unknown backend {name!r}.") from exc
32
+
33
+ module = import_module(module_name)
34
+ loader = getattr(module, loader_name)
35
+ backend = loader()
36
+ _BACKENDS[name] = backend
37
+ return backend
38
+
39
+
40
+ def get_backend(backend: str | Backend | None = None) -> Backend:
41
+ """Resolve a backend object from a name, instance, or environment default."""
42
+ if isinstance(backend, Backend):
43
+ return backend
44
+ if backend is None:
45
+ backend = os.environ.get("WDM_BACKEND")
46
+ if backend is None:
47
+ return NUMPY_BACKEND
48
+ try:
49
+ return _BACKENDS[backend]
50
+ except KeyError as exc:
51
+ try:
52
+ return _load_builtin_backend(backend)
53
+ except ValueError:
54
+ available = ", ".join(
55
+ sorted(set(_BACKENDS) | set(_BUILTIN_BACKEND_LOADERS))
56
+ )
57
+ raise ValueError(
58
+ f"Unknown backend {backend!r}. Available backends: {available}."
59
+ ) from exc
60
+
61
+
62
+ __all__ = ["Backend", "NUMPY_BACKEND", "get_backend", "register_backend"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Backend:
9
+ """Minimal backend wrapper around an array namespace and FFT namespace."""
10
+
11
+ name: str
12
+ xp: Any
13
+ fft: Any
14
+
15
+ def asarray(self, value: Any, dtype: Any | None = None) -> Any:
16
+ """Convert a value into an array for this backend."""
17
+ return self.xp.asarray(value, dtype=dtype)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import Backend
4
+
5
+
6
+ def load_cupy_backend() -> Backend:
7
+ try:
8
+ import cupy as cp
9
+ import cupy.fft as cpfft
10
+ except ImportError as exc:
11
+ raise ImportError(
12
+ "CuPy backend requested, but cupy is not installed. Install a compatible cupy package first."
13
+ ) from exc
14
+
15
+ return Backend(name="cupy", xp=cp, fft=cpfft)
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import Backend
4
+
5
+
6
+ def load_jax_backend() -> Backend:
7
+ try:
8
+ import jax
9
+ jax.config.update("jax_enable_x64", True)
10
+ import jax.numpy as jnp
11
+ import jax.numpy.fft as jfft
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "JAX backend requested, but jax is not installed. Install a compatible jax package first."
15
+ ) from exc
16
+
17
+ return Backend(name="jax", xp=jnp, fft=jfft)
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from .base import Backend
6
+
7
+
8
+ NUMPY_BACKEND = Backend(name="numpy", xp=np, fft=np.fft)
@@ -0,0 +1,284 @@
1
+ """Benchmarking CLI for WDM transforms across different backends and input sizes.
2
+
3
+ This module is excluded from pytest coverage and should not be imported
4
+ when loading the main wdm_transform package.
5
+
6
+ Run with: python -m wdm_transform.benchmarking [options]
7
+ """
8
+
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+
15
+ from . import backends, transforms
16
+
17
+
18
+ def validate_backend_available(backend_name: str) -> bool: # pragma: no cover
19
+ """Check if a backend is available."""
20
+ try:
21
+ backends.get_backend(backend_name)
22
+ return True
23
+ except ValueError:
24
+ return False
25
+
26
+
27
+ def generate_benchmark_data(n: int) -> np.ndarray: # pragma: no cover
28
+ """Generate random complex benchmark data."""
29
+ np.random.seed(42)
30
+ return np.random.randn(n) + 1j * np.random.randn(n)
31
+
32
+
33
+ def find_factorization(n: int) -> tuple[int, int] | None: # pragma: no cover
34
+ """Find a factorization n = nt * nf where both nt and nf are even.
35
+
36
+ Prioritizes balanced factorizations (nt close to sqrt(n)).
37
+ Returns (nt, nf) or None if no valid factorization exists.
38
+ """
39
+ if n < 4:
40
+ return None
41
+
42
+ # Try factorizations starting from sqrt(n) and working outward
43
+ sqrt_n = int(np.sqrt(n))
44
+
45
+ for nt in range(sqrt_n, 1, -1):
46
+ if n % nt != 0:
47
+ continue
48
+ nf = n // nt
49
+ # Both must be even
50
+ if nt % 2 == 0 and nf % 2 == 0:
51
+ return (nt, nf)
52
+
53
+ return None
54
+
55
+
56
+ def benchmark_forward(
57
+ data: np.ndarray,
58
+ backend_name: str,
59
+ nt: int,
60
+ nf: int,
61
+ num_runs: int = 3,
62
+ ) -> float: # pragma: no cover
63
+ """Benchmark the forward WDM transform.
64
+
65
+ Returns the mean runtime in seconds.
66
+ """
67
+ backend = backends.get_backend(backend_name)
68
+ fixed_params = {"a": 0.4, "d": 0.8, "dt": 1.0, "backend": backend}
69
+
70
+ # Convert data to backend array
71
+ backend_data = backend.asarray(data)
72
+
73
+ # Warmup run
74
+ transforms.forward_wdm(backend_data, nt=nt, nf=nf, **fixed_params)
75
+
76
+ # Timed runs
77
+ times = []
78
+ for _ in range(num_runs):
79
+ start = time.perf_counter()
80
+ transforms.forward_wdm(backend_data, nt=nt, nf=nf, **fixed_params)
81
+ elapsed = time.perf_counter() - start
82
+ times.append(elapsed)
83
+
84
+ return np.mean(times)
85
+
86
+
87
+ def benchmark_inverse(
88
+ coeffs: np.ndarray,
89
+ backend_name: str,
90
+ num_runs: int = 3,
91
+ ) -> float: # pragma: no cover
92
+ """Benchmark the inverse WDM transform.
93
+
94
+ Returns the mean runtime in seconds.
95
+ """
96
+ backend = backends.get_backend(backend_name)
97
+ fixed_params = {"a": 0.4, "d": 0.8, "dt": 1.0, "backend": backend}
98
+
99
+ # Convert coeffs to backend array
100
+ backend_coeffs = backend.asarray(coeffs)
101
+
102
+ # Warmup run
103
+ transforms.inverse_wdm(backend_coeffs, **fixed_params)
104
+
105
+ # Timed runs
106
+ times = []
107
+ for _ in range(num_runs):
108
+ start = time.perf_counter()
109
+ transforms.inverse_wdm(backend_coeffs, **fixed_params)
110
+ elapsed = time.perf_counter() - start
111
+ times.append(elapsed)
112
+
113
+ return np.mean(times)
114
+
115
+
116
+ def run_benchmarks( # pragma: no cover
117
+ backends_to_test: list[str],
118
+ n_values: list[int],
119
+ num_runs: int = 3,
120
+ ) -> dict[str, Any]:
121
+ """Run benchmark suite across backends and input sizes.
122
+
123
+ Returns results dictionary with structure:
124
+ {
125
+ 'forward': {backend_name: {n: time_in_seconds}},
126
+ 'inverse': {backend_name: {n: time_in_seconds}},
127
+ }
128
+ """
129
+ results = {"forward": {}, "inverse": {}}
130
+
131
+ # Validate backends
132
+ available_backends = [b for b in backends_to_test if validate_backend_available(b)]
133
+ if not available_backends:
134
+ print(f"ERROR: No backends available from: {backends_to_test}")
135
+ return results
136
+
137
+ if len(available_backends) < len(backends_to_test):
138
+ unavailable = set(backends_to_test) - set(available_backends)
139
+ print(f"WARNING: Skipping unavailable backends: {unavailable}")
140
+
141
+ # Forward transform benchmarks
142
+ print("\n" + "=" * 70)
143
+ print("FORWARD WDM TRANSFORM BENCHMARKS")
144
+ print("=" * 70)
145
+
146
+ for backend_name in available_backends:
147
+ results["forward"][backend_name] = {}
148
+ print(f"\nBackend: {backend_name}")
149
+ print("-" * 40)
150
+
151
+ for n in n_values:
152
+ # Find a valid factorization for this N
153
+ factorization = find_factorization(n)
154
+ if factorization is None:
155
+ print(f" N={n:>8}: SKIPPED (no valid factorization)")
156
+ continue
157
+
158
+ nt, nf = factorization
159
+ data = generate_benchmark_data(nt * nf)
160
+ try:
161
+ mean_time = benchmark_forward(data, backend_name, nt, nf, num_runs)
162
+ results["forward"][backend_name][n] = mean_time
163
+ print(f" N={n:>8}: {mean_time*1e3:>10.4f} ms (nt={nt}, nf={nf})")
164
+ except Exception as e:
165
+ print(f" N={n:>8}: FAILED ({type(e).__name__}: {e})")
166
+
167
+ # Inverse transform benchmarks
168
+ print("\n" + "=" * 70)
169
+ print("INVERSE WDM TRANSFORM BENCHMARKS")
170
+ print("=" * 70)
171
+
172
+ for backend_name in available_backends:
173
+ results["inverse"][backend_name] = {}
174
+ print(f"\nBackend: {backend_name}")
175
+ print("-" * 40)
176
+
177
+ for n in n_values:
178
+ # Find a valid factorization for this N
179
+ factorization = find_factorization(n)
180
+ if factorization is None:
181
+ print(f" N={n:>8}: SKIPPED (no valid factorization)")
182
+ continue
183
+
184
+ nt, nf = factorization
185
+ coeffs = generate_benchmark_data(nt * nf).reshape(nt, nf)
186
+ try:
187
+ mean_time = benchmark_inverse(coeffs, backend_name, num_runs)
188
+ results["inverse"][backend_name][n] = mean_time
189
+ print(f" N={n:>8}: {mean_time*1e3:>10.4f} ms (nt={nt}, nf={nf})")
190
+ except Exception as e:
191
+ print(f" N={n:>8}: FAILED ({type(e).__name__}: {e})")
192
+
193
+ return results
194
+
195
+
196
+ def print_summary(results: dict[str, Any]) -> None: # pragma: no cover
197
+ """Print a summary table of results."""
198
+ print("\n" + "=" * 70)
199
+ print("SUMMARY TABLE")
200
+ print("=" * 70)
201
+
202
+ for transform_type, backends_data in results.items():
203
+ if not backends_data:
204
+ continue
205
+
206
+ print(f"\n{transform_type.upper()} TRANSFORM:")
207
+ print("-" * 40)
208
+
209
+ # Get all N values
210
+ all_n = set()
211
+ for n_dict in backends_data.values():
212
+ all_n.update(n_dict.keys())
213
+ all_n = sorted(all_n)
214
+
215
+ # Header
216
+ header = f"{'N':>10} | " + " | ".join(f"{b:>12}" for b in backends_data.keys())
217
+ print(header)
218
+ print("-" * len(header))
219
+
220
+ # Rows
221
+ for n in all_n:
222
+ row = f"{n:>10} | "
223
+ times = [backends_data.get(b, {}).get(n) for b in backends_data.keys()]
224
+ time_strs = [
225
+ f"{t*1e3:>10.4f} ms" if t is not None else f"{'SKIPPED':>12}" for t in times
226
+ ]
227
+ row += " | ".join(time_strs)
228
+ print(row)
229
+
230
+
231
+ def main() -> None: # pragma: no cover
232
+ """CLI entry point for benchmarking."""
233
+ import argparse
234
+
235
+ parser = argparse.ArgumentParser(
236
+ description="Benchmark WDM transforms across backends and input sizes.",
237
+ formatter_class=argparse.RawDescriptionHelpFormatter,
238
+ epilog="""
239
+ Examples:
240
+ python -m wdm_transform.benchmarking
241
+ python -m wdm_transform.benchmarking --backends numpy jax
242
+ python -m wdm_transform.benchmarking --n 2048 4096 8192
243
+ python -m wdm_transform.benchmarking --runs 5
244
+ """,
245
+ )
246
+
247
+ parser.add_argument(
248
+ "--backends",
249
+ nargs="+",
250
+ default=["numpy"],
251
+ choices=["numpy", "jax", "cupy"],
252
+ help="Backends to benchmark (default: numpy)",
253
+ )
254
+
255
+ parser.add_argument(
256
+ "--n",
257
+ nargs="+",
258
+ type=int,
259
+ default=[2048, 4096, 8192, 16384],
260
+ help="Input sizes to test (default: 2048 4096 8192 16384)",
261
+ )
262
+
263
+ parser.add_argument(
264
+ "--runs",
265
+ type=int,
266
+ default=3,
267
+ help="Number of runs per benchmark (default: 3)",
268
+ )
269
+
270
+ args = parser.parse_args()
271
+
272
+ results = run_benchmarks(
273
+ backends_to_test=args.backends,
274
+ n_values=args.n,
275
+ num_runs=args.runs,
276
+ )
277
+
278
+ print_summary(results)
279
+ print("\n")
280
+
281
+
282
+ if __name__ == "__main__": # pragma: no cover
283
+ main()
284
+
@@ -0,0 +1,4 @@
1
+ from .series import FrequencySeries, TimeSeries
2
+ from .wdm import WDM
3
+
4
+ __all__ = ["FrequencySeries", "TimeSeries", "WDM"]