torchwavelets 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.
- torchwavelets/__init__.py +62 -0
- torchwavelets/_version.py +1 -0
- torchwavelets/compat.py +136 -0
- torchwavelets/core/__init__.py +13 -0
- torchwavelets/core/filters.py +211 -0
- torchwavelets/core/modes.py +109 -0
- torchwavelets/dtcwt/__init__.py +24 -0
- torchwavelets/dtcwt/_autocast.py +24 -0
- torchwavelets/dtcwt/coeffs.py +76 -0
- torchwavelets/dtcwt/data/__init__.py +5 -0
- torchwavelets/dtcwt/data/antonini.npz +0 -0
- torchwavelets/dtcwt/data/farras.npz +0 -0
- torchwavelets/dtcwt/data/legall.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_a.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_a2.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_b.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_b_bp.npz +0 -0
- torchwavelets/dtcwt/data/qshift_06.npz +0 -0
- torchwavelets/dtcwt/data/qshift_32.npz +0 -0
- torchwavelets/dtcwt/data/qshift_a.npz +0 -0
- torchwavelets/dtcwt/data/qshift_b.npz +0 -0
- torchwavelets/dtcwt/data/qshift_b_bp.npz +0 -0
- torchwavelets/dtcwt/data/qshift_c.npz +0 -0
- torchwavelets/dtcwt/data/qshift_d.npz +0 -0
- torchwavelets/dtcwt/lowlevel.py +296 -0
- torchwavelets/dtcwt/transform1d.py +253 -0
- torchwavelets/dtcwt/transform2d.py +298 -0
- torchwavelets/dtcwt/transform3d.py +420 -0
- torchwavelets/dtcwt/transform_funcs.py +93 -0
- torchwavelets/dwt/__init__.py +6 -0
- torchwavelets/dwt/_validation.py +59 -0
- torchwavelets/dwt/lowlevel.py +153 -0
- torchwavelets/dwt/transform1d.py +175 -0
- torchwavelets/dwt/transform2d.py +195 -0
- torchwavelets/packets.py +439 -0
- torchwavelets/py.typed +0 -0
- torchwavelets/scatter/__init__.py +6 -0
- torchwavelets/scatter/lowlevel.py +221 -0
- torchwavelets/scatter/network.py +204 -0
- torchwavelets/swt/__init__.py +14 -0
- torchwavelets/swt/lowlevel.py +53 -0
- torchwavelets/swt/transform.py +386 -0
- torchwavelets-0.1.0.dist-info/METADATA +303 -0
- torchwavelets-0.1.0.dist-info/RECORD +47 -0
- torchwavelets-0.1.0.dist-info/WHEEL +5 -0
- torchwavelets-0.1.0.dist-info/licenses/LICENSE +29 -0
- torchwavelets-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""torchwavelets: differentiable discrete wavelet transforms for PyTorch."""
|
|
2
|
+
|
|
3
|
+
from . import compat # noqa: F401 (drop-in pytorch_wavelets API)
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .core.modes import SUPPORTED_MODES
|
|
6
|
+
from .dtcwt.transform1d import DTCWT1D, IDTCWT1D, dtcwt1d, idtcwt1d
|
|
7
|
+
from .dtcwt.transform2d import DTCWT2D, IDTCWT2D, dtcwt2d, idtcwt2d
|
|
8
|
+
from .dtcwt.transform3d import DTCWT3D, IDTCWT3D, dtcwt3d, idtcwt3d
|
|
9
|
+
from .dwt.transform1d import DWT1D, IDWT1D, dwt1d, idwt1d
|
|
10
|
+
from .dwt.transform2d import DWT2D, IDWT2D, dwt2d, idwt2d
|
|
11
|
+
from .packets import WaveletPacket, WaveletPacket2D
|
|
12
|
+
from .scatter.lowlevel import ScatLayer, ScatLayerj2
|
|
13
|
+
from .scatter.network import DTCWTScattering, dtcwt_scattering
|
|
14
|
+
from .swt.transform import (
|
|
15
|
+
ISWT1D,
|
|
16
|
+
ISWT2D,
|
|
17
|
+
SWT1D,
|
|
18
|
+
SWT2D,
|
|
19
|
+
iswt1d,
|
|
20
|
+
iswt2d,
|
|
21
|
+
swt1d,
|
|
22
|
+
swt2d,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"SUPPORTED_MODES",
|
|
28
|
+
"dwt1d",
|
|
29
|
+
"idwt1d",
|
|
30
|
+
"dwt2d",
|
|
31
|
+
"idwt2d",
|
|
32
|
+
"dtcwt2d",
|
|
33
|
+
"idtcwt2d",
|
|
34
|
+
"DWT1D",
|
|
35
|
+
"IDWT1D",
|
|
36
|
+
"DWT2D",
|
|
37
|
+
"IDWT2D",
|
|
38
|
+
"DTCWT2D",
|
|
39
|
+
"IDTCWT2D",
|
|
40
|
+
"DTCWT1D",
|
|
41
|
+
"IDTCWT1D",
|
|
42
|
+
"DTCWT3D",
|
|
43
|
+
"IDTCWT3D",
|
|
44
|
+
"dtcwt1d",
|
|
45
|
+
"idtcwt1d",
|
|
46
|
+
"dtcwt3d",
|
|
47
|
+
"idtcwt3d",
|
|
48
|
+
"ScatLayer",
|
|
49
|
+
"ScatLayerj2",
|
|
50
|
+
"DTCWTScattering",
|
|
51
|
+
"dtcwt_scattering",
|
|
52
|
+
"WaveletPacket",
|
|
53
|
+
"WaveletPacket2D",
|
|
54
|
+
"swt1d",
|
|
55
|
+
"iswt1d",
|
|
56
|
+
"swt2d",
|
|
57
|
+
"iswt2d",
|
|
58
|
+
"SWT1D",
|
|
59
|
+
"ISWT1D",
|
|
60
|
+
"SWT2D",
|
|
61
|
+
"ISWT2D",
|
|
62
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
torchwavelets/compat.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Drop-in compatibility with the ``pytorch_wavelets`` API.
|
|
2
|
+
|
|
3
|
+
The upstream class names are plain aliases: the constructor signatures and
|
|
4
|
+
call semantics are identical, only the semantics are fixed (precision follows
|
|
5
|
+
the input dtype, autocast works, and boundary handling matches PyWavelets
|
|
6
|
+
exactly). For the DTCWT, the bandpass coefficients are converted from the
|
|
7
|
+
native complex format to the upstream ``(..., 2)`` real/imag layout.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
from .dtcwt.transform2d import DTCWT2D, IDTCWT2D
|
|
15
|
+
from .dwt.transform1d import DWT1D, IDWT1D
|
|
16
|
+
from .dwt.transform2d import DWT2D, IDWT2D
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DWTForward",
|
|
20
|
+
"DWTInverse",
|
|
21
|
+
"DWT1DForward",
|
|
22
|
+
"DWT1DInverse",
|
|
23
|
+
"DTCWTForward",
|
|
24
|
+
"DTCWTInverse",
|
|
25
|
+
"DWT",
|
|
26
|
+
"IDWT",
|
|
27
|
+
"DWT1D",
|
|
28
|
+
"IDWT1D",
|
|
29
|
+
"DTCWT",
|
|
30
|
+
"IDTCWT",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DWTForward(DWT2D):
|
|
35
|
+
"""``pytorch_wavelets``-compatible 2-D DWT forward (upstream defaults)."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, J=1, wave="db1", mode="zero"):
|
|
38
|
+
super().__init__(J=J, wave=wave, mode=mode)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DWTInverse(IDWT2D):
|
|
42
|
+
"""``pytorch_wavelets``-compatible 2-D DWT inverse (upstream defaults)."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, wave="db1", mode="zero"):
|
|
45
|
+
super().__init__(wave=wave, mode=mode)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DWT1DForward(DWT1D):
|
|
49
|
+
"""``pytorch_wavelets``-compatible 1-D DWT forward (upstream defaults)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, J=1, wave="db1", mode="zero"):
|
|
52
|
+
super().__init__(J=J, wave=wave, mode=mode)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DWT1DInverse(IDWT1D):
|
|
56
|
+
"""``pytorch_wavelets``-compatible 1-D DWT inverse (upstream defaults)."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, wave="db1", mode="zero"):
|
|
59
|
+
super().__init__(wave=wave, mode=mode)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DTCWTForward(DTCWT2D):
|
|
63
|
+
"""``pytorch_wavelets``-compatible DTCWT forward.
|
|
64
|
+
|
|
65
|
+
Returns bandpasses in the upstream layout ``(N, C, 6, H, W, 2)``
|
|
66
|
+
(real and imaginary parts along the last dimension). ``ri_dim`` must be
|
|
67
|
+
the default ``-1`` (the native complex layout is fixed).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
biort="near_sym_a",
|
|
73
|
+
qshift="qshift_a",
|
|
74
|
+
J=3,
|
|
75
|
+
skip_hps=False,
|
|
76
|
+
include_scale=False,
|
|
77
|
+
o_dim=2,
|
|
78
|
+
ri_dim=-1,
|
|
79
|
+
mode="symmetric",
|
|
80
|
+
):
|
|
81
|
+
if ri_dim != -1:
|
|
82
|
+
raise ValueError("torchwavelets DTCWT only supports ri_dim=-1")
|
|
83
|
+
if mode != "symmetric":
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"torchwavelets DTCWT only supports mode='symmetric' (Kingsbury boundary handling)"
|
|
86
|
+
)
|
|
87
|
+
super().__init__(
|
|
88
|
+
biort=biort,
|
|
89
|
+
qshift=qshift,
|
|
90
|
+
J=J,
|
|
91
|
+
skip_hps=skip_hps,
|
|
92
|
+
include_scale=include_scale,
|
|
93
|
+
o_dim=o_dim,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def forward(self, x: torch.Tensor):
|
|
97
|
+
yl, yh = super().forward(x)
|
|
98
|
+
if yh is None:
|
|
99
|
+
return yl, yh
|
|
100
|
+
return yl, [None if h is None else torch.stack((h.real, h.imag), dim=-1) for h in yh]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DTCWTInverse(IDTCWT2D):
|
|
104
|
+
"""``pytorch_wavelets``-compatible DTCWT inverse (accepts the upstream
|
|
105
|
+
``(..., 2)`` real/imag layout or the native complex format)."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, biort="near_sym_a", qshift="qshift_a", o_dim=2, ri_dim=-1, mode="symmetric"):
|
|
108
|
+
if ri_dim != -1:
|
|
109
|
+
raise ValueError("torchwavelets DTCWT only supports ri_dim=-1")
|
|
110
|
+
if mode != "symmetric":
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"torchwavelets DTCWT only supports mode='symmetric' (Kingsbury boundary handling)"
|
|
113
|
+
)
|
|
114
|
+
super().__init__(biort=biort, qshift=qshift, o_dim=o_dim)
|
|
115
|
+
|
|
116
|
+
def forward(self, coeffs):
|
|
117
|
+
yl, yh = coeffs
|
|
118
|
+
yh = [None if h is None else _to_complex(h) for h in yh]
|
|
119
|
+
return super().forward((yl, yh))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _to_complex(h: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
if h.dtype.is_complex:
|
|
124
|
+
return h
|
|
125
|
+
if h.shape[-1] == 2:
|
|
126
|
+
return torch.complex(h[..., 0], h[..., 1])
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Expected a complex tensor or a (..., 2) real/imag tensor; got shape {tuple(h.shape)}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# pytorch_wavelets' historical aliases
|
|
133
|
+
DWT = DWTForward
|
|
134
|
+
IDWT = DWTInverse
|
|
135
|
+
DTCWT = DTCWTForward
|
|
136
|
+
IDTCWT = DTCWTInverse
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Core utilities: filter banks and boundary modes."""
|
|
2
|
+
|
|
3
|
+
from .filters import FilterBank, filter_bank, resolve_1d, resolve_2d
|
|
4
|
+
from .modes import SUPPORTED_MODES, validate_mode
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"FilterBank",
|
|
8
|
+
"filter_bank",
|
|
9
|
+
"resolve_1d",
|
|
10
|
+
"resolve_2d",
|
|
11
|
+
"SUPPORTED_MODES",
|
|
12
|
+
"validate_mode",
|
|
13
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Filter-bank handling: PyWavelets-compatible wavelets and custom banks.
|
|
2
|
+
|
|
3
|
+
Custom banks are validated for perfect reconstruction at creation time, so a
|
|
4
|
+
biorthogonal bank passed as a 2-tuple (which assumes orthogonality) raises
|
|
5
|
+
immediately instead of silently producing a wrong inverse.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pywt
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn.functional as F
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"FilterBank",
|
|
20
|
+
"filter_bank",
|
|
21
|
+
"wave_name",
|
|
22
|
+
"resolve_1d",
|
|
23
|
+
"resolve_2d",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FilterBank:
|
|
29
|
+
"""A 1-D two-channel filter bank in PyWavelets' natural (non-reversed) order.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
h0: analysis lowpass (``pywt.Wavelet.dec_lo``).
|
|
33
|
+
h1: analysis highpass (``pywt.Wavelet.dec_hi``).
|
|
34
|
+
g0: synthesis lowpass (``pywt.Wavelet.rec_lo``).
|
|
35
|
+
g1: synthesis highpass (``pywt.Wavelet.rec_hi``).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
h0: np.ndarray
|
|
39
|
+
h1: np.ndarray
|
|
40
|
+
g0: np.ndarray
|
|
41
|
+
g1: np.ndarray
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def length(self) -> int:
|
|
45
|
+
"""Filter length (number of taps)."""
|
|
46
|
+
return int(self.h0.size)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize(wave: object):
|
|
50
|
+
"""Return a hashable cache key for ``wave``."""
|
|
51
|
+
if isinstance(wave, pywt.Wavelet):
|
|
52
|
+
# Key on the actual filter bank, not just the name: pywt allows
|
|
53
|
+
# ``pywt.Wavelet("db2", filter_bank=<custom bank>)``.
|
|
54
|
+
return (
|
|
55
|
+
"pywt",
|
|
56
|
+
wave.name,
|
|
57
|
+
tuple(np.asarray(wave.dec_lo, dtype=np.float64).ravel().tolist()),
|
|
58
|
+
tuple(np.asarray(wave.dec_hi, dtype=np.float64).ravel().tolist()),
|
|
59
|
+
tuple(np.asarray(wave.rec_lo, dtype=np.float64).ravel().tolist()),
|
|
60
|
+
tuple(np.asarray(wave.rec_hi, dtype=np.float64).ravel().tolist()),
|
|
61
|
+
)
|
|
62
|
+
if isinstance(wave, str):
|
|
63
|
+
return wave
|
|
64
|
+
if not isinstance(wave, (tuple, list, np.ndarray)):
|
|
65
|
+
raise ValueError(f"Unsupported wavelet specification: {wave!r}")
|
|
66
|
+
arrs = tuple(tuple(np.asarray(w, dtype=np.float64).ravel()) for w in wave)
|
|
67
|
+
if len(arrs) not in (2, 4):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"A custom filter bank must be a 2-tuple (h0, h1) or "
|
|
70
|
+
f"4-tuple (h0, h1, g0, g1); got {len(arrs)} arrays."
|
|
71
|
+
)
|
|
72
|
+
return ("custom", arrs)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def wave_name(wave: object) -> str:
|
|
76
|
+
"""A short human-readable name for a wavelet specification."""
|
|
77
|
+
if isinstance(wave, pywt.Wavelet):
|
|
78
|
+
return wave.name
|
|
79
|
+
if isinstance(wave, str):
|
|
80
|
+
return wave
|
|
81
|
+
return "custom"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check_pr(bank: FilterBank, tol: float = 1e-5) -> None:
|
|
85
|
+
"""Verify perfect reconstruction of a custom bank on a random fp64 signal.
|
|
86
|
+
|
|
87
|
+
Raises ``ValueError`` if the bank is not perfectly reconstructing (for
|
|
88
|
+
example, a biorthogonal pair given as a 2-tuple, where the synthesis
|
|
89
|
+
filters would wrongly equal the analysis filters).
|
|
90
|
+
"""
|
|
91
|
+
if len(bank.h0) != len(bank.h1) or len(bank.g0) != len(bank.g1):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Custom filter banks must have equal-length analysis filters "
|
|
94
|
+
f"(got {len(bank.h0)} vs {len(bank.h1)}) and equal-length "
|
|
95
|
+
f"synthesis filters (got {len(bank.g0)} vs {len(bank.g1)})."
|
|
96
|
+
)
|
|
97
|
+
if len(bank.h0) != len(bank.g0):
|
|
98
|
+
raise NotImplementedError(
|
|
99
|
+
"Custom banks with different analysis/synthesis filter lengths "
|
|
100
|
+
"(e.g. LeGall 5/3) are not supported yet; got analysis length "
|
|
101
|
+
f"{len(bank.h0)} and synthesis length {len(bank.g0)}."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
n = 64
|
|
105
|
+
L = len(bank.h0)
|
|
106
|
+
x = torch.randn(n, dtype=torch.float64)
|
|
107
|
+
h0 = torch.tensor(bank.h0[::-1].copy(), dtype=torch.float64)[None, None, :]
|
|
108
|
+
h1 = torch.tensor(bank.h1[::-1].copy(), dtype=torch.float64)[None, None, :]
|
|
109
|
+
g0 = torch.tensor(bank.g0.copy(), dtype=torch.float64)[None, None, :]
|
|
110
|
+
g1 = torch.tensor(bank.g1.copy(), dtype=torch.float64)[None, None, :]
|
|
111
|
+
xp = F.pad(x[None, None, :], (L - 2, L - 2))
|
|
112
|
+
lo = F.conv1d(xp, h0, stride=2)
|
|
113
|
+
hi = F.conv1d(xp, h1, stride=2)
|
|
114
|
+
xr = F.conv_transpose1d(lo, g0, stride=2, padding=(L - 2,))
|
|
115
|
+
xr = xr + F.conv_transpose1d(hi, g1, stride=2, padding=(L - 2,))
|
|
116
|
+
xr = xr[0, 0, :n]
|
|
117
|
+
rel_err = (x - xr).norm().item() / x.norm().item()
|
|
118
|
+
if rel_err > tol:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"The custom filter bank is not perfectly reconstructing "
|
|
121
|
+
f"(relative error {rel_err:.2e}). For a biorthogonal bank pass the "
|
|
122
|
+
"4-tuple (h0, h1, g0, g1) with explicit synthesis filters; a "
|
|
123
|
+
"2-tuple assumes an orthogonal bank with g = h."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@lru_cache(maxsize=256)
|
|
128
|
+
def _bank_from_key(key) -> FilterBank:
|
|
129
|
+
if isinstance(key, str):
|
|
130
|
+
w = pywt.Wavelet(key)
|
|
131
|
+
return FilterBank(
|
|
132
|
+
np.array(w.dec_lo),
|
|
133
|
+
np.array(w.dec_hi),
|
|
134
|
+
np.array(w.rec_lo),
|
|
135
|
+
np.array(w.rec_hi),
|
|
136
|
+
)
|
|
137
|
+
if key[0] == "pywt":
|
|
138
|
+
_, name, dec_lo, dec_hi, rec_lo, rec_hi = key
|
|
139
|
+
w = pywt.Wavelet(name, filter_bank=(dec_lo, dec_hi, rec_lo, rec_hi))
|
|
140
|
+
return FilterBank(
|
|
141
|
+
np.array(w.dec_lo),
|
|
142
|
+
np.array(w.dec_hi),
|
|
143
|
+
np.array(w.rec_lo),
|
|
144
|
+
np.array(w.rec_hi),
|
|
145
|
+
)
|
|
146
|
+
_, arrs = key
|
|
147
|
+
if len(arrs) == 2:
|
|
148
|
+
# Custom orthogonal bank: synthesis filters are the time-reversed
|
|
149
|
+
# analysis filters (this is exactly pywt's rec_lo convention for
|
|
150
|
+
# orthogonal wavelets, e.g. db2.rec_lo == db2.dec_lo[::-1]).
|
|
151
|
+
h0, h1 = arrs
|
|
152
|
+
bank = FilterBank(np.array(h0), np.array(h1), np.array(h0[::-1]), np.array(h1[::-1]))
|
|
153
|
+
else:
|
|
154
|
+
bank = FilterBank(
|
|
155
|
+
np.array(arrs[0]), np.array(arrs[1]), np.array(arrs[2]), np.array(arrs[3])
|
|
156
|
+
)
|
|
157
|
+
_check_pr(bank)
|
|
158
|
+
return bank
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def filter_bank(wave) -> FilterBank:
|
|
162
|
+
"""Resolve ``wave`` to a :class:`FilterBank`.
|
|
163
|
+
|
|
164
|
+
``wave`` may be a PyWavelets wavelet name (``"db4"``), a
|
|
165
|
+
:class:`pywt.Wavelet`, a 2-tuple ``(h0, h1)`` (orthogonal custom bank), or
|
|
166
|
+
a 4-tuple ``(h0, h1, g0, g1)`` (general bank). Custom banks are validated
|
|
167
|
+
for perfect reconstruction and raise ``ValueError`` otherwise.
|
|
168
|
+
"""
|
|
169
|
+
return _bank_from_key(_normalize(wave))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@lru_cache(maxsize=1024)
|
|
173
|
+
def _resolve_1d_cached(key, dtype, device):
|
|
174
|
+
bank = _bank_from_key(key)
|
|
175
|
+
h0 = torch.tensor(bank.h0[::-1].copy(), dtype=dtype, device=device).reshape(1, 1, 1, -1)
|
|
176
|
+
h1 = torch.tensor(bank.h1[::-1].copy(), dtype=dtype, device=device).reshape(1, 1, 1, -1)
|
|
177
|
+
g0 = torch.tensor(bank.g0.copy(), dtype=dtype, device=device).reshape(1, 1, 1, -1)
|
|
178
|
+
g1 = torch.tensor(bank.g1.copy(), dtype=dtype, device=device).reshape(1, 1, 1, -1)
|
|
179
|
+
return (h0, h1, g0, g1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def resolve_1d(
|
|
183
|
+
wave, dtype: torch.dtype, device: torch.device
|
|
184
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
185
|
+
"""Analysis (reversed, for correlation) and synthesis (natural) 1-D filters.
|
|
186
|
+
|
|
187
|
+
Returns tensors of shape ``(1, 1, 1, L)``, ready to filter a signal along
|
|
188
|
+
the width dimension (``dim=3``) of a 4-D tensor. The analysis filters are
|
|
189
|
+
reversed so ``conv2d`` implements correlation like PyWavelets' ``dwt``.
|
|
190
|
+
Cached per ``(wave, dtype, device)``.
|
|
191
|
+
"""
|
|
192
|
+
return _resolve_1d_cached(_normalize(wave), dtype, device)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def resolve_2d(wave, dtype: torch.dtype, device: torch.device) -> tuple[torch.Tensor, ...]:
|
|
196
|
+
"""Like :func:`resolve_1d` but returns column and row orientations.
|
|
197
|
+
|
|
198
|
+
Returns ``(h0_col, h1_col, h0_row, h1_row, g0_col, g1_col, g0_row, g1_row)``
|
|
199
|
+
with column filters shaped ``(1, 1, L, 1)`` and row filters ``(1, 1, 1, L)``.
|
|
200
|
+
"""
|
|
201
|
+
h0, h1, g0, g1 = resolve_1d(wave, dtype, device)
|
|
202
|
+
return (
|
|
203
|
+
h0.reshape(1, 1, -1, 1),
|
|
204
|
+
h1.reshape(1, 1, -1, 1),
|
|
205
|
+
h0,
|
|
206
|
+
h1,
|
|
207
|
+
g0.reshape(1, 1, -1, 1),
|
|
208
|
+
g1.reshape(1, 1, -1, 1),
|
|
209
|
+
g0,
|
|
210
|
+
g1,
|
|
211
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Boundary modes: PyWavelets-exact padding for all supported modes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"SUPPORTED_MODES",
|
|
11
|
+
"validate_mode",
|
|
12
|
+
"output_length",
|
|
13
|
+
"padding_sizes",
|
|
14
|
+
"pad_indices",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
#: Modes supported by torchwavelets. ``zero``, ``symmetric``, ``reflect``,
|
|
18
|
+
#: ``periodization``, ``periodic``, ``constant`` and ``smooth`` match
|
|
19
|
+
#: PyWavelets exactly (``smooth`` is a linear extrapolation from the edges).
|
|
20
|
+
#: ``replicate`` (edge extension) is an alias for ``constant``.
|
|
21
|
+
SUPPORTED_MODES = (
|
|
22
|
+
"zero",
|
|
23
|
+
"symmetric",
|
|
24
|
+
"reflect",
|
|
25
|
+
"periodization",
|
|
26
|
+
"periodic",
|
|
27
|
+
"constant",
|
|
28
|
+
"replicate",
|
|
29
|
+
"smooth",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_ALIASES = {
|
|
33
|
+
"per": "periodization",
|
|
34
|
+
"wrap": "periodic",
|
|
35
|
+
"edge": "constant",
|
|
36
|
+
"replicate": "constant",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_mode(mode: str) -> str:
|
|
41
|
+
"""Normalize ``mode`` (accepts pywt aliases like ``per``/``wrap``/``edge``)."""
|
|
42
|
+
if mode in _ALIASES:
|
|
43
|
+
return _ALIASES[mode]
|
|
44
|
+
if mode not in SUPPORTED_MODES:
|
|
45
|
+
raise ValueError(f"Unknown mode {mode!r}. Supported modes: {SUPPORTED_MODES}")
|
|
46
|
+
return mode
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def output_length(n: int, length: int, mode: str) -> int:
|
|
50
|
+
"""Length of a single-level decimated transform, as PyWavelets computes it.
|
|
51
|
+
|
|
52
|
+
Equivalent to ``pywt.dwt_coeff_len`` for the modes PyWavelets supports.
|
|
53
|
+
"""
|
|
54
|
+
if mode == "periodization":
|
|
55
|
+
return (n + 1) // 2
|
|
56
|
+
return (n + length - 1) // 2
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def padding_sizes(n: int, length: int, mode: str) -> tuple[int, int]:
|
|
60
|
+
"""Total padding ``(before, after)`` needed so the strided convolution
|
|
61
|
+
reproduces PyWavelets' output length exactly.
|
|
62
|
+
|
|
63
|
+
The padding is ``(L - 2)`` before the signal and the remainder after —
|
|
64
|
+
this is the distribution PyWavelets uses for every extension mode, and it
|
|
65
|
+
holds for arbitrary ``n`` (no limit relative to the filter length).
|
|
66
|
+
"""
|
|
67
|
+
out = output_length(n, length, mode)
|
|
68
|
+
p = 2 * (out - 1) - n + length
|
|
69
|
+
before = length - 2
|
|
70
|
+
return before, p - before
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@lru_cache(maxsize=512)
|
|
74
|
+
def _pad_indices_cached(n, before, after, mode, device):
|
|
75
|
+
"""Long index tensor of length ``n + before + after`` extending a signal of
|
|
76
|
+
length ``n`` with the given boundary mode (for ``mode != 'zero'``).
|
|
77
|
+
Cached per ``(n, before, after, mode, device)``; do not mutate the result.
|
|
78
|
+
"""
|
|
79
|
+
idx = torch.arange(-before, n + after, device=device)
|
|
80
|
+
if mode == "symmetric":
|
|
81
|
+
# Half-sample symmetric extension (period 2n, mirrors at -0.5 and n-0.5).
|
|
82
|
+
rel = idx + 0.5
|
|
83
|
+
rng = 2 * n
|
|
84
|
+
normed = torch.remainder(rel, rng)
|
|
85
|
+
ref = torch.where(normed >= n, rng - normed, normed)
|
|
86
|
+
idx = (ref - 0.5).long()
|
|
87
|
+
elif mode == "reflect":
|
|
88
|
+
# Whole-sample symmetric extension (period 2n-2, mirrors at 0 and n-1).
|
|
89
|
+
if n <= 1:
|
|
90
|
+
idx = torch.zeros_like(idx)
|
|
91
|
+
else:
|
|
92
|
+
rng = 2 * n - 2
|
|
93
|
+
normed = torch.remainder(idx, rng)
|
|
94
|
+
idx = torch.where(normed >= n, rng - normed, normed).long()
|
|
95
|
+
elif mode == "periodic":
|
|
96
|
+
idx = torch.remainder(idx, n)
|
|
97
|
+
elif mode == "constant":
|
|
98
|
+
# Edge extension (PyWavelets' 'constant' mode pads with the boundary
|
|
99
|
+
# value; this is the same as torch's 'replicate' padding).
|
|
100
|
+
idx = torch.clamp(idx, 0, n - 1)
|
|
101
|
+
else: # pragma: no cover - validated upstream
|
|
102
|
+
raise ValueError(f"pad_indices does not support mode {mode!r}")
|
|
103
|
+
return idx
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def pad_indices(n: int, before: int, after: int, mode: str, device: torch.device) -> torch.Tensor:
|
|
107
|
+
"""Long index tensor extending a signal of length ``n`` with the given
|
|
108
|
+
boundary mode (for ``mode != 'zero'``)."""
|
|
109
|
+
return _pad_indices_cached(n, before, after, mode, device)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Dual-tree complex wavelet transforms."""
|
|
2
|
+
|
|
3
|
+
from .coeffs import biort, level1, qshift
|
|
4
|
+
from .transform1d import DTCWT1D, IDTCWT1D, dtcwt1d, idtcwt1d
|
|
5
|
+
from .transform2d import DTCWT2D, IDTCWT2D, dtcwt2d, idtcwt2d
|
|
6
|
+
from .transform3d import DTCWT3D, IDTCWT3D, dtcwt3d, idtcwt3d
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DTCWT1D",
|
|
10
|
+
"IDTCWT1D",
|
|
11
|
+
"DTCWT2D",
|
|
12
|
+
"IDTCWT2D",
|
|
13
|
+
"DTCWT3D",
|
|
14
|
+
"IDTCWT3D",
|
|
15
|
+
"dtcwt1d",
|
|
16
|
+
"idtcwt1d",
|
|
17
|
+
"dtcwt2d",
|
|
18
|
+
"idtcwt2d",
|
|
19
|
+
"dtcwt3d",
|
|
20
|
+
"idtcwt3d",
|
|
21
|
+
"biort",
|
|
22
|
+
"level1",
|
|
23
|
+
"qshift",
|
|
24
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Autocast handling for the DTCWT family.
|
|
2
|
+
|
|
3
|
+
The DTCWT assembles complex bandpasses with ``torch.complex``, which rejects
|
|
4
|
+
half and bfloat16. Under ``torch.autocast`` the convolution outputs are cast
|
|
5
|
+
to the autocast dtype, so the whole computation runs in an autocast-disabled
|
|
6
|
+
scope: inputs are promoted to float32 internally (the documented half/bf16
|
|
7
|
+
behavior) and the result keeps that precision.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def autocast_off(device: torch.device) -> torch.autocast:
|
|
16
|
+
"""Return an autocast-disabled context for ``device``'s type.
|
|
17
|
+
|
|
18
|
+
A plain factory rather than a ``contextlib.contextmanager``: Dynamo
|
|
19
|
+
inlines regular function calls and natively supports ``torch.autocast``,
|
|
20
|
+
but generator-based context managers cannot be traced, so they break
|
|
21
|
+
``torch.compile(fullgraph=True)``.
|
|
22
|
+
"""
|
|
23
|
+
device_type = "cuda" if device.type == "cuda" else "cpu"
|
|
24
|
+
return torch.autocast(device_type=device_type, enabled=False)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Dual-tree complex wavelet filter banks (Kingsbury's design).
|
|
2
|
+
|
|
3
|
+
The filter coefficients are the standard DTCWT banks:
|
|
4
|
+
|
|
5
|
+
- Level 1 (biorthogonal): ``antonini``, ``farras``, ``legall``, ``near_sym_a``,
|
|
6
|
+
``near_sym_b``, ``near_sym_a2``, ``near_sym_b_bp``
|
|
7
|
+
- Levels 2+ (quarter-sample shift): ``qshift_06``, ``qshift_32``, ``qshift_a``,
|
|
8
|
+
``qshift_b``, ``qshift_c``, ``qshift_d``, ``qshift_b_bp``
|
|
9
|
+
|
|
10
|
+
The coefficients originate from Nick Kingsbury's dual-tree complex wavelet
|
|
11
|
+
transform toolbox (Cambridge University), which is free for research use only.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from importlib.resources import files
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
__all__ = ["level1", "qshift", "biort", "pywt_coeffs"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=32)
|
|
25
|
+
def _load_from_file(basename: str, varnames: tuple) -> tuple[np.ndarray, ...]:
|
|
26
|
+
"""Load filter vectors from the bundled ``.npz`` data files."""
|
|
27
|
+
data = files("torchwavelets.dtcwt.data").joinpath(basename + ".npz")
|
|
28
|
+
with data.open("rb") as f:
|
|
29
|
+
mat = dict(np.load(f))
|
|
30
|
+
try:
|
|
31
|
+
return tuple(mat[k] for k in varnames)
|
|
32
|
+
except KeyError as e: # pragma: no cover - data files are fixed
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Wavelet {basename!r} does not define ({', '.join(varnames)}) "
|
|
35
|
+
f"coefficients; available keys: {sorted(mat)}"
|
|
36
|
+
) from e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def level1(name: str, compact: bool = False) -> tuple[np.ndarray, ...]:
|
|
40
|
+
"""Load level-1 (biorthogonal) DTCWT filters by name.
|
|
41
|
+
|
|
42
|
+
Returns ``(h0o, g0o, h1o, g1o)`` (compact) or the 8-tuple
|
|
43
|
+
``(h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b)``. ``near_sym_b_bp`` also
|
|
44
|
+
returns the bandpass ``h2o``/``g2o`` (compact) or ``h2a, h2b, g2a, g2b``.
|
|
45
|
+
"""
|
|
46
|
+
if compact:
|
|
47
|
+
if name == "near_sym_b_bp":
|
|
48
|
+
return _load_from_file(name, ("h0o", "g0o", "h1o", "g1o", "h2o", "g2o"))
|
|
49
|
+
return _load_from_file(name, ("h0o", "g0o", "h1o", "g1o"))
|
|
50
|
+
return _load_from_file(name, ("h0a", "h0b", "g0a", "g0b", "h1a", "h1b", "g1a", "g1b"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def qshift(name: str) -> tuple[np.ndarray, ...]:
|
|
54
|
+
"""Load level >= 2 (quarter-sample shift) DTCWT filters by name.
|
|
55
|
+
|
|
56
|
+
Returns ``(h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b)`` (``qshift_b_bp``
|
|
57
|
+
also returns the bandpass ``h2a, h2b, g2a, g2b``).
|
|
58
|
+
"""
|
|
59
|
+
if name == "qshift_b_bp":
|
|
60
|
+
return _load_from_file(
|
|
61
|
+
name,
|
|
62
|
+
("h0a", "h0b", "g0a", "g0b", "h1a", "h1b", "g1a", "g1b", "h2a", "h2b", "g2a", "g2b"),
|
|
63
|
+
)
|
|
64
|
+
return _load_from_file(name, ("h0a", "h0b", "g0a", "g0b", "h1a", "h1b", "g1a", "g1b"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def biort(name: str) -> tuple[np.ndarray, ...]:
|
|
68
|
+
"""Deprecated alias of :func:`level1` (``compact=True``)."""
|
|
69
|
+
return level1(name, compact=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def pywt_coeffs(name: str):
|
|
73
|
+
"""Wrap a PyWavelets wavelet (for custom level-1 banks)."""
|
|
74
|
+
import pywt
|
|
75
|
+
|
|
76
|
+
return pywt.Wavelet(name)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|