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,153 @@
1
+ """Low-level 1-D analysis/synthesis filter banks operating on 4-D tensors.
2
+
3
+ All functions operate on ``(N, C, H, W)`` tensors and filter along one spatial
4
+ axis (``dim=2`` for columns, ``dim=3`` for rows). Filters must be 4-D tensors
5
+ of shape ``(1, 1, L, 1)`` (column) or ``(1, 1, 1, L)`` (row); analysis filters
6
+ must already be reversed (see :mod:`torchwavelets.core.filters`).
7
+
8
+ Everything is built from plain torch ops (``conv2d`` / ``conv_transpose2d`` /
9
+ index-based padding), so gradients flow through all dtypes, autocast works
10
+ natively, and no custom ``autograd.Function`` is required.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+ from ..core.modes import pad_indices, padding_sizes
19
+
20
+ __all__ = ["afb1d", "sfb1d"]
21
+
22
+
23
+ def _dim_slice(dim: int, sl) -> tuple[slice, ...]:
24
+ s = [slice(None)] * 4
25
+ s[dim] = sl
26
+ return tuple(s)
27
+
28
+
29
+ def _cat_last(x: torch.Tensor, dim: int) -> torch.Tensor:
30
+ """Append a copy of the last sample along ``dim`` (used for odd lengths)."""
31
+ idx = torch.tensor([x.shape[dim] - 1], device=x.device)
32
+ return torch.cat((x, x.index_select(dim, idx)), dim=dim)
33
+
34
+
35
+ def afb1d(
36
+ x: torch.Tensor,
37
+ h0: torch.Tensor,
38
+ h1: torch.Tensor,
39
+ mode: str,
40
+ dim: int,
41
+ ) -> torch.Tensor:
42
+ """1-D analysis filter bank along ``dim`` with decimation by 2.
43
+
44
+ Args:
45
+ x: 4-D input ``(N, C, H, W)``.
46
+ h0, h1: 4-D analysis filters (reversed), shape ``(1, 1, L, 1)`` if
47
+ ``dim == 2`` else ``(1, 1, 1, L)``.
48
+ mode: boundary mode (see :mod:`torchwavelets.core.modes`).
49
+ dim: spatial dimension to filter along (2 or 3).
50
+
51
+ Returns:
52
+ ``(N, 2C, ...)`` tensor with the lowpass/highpass outputs interleaved
53
+ along the channel dimension as ``[lo_0, hi_0, lo_1, hi_1, ...]``.
54
+ """
55
+ C = x.shape[1]
56
+ L = h0.numel()
57
+ stride = (2, 1) if dim == 2 else (1, 2)
58
+ h = torch.cat([h0, h1] * C, dim=0)
59
+
60
+ if mode == "periodization":
61
+ # Circular transform with the *reversed* analysis filter (conv2d is a
62
+ # correlation): c[k] = sum_m h[m] * x[(2k + L/2 - m) mod n], which is
63
+ # PyWavelets' periodization convention (bit-exact vs pywt.dwt).
64
+ n = x.shape[dim]
65
+ if n % 2 == 1:
66
+ x = _cat_last(x, dim)
67
+ n += 1
68
+ shift = L // 2 - 1
69
+ idx = (torch.arange(n + L - 2, device=x.device) - shift) % n
70
+ xp = x.index_select(dim, idx)
71
+ return F.conv2d(xp, h, stride=stride, groups=C)
72
+
73
+ n = x.shape[dim]
74
+ before, after = padding_sizes(n, L, mode)
75
+ if mode == "zero":
76
+ shape = list(x.shape)
77
+ shape[dim] = n + before + after
78
+ padded = torch.zeros(shape, dtype=x.dtype, device=x.device)
79
+ padded[_dim_slice(dim, slice(before, before + n))] = x
80
+ elif mode == "smooth":
81
+ # PyWavelets 'smooth': linear extrapolation from the edges,
82
+ # left pad (k = before..1): x0 + k*(x0 - x1), right pad (k = 1..after):
83
+ # x[-1] + k*(x[-1] - x[-2]).
84
+ x0 = x[_dim_slice(dim, slice(0, 1))]
85
+ x1 = x[_dim_slice(dim, slice(1, 2))] if n >= 2 else torch.zeros_like(x0)
86
+ xm1 = x[_dim_slice(dim, slice(-1, None))]
87
+ xm2 = x[_dim_slice(dim, slice(-2, -1))] if n >= 2 else torch.zeros_like(xm1)
88
+ kshape = [1, 1, 1, 1]
89
+ kshape[dim] = -1
90
+ k_left = torch.arange(before, 0, -1, dtype=x.dtype, device=x.device).view(kshape)
91
+ k_right = torch.arange(1, after + 1, dtype=x.dtype, device=x.device).view(kshape)
92
+ left = x0 + k_left * (x0 - x1)
93
+ right = xm1 + k_right * (xm1 - xm2)
94
+ padded = torch.cat([left, x, right], dim=dim)
95
+ else:
96
+ idx = pad_indices(n, before, after, mode, x.device)
97
+ padded = x.index_select(dim, idx)
98
+ return F.conv2d(padded, h, stride=stride, groups=C)
99
+
100
+
101
+ def sfb1d(
102
+ lo: torch.Tensor,
103
+ hi: torch.Tensor,
104
+ g0: torch.Tensor,
105
+ g1: torch.Tensor,
106
+ mode: str,
107
+ dim: int,
108
+ ) -> torch.Tensor:
109
+ """1-D synthesis filter bank along ``dim`` with upsampling by 2.
110
+
111
+ Args:
112
+ lo, hi: 4-D lowpass/highpass coefficients ``(N, C, ...)``.
113
+ g0, g1: 4-D synthesis filters (natural order), shape ``(1, 1, L, 1)``
114
+ if ``dim == 2`` else ``(1, 1, 1, L)``.
115
+ mode: boundary mode.
116
+ dim: spatial dimension (2 or 3).
117
+
118
+ Returns:
119
+ Reconstructed ``(N, C, ...)`` tensor.
120
+ """
121
+ C = lo.shape[1]
122
+ L = g0.numel()
123
+ stride = (2, 1) if dim == 2 else (1, 2)
124
+ g0 = torch.cat([g0] * C, dim=0)
125
+ g1 = torch.cat([g1] * C, dim=0)
126
+
127
+ if mode == "periodization":
128
+ # Circular synthesis: y[m] = sum_j g[j] * u[(m + j - L/2) mod 2n] with
129
+ # u the upsampled (zero-inserted) coefficient signal. g is flipped
130
+ # because conv2d is a correlation (PyWavelets' periodization, exact).
131
+ n_in = lo.shape[dim]
132
+ n_out = 2 * n_in
133
+ u_lo = torch.zeros(
134
+ *lo.shape[:dim], n_out, *lo.shape[dim + 1 :], dtype=lo.dtype, device=lo.device
135
+ )
136
+ u_hi = torch.zeros_like(u_lo)
137
+ u_lo[_dim_slice(dim, slice(None, None, 2))] = lo
138
+ u_hi[_dim_slice(dim, slice(None, None, 2))] = hi
139
+ idx = (torch.arange(n_out + L - 1, device=lo.device) - L // 2) % n_out
140
+ # Flip along the filter-tap dimension: conv2d is a correlation.
141
+ # g0/g1 are already tiled to C channels above.
142
+ flip = -2 if dim == 2 else -1
143
+ g0 = g0.flip(flip)
144
+ g1 = g1.flip(flip)
145
+ y = F.conv2d(u_lo.index_select(dim, idx), g0, groups=C) + F.conv2d(
146
+ u_hi.index_select(dim, idx), g1, groups=C
147
+ )
148
+ return y
149
+
150
+ pad = (L - 2, 0) if dim == 2 else (0, L - 2)
151
+ y = F.conv_transpose2d(lo, g0, stride=stride, padding=pad, groups=C)
152
+ y = y + F.conv_transpose2d(hi, g1, stride=stride, padding=pad, groups=C)
153
+ return y
@@ -0,0 +1,175 @@
1
+ """1-D discrete wavelet transform (functional and module API).
2
+
3
+ The module API resolves its filters into registered buffers at construction
4
+ time (eager) and only performs torch ops (``.to(dtype, device)`` + convs) in
5
+ ``forward``, so it is fully traceable by ``torch.compile``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+
12
+ from ..core.filters import filter_bank, resolve_1d, wave_name
13
+ from ..core.modes import validate_mode
14
+ from . import lowlevel
15
+ from ._validation import check_reflect_length, check_spatial, validate_level, warn_if_too_high
16
+
17
+ __all__ = ["dwt1d", "idwt1d", "DWT1D", "IDWT1D"]
18
+
19
+ _IS_COMPILING = getattr(torch.compiler, "is_compiling", lambda: False)
20
+
21
+
22
+ def _dwt1d_impl(x: torch.Tensor, h0, h1, mode: str, level: int):
23
+ yl = x[:, :, None, :]
24
+ highs = []
25
+ for _ in range(level):
26
+ lohi = lowlevel.afb1d(yl, h0, h1, mode, 3)
27
+ yl = lohi[:, ::2]
28
+ highs.append(lohi[:, 1::2][:, :, 0])
29
+ return yl[:, :, 0], highs
30
+
31
+
32
+ def _idwt1d_impl(yl: torch.Tensor, highs, g0, g1, mode: str) -> torch.Tensor:
33
+ y = yl[:, :, None, :]
34
+ for h in reversed(highs):
35
+ if h is None:
36
+ h = torch.zeros_like(y[:, :, 0])
37
+ elif y.shape[3] == h.shape[2] + 1:
38
+ # PyWavelets convention: drop the extra sample introduced by an
39
+ # odd-length previous level before synthesizing.
40
+ y = y[..., :-1]
41
+ if y.shape[3] != h.shape[2]:
42
+ raise ValueError(
43
+ "coefficient shape mismatch: approximation "
44
+ f"{tuple(y.shape)} vs detail {tuple(h.shape)} at the same level"
45
+ )
46
+ y = lowlevel.sfb1d(y, h[:, :, None, :], g0, g1, mode, 3)
47
+ return y[:, :, 0]
48
+
49
+
50
+ def dwt1d(
51
+ x: torch.Tensor, wave="db1", mode: str = "symmetric", level: int = 1
52
+ ) -> tuple[torch.Tensor, list[torch.Tensor]]:
53
+ """Forward 1-D DWT.
54
+
55
+ Args:
56
+ x (torch.Tensor): ``(N, C, L)`` input.
57
+ wave (str or pywt.Wavelet or tuple): wavelet name, ``pywt.Wavelet`` or
58
+ custom filter bank.
59
+ mode (str): boundary mode.
60
+ level (int): number of decomposition levels (``0`` returns ``(x, [])``).
61
+
62
+ Returns:
63
+ tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)`` where ``yl`` is
64
+ the final lowpass of shape ``(N, C, L')`` and ``yh`` is a list of
65
+ ``level`` highpass tensors, finest first. The output dtype matches the
66
+ input dtype.
67
+ """
68
+ if x.ndim != 3:
69
+ raise ValueError(f"dwt1d expects a 3-D input (N, C, L); got shape {tuple(x.shape)}")
70
+ check_spatial(x.shape[-1:], ("L",), "dwt1d")
71
+ validate_level(level)
72
+ mode = validate_mode(mode)
73
+ check_reflect_length(x.shape[-1:], ("L",), mode, "dwt1d")
74
+
75
+ warn_if_too_high(x.shape[-1], filter_bank(wave).length, level)
76
+ h0, h1, _, _ = resolve_1d(wave, x.dtype, x.device)
77
+ return _dwt1d_impl(x, h0, h1, mode, level)
78
+
79
+
80
+ def idwt1d(coeffs, wave="db1", mode: str = "symmetric") -> torch.Tensor:
81
+ """Inverse 1-D DWT.
82
+
83
+ Args:
84
+ coeffs (tuple): ``(yl, yh)`` as returned by :func:`dwt1d` (any
85
+ highpass entry may be ``None``, in which case it is treated as
86
+ zeros).
87
+ wave (str or pywt.Wavelet or tuple): as in :func:`dwt1d`.
88
+ mode (str): as in :func:`dwt1d`.
89
+
90
+ Returns:
91
+ torch.Tensor: reconstructed ``(N, C, L')`` tensor (dtype matches the
92
+ coefficients).
93
+
94
+ This is a ``pywt.waverec``-style inverse: the coefficient pyramid from
95
+ :func:`dwt1d` is inverted level by level, and (like ``pywt.waverec``) an
96
+ odd-length previous level's extra sample is dropped inside the
97
+ reconstruction. Unlike ``pywt.idwt`` (single-level), the lowpass/detail
98
+ pair of a level does not need to match in length.
99
+ """
100
+ yl, highs = coeffs
101
+ if yl is None:
102
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
103
+ if yl.ndim != 3:
104
+ raise ValueError(f"idwt1d expects 3-D coefficients; got shape {tuple(yl.shape)}")
105
+ check_spatial(yl.shape[-1:], ("L",), "idwt1d")
106
+ mode = validate_mode(mode)
107
+
108
+ _, _, g0, g1 = resolve_1d(wave, yl.dtype, yl.device)
109
+ return _idwt1d_impl(yl, highs, g0, g1, mode)
110
+
111
+
112
+ class DWT1D(torch.nn.Module):
113
+ """1-D DWT module.
114
+
115
+ Args:
116
+ J (int): number of decomposition levels.
117
+ wave (str or pywt.Wavelet or tuple): as in :func:`dwt1d`.
118
+ mode (str): as in :func:`dwt1d`.
119
+
120
+ The precision follows the input tensor: feed a float16 tensor and the
121
+ output is float16, regardless of the dtype used at construction time.
122
+ The module is ``torch.compile``-compatible (filters are resolved eagerly
123
+ at construction; only torch ops run in ``forward``).
124
+ """
125
+
126
+ def __init__(self, J: int = 1, wave="db1", mode: str = "symmetric"):
127
+ super().__init__()
128
+ validate_level(J)
129
+ self.J = J
130
+ self.wave = wave
131
+ self.mode = validate_mode(mode)
132
+ self._bank_length = filter_bank(wave).length
133
+ h0, h1, _, _ = resolve_1d(wave, torch.float64, torch.device("cpu"))
134
+ self.register_buffer("h0", h0)
135
+ self.register_buffer("h1", h1)
136
+
137
+ def forward(self, x: torch.Tensor):
138
+ if x.ndim != 3:
139
+ raise ValueError(f"DWT1D expects a 3-D input (N, C, L); got shape {tuple(x.shape)}")
140
+ check_spatial(x.shape[-1:], ("L",), "DWT1D")
141
+ check_reflect_length(x.shape[-1:], ("L",), self.mode, "DWT1D")
142
+ if not _IS_COMPILING():
143
+ warn_if_too_high(x.shape[-1], self._bank_length, self.J)
144
+ h0 = self.h0.to(dtype=x.dtype, device=x.device)
145
+ h1 = self.h1.to(dtype=x.dtype, device=x.device)
146
+ return _dwt1d_impl(x, h0, h1, self.mode, self.J)
147
+
148
+ def extra_repr(self) -> str:
149
+ return f"J={self.J}, wave={wave_name(self.wave)!r}, mode={self.mode!r}"
150
+
151
+
152
+ class IDWT1D(torch.nn.Module):
153
+ """1-D inverse DWT module (see :func:`idwt1d`)."""
154
+
155
+ def __init__(self, wave="db1", mode: str = "symmetric"):
156
+ super().__init__()
157
+ self.wave = wave
158
+ self.mode = validate_mode(mode)
159
+ _, _, g0, g1 = resolve_1d(wave, torch.float64, torch.device("cpu"))
160
+ self.register_buffer("g0", g0)
161
+ self.register_buffer("g1", g1)
162
+
163
+ def forward(self, coeffs):
164
+ yl, _ = coeffs
165
+ if yl is None:
166
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
167
+ if yl.ndim != 3:
168
+ raise ValueError(f"IDWT1D expects 3-D coefficients; got shape {tuple(yl.shape)}")
169
+ check_spatial(yl.shape[-1:], ("L",), "IDWT1D")
170
+ g0 = self.g0.to(dtype=yl.dtype, device=yl.device)
171
+ g1 = self.g1.to(dtype=yl.dtype, device=yl.device)
172
+ return _idwt1d_impl(yl, coeffs[1], g0, g1, self.mode)
173
+
174
+ def extra_repr(self) -> str:
175
+ return f"wave={wave_name(self.wave)!r}, mode={self.mode!r}"
@@ -0,0 +1,195 @@
1
+ """2-D discrete wavelet transform (functional and module API).
2
+
3
+ The module API resolves its filters into registered buffers at construction
4
+ time (eager) and only performs torch ops (``.to(dtype, device)`` + convs) in
5
+ ``forward``, so it is fully traceable by ``torch.compile``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+
12
+ from ..core.filters import filter_bank, resolve_2d, wave_name
13
+ from ..core.modes import validate_mode
14
+ from . import lowlevel
15
+ from ._validation import check_reflect_length, check_spatial, validate_level, warn_if_too_high
16
+
17
+ __all__ = ["dwt2d", "idwt2d", "DWT2D", "IDWT2D"]
18
+
19
+ _IS_COMPILING = getattr(torch.compiler, "is_compiling", lambda: False)
20
+
21
+ _DWT2D_NAMES = ("h0_col", "h1_col", "h0_row", "h1_row")
22
+ _IDWT2D_NAMES = ("g0_col", "g1_col", "g0_row", "g1_row")
23
+
24
+
25
+ def _dwt2d_impl(x: torch.Tensor, h0c, h1c, h0r, h1r, mode: str, level: int):
26
+ yl = x
27
+ highs = []
28
+ for _ in range(level):
29
+ lohi = lowlevel.afb1d(yl, h0r, h1r, mode, 3) # rows
30
+ y = lowlevel.afb1d(lohi, h0c, h1c, mode, 2) # columns
31
+ s = y.shape
32
+ y = y.reshape(s[0], s[1] // 4, 4, s[-2], s[-1])
33
+ yl = y[:, :, 0].contiguous()
34
+ highs.append(y[:, :, 1:].contiguous())
35
+ return yl, highs
36
+
37
+
38
+ def _idwt2d_impl(yl: torch.Tensor, highs, g0c, g1c, g0r, g1r, mode: str) -> torch.Tensor:
39
+ y = yl
40
+ for h in reversed(highs):
41
+ if h is None:
42
+ h = torch.zeros(
43
+ y.shape[0],
44
+ y.shape[1],
45
+ 3,
46
+ y.shape[-2],
47
+ y.shape[-1],
48
+ dtype=y.dtype,
49
+ device=y.device,
50
+ )
51
+ else:
52
+ # PyWavelets convention: drop extra samples introduced by
53
+ # odd-length previous levels before synthesizing.
54
+ if y.shape[-2] == h.shape[-2] + 1:
55
+ y = y[..., :-1, :]
56
+ if y.shape[-1] == h.shape[-1] + 1:
57
+ y = y[..., :-1]
58
+ if y.shape[-2] != h.shape[-2] or y.shape[-1] != h.shape[-1]:
59
+ raise ValueError(
60
+ "coefficient shape mismatch: approximation "
61
+ f"{tuple(y.shape)} vs detail {tuple(h.shape)} at the same level"
62
+ )
63
+ lh, hl, hh = torch.unbind(h, dim=2)
64
+ lo = lowlevel.sfb1d(y, lh, g0c, g1c, mode, 2)
65
+ hi = lowlevel.sfb1d(hl, hh, g0c, g1c, mode, 2)
66
+ y = lowlevel.sfb1d(lo, hi, g0r, g1r, mode, 3)
67
+ return y
68
+
69
+
70
+ def dwt2d(
71
+ x: torch.Tensor, wave="db1", mode: str = "symmetric", level: int = 1
72
+ ) -> tuple[torch.Tensor, list[torch.Tensor]]:
73
+ """Forward 2-D DWT (separable, row then column filtering).
74
+
75
+ Args:
76
+ x (torch.Tensor): ``(N, C, H, W)`` input.
77
+ wave (str or pywt.Wavelet or tuple): wavelet name, ``pywt.Wavelet`` or
78
+ custom filter bank.
79
+ mode (str): boundary mode.
80
+ level (int): number of decomposition levels (``0`` returns ``(x, [])``).
81
+
82
+ Returns:
83
+ ``(yl, yh)`` where ``yl`` is the final lowpass ``(N, C, H', W')`` and
84
+ ``yh`` is a list of ``level`` tensors of shape
85
+ ``(N, C, 3, H'', W'')`` holding the ``(LH, HL, HH)`` subbands
86
+ (finest first). ``LH``/``HL``/``HH`` match PyWavelets'
87
+ ``cH``/``cV``/``cD``. The output dtype matches the input dtype.
88
+ """
89
+ if x.ndim != 4:
90
+ raise ValueError(f"dwt2d expects a 4-D input (N, C, H, W); got shape {tuple(x.shape)}")
91
+ check_spatial(x.shape[2:], ("H", "W"), "dwt2d")
92
+ mode = validate_mode(mode)
93
+ check_reflect_length(x.shape[2:], ("H", "W"), mode, "dwt2d")
94
+ validate_level(level)
95
+ mode = validate_mode(mode)
96
+
97
+ warn_if_too_high(min(x.shape[-2:]), filter_bank(wave).length, level)
98
+ h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, x.dtype, x.device)
99
+ return _dwt2d_impl(x, h0c, h1c, h0r, h1r, mode, level)
100
+
101
+
102
+ def idwt2d(coeffs, wave="db1", mode: str = "symmetric") -> torch.Tensor:
103
+ """Inverse 2-D DWT.
104
+
105
+ Args:
106
+ coeffs (tuple): ``(yl, yh)`` as returned by :func:`dwt2d` (any
107
+ highpass entry may be ``None``, in which case it is treated as
108
+ zeros).
109
+ wave (str or pywt.Wavelet or tuple): as in :func:`dwt2d`.
110
+ mode (str): as in :func:`dwt2d`.
111
+
112
+ Returns:
113
+ torch.Tensor: reconstructed ``(N, C, H', W')`` tensor (dtype matches
114
+ the coefficients).
115
+ """
116
+ yl, highs = coeffs
117
+ if yl is None:
118
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
119
+ if yl.ndim != 4:
120
+ raise ValueError(f"idwt2d expects 4-D coefficients; got shape {tuple(yl.shape)}")
121
+ check_spatial(yl.shape[2:], ("H", "W"), "idwt2d")
122
+ mode = validate_mode(mode)
123
+
124
+ _, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, yl.dtype, yl.device)
125
+ return _idwt2d_impl(yl, highs, g0c, g1c, g0r, g1r, validate_mode(mode))
126
+
127
+
128
+ class DWT2D(torch.nn.Module):
129
+ """2-D DWT module.
130
+
131
+ Args:
132
+ J (int): number of decomposition levels.
133
+ wave (str or pywt.Wavelet or tuple): as in :func:`dwt2d`.
134
+ mode (str): as in :func:`dwt2d`.
135
+
136
+ The precision follows the input tensor: feed a float16 tensor and the
137
+ output is float16, regardless of the dtype used at construction time.
138
+ The module is ``torch.compile``-compatible (filters are resolved eagerly
139
+ at construction; only torch ops run in ``forward``).
140
+ """
141
+
142
+ def __init__(self, J: int = 1, wave="db1", mode: str = "symmetric"):
143
+ super().__init__()
144
+ validate_level(J)
145
+ self.J = J
146
+ self.wave = wave
147
+ self.mode = validate_mode(mode)
148
+ self._bank_length = filter_bank(wave).length
149
+ filts = resolve_2d(wave, torch.float64, torch.device("cpu"))
150
+ for name, t in zip(_DWT2D_NAMES, filts[:4], strict=True):
151
+ self.register_buffer(name, t)
152
+
153
+ def forward(self, x: torch.Tensor):
154
+ if x.ndim != 4:
155
+ raise ValueError(f"DWT2D expects a 4-D input (N, C, H, W); got shape {tuple(x.shape)}")
156
+ check_spatial(x.shape[2:], ("H", "W"), "DWT2D")
157
+ check_reflect_length(x.shape[2:], ("H", "W"), self.mode, "DWT2D")
158
+ if not _IS_COMPILING():
159
+ warn_if_too_high(min(x.shape[-2:]), self._bank_length, self.J)
160
+ h0c = self.h0_col.to(dtype=x.dtype, device=x.device)
161
+ h1c = self.h1_col.to(dtype=x.dtype, device=x.device)
162
+ h0r = self.h0_row.to(dtype=x.dtype, device=x.device)
163
+ h1r = self.h1_row.to(dtype=x.dtype, device=x.device)
164
+ return _dwt2d_impl(x, h0c, h1c, h0r, h1r, self.mode, self.J)
165
+
166
+ def extra_repr(self) -> str:
167
+ return f"J={self.J}, wave={wave_name(self.wave)!r}, mode={self.mode!r}"
168
+
169
+
170
+ class IDWT2D(torch.nn.Module):
171
+ """2-D inverse DWT module (see :func:`idwt2d`)."""
172
+
173
+ def __init__(self, wave="db1", mode: str = "symmetric"):
174
+ super().__init__()
175
+ self.wave = wave
176
+ self.mode = validate_mode(mode)
177
+ filts = resolve_2d(wave, torch.float64, torch.device("cpu"))
178
+ for name, t in zip(_IDWT2D_NAMES, filts[4:], strict=True):
179
+ self.register_buffer(name, t)
180
+
181
+ def forward(self, coeffs):
182
+ yl, _ = coeffs
183
+ if yl is None:
184
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
185
+ if yl.ndim != 4:
186
+ raise ValueError(f"IDWT2D expects 4-D coefficients; got shape {tuple(yl.shape)}")
187
+ check_spatial(yl.shape[2:], ("H", "W"), "IDWT2D")
188
+ g0c = self.g0_col.to(dtype=yl.dtype, device=yl.device)
189
+ g1c = self.g1_col.to(dtype=yl.dtype, device=yl.device)
190
+ g0r = self.g0_row.to(dtype=yl.dtype, device=yl.device)
191
+ g1r = self.g1_row.to(dtype=yl.dtype, device=yl.device)
192
+ return _idwt2d_impl(yl, coeffs[1], g0c, g1c, g0r, g1r, self.mode)
193
+
194
+ def extra_repr(self) -> str:
195
+ return f"wave={wave_name(self.wave)!r}, mode={self.mode!r}"