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.
- wdm_transform-0.1.1/CHANGELOG.md +15 -0
- wdm_transform-0.1.1/MANIFEST.in +5 -0
- wdm_transform-0.1.1/PKG-INFO +67 -0
- wdm_transform-0.1.1/README.md +29 -0
- wdm_transform-0.1.1/pyproject.toml +121 -0
- wdm_transform-0.1.1/setup.cfg +4 -0
- wdm_transform-0.1.1/src/wdm_transform/__init__.py +13 -0
- wdm_transform-0.1.1/src/wdm_transform/backends/__init__.py +62 -0
- wdm_transform-0.1.1/src/wdm_transform/backends/base.py +17 -0
- wdm_transform-0.1.1/src/wdm_transform/backends/cupy_backend.py +15 -0
- wdm_transform-0.1.1/src/wdm_transform/backends/jax_backend.py +17 -0
- wdm_transform-0.1.1/src/wdm_transform/backends/numpy_backend.py +8 -0
- wdm_transform-0.1.1/src/wdm_transform/benchmarking.py +284 -0
- wdm_transform-0.1.1/src/wdm_transform/datatypes/__init__.py +4 -0
- wdm_transform-0.1.1/src/wdm_transform/datatypes/series.py +123 -0
- wdm_transform-0.1.1/src/wdm_transform/datatypes/wdm.py +206 -0
- wdm_transform-0.1.1/src/wdm_transform/freqseries.py +3 -0
- wdm_transform-0.1.1/src/wdm_transform/plotting.py +302 -0
- wdm_transform-0.1.1/src/wdm_transform/timeseries.py +3 -0
- wdm_transform-0.1.1/src/wdm_transform/transforms/__init__.py +78 -0
- wdm_transform-0.1.1/src/wdm_transform/transforms/jax_backend.py +384 -0
- wdm_transform-0.1.1/src/wdm_transform/transforms/xp_backend.py +377 -0
- wdm_transform-0.1.1/src/wdm_transform/wdm.py +3 -0
- wdm_transform-0.1.1/src/wdm_transform/windows.py +164 -0
- wdm_transform-0.1.1/src/wdm_transform.egg-info/PKG-INFO +67 -0
- wdm_transform-0.1.1/src/wdm_transform.egg-info/SOURCES.txt +32 -0
- wdm_transform-0.1.1/src/wdm_transform.egg-info/dependency_links.txt +1 -0
- wdm_transform-0.1.1/src/wdm_transform.egg-info/requires.txt +21 -0
- wdm_transform-0.1.1/src/wdm_transform.egg-info/top_level.txt +1 -0
- wdm_transform-0.1.1/tests/conftest.py +62 -0
- wdm_transform-0.1.1/tests/test_api_helpers.py +61 -0
- wdm_transform-0.1.1/tests/test_docs.py +82 -0
- wdm_transform-0.1.1/tests/test_roundtrip.py +199 -0
- wdm_transform-0.1.1/tests/test_validation.py +113 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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,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
|
+
|