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.
Files changed (47) hide show
  1. torchwavelets/__init__.py +62 -0
  2. torchwavelets/_version.py +1 -0
  3. torchwavelets/compat.py +136 -0
  4. torchwavelets/core/__init__.py +13 -0
  5. torchwavelets/core/filters.py +211 -0
  6. torchwavelets/core/modes.py +109 -0
  7. torchwavelets/dtcwt/__init__.py +24 -0
  8. torchwavelets/dtcwt/_autocast.py +24 -0
  9. torchwavelets/dtcwt/coeffs.py +76 -0
  10. torchwavelets/dtcwt/data/__init__.py +5 -0
  11. torchwavelets/dtcwt/data/antonini.npz +0 -0
  12. torchwavelets/dtcwt/data/farras.npz +0 -0
  13. torchwavelets/dtcwt/data/legall.npz +0 -0
  14. torchwavelets/dtcwt/data/near_sym_a.npz +0 -0
  15. torchwavelets/dtcwt/data/near_sym_a2.npz +0 -0
  16. torchwavelets/dtcwt/data/near_sym_b.npz +0 -0
  17. torchwavelets/dtcwt/data/near_sym_b_bp.npz +0 -0
  18. torchwavelets/dtcwt/data/qshift_06.npz +0 -0
  19. torchwavelets/dtcwt/data/qshift_32.npz +0 -0
  20. torchwavelets/dtcwt/data/qshift_a.npz +0 -0
  21. torchwavelets/dtcwt/data/qshift_b.npz +0 -0
  22. torchwavelets/dtcwt/data/qshift_b_bp.npz +0 -0
  23. torchwavelets/dtcwt/data/qshift_c.npz +0 -0
  24. torchwavelets/dtcwt/data/qshift_d.npz +0 -0
  25. torchwavelets/dtcwt/lowlevel.py +296 -0
  26. torchwavelets/dtcwt/transform1d.py +253 -0
  27. torchwavelets/dtcwt/transform2d.py +298 -0
  28. torchwavelets/dtcwt/transform3d.py +420 -0
  29. torchwavelets/dtcwt/transform_funcs.py +93 -0
  30. torchwavelets/dwt/__init__.py +6 -0
  31. torchwavelets/dwt/_validation.py +59 -0
  32. torchwavelets/dwt/lowlevel.py +153 -0
  33. torchwavelets/dwt/transform1d.py +175 -0
  34. torchwavelets/dwt/transform2d.py +195 -0
  35. torchwavelets/packets.py +439 -0
  36. torchwavelets/py.typed +0 -0
  37. torchwavelets/scatter/__init__.py +6 -0
  38. torchwavelets/scatter/lowlevel.py +221 -0
  39. torchwavelets/scatter/network.py +204 -0
  40. torchwavelets/swt/__init__.py +14 -0
  41. torchwavelets/swt/lowlevel.py +53 -0
  42. torchwavelets/swt/transform.py +386 -0
  43. torchwavelets-0.1.0.dist-info/METADATA +303 -0
  44. torchwavelets-0.1.0.dist-info/RECORD +47 -0
  45. torchwavelets-0.1.0.dist-info/WHEEL +5 -0
  46. torchwavelets-0.1.0.dist-info/licenses/LICENSE +29 -0
  47. 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"
@@ -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)
@@ -0,0 +1,5 @@
1
+ """Bundled DTCWT filter-bank data files (``.npz``).
2
+
3
+ This package contains no code; it exists so setuptools includes the ``.npz``
4
+ filter files in wheels and sdists (see ``[tool.setuptools.package-data]``).
5
+ """
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file