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,420 @@
1
+ """3-D dual-tree complex wavelet transform (functional and module API).
2
+
3
+ The 3-D DTCWT (Kingsbury's design, as in the ``dtcwt`` numpy package) runs
4
+ four separable trees with the level-1 biorthogonal and level-2+
5
+ quarter-sample-shift filters on a doubled work grid and combines the octant
6
+ outputs into 28 complex orientation subbands per level (7 ``(H/L)`` face
7
+ combinations x 4 subbands each).
8
+
9
+ Precision follows the input (float32 -> complex64, float64 -> complex128);
10
+ half inputs are computed in float32. Boundary handling is Kingsbury's
11
+ symmetric extension (``ext_mode=4``: each input dimension must be even;
12
+ higher levels are edge-padded to a multiple of 4).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+
20
+ from ..dwt._validation import check_spatial
21
+ from ._autocast import autocast_off
22
+ from .coeffs import biort as _biort
23
+ from .coeffs import qshift as _qshift
24
+ from .lowlevel import coldfilt, colfilter, colifilt
25
+ from .transform2d import _compute_dtype, _prep_filt, _validate_names
26
+
27
+ __all__ = ["dtcwt3d", "idtcwt3d", "DTCWT3D", "IDTCWT3D"]
28
+
29
+
30
+ def _axis_fn(x: torch.Tensor, fn, axis: int):
31
+ """Apply ``fn`` (filters the dim-2 of a 4-D view) along ``axis`` of 5-D."""
32
+ order = [0, 1, axis] + [i for i in (2, 3, 4) if i != axis]
33
+ xp = x.permute(order)
34
+ shp = xp.shape
35
+ y = fn(xp.reshape(shp[0], shp[1], shp[2], shp[3] * shp[4]))
36
+ out_shp = list(shp)
37
+ out_shp[2] = y.shape[2] # the filtered dim may change size
38
+ y = y.reshape(out_shp)
39
+ inv = [0, 1] + [order.index(i) for i in (2, 3, 4)]
40
+ return y.permute(inv)
41
+
42
+
43
+ def _colfilter_axis(x, h, axis):
44
+ return _axis_fn(x, lambda t: colfilter(t, h), axis)
45
+
46
+
47
+ def _coldfilt_axis(x, ha, hb, highpass, axis):
48
+ return _axis_fn(x, lambda t: coldfilt(t, ha, hb, highpass), axis)
49
+
50
+
51
+ def _colifilt_axis(x, ha, hb, highpass, axis):
52
+ return _axis_fn(x, lambda t: colifilt(t, ha, hb, highpass), axis)
53
+
54
+
55
+ def _pad_to_multiple(x, mult: int) -> torch.Tensor:
56
+ for axis in (2, 3, 4):
57
+ n = x.shape[axis]
58
+ if n % mult != 0:
59
+ before = (
60
+ x[..., :1, :, :]
61
+ if axis == 2
62
+ else x[..., :, :1, :]
63
+ if axis == 3
64
+ else x[..., :, :, :1]
65
+ )
66
+ after = (
67
+ x[..., -1:, :, :]
68
+ if axis == 2
69
+ else x[..., :, -1:, :]
70
+ if axis == 3
71
+ else x[..., :, :, -1:]
72
+ )
73
+ x = torch.cat((before, x, after), dim=axis)
74
+ return x
75
+
76
+
77
+ def _cube2c(y: torch.Tensor) -> torch.Tensor:
78
+ """Octant (quad-tree) -> 4 complex subbands (numpy dtcwt cube2c)."""
79
+ A = y[..., 0::2, 0::2, 0::2]
80
+ B = y[..., 0::2, 1::2, 0::2]
81
+ C = y[..., 1::2, 0::2, 0::2]
82
+ D = y[..., 1::2, 1::2, 0::2]
83
+ E = y[..., 0::2, 0::2, 1::2]
84
+ F = y[..., 0::2, 1::2, 1::2]
85
+ G = y[..., 1::2, 0::2, 1::2]
86
+ H = y[..., 1::2, 1::2, 1::2]
87
+ p = 0.5 * (A - G - D - F) + 0.5j * (B - H + C + E)
88
+ q = 0.5 * (A - G + D + F) + 0.5j * (-B + H + C + E)
89
+ r = 0.5 * (A + G + D - F) + 0.5j * (B + H - C + E)
90
+ s = 0.5 * (A + G - D + F) + 0.5j * (-B - H - C + E)
91
+ return torch.stack((p, q, r, s), dim=-1)
92
+
93
+
94
+ def _c2cube(z: torch.Tensor) -> torch.Tensor:
95
+ """Inverse of :func:`_cube2c` (numpy dtcwt c2cube)."""
96
+ p, q, r, s = torch.unbind(z, dim=-1)
97
+ pr, pi = p.real, p.imag
98
+ qr, qi = q.real, q.imag
99
+ rr, ri = r.real, r.imag
100
+ sr, si = s.real, s.imag
101
+ shp = z.shape[:-1]
102
+ y = torch.zeros(shp[:2] + tuple(2 * s for s in shp[2:]), dtype=pr.dtype, device=z.device)
103
+ y[..., 0::2, 0::2, 0::2] = pr + qr + rr + sr
104
+ y[..., 1::2, 0::2, 1::2] = -pr - qr + rr + sr
105
+ y[..., 1::2, 1::2, 0::2] = -pr + qr + rr - sr
106
+ y[..., 0::2, 1::2, 1::2] = -pr + qr - rr + sr
107
+ y[..., 0::2, 1::2, 0::2] = pi - qi + ri - si
108
+ y[..., 1::2, 1::2, 1::2] = -pi + qi + ri - si
109
+ y[..., 1::2, 0::2, 0::2] = pi + qi - ri - si
110
+ y[..., 0::2, 0::2, 1::2] = pi + qi + ri + si
111
+ return y * 0.5
112
+
113
+
114
+ def _resolve(biort, qshift, dtype, device):
115
+ _validate_names(biort, qshift)
116
+ h0o, g0o, h1o, g1o = _biort(biort)
117
+ h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift)
118
+ return {
119
+ "h0o": _prep_filt(h0o, dtype, device),
120
+ "g0o": _prep_filt(g0o, dtype, device),
121
+ "h1o": _prep_filt(h1o, dtype, device),
122
+ "g1o": _prep_filt(g1o, dtype, device),
123
+ "h0a": _prep_filt(h0a, dtype, device),
124
+ "h0b": _prep_filt(h0b, dtype, device),
125
+ "h1a": _prep_filt(h1a, dtype, device),
126
+ "h1b": _prep_filt(h1b, dtype, device),
127
+ "g0a": _prep_filt(g0a, dtype, device),
128
+ "g0b": _prep_filt(g0b, dtype, device),
129
+ "g1a": _prep_filt(g1a, dtype, device),
130
+ "g1b": _prep_filt(g1b, dtype, device),
131
+ }
132
+
133
+
134
+ def _level1_xfm(x, h0o, h1o):
135
+ D, H, W = x.shape[2:]
136
+ L = h0o.numel()
137
+ d2, h2, w2 = 2 * D, 2 * H, 2 * W
138
+ if L % 2 == 0:
139
+ d2, h2, w2 = d2 + 2, h2 + 2, w2 + 2
140
+ work = x.new_zeros((x.shape[0], x.shape[1], d2, h2, w2))
141
+ if L % 2 == 0:
142
+ work[..., :D, :H, :W] = x
143
+ work[..., D, :H, :W] = x[..., -1:, :, :]
144
+ work[..., :D, H, :W] = x[..., :, -1:, :]
145
+ work[..., :D, :H, W] = x[..., :, :, -1:]
146
+ work[..., D, H, W] = x[..., -1:, -1:, -1:]
147
+ else:
148
+ work[..., :D, :H, :W] = x
149
+
150
+ # 1: filter along W (first-D x first-H block; fills both W halves)
151
+ blk = work[..., :D, :H, :W]
152
+ work[..., :D, :H, W:] = _colfilter_axis(blk, h1o, 4)
153
+ work[..., :D, :H, :W] = _colfilter_axis(blk, h0o, 4)
154
+
155
+ # 2: filter along H (into 2H), then along D
156
+ blk2 = work[..., :D, :H, :]
157
+ y2 = torch.cat((_colfilter_axis(blk2, h0o, 3), _colfilter_axis(blk2, h1o, 3)), dim=3)
158
+ work[..., :D, :, :] = _colfilter_axis(y2, h0o, 2)
159
+ work[..., D:, :, :] = _colfilter_axis(y2, h1o, 2)
160
+
161
+ return (
162
+ work[..., :D, :H, :W],
163
+ torch.cat(
164
+ (
165
+ _cube2c(work[..., :D, H : 2 * H, :W]),
166
+ _cube2c(work[..., D : 2 * D, :H, :W]),
167
+ _cube2c(work[..., D : 2 * D, H : 2 * H, :W]),
168
+ _cube2c(work[..., :D, :H, W : 2 * W]),
169
+ _cube2c(work[..., :D, H : 2 * H, W : 2 * W]),
170
+ _cube2c(work[..., D : 2 * D, :H, W : 2 * W]),
171
+ _cube2c(work[..., D : 2 * D, H : 2 * H, W : 2 * W]),
172
+ ),
173
+ dim=-1,
174
+ ),
175
+ )
176
+
177
+
178
+ def _level2_xfm(x, h0a, h0b, h1a, h1b):
179
+ x = _pad_to_multiple(x, 4)
180
+ D, H, W = x.shape[2:]
181
+
182
+ # 1: filter along W (full D, H); both outputs from the original input
183
+ hi_w = _coldfilt_axis(x, h1b, h1a, True, 4)
184
+ lo_w = _coldfilt_axis(x, h0b, h0a, False, 4)
185
+ work = torch.cat((lo_w, hi_w), dim=4)
186
+
187
+ # 2: filter along H, concatenate, then along D
188
+ a = _coldfilt_axis(work, h0b, h0a, False, 3)
189
+ b = _coldfilt_axis(work, h1b, h1a, True, 3)
190
+ y2 = torch.cat((a, b), dim=3)
191
+ work = torch.cat(
192
+ (
193
+ _coldfilt_axis(y2, h0b, h0a, False, 2),
194
+ _coldfilt_axis(y2, h1b, h1a, True, 2),
195
+ ),
196
+ dim=2,
197
+ )
198
+
199
+ D2, H2, W2 = work.shape[2:]
200
+ s0a = slice(0, D2 >> 1)
201
+ s0b = slice(D2 >> 1, None)
202
+ s1a = slice(0, H2 >> 1)
203
+ s1b = slice(H2 >> 1, None)
204
+ s2a = slice(0, W2 >> 1)
205
+ s2b = slice(W2 >> 1, None)
206
+ return (
207
+ work[..., s0a, s1a, s2a],
208
+ torch.cat(
209
+ (
210
+ _cube2c(work[..., s0a, s1b, s2a]),
211
+ _cube2c(work[..., s0b, s1a, s2a]),
212
+ _cube2c(work[..., s0b, s1b, s2a]),
213
+ _cube2c(work[..., s0a, s1a, s2b]),
214
+ _cube2c(work[..., s0a, s1b, s2b]),
215
+ _cube2c(work[..., s0b, s1a, s2b]),
216
+ _cube2c(work[..., s0b, s1b, s2b]),
217
+ ),
218
+ dim=-1,
219
+ ),
220
+ )
221
+
222
+
223
+ def _level1_ifm(yl, yh, g0o, g1o):
224
+ D, H, W = yl.shape[2:]
225
+ L = g0o.numel()
226
+ d2, h2, w2 = 2 * D, 2 * H, 2 * W
227
+ if L % 2 == 0:
228
+ d2, h2, w2 = d2 + 2, h2 + 2, w2 + 2
229
+ work = yl.new_zeros((yl.shape[0], yl.shape[1], d2, h2, w2))
230
+ work[..., :D, :H, :W] = yl
231
+ work[..., :D, H : 2 * H, :W] = _c2cube(yh[..., 0:4])
232
+ work[..., D : 2 * D, :H, :W] = _c2cube(yh[..., 4:8])
233
+ work[..., D : 2 * D, H : 2 * H, :W] = _c2cube(yh[..., 8:12])
234
+ work[..., :D, :H, W : 2 * W] = _c2cube(yh[..., 12:16])
235
+ work[..., :D, H : 2 * H, W : 2 * W] = _c2cube(yh[..., 16:20])
236
+ work[..., D : 2 * D, :H, W : 2 * W] = _c2cube(yh[..., 20:24])
237
+ work[..., D : 2 * D, H : 2 * H, W : 2 * W] = _c2cube(yh[..., 24:28])
238
+
239
+ # rows (H): filter both H halves along H, then combine along D
240
+ y = _colfilter_axis(work[..., :, :H, :], g0o, 3) + _colfilter_axis(work[..., :, H:, :], g1o, 3)
241
+ work[..., :D, :H, :] = _colfilter_axis(y[..., :D, :, :], g0o, 2) + _colfilter_axis(
242
+ y[..., D:, :, :], g1o, 2
243
+ )
244
+
245
+ # depth (D): filter both D halves along W, combine into the first W half
246
+ z = _colfilter_axis(work[..., :D, :H, :W], g0o, 4) + _colfilter_axis(
247
+ work[..., :D, :H, W:], g1o, 4
248
+ )
249
+ work[..., :D, :H, :W] = z
250
+
251
+ if L % 2 == 0:
252
+ return work[..., 1 : d2 >> 1, 1 : h2 >> 1, 1 : w2 >> 1]
253
+ return work[..., :D, :H, :W]
254
+
255
+
256
+ def _level2_ifm(yl, yh, g0a, g0b, g1a, g1b, prev_level_size):
257
+ D, H, W = yl.shape[2:]
258
+ work = yl.new_zeros((yl.shape[0], yl.shape[1], 2 * D, 2 * H, 2 * W))
259
+ work[..., :D, :H, :W] = yl
260
+ work[..., :D, H:, :W] = _c2cube(yh[..., 0:4])
261
+ work[..., D:, :H, :W] = _c2cube(yh[..., 4:8])
262
+ work[..., D:, H:, :W] = _c2cube(yh[..., 8:12])
263
+ work[..., :D, :H, W:] = _c2cube(yh[..., 12:16])
264
+ work[..., :D, H:, W:] = _c2cube(yh[..., 16:20])
265
+ work[..., D:, :H, W:] = _c2cube(yh[..., 20:24])
266
+ work[..., D:, H:, W:] = _c2cube(yh[..., 24:28])
267
+
268
+ # rows (H): combine the H halves, then the D halves
269
+ y = _colifilt_axis(work[..., :, :H, :], g0b, g0a, False, 3) + _colifilt_axis(
270
+ work[..., :, H:, :], g1b, g1a, True, 3
271
+ )
272
+ work = _colifilt_axis(y[..., :D, :, :], g0b, g0a, False, 2) + _colifilt_axis(
273
+ y[..., D:, :, :], g1b, g1a, True, 2
274
+ )
275
+
276
+ # depth (D): combine the W halves
277
+ work = _colifilt_axis(work[..., :, :, :W], g0b, g0a, False, 4) + _colifilt_axis(
278
+ work[..., :, :, W:], g1b, g1a, True, 4
279
+ )
280
+
281
+ if 2 * yh.shape[2] != prev_level_size[0]:
282
+ work = work[..., 1:-1, :, :]
283
+ if 2 * yh.shape[3] != prev_level_size[1]:
284
+ work = work[..., :, 1:-1, :]
285
+ if 2 * yh.shape[4] != prev_level_size[2]:
286
+ work = work[..., :, :, 1:-1]
287
+ return work
288
+
289
+
290
+ def _dtcwt3d_impl(x, f, J):
291
+ yh = []
292
+ yl = x
293
+ for level in range(J):
294
+ if level == 0:
295
+ yl, h = _level1_xfm(yl, f["h0o"], f["h1o"])
296
+ else:
297
+ yl, h = _level2_xfm(yl, f["h0a"], f["h0b"], f["h1a"], f["h1b"])
298
+ yh.append(h)
299
+ return yl, yh
300
+
301
+
302
+ def _validate_even3d(dims):
303
+ """The 3-D work grid requires even spatial dims at every level."""
304
+ check_spatial(dims, ("D", "H", "W"), "dtcwt3d")
305
+ bad = [d for d in dims if d % 2 != 0]
306
+ if bad:
307
+ raise ValueError(
308
+ "dtcwt3d requires even spatial dimensions (the doubled work grid "
309
+ f"needs even sizes); got {tuple(dims)}"
310
+ )
311
+
312
+
313
+ def _idtcwt3d_impl(yl, yh, f):
314
+ J = len(yh)
315
+ for level in range(J - 1, 0, -1):
316
+ prev = yh[level - 1].shape[2:5]
317
+ yl = _level2_ifm(yl, yh[level], f["g0a"], f["g0b"], f["g1a"], f["g1b"], prev)
318
+ yl = _level1_ifm(yl, yh[0], f["g0o"], f["g1o"])
319
+ return yl
320
+
321
+
322
+ def dtcwt3d(
323
+ x: torch.Tensor, biort="near_sym_a", qshift="qshift_a", J=3
324
+ ) -> tuple[torch.Tensor, list[torch.Tensor] | None]:
325
+ """Forward 3-D DTCWT.
326
+
327
+ Args:
328
+ x (torch.Tensor): ``(N, C, D, H, W)`` input with even spatial dims.
329
+ biort (str): filter bank name.
330
+ qshift (str): filter bank name.
331
+ J (int): number of levels.
332
+
333
+ Returns:
334
+ tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)``: real lowpass
335
+ ``(N, C, D', H', W')`` and a list of ``J`` complex bandpasses
336
+ ``(N, C, D'', H'', W'', 28)`` (finest first).
337
+ """
338
+ if x.ndim != 5:
339
+ raise ValueError(f"dtcwt3d expects (N, C, D, H, W); got {tuple(x.shape)}")
340
+ if isinstance(J, bool) or not isinstance(J, int) or J < 0:
341
+ raise ValueError(f"J must be a non-negative int; got {J!r}")
342
+ if J == 0:
343
+ return x, None
344
+ _validate_even3d(x.shape[2:])
345
+ dtype = _compute_dtype(x)
346
+ f = _resolve(biort, qshift, dtype, x.device)
347
+ with autocast_off(x.device):
348
+ return _dtcwt3d_impl(x.to(dtype), f, J)
349
+
350
+
351
+ def idtcwt3d(coeffs, biort="near_sym_a", qshift="qshift_a") -> torch.Tensor:
352
+ """Inverse 3-D DTCWT (exact inverse of :func:`dtcwt3d`)."""
353
+ yl, yh = coeffs
354
+ if yl is None:
355
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
356
+ dtype = _compute_dtype(yl)
357
+ f = _resolve(biort, qshift, dtype, yl.device)
358
+ with autocast_off(yl.device):
359
+ return _idtcwt3d_impl(yl.to(dtype), yh, f)
360
+
361
+
362
+ class DTCWT3D(nn.Module):
363
+ """3-D DTCWT module (see :func:`dtcwt3d`)."""
364
+
365
+ def __init__(self, biort="near_sym_a", qshift="qshift_a", J=3):
366
+ super().__init__()
367
+ if isinstance(J, bool) or not isinstance(J, int) or J < 0:
368
+ raise ValueError(f"J must be a non-negative int; got {J!r}")
369
+ self.biort = biort
370
+ self.qshift = qshift
371
+ self.J = J
372
+ filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
373
+ for name in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b"):
374
+ self.register_buffer(name, filts[name])
375
+
376
+ def forward(self, x: torch.Tensor):
377
+ if x.ndim != 5:
378
+ raise ValueError(f"DTCWT3D expects (N, C, D, H, W); got {tuple(x.shape)}")
379
+ if self.J == 0:
380
+ return x, None
381
+ _validate_even3d(x.shape[2:])
382
+ dtype = _compute_dtype(x)
383
+ f = {
384
+ n: getattr(self, n).to(dtype=dtype, device=x.device)
385
+ for n in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b")
386
+ }
387
+ with autocast_off(x.device):
388
+ return _dtcwt3d_impl(x.to(dtype), f, self.J)
389
+
390
+ def extra_repr(self) -> str:
391
+ return f"biort={self.biort!r}, qshift={self.qshift!r}, J={self.J}"
392
+
393
+
394
+ class IDTCWT3D(nn.Module):
395
+ """3-D inverse DTCWT module (see :func:`idtcwt3d`)."""
396
+
397
+ def __init__(self, biort="near_sym_a", qshift="qshift_a"):
398
+ super().__init__()
399
+ self.biort = biort
400
+ self.qshift = qshift
401
+ filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
402
+ for name in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b"):
403
+ self.register_buffer(name, filts[name])
404
+
405
+ def forward(self, coeffs):
406
+ yl, yh = coeffs
407
+ if yl is None:
408
+ raise ValueError("The lowpass coefficient tensor yl cannot be None")
409
+ if yl.ndim != 5:
410
+ raise ValueError(f"IDTCWT3D expects 5-D coefficients; got shape {tuple(yl.shape)}")
411
+ dtype = _compute_dtype(yl)
412
+ f = {
413
+ n: getattr(self, n).to(dtype=dtype, device=yl.device)
414
+ for n in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b")
415
+ }
416
+ with autocast_off(yl.device):
417
+ return _idtcwt3d_impl(yl.to(dtype), yh, f)
418
+
419
+ def extra_repr(self) -> str:
420
+ return f"biort={self.biort!r}, qshift={self.qshift!r}"
@@ -0,0 +1,93 @@
1
+ """Single-scale DTCWT stages (plain functions, native complex tensors)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+ from .lowlevel import (
8
+ coldfilt,
9
+ colfilter,
10
+ colifilt,
11
+ highs_to_orientations,
12
+ orientations_to_highs,
13
+ rowdfilt,
14
+ rowfilter,
15
+ rowifilt,
16
+ )
17
+
18
+ __all__ = ["get_dimensions5", "fwd_j1", "inv_j1", "fwd_j2plus", "inv_j2plus"]
19
+
20
+
21
+ def get_dimensions5(o_dim: int) -> tuple[int, int, int]:
22
+ """Map ``o_dim`` to ``(o_dim, h_dim, w_dim)`` for 5-D complex tensors
23
+ ``(N, C, O, H, W)``."""
24
+ o_dim = o_dim % 5
25
+ if o_dim == 4:
26
+ h_dim, w_dim = 2, 3
27
+ elif o_dim == 3:
28
+ h_dim, w_dim = 2, 4
29
+ else:
30
+ h_dim, w_dim = 3, 4
31
+ return o_dim, h_dim, w_dim
32
+
33
+
34
+ def fwd_j1(x: torch.Tensor, h0o, h1o, skip_hps: bool, o_dim: int):
35
+ """Level-1 forward DTCWT (biorthogonal filters)."""
36
+ if not skip_hps:
37
+ lo = rowfilter(x, h0o)
38
+ hi = rowfilter(x, h1o)
39
+ ll = colfilter(lo, h0o)
40
+ lh = colfilter(lo, h1o)
41
+ hl = colfilter(hi, h0o)
42
+ hh = colfilter(hi, h1o)
43
+ highs = highs_to_orientations(lh, hl, hh, o_dim)
44
+ else:
45
+ ll = colfilter(rowfilter(x, h0o), h0o)
46
+ highs = None
47
+ return ll, highs
48
+
49
+
50
+ def inv_j1(ll: torch.Tensor, highs, g0o, g1o, o_dim: int, h_dim: int, w_dim: int) -> torch.Tensor:
51
+ """Level-1 inverse DTCWT."""
52
+ if highs is None:
53
+ return rowfilter(colfilter(ll, g0o), g0o)
54
+ lh, hl, hh = orientations_to_highs(highs, o_dim)
55
+
56
+ # Possibly crop the lowpass to match the bandpass sizes.
57
+ r, c = ll.shape[2:]
58
+ r1, c1 = highs.shape[h_dim], highs.shape[w_dim]
59
+ if r != r1 * 2:
60
+ ll = ll[:, :, 1:-1]
61
+ if c != c1 * 2:
62
+ ll = ll[:, :, :, 1:-1]
63
+
64
+ hi = colfilter(hh, g1o) + colfilter(hl, g0o)
65
+ lo = colfilter(lh, g1o) + colfilter(ll, g0o)
66
+ return rowfilter(hi, g1o) + rowfilter(lo, g0o)
67
+
68
+
69
+ def fwd_j2plus(x: torch.Tensor, h0a, h1a, h0b, h1b, skip_hps: bool, o_dim: int):
70
+ """Level >= 2 forward DTCWT (quarter-sample-shift filters)."""
71
+ if not skip_hps:
72
+ lo = rowdfilt(x, h0b, h0a, False)
73
+ hi = rowdfilt(x, h1b, h1a, True)
74
+ ll = coldfilt(lo, h0b, h0a, False)
75
+ lh = coldfilt(lo, h1b, h1a, True)
76
+ hl = coldfilt(hi, h0b, h0a, False)
77
+ hh = coldfilt(hi, h1b, h1a, True)
78
+ highs = highs_to_orientations(lh, hl, hh, o_dim)
79
+ else:
80
+ ll = coldfilt(rowdfilt(x, h0b, h0a, False), h0b, h0a, False)
81
+ highs = None
82
+ return ll, highs
83
+
84
+
85
+ def inv_j2plus(ll: torch.Tensor, highs, g0a, g1a, g0b, g1b, o_dim: int, h_dim: int, w_dim: int):
86
+ """Level >= 2 inverse DTCWT."""
87
+ if highs is None:
88
+ return rowifilt(colifilt(ll, g0b, g0a, False), g0b, g0a, False)
89
+ lh, hl, hh = orientations_to_highs(highs, o_dim)
90
+
91
+ hi = colifilt(hh, g1b, g1a, True) + colifilt(hl, g0b, g0a, False)
92
+ lo = colifilt(lh, g1b, g1a, True) + colifilt(ll, g0b, g0a, False)
93
+ return rowifilt(hi, g1b, g1a, True) + rowifilt(lo, g0b, g0a, False)
@@ -0,0 +1,6 @@
1
+ """Discrete wavelet transforms."""
2
+
3
+ from .transform1d import DWT1D, IDWT1D, dwt1d, idwt1d
4
+ from .transform2d import DWT2D, IDWT2D, dwt2d, idwt2d
5
+
6
+ __all__ = ["DWT1D", "IDWT1D", "dwt1d", "idwt1d", "DWT2D", "IDWT2D", "dwt2d", "idwt2d"]
@@ -0,0 +1,59 @@
1
+ """Shared input validation for the transforms.
2
+
3
+ Everything here is pure Python / math on purpose: no PyWavelets call, so the
4
+ module API stays fully traceable under ``torch.compile``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import warnings
11
+
12
+
13
+ def check_spatial(shape: tuple[int, ...], names: tuple[str, ...], where: str) -> None:
14
+ """Raise a clear error for zero-size spatial dimensions."""
15
+ for name, size in zip(names, shape, strict=True):
16
+ if size == 0:
17
+ raise ValueError(
18
+ f"{where} requires non-empty spatial dimensions; got "
19
+ f"zero-size {name!r} in {tuple(shape)}"
20
+ )
21
+
22
+
23
+ def check_reflect_length(
24
+ shape: tuple[int, ...], names: tuple[str, ...], mode: str, where: str
25
+ ) -> None:
26
+ """Mirror PyWavelets: reflect needs a length > 1 along every transformed axis."""
27
+ if mode != "reflect":
28
+ return
29
+ for name, size in zip(names, shape, strict=True):
30
+ if size == 1:
31
+ raise ValueError(
32
+ f"Input data length must be greater than 1 for [anti]reflect "
33
+ f"mode ({where}: {name!r} has length 1)."
34
+ )
35
+
36
+
37
+ def validate_level(level: int) -> None:
38
+ if isinstance(level, bool) or not isinstance(level, int) or level < 0:
39
+ raise ValueError(f"level must be a non-negative int; got {level!r}")
40
+
41
+
42
+ def dwt_max_level(data_len: int, filter_len: int) -> int:
43
+ """Maximum useful decomposition level (matches ``pywt.dwt_max_level``)."""
44
+ if filter_len < 2:
45
+ raise ValueError(f"invalid filter length: {filter_len}")
46
+ if data_len < filter_len - 1:
47
+ return 0
48
+ return int(math.floor(math.log2(data_len / (filter_len - 1))))
49
+
50
+
51
+ def warn_if_too_high(n: int, length: int, level: int) -> None:
52
+ """Mirror PyWavelets' warning when the level is too high."""
53
+ if level > 0 and level > dwt_max_level(n, length):
54
+ warnings.warn(
55
+ f"Level value of {level} is too high: all coefficients will "
56
+ "experience boundary effects.",
57
+ UserWarning,
58
+ stacklevel=3,
59
+ )