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
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,296 @@
1
+ """Low-level dual-tree complex wavelet filter banks.
2
+
3
+ These are the exact Kingsbury DTCWT operations (ported from the original
4
+ MATLAB toolbox via fbcotter/pytorch_wavelets): column/row filtering with
5
+ biorthogonal filters at level 1 and quarter-sample-shift filters at level 2+,
6
+ plus the quad-to-complex conversions. Everything is built from torch ops, so
7
+ gradients flow through all dtypes and ``torch.compile`` works.
8
+
9
+ Boundary handling follows Kingsbury's ``symmetric`` (half-sample) extension.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import numpy as np
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+ from ..core.modes import pad_indices
19
+
20
+ __all__ = [
21
+ "colfilter",
22
+ "rowfilter",
23
+ "coldfilt",
24
+ "rowdfilt",
25
+ "colifilt",
26
+ "rowifilt",
27
+ "q2c",
28
+ "c2q",
29
+ "highs_to_orientations",
30
+ "orientations_to_highs",
31
+ ]
32
+
33
+
34
+ def colfilter(X: torch.Tensor, h: torch.Tensor) -> torch.Tensor:
35
+ """Column filter (filter along the rows) with decimation by 2."""
36
+ b, ch, row, col = X.shape
37
+ m = h.shape[2] // 2
38
+ xe = pad_indices(row, m, m, "symmetric", X.device)
39
+ return F.conv2d(X[:, :, xe], h.repeat(ch, 1, 1, 1), groups=ch)
40
+
41
+
42
+ def rowfilter(X: torch.Tensor, h: torch.Tensor) -> torch.Tensor:
43
+ """Row filter (filter along the columns) with decimation by 2.
44
+
45
+ ``h`` is a column filter ``(1, 1, L, 1)``; it is transposed internally.
46
+ """
47
+ b, ch, row, col = X.shape
48
+ m = h.shape[2] // 2
49
+ h = h.transpose(2, 3).contiguous()
50
+ xe = pad_indices(col, m, m, "symmetric", X.device)
51
+ return F.conv2d(X[:, :, :, xe], h.repeat(ch, 1, 1, 1), groups=ch)
52
+
53
+
54
+ def coldfilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
55
+ """Filter two-phase column filter bank with decimation by 2.
56
+
57
+ ``X`` must have a row count divisible by 4. Combines the a/b tree filters
58
+ and interleaves their outputs.
59
+ """
60
+ batch, ch, r, c = X.shape
61
+ r2 = r // 2
62
+ if r % 4 != 0:
63
+ raise ValueError(f"No. of rows in X must be a multiple of 4\nX was {X.shape}")
64
+
65
+ m = ha.shape[2]
66
+ xe = pad_indices(r, m, m, "symmetric", X.device)
67
+ X = torch.cat((X[:, :, xe[2::2]], X[:, :, xe[3::2]]), dim=1)
68
+ h = torch.cat((ha.repeat(ch, 1, 1, 1), hb.repeat(ch, 1, 1, 1)), dim=0)
69
+ X = F.conv2d(X, h, stride=(2, 1), groups=ch * 2)
70
+
71
+ if highpass:
72
+ X = torch.stack((X[:, ch:], X[:, :ch]), dim=-2).view(batch, ch, r2, c)
73
+ else:
74
+ X = torch.stack((X[:, :ch], X[:, ch:]), dim=-2).view(batch, ch, r2, c)
75
+ return X
76
+
77
+
78
+ def rowdfilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
79
+ """Filter two-phase row filter bank with decimation by 2.
80
+
81
+ ``ha``/``hb`` are column filters ``(1, 1, L, 1)``, reshaped internally.
82
+ """
83
+ batch, ch, r, c = X.shape
84
+ c2 = c // 2
85
+ if c % 4 != 0:
86
+ raise ValueError(f"No. of cols in X must be a multiple of 4\nX was {X.shape}")
87
+
88
+ m = ha.shape[2]
89
+ xe = pad_indices(c, m, m, "symmetric", X.device)
90
+ X = torch.cat((X[:, :, :, xe[2::2]], X[:, :, :, xe[3::2]]), dim=1)
91
+ h = torch.cat(
92
+ (ha.reshape(1, 1, 1, m).repeat(ch, 1, 1, 1), hb.reshape(1, 1, 1, m).repeat(ch, 1, 1, 1)),
93
+ dim=0,
94
+ )
95
+ X = F.conv2d(X, h, stride=(1, 2), groups=ch * 2)
96
+
97
+ if highpass:
98
+ Y = torch.stack((X[:, ch:], X[:, :ch]), dim=-1).view(batch, ch, r, c2)
99
+ else:
100
+ Y = torch.stack((X[:, :ch], X[:, ch:]), dim=-1).view(batch, ch, r, c2)
101
+ return Y
102
+
103
+
104
+ def colifilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
105
+ """Inverse of :func:`coldfilt` (with upsampling)."""
106
+ m = ha.shape[2]
107
+ m2 = m // 2
108
+ hao, hae = ha[:, :, 1::2], ha[:, :, ::2]
109
+ hbo, hbe = hb[:, :, 1::2], hb[:, :, ::2]
110
+ batch, ch, r, c = X.shape
111
+ if r % 2 != 0:
112
+ raise ValueError(f"No. of rows in X must be a multiple of 2.\nX was {X.shape}")
113
+ xe = pad_indices(r, m2, m2, "symmetric", X.device)
114
+
115
+ if m2 % 2 == 0:
116
+ h1, h2, h3, h4 = hae, hbe, hao, hbo
117
+ if highpass:
118
+ X = torch.cat(
119
+ (X[:, :, xe[1:-2:2]], X[:, :, xe[:-2:2]], X[:, :, xe[3::2]], X[:, :, xe[2::2]]),
120
+ dim=1,
121
+ )
122
+ else:
123
+ X = torch.cat(
124
+ (X[:, :, xe[:-2:2]], X[:, :, xe[1:-2:2]], X[:, :, xe[2::2]], X[:, :, xe[3::2]]),
125
+ dim=1,
126
+ )
127
+ else:
128
+ h1, h2, h3, h4 = hao, hbo, hae, hbe
129
+ if highpass:
130
+ X = torch.cat(
131
+ (
132
+ X[:, :, xe[2:-1:2]],
133
+ X[:, :, xe[1:-1:2]],
134
+ X[:, :, xe[2:-1:2]],
135
+ X[:, :, xe[1:-1:2]],
136
+ ),
137
+ dim=1,
138
+ )
139
+ else:
140
+ X = torch.cat(
141
+ (
142
+ X[:, :, xe[1:-1:2]],
143
+ X[:, :, xe[2:-1:2]],
144
+ X[:, :, xe[1:-1:2]],
145
+ X[:, :, xe[2:-1:2]],
146
+ ),
147
+ dim=1,
148
+ )
149
+ h = torch.cat(
150
+ (
151
+ h1.repeat(ch, 1, 1, 1),
152
+ h2.repeat(ch, 1, 1, 1),
153
+ h3.repeat(ch, 1, 1, 1),
154
+ h4.repeat(ch, 1, 1, 1),
155
+ ),
156
+ dim=0,
157
+ )
158
+ X = F.conv2d(X, h, groups=4 * ch)
159
+ # Stack 4 tensors of shape [batch, ch, r2, c] into [batch, ch, r*2, c]
160
+ X = torch.stack(
161
+ [X[:, :ch], X[:, ch : 2 * ch], X[:, 2 * ch : 3 * ch], X[:, 3 * ch :]], dim=3
162
+ ).view(batch, ch, r * 2, c)
163
+ return X
164
+
165
+
166
+ def rowifilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
167
+ """Inverse of :func:`rowdfilt` (with upsampling)."""
168
+ m = ha.shape[2]
169
+ m2 = m // 2
170
+ hao, hae = ha[:, :, 1::2], ha[:, :, ::2]
171
+ hbo, hbe = hb[:, :, 1::2], hb[:, :, ::2]
172
+ batch, ch, r, c = X.shape
173
+ if c % 2 != 0:
174
+ raise ValueError(f"No. of cols in X must be a multiple of 2.\nX was {X.shape}")
175
+ xe = pad_indices(c, m2, m2, "symmetric", X.device)
176
+
177
+ if m2 % 2 == 0:
178
+ h1, h2, h3, h4 = hae, hbe, hao, hbo
179
+ if highpass:
180
+ X = torch.cat(
181
+ (
182
+ X[:, :, :, xe[1:-2:2]],
183
+ X[:, :, :, xe[:-2:2]],
184
+ X[:, :, :, xe[3::2]],
185
+ X[:, :, :, xe[2::2]],
186
+ ),
187
+ dim=1,
188
+ )
189
+ else:
190
+ X = torch.cat(
191
+ (
192
+ X[:, :, :, xe[:-2:2]],
193
+ X[:, :, :, xe[1:-2:2]],
194
+ X[:, :, :, xe[2::2]],
195
+ X[:, :, :, xe[3::2]],
196
+ ),
197
+ dim=1,
198
+ )
199
+ else:
200
+ h1, h2, h3, h4 = hao, hbo, hae, hbe
201
+ if highpass:
202
+ X = torch.cat(
203
+ (
204
+ X[:, :, :, xe[2:-1:2]],
205
+ X[:, :, :, xe[1:-1:2]],
206
+ X[:, :, :, xe[2:-1:2]],
207
+ X[:, :, :, xe[1:-1:2]],
208
+ ),
209
+ dim=1,
210
+ )
211
+ else:
212
+ X = torch.cat(
213
+ (
214
+ X[:, :, :, xe[1:-1:2]],
215
+ X[:, :, :, xe[2:-1:2]],
216
+ X[:, :, :, xe[1:-1:2]],
217
+ X[:, :, :, xe[2:-1:2]],
218
+ ),
219
+ dim=1,
220
+ )
221
+ h = torch.cat(
222
+ (
223
+ h1.repeat(ch, 1, 1, 1),
224
+ h2.repeat(ch, 1, 1, 1),
225
+ h3.repeat(ch, 1, 1, 1),
226
+ h4.repeat(ch, 1, 1, 1),
227
+ ),
228
+ dim=0,
229
+ ).reshape(4 * ch, 1, 1, m2)
230
+ X = F.conv2d(X, h, groups=4 * ch)
231
+ X = torch.stack(
232
+ [X[:, :ch], X[:, ch : 2 * ch], X[:, 2 * ch : 3 * ch], X[:, 3 * ch :]], dim=4
233
+ ).view(batch, ch, r, c * 2)
234
+ return X
235
+
236
+
237
+ def q2c(y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
238
+ """Convert quad-number subimages to two complex subimages.
239
+
240
+ Returns ``(z1, z2)`` complex tensors (each of half the spatial size).
241
+ """
242
+ y = y / np.sqrt(2)
243
+ a, b = y[:, :, 0::2, 0::2], y[:, :, 0::2, 1::2]
244
+ c, d = y[:, :, 1::2, 0::2], y[:, :, 1::2, 1::2]
245
+ z1 = torch.complex(a - d, b + c)
246
+ z2 = torch.complex(a + d, b - c)
247
+ return z1, z2
248
+
249
+
250
+ def c2q(w1: torch.Tensor, w2: torch.Tensor) -> torch.Tensor:
251
+ """Convert two complex subimages to a quad-number image (inverse of q2c)."""
252
+ w1r, w1i = w1.real, w1.imag
253
+ w2r, w2i = w2.real, w2.imag
254
+
255
+ x1 = w1r + w2r
256
+ x2 = w1i + w2i
257
+ x3 = w1i - w2i
258
+ x4 = -w1r + w2r
259
+
260
+ b, ch, r, c = w1r.shape
261
+ y = w1r.new_zeros((b, ch, r * 2, c * 2))
262
+ y[:, :, ::2, ::2] = x1
263
+ y[:, :, ::2, 1::2] = x2
264
+ y[:, :, 1::2, ::2] = x3
265
+ y[:, :, 1::2, 1::2] = x4
266
+ y /= np.sqrt(2)
267
+ return y
268
+
269
+
270
+ def highs_to_orientations(lh, hl, hh, o_dim: int = 2) -> torch.Tensor:
271
+ """Combine the three quad bandpasses into 6 complex orientations.
272
+
273
+ Returns a complex tensor with the 6 orientations stacked along ``o_dim``,
274
+ in the order 15, 45, 75, 105, 135, 165 degrees.
275
+ """
276
+ (z15, z165), (z45, z135), (z75, z105) = q2c(lh), q2c(hh), q2c(hl)
277
+ return torch.stack([z15, z45, z75, z105, z135, z165], dim=o_dim)
278
+
279
+
280
+ def orientations_to_highs(highs: torch.Tensor, o_dim: int = 2):
281
+ """Inverse of :func:`highs_to_orientations`."""
282
+ dev = highs.device
283
+ z15, z165 = torch.unbind(
284
+ torch.index_select(highs, o_dim, torch.tensor([0, 5], device=dev)), dim=o_dim
285
+ )
286
+ z45, z135 = torch.unbind(
287
+ torch.index_select(highs, o_dim, torch.tensor([1, 4], device=dev)), dim=o_dim
288
+ )
289
+ z75, z105 = torch.unbind(
290
+ torch.index_select(highs, o_dim, torch.tensor([2, 3], device=dev)), dim=o_dim
291
+ )
292
+
293
+ lh = c2q(z15, z165)
294
+ hl = c2q(z75, z105)
295
+ hh = c2q(z45, z135)
296
+ return lh, hl, hh
@@ -0,0 +1,253 @@
1
+ """1-D dual-tree complex wavelet transform (functional and module API).
2
+
3
+ The 1-D DTCWT produces complex bandpass coefficients from the two
4
+ decimation phases of the highpass filtering (real = even phase, imaginary =
5
+ odd phase), so a single 1-D DTCWT level yields a complex band that is
6
+ approximately shift-invariant and analytic. Levels 2+ use the
7
+ quarter-sample-shift filters, exactly as in the 2-D transform.
8
+
9
+ Precision follows the input (float32 -> complex64, float64 -> complex128);
10
+ half inputs are computed in float32 (no usable complex16 kernels).
11
+ Boundary handling is Kingsbury's symmetric extension.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+
19
+ from ..dwt._validation import check_spatial
20
+ from ._autocast import autocast_off
21
+ from .coeffs import biort as _biort
22
+ from .coeffs import qshift as _qshift
23
+ from .lowlevel import coldfilt, colfilter, colifilt
24
+ from .transform2d import _compute_dtype, _prep_filt, _validate_names
25
+
26
+ __all__ = ["dtcwt1d", "idtcwt1d", "DTCWT1D", "IDTCWT1D"]
27
+
28
+
29
+ def _to_col(x: torch.Tensor) -> torch.Tensor:
30
+ """``(N, C, L)`` -> ``(N, C, L, 1)`` column layout (like numpy dtcwt)."""
31
+ return x.unsqueeze(-1)
32
+
33
+
34
+ def _from_col(x: torch.Tensor) -> torch.Tensor:
35
+ return x.squeeze(-1)
36
+
37
+
38
+ def _complex_from_phases(hi: torch.Tensor) -> torch.Tensor:
39
+ """Highpass phases -> complex bandpass: ``hi[..., ::2] + 1j hi[..., 1::2]``."""
40
+ return torch.complex(hi[..., ::2, :], hi[..., 1::2, :])
41
+
42
+
43
+ def _phases_from_complex(z: torch.Tensor) -> torch.Tensor:
44
+ """Complex bandpass -> interleaved real/imag phases (inverse of above)."""
45
+ b, ch, r, c = z.shape
46
+ out = torch.empty((b, ch, 2 * r, c), dtype=z.real.dtype, device=z.device)
47
+ out[..., ::2, :] = z.real
48
+ out[..., 1::2, :] = z.imag
49
+ return out
50
+
51
+
52
+ def _resolve(biort, qshift, dtype, device):
53
+ _validate_names(biort, qshift)
54
+ h0o, g0o, h1o, g1o = _biort(biort)
55
+ h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift)
56
+ return {
57
+ "h0o": _prep_filt(h0o, dtype, device),
58
+ "g0o": _prep_filt(g0o, dtype, device),
59
+ "h1o": _prep_filt(h1o, dtype, device),
60
+ "g1o": _prep_filt(g1o, dtype, device),
61
+ "h0a": _prep_filt(h0a, dtype, device),
62
+ "h0b": _prep_filt(h0b, dtype, device),
63
+ "h1a": _prep_filt(h1a, dtype, device),
64
+ "h1b": _prep_filt(h1b, dtype, device),
65
+ "g0a": _prep_filt(g0a, dtype, device),
66
+ "g0b": _prep_filt(g0b, dtype, device),
67
+ "g1a": _prep_filt(g1a, dtype, device),
68
+ "g1b": _prep_filt(g1b, dtype, device),
69
+ }
70
+
71
+
72
+ def _dtcwt1d_impl(x, f, J, include_scale):
73
+ """Forward on the column layout ``(N, C, L, 1)``."""
74
+ scales = [x.new_zeros([])] * J
75
+ hi = colfilter(x, f["h1o"])
76
+ lo = colfilter(x, f["h0o"])
77
+ yh = [_complex_from_phases(hi)]
78
+ if include_scale:
79
+ scales[0] = lo
80
+ for j in range(1, J):
81
+ if lo.shape[2] % 4 != 0:
82
+ lo = torch.cat((lo[:, :, 0:1], lo, lo[:, :, -1:]), dim=2)
83
+ hi = coldfilt(lo, f["h1b"], f["h1a"], True)
84
+ lo = coldfilt(lo, f["h0b"], f["h0a"], False)
85
+ yh.append(_complex_from_phases(hi))
86
+ if include_scale:
87
+ scales[j] = lo
88
+ if include_scale:
89
+ return scales, yh
90
+ return lo, yh
91
+
92
+
93
+ def _idtcwt1d_impl(lo, yh, f):
94
+ """Inverse on the column layout."""
95
+ J = len(yh)
96
+ if J == 0:
97
+ return lo
98
+ for j in range(J - 1, 0, -1):
99
+ hj = yh[j]
100
+ if hj is None: # skipped bandpass: treat as complex zeros
101
+ cdtype = torch.complex128 if lo.dtype == torch.float64 else torch.complex64
102
+ hj = torch.zeros(
103
+ (lo.shape[0], lo.shape[1], lo.shape[2] // 2, lo.shape[3]),
104
+ dtype=cdtype,
105
+ device=lo.device,
106
+ )
107
+ hi = _phases_from_complex(hj)
108
+ lo = colifilt(lo, f["g0b"], f["g0a"], False) + colifilt(hi, f["g1b"], f["g1a"], True)
109
+ nxt = yh[j - 1]
110
+ if nxt is not None and lo.shape[2] != 2 * nxt.shape[2]:
111
+ lo = lo[:, :, 1:-1]
112
+ hj = yh[0]
113
+ if hj is None:
114
+ cdtype = torch.complex128 if lo.dtype == torch.float64 else torch.complex64
115
+ hj = torch.zeros(
116
+ (lo.shape[0], lo.shape[1], lo.shape[2] // 2, lo.shape[3]),
117
+ dtype=cdtype,
118
+ device=lo.device,
119
+ )
120
+ hi = _phases_from_complex(hj)
121
+ z = colfilter(lo, f["g0o"]) + colfilter(hi, f["g1o"])
122
+ return z
123
+
124
+
125
+ def dtcwt1d(
126
+ x: torch.Tensor, biort="near_sym_a", qshift="qshift_a", J=3, include_scale=False
127
+ ) -> tuple[torch.Tensor | list[torch.Tensor], list[torch.Tensor] | None]:
128
+ """Forward 1-D dual-tree complex wavelet transform.
129
+
130
+ Args:
131
+ x (torch.Tensor): ``(N, C, L)`` input.
132
+ biort (str): filter bank name.
133
+ qshift (str): filter bank name.
134
+ J (int): number of levels (``0`` returns ``(x, None)``).
135
+ include_scale (bool): also return the intermediate lowpasses.
136
+
137
+ Odd-length inputs are padded by one sample (the last sample is repeated)
138
+ and the inverse returns the padded length ``L + 1``, matching the 2-D
139
+ transform's convention.
140
+
141
+ Returns:
142
+ tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)``: lowpass
143
+ ``(N, C, L')`` and a list of ``J`` complex bandpasses ``(N, C, L'')``
144
+ (finest first).
145
+ """
146
+ if x.ndim != 3:
147
+ raise ValueError(f"dtcwt1d expects (N, C, L); got {tuple(x.shape)}")
148
+ check_spatial(x.shape[-1:], ("L",), "dtcwt1d")
149
+ if isinstance(J, bool) or not isinstance(J, int) or J < 0:
150
+ raise ValueError(f"J must be a non-negative int; got {J!r}")
151
+ dtype = _compute_dtype(x)
152
+ f = _resolve(biort, qshift, dtype, x.device)
153
+ x = x.to(dtype)
154
+ if x.shape[-1] % 2 != 0:
155
+ x = torch.cat((x, x[..., -1:]), dim=-1)
156
+ x4 = _to_col(x)
157
+ if J == 0:
158
+ return x, None
159
+ with autocast_off(x.device):
160
+ out = _dtcwt1d_impl(x4, f, J, bool(include_scale))
161
+ if include_scale:
162
+ scales, yh = out
163
+ return [s.squeeze(-1) for s in scales], [h.squeeze(-1) for h in yh]
164
+ lo, yh = out
165
+ return lo.squeeze(-1), [h.squeeze(-1) for h in yh]
166
+
167
+
168
+ def idtcwt1d(coeffs, biort="near_sym_a", qshift="qshift_a") -> torch.Tensor:
169
+ """Inverse 1-D DTCWT (exact inverse of :func:`dtcwt1d`)."""
170
+ yl, yh = coeffs
171
+ if yl is None:
172
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
173
+ dtype = _compute_dtype(yl)
174
+ f = _resolve(biort, qshift, dtype, yl.device)
175
+ lo = _to_col(yl.to(dtype))
176
+ if yh is None: # J == 0 forward
177
+ return yl
178
+ highs = [None if h is None else _to_col(h) for h in yh]
179
+ with autocast_off(yl.device):
180
+ z = _idtcwt1d_impl(lo, highs, f)
181
+ return _from_col(z)
182
+
183
+
184
+ class DTCWT1D(nn.Module):
185
+ """1-D DTCWT module (see :func:`dtcwt1d`)."""
186
+
187
+ def __init__(self, biort="near_sym_a", qshift="qshift_a", J=3, include_scale=False):
188
+ super().__init__()
189
+ if isinstance(J, bool) or not isinstance(J, int) or J < 0:
190
+ raise ValueError(f"J must be a non-negative int; got {J!r}")
191
+ self.biort = biort
192
+ self.qshift = qshift
193
+ self.J = J
194
+ self.include_scale = bool(include_scale)
195
+ filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
196
+ for name in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b"):
197
+ self.register_buffer(name, filts[name])
198
+
199
+ def forward(self, x: torch.Tensor):
200
+ if x.ndim != 3:
201
+ raise ValueError(f"DTCWT1D expects (N, C, L); got {tuple(x.shape)}")
202
+ check_spatial(x.shape[-1:], ("L",), "DTCWT1D")
203
+ if self.J == 0:
204
+ return x, None
205
+ dtype = _compute_dtype(x)
206
+ x = x.to(dtype)
207
+ if x.shape[-1] % 2 != 0:
208
+ x = torch.cat((x, x[..., -1:]), dim=-1)
209
+ f = {
210
+ n: getattr(self, n).to(dtype=dtype, device=x.device)
211
+ for n in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b")
212
+ }
213
+ with autocast_off(x.device):
214
+ out = _dtcwt1d_impl(_to_col(x), f, self.J, self.include_scale)
215
+ if self.include_scale:
216
+ scales, yh = out
217
+ return [s.squeeze(-1) for s in scales], [h.squeeze(-1) for h in yh]
218
+ lo, yh = out
219
+ return lo.squeeze(-1), [h.squeeze(-1) for h in yh]
220
+
221
+ def extra_repr(self) -> str:
222
+ return f"biort={self.biort!r}, qshift={self.qshift!r}, J={self.J}"
223
+
224
+
225
+ class IDTCWT1D(nn.Module):
226
+ """1-D inverse DTCWT module (see :func:`idtcwt1d`)."""
227
+
228
+ def __init__(self, biort="near_sym_a", qshift="qshift_a"):
229
+ super().__init__()
230
+ self.biort = biort
231
+ self.qshift = qshift
232
+ filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
233
+ for name in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b"):
234
+ self.register_buffer(name, filts[name])
235
+
236
+ def forward(self, coeffs):
237
+ yl, yh = coeffs
238
+ if yl is None:
239
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
240
+ dtype = _compute_dtype(yl)
241
+ f = {
242
+ n: getattr(self, n).to(dtype=dtype, device=yl.device)
243
+ for n in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b")
244
+ }
245
+ lo = _to_col(yl.to(dtype))
246
+ if yh is None: # J == 0 forward
247
+ return yl
248
+ highs = [None if h is None else _to_col(h) for h in yh]
249
+ with autocast_off(yl.device):
250
+ return _from_col(_idtcwt1d_impl(lo, highs, f))
251
+
252
+ def extra_repr(self) -> str:
253
+ return f"biort={self.biort!r}, qshift={self.qshift!r}"