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,204 @@
1
+ """General (arbitrary-order) DTCWT scattering network.
2
+
3
+ A scattering network computes locally shift-invariant feature maps by
4
+ iterating wavelet bandpasses with the smooth modulus ``sqrt(|Wx|^2 + b^2) - b``
5
+ and smoothing the intermediate invariants with the wavelet lowpass. With the
6
+ DTCWT's near shift-invariance this gives a small, cheap, highly invariant
7
+ front-end for classification and segmentation.
8
+
9
+ Paths are sequences of increasing scales ``(j1 < j2 < ... < jm)`` with
10
+ ``m <= max_order``. For each path the network returns one invariant:
11
+ ``avg_pool2d(lowpass, 2)`` when the path can still be extended (the smoothed
12
+ modulus feeding the next order) and the raw ``|bandpass|`` at the terminal
13
+ paths. With ``(J=2, max_order=2)`` the output matches
14
+ :class:`~torchwavelets.scatter.lowlevel.ScatLayerj2` exactly.
15
+
16
+ Shift invariance: the raw level-1 modulus ``|W_1 x|`` is only *weakly* shift
17
+ invariant (the single-tree level-1 design), so individual raw bandpass
18
+ invariants drift with sub-scale shifts; the smoothed invariants (pooled
19
+ lowpasses) and the order-0 lowpass are exactly invariant at dyadic integer
20
+ shifts, and the invariance improves with scale.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+ import torch.nn.functional as F
28
+
29
+ from ..dtcwt._autocast import autocast_off
30
+ from ..dtcwt.coeffs import biort as _biort
31
+ from ..dtcwt.coeffs import qshift as _qshift
32
+ from ..dtcwt.transform2d import (
33
+ _compute_dtype,
34
+ _dtcwt2d_impl,
35
+ _prep_filt,
36
+ _validate_names,
37
+ )
38
+
39
+ __all__ = ["DTCWTScattering", "dtcwt_scattering"]
40
+
41
+
42
+ def _smooth_mag(highs: torch.Tensor, magbias: float) -> torch.Tensor:
43
+ """Smooth modulus ``sqrt(|z|^2 + b^2) - b`` of complex bandpasses."""
44
+ return torch.sqrt(highs.abs().pow(2) + magbias**2) - magbias
45
+
46
+
47
+ def _resolve_filters(biort, qshift, dtype, device):
48
+ _validate_names(biort, qshift)
49
+ h0o, _, h1o, _ = _biort(biort)
50
+ h0a, h0b, _, _, h1a, h1b, _, _ = _qshift(qshift)
51
+ return {
52
+ "h0o": _prep_filt(h0o, dtype, device),
53
+ "h1o": _prep_filt(h1o, dtype, device),
54
+ "h0a": _prep_filt(h0a, dtype, device),
55
+ "h0b": _prep_filt(h0b, dtype, device),
56
+ "h1a": _prep_filt(h1a, dtype, device),
57
+ "h1b": _prep_filt(h1b, dtype, device),
58
+ }
59
+
60
+
61
+ def _dtcwt_levels(u: torch.Tensor, f, levels: int):
62
+ """``levels``-level DTCWT of ``u``; returns ``(lowpass, bandpass)``."""
63
+ yl, yh = _dtcwt2d_impl(u, f, levels, [False] * levels, [False] * levels, 2)
64
+ return yl, yh[-1]
65
+
66
+
67
+ def _scattering_impl(x, f, J, max_order, magbias):
68
+ """BFS path expansion; returns ``(s0, [(path, invariant), ...])``.
69
+
70
+ Invariant semantics (matching ScatLayerj2): the invariant of an
71
+ extendable path is the smoothed (avg-pooled) lowpass of the level-1
72
+ transform of its modulus; terminal paths return the raw modulus.
73
+ """
74
+ if J < 1:
75
+ raise ValueError("J must be >= 1 for a scattering network")
76
+ # order-0 invariant: the smoothed final lowpass
77
+ yl, _ = _dtcwt_levels(x, f, J)
78
+ s0 = F.avg_pool2d(yl, 2)
79
+
80
+ results = []
81
+ # frontier entries: (u, current_scale, path, pending_path)
82
+ # pending_path = the path whose smoothed invariant comes from the level-1
83
+ # transform of u (computed at this node).
84
+ frontier = [(x, 0, (), None)]
85
+ for _ in range(max_order):
86
+ next_frontier = []
87
+ for u, scale, path, pending in frontier:
88
+ # level-1 transform of u: needed for the pending invariant and as
89
+ # the first step of the j-loop below.
90
+ ll1, hh1 = _dtcwt_levels(u, f, 1)
91
+ u1 = _flatten_orientations(_smooth_mag(hh1, magbias))
92
+ if pending is not None:
93
+ results.append((pending, F.avg_pool2d(ll1, 2)))
94
+ for j in range(scale + 1, J + 1):
95
+ if j == scale + 1:
96
+ ll, hh, u_next = ll1, hh1, u1
97
+ else:
98
+ ll, hh = _dtcwt_levels(u, f, j - scale)
99
+ u_next = _flatten_orientations(_smooth_mag(hh, magbias))
100
+ new_path = path + (j,)
101
+ if j < J and len(new_path) < max_order:
102
+ next_frontier.append((u_next, j, new_path, new_path))
103
+ else:
104
+ results.append((new_path, _upstream_order(u_next, len(new_path))))
105
+ frontier = next_frontier
106
+ results.sort(key=lambda pv: (len(pv[0]), pv[0]))
107
+ return s0, results
108
+
109
+
110
+ def _flatten_orientations(u: torch.Tensor) -> torch.Tensor:
111
+ """``(N, C, 6, H, W)`` modulus -> ``(N, 6*C, H, W)``."""
112
+ b, ch, _, h, w = u.shape
113
+ return u.reshape(b, ch * 6, h, w)
114
+
115
+
116
+ def _upstream_order(u: torch.Tensor, m: int) -> torch.Tensor:
117
+ """Rotate the orientation blocks to the reference channel order.
118
+
119
+ Internally the moduli propagate channel-major as
120
+ ``(C, o1st, o2nd, ..., om)``; the reference (``ScatLayerj2``) stacks the
121
+ first orientation block last: ``(C, o2nd, ..., om, o1st)``. For a
122
+ first-order modulus (``m == 1``) this is the identity — the reference
123
+ keeps the moduli channel-major ``(C, o1st)``.
124
+ """
125
+ b, c6, h, w = u.shape
126
+ c = c6 // 6**m
127
+ blocks = u.reshape(b, c, *([6] * m), h, w)
128
+ perm = (0, 1) + tuple(range(3, m + 2)) + (2, m + 2, m + 3)
129
+ return blocks.permute(perm).reshape(b, c6, h, w)
130
+
131
+
132
+ def dtcwt_scattering(
133
+ x: torch.Tensor,
134
+ J: int = 3,
135
+ max_order: int = 2,
136
+ biort="near_sym_a",
137
+ qshift="qshift_a",
138
+ magbias: float = 1e-2,
139
+ ) -> tuple[torch.Tensor, list[tuple[tuple[int, ...], torch.Tensor]]]:
140
+ """Forward DTCWT scattering network.
141
+
142
+ Args:
143
+ x (torch.Tensor): ``(N, C, H, W)`` input.
144
+ J (int): number of wavelet scales.
145
+ max_order (int): maximum scattering order.
146
+ biort (str): filter bank name.
147
+ qshift (str): filter bank name.
148
+ magbias (float): smoothing bias of the modulus.
149
+
150
+ Returns:
151
+ tuple[torch.Tensor, list[tuple[tuple[int, ...], torch.Tensor]]]:
152
+ ``(s0, invariants)``: the smoothed order-0 lowpass
153
+ ``(N, C, H/2^J, W/2^J)`` and a list of ``(path, tensor)`` pairs, one
154
+ per path (orders 1..max_order, ordered by order then scale). The
155
+ invariant size depends only on the path's final scale ``jm``:
156
+ ``H/2^jm``.
157
+ """
158
+ if x.ndim != 4:
159
+ raise ValueError(f"dtcwt_scattering expects (N, C, H, W); got {tuple(x.shape)}")
160
+ if isinstance(J, bool) or not isinstance(J, int) or J < 1:
161
+ raise ValueError(f"J must be a positive int; got {J!r}")
162
+ if isinstance(max_order, bool) or not isinstance(max_order, int) or max_order < 1:
163
+ raise ValueError(f"max_order must be a positive int; got {max_order!r}")
164
+ dtype = _compute_dtype(x)
165
+ f = _resolve_filters(biort, qshift, dtype, x.device)
166
+ with autocast_off(x.device):
167
+ return _scattering_impl(x.to(dtype), f, J, max_order, float(magbias))
168
+
169
+
170
+ class DTCWTScattering(nn.Module):
171
+ """DTCWT scattering network module (see :func:`dtcwt_scattering`)."""
172
+
173
+ def __init__(self, J=3, max_order=2, biort="near_sym_a", qshift="qshift_a", magbias=1e-2):
174
+ super().__init__()
175
+ if isinstance(J, bool) or not isinstance(J, int) or J < 1:
176
+ raise ValueError(f"J must be a positive int; got {J!r}")
177
+ if isinstance(max_order, bool) or not isinstance(max_order, int) or max_order < 1:
178
+ raise ValueError(f"max_order must be a positive int; got {max_order!r}")
179
+ self.J = J
180
+ self.max_order = max_order
181
+ self.biort = biort
182
+ self.qshift = qshift
183
+ self.magbias = float(magbias)
184
+ filts = _resolve_filters(biort, qshift, torch.float64, torch.device("cpu"))
185
+ for name, t in filts.items():
186
+ self.register_buffer(name, t)
187
+
188
+ def forward(self, x: torch.Tensor):
189
+ if x.ndim != 4:
190
+ raise ValueError(f"DTCWTScattering expects (N, C, H, W); got {tuple(x.shape)}")
191
+ dtype = _compute_dtype(x)
192
+ f = {
193
+ n: getattr(self, n).to(dtype=dtype, device=x.device)
194
+ for n in ("h0o", "h1o", "h0a", "h0b", "h1a", "h1b")
195
+ }
196
+ with autocast_off(x.device):
197
+ return _scattering_impl(x.to(dtype), f, self.J, self.max_order, self.magbias)
198
+
199
+ def extra_repr(self) -> str:
200
+ return (
201
+ f"J={self.J}, max_order={self.max_order}, "
202
+ f"biort={self.biort!r}, qshift={self.qshift!r}, "
203
+ f"magbias={self.magbias}"
204
+ )
@@ -0,0 +1,14 @@
1
+ """Stationary (undecimated) wavelet transforms."""
2
+
3
+ from .transform import (
4
+ ISWT1D,
5
+ ISWT2D,
6
+ SWT1D,
7
+ SWT2D,
8
+ iswt1d,
9
+ iswt2d,
10
+ swt1d,
11
+ swt2d,
12
+ )
13
+
14
+ __all__ = ["swt1d", "iswt1d", "swt2d", "iswt2d", "SWT1D", "ISWT1D", "SWT2D", "ISWT2D"]
@@ -0,0 +1,53 @@
1
+ """Low-level stationary (undecimated / a trous) wavelet filter banks.
2
+
3
+ The SWT is the undecimated periodic wavelet transform: at level ``j`` the
4
+ analysis filters are dilated (zero-stuffed) by ``2**(j-1)`` and applied with
5
+ no decimation, so every subband has the same length as the input. The
6
+ phase conventions match PyWavelets exactly (verified bit-for-bit by the
7
+ parity tests).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import torch
13
+ import torch.nn.functional as F
14
+
15
+ __all__ = ["swt_axis", "dilate_filter"]
16
+
17
+
18
+ def dilate_filter(h: torch.Tensor, fstep: int) -> torch.Tensor:
19
+ """Insert ``fstep - 1`` zeros between filter taps (a trous dilation)."""
20
+ L = h.numel()
21
+ e = torch.zeros(1, 1, L * fstep, 1, dtype=h.dtype, device=h.device)
22
+ e[0, 0, ::fstep, 0] = h.reshape(-1)
23
+ return e
24
+
25
+
26
+ def swt_axis(x: torch.Tensor, h: torch.Tensor, dim: int, fstep: int) -> torch.Tensor:
27
+ """One stationary analysis filter along ``dim`` (no decimation).
28
+
29
+ Args:
30
+ x: ``(N, C, H, W)`` input.
31
+ h: reversed analysis filter, ``(1, 1, L, 1)`` (column orientation;
32
+ transposed internally for ``dim == 3``).
33
+ dim: spatial dimension (2 or 3).
34
+ fstep: dilation factor ``2**(level - 1)``.
35
+
36
+ Returns:
37
+ ``(N, C, ...)`` with the same spatial size as ``x``.
38
+ """
39
+ n = x.shape[dim]
40
+ L = h.numel()
41
+ Ld = L * fstep # dilated filter length
42
+ e = dilate_filter(h, fstep)
43
+ if dim == 3:
44
+ e = e.transpose(2, 3).contiguous()
45
+ # pywt: out[i] = sum_m dec[m] x[(i + (L/2 - m)*fstep) mod n]; with the
46
+ # reversed-filter convention this is out[i] = sum_k h_rev[k]
47
+ # x[(i + (k - L/2 + 1)*fstep) mod n].
48
+ shift = fstep * (L // 2 - 1)
49
+ idx = (torch.arange(n + Ld - 1, device=x.device) - shift) % n
50
+ xp = x.index_select(dim, idx)
51
+ stride = (1, 1)
52
+ C = x.shape[1]
53
+ return F.conv2d(xp, e.repeat(C, 1, 1, 1), stride=stride, groups=C)
@@ -0,0 +1,386 @@
1
+ """Stationary (undecimated) wavelet transform: functional and module API.
2
+
3
+ The SWT (algorithm a trous) keeps every subband at the input resolution by
4
+ dilating the analysis filters by ``2**(level-1)`` and skipping decimation.
5
+ Coefficients follow the PyWavelets conventions exactly (coarsest first), and
6
+ ``iswt`` / ``iswt2`` are exact inverses (verified bit-for-bit against pywt
7
+ by the parity tests).
8
+
9
+ Inputs must be divisible by ``2**level`` along every transformed axis, as
10
+ required by the stationary transform (PyWavelets raises the same error).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import cast
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+
20
+ from ..core.filters import resolve_2d
21
+ from ..dwt import lowlevel as dwt_ll
22
+ from ..dwt._validation import check_spatial
23
+ from .lowlevel import swt_axis
24
+
25
+ __all__ = ["swt1d", "iswt1d", "swt2d", "iswt2d", "SWT1D", "ISWT1D", "SWT2D", "ISWT2D"]
26
+
27
+
28
+ def _check_level(level: int) -> None:
29
+ if isinstance(level, bool) or not isinstance(level, int) or level < 1:
30
+ raise ValueError(f"level must be a positive int; got {level!r}")
31
+
32
+
33
+ def _check_divisible(n: int, level: int, name: str) -> None:
34
+ if n % (2**level) != 0:
35
+ raise ValueError(
36
+ f"{name} length {n} is not divisible by 2**level = {2**level}; "
37
+ "the stationary wavelet transform requires it (pad the input)"
38
+ )
39
+
40
+
41
+ def _iswt_idwt(lo: torch.Tensor, hi: torch.Tensor, g0r, g1r) -> torch.Tensor:
42
+ """Single-level periodic inverse DWT on 3-D ``(N, C, L)`` coefficients."""
43
+ lo4 = lo.unsqueeze(2)
44
+ hi4 = hi.unsqueeze(2)
45
+ return dwt_ll.sfb1d(lo4, hi4, g0r, g1r, "periodization", 3).squeeze(2)
46
+
47
+
48
+ def swt1d(
49
+ x: torch.Tensor, wave, level: int, norm: bool = False
50
+ ) -> list[tuple[torch.Tensor, torch.Tensor]]:
51
+ """1-D stationary wavelet transform.
52
+
53
+ Args:
54
+ x (torch.Tensor): ``(N, C, L)`` input.
55
+ wave (str or pywt.Wavelet or tuple): wavelet name / bank (see
56
+ :func:`torchwavelets.core.filters.filter_bank`).
57
+ level (int): number of scales.
58
+ norm (bool): normalize so the coefficient energy equals the input
59
+ energy (orthogonal wavelets only, like PyWavelets).
60
+
61
+ Returns:
62
+ list[tuple[torch.Tensor, torch.Tensor]]: ``[(cA_n, cD_n), ...,
63
+ (cA_1, cD_1)]`` (coarsest first), every subband of shape ``(N, C, L)``.
64
+ """
65
+ _check_level(level)
66
+ if x.ndim != 3:
67
+ raise ValueError(f"swt1d expects (N, C, L); got {tuple(x.shape)}")
68
+ check_spatial(x.shape[-1:], ("L",), "swt1d")
69
+ _check_divisible(x.shape[-1], level, "signal")
70
+ h0c, h1c, _, _, g0c, g1c, _, _ = resolve_2d(wave, x.dtype, x.device)
71
+ if norm:
72
+ h0c, h1c = h0c / 2**0.5, h1c / 2**0.5
73
+ x4 = x.unsqueeze(-1) # (N, C, L, 1)
74
+ ret = []
75
+ a = x4
76
+ for j in range(1, level + 1):
77
+ fstep = 2 ** (j - 1)
78
+ ca = swt_axis(a, h0c, 2, fstep)
79
+ cd = swt_axis(a, h1c, 2, fstep)
80
+ ret.append((ca.squeeze(-1), cd.squeeze(-1)))
81
+ a = ca
82
+ ret.reverse()
83
+ return ret
84
+
85
+
86
+ def iswt1d(coeffs, wave, norm: bool = False) -> torch.Tensor:
87
+ """Inverse 1-D stationary wavelet transform (exact inverse of :func:`swt1d`)."""
88
+ for cA, cD in coeffs:
89
+ if cA.shape != cD.shape:
90
+ raise ValueError(
91
+ "iswt1d requires cA and cD of the same shape; got "
92
+ f"{tuple(cA.shape)} vs {tuple(cD.shape)}"
93
+ )
94
+ _, _, _, _, _, _, g0r, g1r = resolve_2d(wave, coeffs[0][0].dtype, coeffs[0][0].device)
95
+ if norm:
96
+ g0r, g1r = g0r * 2**0.5, g1r * 2**0.5
97
+ n_levels = len(coeffs)
98
+ output = coeffs[0][0].clone()
99
+ N = output.shape[-1]
100
+ for j in range(n_levels, 0, -1):
101
+ step = 2 ** (j - 1)
102
+ cD = coeffs[-j][1]
103
+ for first in range(step):
104
+ idx = torch.arange(first, N, step, device=output.device)
105
+ even = idx[0::2]
106
+ odd = idx[1::2]
107
+ x1 = _iswt_idwt(
108
+ output.index_select(-1, even),
109
+ cD.index_select(-1, even),
110
+ g0r,
111
+ g1r,
112
+ )
113
+ x2 = _iswt_idwt(
114
+ output.index_select(-1, odd),
115
+ cD.index_select(-1, odd),
116
+ g0r,
117
+ g1r,
118
+ )
119
+ x2 = torch.roll(x2, 1, dims=-1)
120
+ output[..., idx] = (x1 + x2) / 2
121
+ return output
122
+
123
+
124
+ def swt2d(
125
+ x: torch.Tensor, wave, level: int, norm: bool = False
126
+ ) -> list[tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor, torch.Tensor]]]:
127
+ """2-D stationary wavelet transform.
128
+
129
+ Args:
130
+ x (torch.Tensor): ``(N, C, H, W)`` input.
131
+ wave (str or pywt.Wavelet or tuple): as in :func:`swt1d`.
132
+ level (int): as in :func:`swt1d`.
133
+ norm (bool): as in :func:`swt1d`.
134
+
135
+ Returns:
136
+ list[tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor, torch.Tensor]]]:
137
+ ``[(cA_j, (cH_j, cV_j, cD_j)), ...]`` (coarsest first), all subbands
138
+ of shape ``(N, C, H, W)``.
139
+ """
140
+ _check_level(level)
141
+ if x.ndim != 4:
142
+ raise ValueError(f"swt2d expects (N, C, H, W); got {tuple(x.shape)}")
143
+ check_spatial(x.shape[2:], ("H", "W"), "swt2d")
144
+ _check_divisible(x.shape[-2], level, "height")
145
+ _check_divisible(x.shape[-1], level, "width")
146
+ h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, x.dtype, x.device)
147
+ if norm:
148
+ h0c, h1c, h0r, h1r = h0c / 2**0.5, h1c / 2**0.5, h0r / 2**0.5, h1r / 2**0.5
149
+ ret = []
150
+ a = x
151
+ for j in range(1, level + 1):
152
+ fstep = 2 ** (j - 1)
153
+ # axis 0 (rows, dim 2), then axis 1 (cols, dim 3), like pywt.swt2
154
+ low = swt_axis(a, h0c, 2, fstep)
155
+ high = swt_axis(a, h1c, 2, fstep)
156
+ aa = swt_axis(low, h0r, 3, fstep)
157
+ ad = swt_axis(low, h1r, 3, fstep)
158
+ da = swt_axis(high, h0r, 3, fstep)
159
+ dd = swt_axis(high, h1r, 3, fstep)
160
+ ret.append((aa, (da, ad, dd)))
161
+ a = aa
162
+ ret.reverse()
163
+ return ret
164
+
165
+
166
+ def iswt2d(coeffs, wave, norm: bool = False) -> torch.Tensor:
167
+ """Inverse 2-D stationary wavelet transform (exact inverse of :func:`swt2d`)."""
168
+ for cA, cs in coeffs:
169
+ if any(c.shape != cA.shape for c in cs):
170
+ raise ValueError(
171
+ "iswt2d requires all subbands of a level to share the shape "
172
+ f"{tuple(cA.shape)}; got {[tuple(c.shape) for c in cs]}"
173
+ )
174
+ _, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, coeffs[0][0].dtype, coeffs[0][0].device)
175
+ if norm:
176
+ g0c, g1c, g0r, g1r = g0c * 2**0.5, g1c * 2**0.5, g0r * 2**0.5, g1r * 2**0.5
177
+ n_levels = len(coeffs)
178
+ output = coeffs[0][0].clone()
179
+ H, W = output.shape[-2:]
180
+ for j in range(n_levels):
181
+ step = 2 ** (n_levels - j - 1)
182
+ cH, cV, cD = coeffs[j][1]
183
+ for fh in range(step):
184
+ for fw in range(step):
185
+ approx = output.clone()
186
+ out_slice = output[..., fh::step, fw::step]
187
+ out_slice.zero_()
188
+ for oh in (0, 1):
189
+ for ow in (0, 1):
190
+ h_eo = slice(fh + oh * step, H, 2 * step)
191
+ w_eo = slice(fw + ow * step, W, 2 * step)
192
+ ll = approx[..., h_eo, w_eo]
193
+ lh = cH[..., h_eo, w_eo]
194
+ hl = cV[..., h_eo, w_eo]
195
+ hh = cD[..., h_eo, w_eo]
196
+ lo = dwt_ll.sfb1d(ll, lh, g0c, g1c, "periodization", 2)
197
+ hi = dwt_ll.sfb1d(hl, hh, g0c, g1c, "periodization", 2)
198
+ xr = dwt_ll.sfb1d(lo, hi, g0r, g1r, "periodization", 3)
199
+ if oh:
200
+ xr = torch.roll(xr, 1, dims=-2)
201
+ if ow:
202
+ xr = torch.roll(xr, 1, dims=-1)
203
+ out_slice += xr
204
+ out_slice /= 4
205
+ return output
206
+
207
+
208
+ class SWT1D(nn.Module):
209
+ """1-D stationary wavelet transform module (see :func:`swt1d`)."""
210
+
211
+ def __init__(self, wave, level: int, norm: bool = False):
212
+ super().__init__()
213
+ _check_level(level)
214
+ self.wave = wave
215
+ self.level = level
216
+ self.norm = norm
217
+ h0c, h1c, _, _, _, _, _, _ = resolve_2d(wave, torch.float64, torch.device("cpu"))
218
+ self.register_buffer("h0c", h0c)
219
+ self.register_buffer("h1c", h1c)
220
+
221
+ def forward(self, x: torch.Tensor):
222
+ h0c = cast(torch.Tensor, self.h0c).to(dtype=x.dtype, device=x.device)
223
+ h1c = cast(torch.Tensor, self.h1c).to(dtype=x.dtype, device=x.device)
224
+ if self.norm:
225
+ h0c, h1c = h0c / 2**0.5, h1c / 2**0.5
226
+ _check_level(self.level)
227
+ if x.ndim != 3:
228
+ raise ValueError(f"SWT1D expects (N, C, L); got {tuple(x.shape)}")
229
+ _check_divisible(x.shape[-1], self.level, "signal")
230
+ x4 = x.unsqueeze(-1)
231
+ ret = []
232
+ a = x4
233
+ for j in range(1, self.level + 1):
234
+ fstep = 2 ** (j - 1)
235
+ ca = swt_axis(a, h0c, 2, fstep)
236
+ cd = swt_axis(a, h1c, 2, fstep)
237
+ ret.append((ca.squeeze(-1), cd.squeeze(-1)))
238
+ a = ca
239
+ ret.reverse()
240
+ return ret
241
+
242
+ def extra_repr(self) -> str:
243
+ return f"wave={self.wave!r}, level={self.level}, norm={self.norm}"
244
+
245
+
246
+ class ISWT1D(nn.Module):
247
+ """1-D inverse stationary wavelet transform module (see :func:`iswt1d`)."""
248
+
249
+ def __init__(self, wave, norm: bool = False):
250
+ super().__init__()
251
+ self.wave = wave
252
+ self.norm = norm
253
+ _, _, _, _, _, _, g0r, g1r = resolve_2d(wave, torch.float64, torch.device("cpu"))
254
+ self.register_buffer("g0r", g0r)
255
+ self.register_buffer("g1r", g1r)
256
+
257
+ def forward(self, coeffs):
258
+ g0r = self.g0r.to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
259
+ g1r = self.g1r.to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
260
+ if self.norm:
261
+ g0r, g1r = g0r * 2**0.5, g1r * 2**0.5
262
+ n_levels = len(coeffs)
263
+ output = coeffs[0][0].clone()
264
+ N = output.shape[-1]
265
+ for j in range(n_levels, 0, -1):
266
+ step = 2 ** (j - 1)
267
+ cD = coeffs[-j][1]
268
+ for first in range(step):
269
+ idx = torch.arange(first, N, step, device=output.device)
270
+ even = idx[0::2]
271
+ odd = idx[1::2]
272
+ x1 = _iswt_idwt(
273
+ output.index_select(-1, even),
274
+ cD.index_select(-1, even),
275
+ g0r,
276
+ g1r,
277
+ )
278
+ x2 = _iswt_idwt(
279
+ output.index_select(-1, odd),
280
+ cD.index_select(-1, odd),
281
+ g0r,
282
+ g1r,
283
+ )
284
+ x2 = torch.roll(x2, 1, dims=-1)
285
+ output[..., idx] = (x1 + x2) / 2
286
+ return output
287
+
288
+ def extra_repr(self) -> str:
289
+ return f"wave={self.wave!r}, norm={self.norm}"
290
+
291
+
292
+ class SWT2D(nn.Module):
293
+ """2-D stationary wavelet transform module (see :func:`swt2d`)."""
294
+
295
+ def __init__(self, wave, level: int, norm: bool = False):
296
+ super().__init__()
297
+ _check_level(level)
298
+ self.wave = wave
299
+ self.level = level
300
+ self.norm = norm
301
+ h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, torch.float64, torch.device("cpu"))
302
+ for name, t in (("h0c", h0c), ("h1c", h1c), ("h0r", h0r), ("h1r", h1r)):
303
+ self.register_buffer(name, t)
304
+
305
+ def forward(self, x: torch.Tensor):
306
+ if x.ndim != 4:
307
+ raise ValueError(f"SWT2D expects (N, C, H, W); got {tuple(x.shape)}")
308
+ _check_divisible(x.shape[-2], self.level, "height")
309
+ _check_divisible(x.shape[-1], self.level, "width")
310
+ f = {
311
+ n: getattr(self, n).to(dtype=x.dtype, device=x.device)
312
+ for n in ("h0c", "h1c", "h0r", "h1r")
313
+ }
314
+ if self.norm:
315
+ for n in f:
316
+ f[n] = f[n] / 2**0.5
317
+ ret = []
318
+ a = x
319
+ for j in range(1, self.level + 1):
320
+ fstep = 2 ** (j - 1)
321
+ low = swt_axis(a, f["h0c"], 2, fstep)
322
+ high = swt_axis(a, f["h1c"], 2, fstep)
323
+ aa = swt_axis(low, f["h0r"], 3, fstep)
324
+ ad = swt_axis(low, f["h1r"], 3, fstep)
325
+ da = swt_axis(high, f["h0r"], 3, fstep)
326
+ dd = swt_axis(high, f["h1r"], 3, fstep)
327
+ ret.append((aa, (da, ad, dd)))
328
+ a = aa
329
+ ret.reverse()
330
+ return ret
331
+
332
+ def extra_repr(self) -> str:
333
+ return f"wave={self.wave!r}, level={self.level}, norm={self.norm}"
334
+
335
+
336
+ class ISWT2D(nn.Module):
337
+ """2-D inverse stationary wavelet transform module (see :func:`iswt2d`)."""
338
+
339
+ def __init__(self, wave, norm: bool = False):
340
+ super().__init__()
341
+ self.wave = wave
342
+ self.norm = norm
343
+ _, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, torch.float64, torch.device("cpu"))
344
+ for name, t in (("g0c", g0c), ("g1c", g1c), ("g0r", g0r), ("g1r", g1r)):
345
+ self.register_buffer(name, t)
346
+
347
+ def forward(self, coeffs):
348
+ f = {
349
+ n: getattr(self, n).to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
350
+ for n in ("g0c", "g1c", "g0r", "g1r")
351
+ }
352
+ if self.norm:
353
+ for n in f:
354
+ f[n] = f[n] * 2**0.5
355
+ n_levels = len(coeffs)
356
+ output = coeffs[0][0].clone()
357
+ H, W = output.shape[-2:]
358
+ for j in range(n_levels):
359
+ step = 2 ** (n_levels - j - 1)
360
+ cH, cV, cD = coeffs[j][1]
361
+ for fh in range(step):
362
+ for fw in range(step):
363
+ approx = output.clone()
364
+ out_slice = output[..., fh::step, fw::step]
365
+ out_slice.zero_()
366
+ for oh in (0, 1):
367
+ for ow in (0, 1):
368
+ h_eo = slice(fh + oh * step, H, 2 * step)
369
+ w_eo = slice(fw + ow * step, W, 2 * step)
370
+ ll = approx[..., h_eo, w_eo]
371
+ lh = cH[..., h_eo, w_eo]
372
+ hl = cV[..., h_eo, w_eo]
373
+ hh = cD[..., h_eo, w_eo]
374
+ lo = dwt_ll.sfb1d(ll, lh, f["g0c"], f["g1c"], "periodization", 2)
375
+ hi = dwt_ll.sfb1d(hl, hh, f["g0c"], f["g1c"], "periodization", 2)
376
+ xr = dwt_ll.sfb1d(lo, hi, f["g0r"], f["g1r"], "periodization", 3)
377
+ if oh:
378
+ xr = torch.roll(xr, 1, dims=-2)
379
+ if ow:
380
+ xr = torch.roll(xr, 1, dims=-1)
381
+ out_slice += xr
382
+ out_slice /= 4
383
+ return output
384
+
385
+ def extra_repr(self) -> str:
386
+ return f"wave={self.wave!r}, norm={self.norm}"