torchnd 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {torchnd-0.1.2 → torchnd-0.2.0}/PKG-INFO +30 -3
- {torchnd-0.1.2 → torchnd-0.2.0}/README.md +28 -1
- {torchnd-0.1.2 → torchnd-0.2.0}/pyproject.toml +2 -2
- torchnd-0.2.0/src/torchnd/__init__.py +27 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd/functional.py +310 -9
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd.egg-info/PKG-INFO +30 -3
- {torchnd-0.1.2 → torchnd-0.2.0}/tests/test_functional.py +232 -5
- torchnd-0.1.2/src/torchnd/__init__.py +0 -14
- {torchnd-0.1.2 → torchnd-0.2.0}/setup.cfg +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd/modules.py +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd/py.typed +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd.egg-info/SOURCES.txt +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd.egg-info/dependency_links.txt +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd.egg-info/requires.txt +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/src/torchnd.egg-info/top_level.txt +0 -0
- {torchnd-0.1.2 → torchnd-0.2.0}/tests/test_modules.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: torchnd
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: ND
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: ND operations for PyTorch
|
|
5
5
|
Author-email: Felix Zimmermann <fzimmermann89@gmail.com>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/fzimmermann89/torch_conv_nd
|
|
@@ -31,7 +31,9 @@ Requires-Dist: types-setuptools; extra == "dev"
|
|
|
31
31
|
|
|
32
32
|
# torchnd
|
|
33
33
|
|
|
34
|
-
N-dimensional
|
|
34
|
+
N-dimensional operations for PyTorch with flexible dimension specification.
|
|
35
|
+
|
|
36
|
+
`torchnd` provides convolution, transposed convolution, padding/cropping, linear interpolation, and corresponding adjoint operations for tensors with arbitrary dimension layouts. It supports complex tensors where applicable and uses native PyTorch 1D/2D/3D kernels whenever possible.
|
|
35
37
|
|
|
36
38
|
## Installation
|
|
37
39
|
|
|
@@ -112,6 +114,17 @@ cropped = pad_nd(x, pad=(-1, -1), dims=(0,)) # Negative values crop
|
|
|
112
114
|
unpadded = adjoint_pad_nd(padded, pad=(1, 1, 2, 2), dims=(-2, -1))
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
**Linear interpolation:**
|
|
118
|
+
```python
|
|
119
|
+
from torchnd import linear_interpolation_nd, adjoint_linear_interpolation_nd
|
|
120
|
+
|
|
121
|
+
x = torch.randn(2, 4, 64, 64)
|
|
122
|
+
low = linear_interpolation_nd(x, size=(32, 32), dims=(-2, -1))
|
|
123
|
+
|
|
124
|
+
# Adjoint of the interpolation operator
|
|
125
|
+
high_adjoint = adjoint_linear_interpolation_nd(low, input_size=(64, 64), dims=(-2, -1))
|
|
126
|
+
```
|
|
127
|
+
|
|
115
128
|
**Adjoint operators:**
|
|
116
129
|
```python
|
|
117
130
|
from torchnd import ConvNd
|
|
@@ -149,6 +162,18 @@ Pad or crop a tensor to a target size, centering the original content. Combines
|
|
|
149
162
|
|
|
150
163
|
Computes the adjoint of `pad_nd` via autograd. For a padding operator P, the adjoint P* satisfies the inner product identity: `<Px, y> = <x, P*y>` for all x, y. The adjoint unpads the tensor, summing contributions from padded regions back into the original shape. For constant/zeros mode, the adjoint is equivalent to cropping. For other modes, it correctly handles the adjoint of the boundary extension.
|
|
151
164
|
|
|
165
|
+
### linear_interpolation_nd
|
|
166
|
+
|
|
167
|
+
Resizes selected tensor dimensions using linear interpolation. The `dims` parameter specifies which dimensions are interpolated; if `None`, the last `len(size)` dimensions are used. For more than three interpolated dimensions, the operation is split into chunks of up to three dimensions to use PyTorch's native interpolation kernels.
|
|
168
|
+
|
|
169
|
+
Complex tensors are handled by applying interpolation to real and imaginary parts separately.
|
|
170
|
+
|
|
171
|
+
### adjoint_linear_interpolation_nd
|
|
172
|
+
|
|
173
|
+
Computes the adjoint of `linear_interpolation_nd`. The adjoint is evaluated with PyTorch's native interpolation backward kernels and satisfies the inner product identity `<Ax, y> = <x, A*y>` for the linear interpolation operator A.
|
|
174
|
+
|
|
175
|
+
The adjoint supports autograd, forward-mode AD, and `torch.func.vmap`.
|
|
176
|
+
|
|
152
177
|
### Adjoint convolution
|
|
153
178
|
|
|
154
179
|
The `ConvNd` and `ConvTransposeNd` modules provide `adjoint()` methods. For `ConvNd`, the adjoint is the transposed convolution with conjugated weights (for complex) or identical weights (for real). For `ConvTransposeNd`, the adjoint is the forward convolution with conjugated weights.
|
|
@@ -159,6 +184,8 @@ The adjoint satisfies the inner product identity: `<Ax, y> = <x, A*y>` where A i
|
|
|
159
184
|
|
|
160
185
|
For dimensions beyond 3D, `conv_nd` recursively decomposes the convolution into lower-dimensional operations. A 4D convolution becomes a sum of 3D convolutions applied to strided slices of the input.
|
|
161
186
|
|
|
187
|
+
For interpolation of more than three dimensions, `linear_interpolation_nd` applies native PyTorch interpolation to chunks of up to three dimensions. The adjoint applies the corresponding chunks in reverse order.
|
|
188
|
+
|
|
162
189
|
Complex convolution is handled by decomposing into real operations: `(a+bi)*(c+di) = (ac-bd) + (ad+bc)i`.
|
|
163
190
|
|
|
164
191
|
The implementation uses native PyTorch operations when possible (1D, 2D, 3D) and falls back to recursion only for higher dimensions, minimizing memory overhead with strided views.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# torchnd
|
|
2
2
|
|
|
3
|
-
N-dimensional
|
|
3
|
+
N-dimensional operations for PyTorch with flexible dimension specification.
|
|
4
|
+
|
|
5
|
+
`torchnd` provides convolution, transposed convolution, padding/cropping, linear interpolation, and corresponding adjoint operations for tensors with arbitrary dimension layouts. It supports complex tensors where applicable and uses native PyTorch 1D/2D/3D kernels whenever possible.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -81,6 +83,17 @@ cropped = pad_nd(x, pad=(-1, -1), dims=(0,)) # Negative values crop
|
|
|
81
83
|
unpadded = adjoint_pad_nd(padded, pad=(1, 1, 2, 2), dims=(-2, -1))
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
**Linear interpolation:**
|
|
87
|
+
```python
|
|
88
|
+
from torchnd import linear_interpolation_nd, adjoint_linear_interpolation_nd
|
|
89
|
+
|
|
90
|
+
x = torch.randn(2, 4, 64, 64)
|
|
91
|
+
low = linear_interpolation_nd(x, size=(32, 32), dims=(-2, -1))
|
|
92
|
+
|
|
93
|
+
# Adjoint of the interpolation operator
|
|
94
|
+
high_adjoint = adjoint_linear_interpolation_nd(low, input_size=(64, 64), dims=(-2, -1))
|
|
95
|
+
```
|
|
96
|
+
|
|
84
97
|
**Adjoint operators:**
|
|
85
98
|
```python
|
|
86
99
|
from torchnd import ConvNd
|
|
@@ -118,6 +131,18 @@ Pad or crop a tensor to a target size, centering the original content. Combines
|
|
|
118
131
|
|
|
119
132
|
Computes the adjoint of `pad_nd` via autograd. For a padding operator P, the adjoint P* satisfies the inner product identity: `<Px, y> = <x, P*y>` for all x, y. The adjoint unpads the tensor, summing contributions from padded regions back into the original shape. For constant/zeros mode, the adjoint is equivalent to cropping. For other modes, it correctly handles the adjoint of the boundary extension.
|
|
120
133
|
|
|
134
|
+
### linear_interpolation_nd
|
|
135
|
+
|
|
136
|
+
Resizes selected tensor dimensions using linear interpolation. The `dims` parameter specifies which dimensions are interpolated; if `None`, the last `len(size)` dimensions are used. For more than three interpolated dimensions, the operation is split into chunks of up to three dimensions to use PyTorch's native interpolation kernels.
|
|
137
|
+
|
|
138
|
+
Complex tensors are handled by applying interpolation to real and imaginary parts separately.
|
|
139
|
+
|
|
140
|
+
### adjoint_linear_interpolation_nd
|
|
141
|
+
|
|
142
|
+
Computes the adjoint of `linear_interpolation_nd`. The adjoint is evaluated with PyTorch's native interpolation backward kernels and satisfies the inner product identity `<Ax, y> = <x, A*y>` for the linear interpolation operator A.
|
|
143
|
+
|
|
144
|
+
The adjoint supports autograd, forward-mode AD, and `torch.func.vmap`.
|
|
145
|
+
|
|
121
146
|
### Adjoint convolution
|
|
122
147
|
|
|
123
148
|
The `ConvNd` and `ConvTransposeNd` modules provide `adjoint()` methods. For `ConvNd`, the adjoint is the transposed convolution with conjugated weights (for complex) or identical weights (for real). For `ConvTransposeNd`, the adjoint is the forward convolution with conjugated weights.
|
|
@@ -128,6 +153,8 @@ The adjoint satisfies the inner product identity: `<Ax, y> = <x, A*y>` where A i
|
|
|
128
153
|
|
|
129
154
|
For dimensions beyond 3D, `conv_nd` recursively decomposes the convolution into lower-dimensional operations. A 4D convolution becomes a sum of 3D convolutions applied to strided slices of the input.
|
|
130
155
|
|
|
156
|
+
For interpolation of more than three dimensions, `linear_interpolation_nd` applies native PyTorch interpolation to chunks of up to three dimensions. The adjoint applies the corresponding chunks in reverse order.
|
|
157
|
+
|
|
131
158
|
Complex convolution is handled by decomposing into real operations: `(a+bi)*(c+di) = (ac-bd) + (ad+bc)i`.
|
|
132
159
|
|
|
133
160
|
The implementation uses native PyTorch operations when possible (1D, 2D, 3D) and falls back to recursion only for higher dimensions, minimizing memory overhead with strided views.
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "torchnd"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "ND
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "ND operations for PyTorch"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = {text = "GPL-3.0-or-later"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
2
|
+
|
|
3
|
+
from torchnd.functional import (
|
|
4
|
+
adjoint_linear_interpolation_nd,
|
|
5
|
+
adjoint_nearest_interpolation_nd,
|
|
6
|
+
adjoint_pad_nd,
|
|
7
|
+
conv_nd,
|
|
8
|
+
linear_interpolation_nd,
|
|
9
|
+
nearest_interpolation_nd,
|
|
10
|
+
pad_nd,
|
|
11
|
+
pad_or_crop_to_size,
|
|
12
|
+
)
|
|
13
|
+
from torchnd.modules import ConvNd, ConvTransposeNd
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"adjoint_linear_interpolation_nd",
|
|
17
|
+
"adjoint_nearest_interpolation_nd",
|
|
18
|
+
"adjoint_pad_nd",
|
|
19
|
+
"conv_nd",
|
|
20
|
+
"linear_interpolation_nd",
|
|
21
|
+
"nearest_interpolation_nd",
|
|
22
|
+
"pad_nd",
|
|
23
|
+
"pad_or_crop_to_size",
|
|
24
|
+
"ConvNd",
|
|
25
|
+
"ConvTransposeNd",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -18,11 +18,318 @@ CONV_TRANSPOSE_REGISTRY: dict[int, Callable[..., Tensor]] = {
|
|
|
18
18
|
3: torch.nn.functional.conv_transpose3d,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
LINEAR_INTERPOLATION_MODE: dict[int, str] = {
|
|
22
|
+
1: "linear",
|
|
23
|
+
2: "bilinear",
|
|
24
|
+
3: "trilinear",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
LINEAR_INTERPOLATION_ADJOINT_REGISTRY: dict[int, Callable[..., Tensor]] = {
|
|
28
|
+
1: torch.ops.aten.upsample_linear1d_backward,
|
|
29
|
+
2: torch.ops.aten.upsample_bilinear2d_backward,
|
|
30
|
+
3: torch.ops.aten.upsample_trilinear3d_backward,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
NEAREST_INTERPOLATION_ADJOINT_REGISTRY: dict[int, Callable[..., Tensor]] = {
|
|
34
|
+
1: torch.ops.aten.upsample_nearest1d_backward,
|
|
35
|
+
2: torch.ops.aten.upsample_nearest2d_backward,
|
|
36
|
+
3: torch.ops.aten.upsample_nearest3d_backward,
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
|
|
22
40
|
def _normalize_tuple(value: int | tuple[int, ...], length: int) -> tuple[int, ...]:
|
|
23
41
|
return (value,) * length if isinstance(value, int) else tuple(value)
|
|
24
42
|
|
|
25
43
|
|
|
44
|
+
def _normalize_dims(ndim: int, dims: Sequence[int]) -> tuple[int, ...]:
|
|
45
|
+
normalized = tuple(d % ndim for d in dims)
|
|
46
|
+
if len(set(normalized)) != len(normalized):
|
|
47
|
+
raise ValueError("duplicate dimensions not supported")
|
|
48
|
+
return normalized
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _linear_interpolation_nd_real(
|
|
52
|
+
x: Tensor,
|
|
53
|
+
size: Sequence[int],
|
|
54
|
+
dims: Sequence[int] | None = None,
|
|
55
|
+
align_corners: bool = False,
|
|
56
|
+
) -> Tensor:
|
|
57
|
+
size = tuple(size)
|
|
58
|
+
if dims is None:
|
|
59
|
+
if len(size) > x.ndim:
|
|
60
|
+
raise ValueError("len(size) must be <= x.ndim")
|
|
61
|
+
target_dims = tuple(range(x.ndim - len(size), x.ndim))
|
|
62
|
+
elif len(dims) != len(size):
|
|
63
|
+
raise ValueError("len(dims) must equal len(size)")
|
|
64
|
+
else:
|
|
65
|
+
target_dims = _normalize_dims(x.ndim, dims)
|
|
66
|
+
|
|
67
|
+
for i in range(0, len(target_dims), 3):
|
|
68
|
+
chunk_dims = target_dims[i : i + 3]
|
|
69
|
+
chunk_size = size[i : i + 3]
|
|
70
|
+
|
|
71
|
+
other_dims = tuple(dim for dim in range(x.ndim) if dim not in chunk_dims)
|
|
72
|
+
to_standard = (*other_dims, *chunk_dims)
|
|
73
|
+
from_standard = tuple(sorted(range(len(to_standard)), key=to_standard.__getitem__))
|
|
74
|
+
other_shape = tuple(x.shape[dim] for dim in other_dims)
|
|
75
|
+
|
|
76
|
+
x = x.permute(to_standard)
|
|
77
|
+
x = x.flatten(end_dim=len(other_dims) - 1).unsqueeze(0) if other_dims else x.unsqueeze(0).unsqueeze(0)
|
|
78
|
+
|
|
79
|
+
x = torch.nn.functional.interpolate(
|
|
80
|
+
x,
|
|
81
|
+
size=chunk_size,
|
|
82
|
+
mode=LINEAR_INTERPOLATION_MODE[len(chunk_dims)],
|
|
83
|
+
align_corners=align_corners,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
x = x.squeeze(0).unflatten(0, other_shape).permute(from_standard) if other_dims else x.squeeze(0).squeeze(0)
|
|
87
|
+
return x
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def linear_interpolation_nd(
|
|
91
|
+
x: Tensor,
|
|
92
|
+
size: Sequence[int],
|
|
93
|
+
dims: Sequence[int] | None = None,
|
|
94
|
+
align_corners: bool = False,
|
|
95
|
+
) -> Tensor:
|
|
96
|
+
"""Resize selected tensor dimensions using linear interpolation.
|
|
97
|
+
|
|
98
|
+
The selected dimensions are moved to PyTorch's interpolation layout and
|
|
99
|
+
processed in chunks of up to three dimensions. Complex tensors are
|
|
100
|
+
interpolated component-wise.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
x
|
|
105
|
+
Input tensor.
|
|
106
|
+
size
|
|
107
|
+
Target size for the interpolated dimensions.
|
|
108
|
+
dims
|
|
109
|
+
Dimensions to interpolate. If None, interpolates the last ``len(size)`` dimensions.
|
|
110
|
+
align_corners
|
|
111
|
+
Geometric convention passed to ``torch.nn.functional.interpolate``.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
Tensor with selected dimensions resized to ``size``.
|
|
116
|
+
"""
|
|
117
|
+
if len(size) == 0:
|
|
118
|
+
return x
|
|
119
|
+
if x.is_complex():
|
|
120
|
+
return torch.complex(
|
|
121
|
+
_linear_interpolation_nd_real(x.real, size, dims, align_corners),
|
|
122
|
+
_linear_interpolation_nd_real(x.imag, size, dims, align_corners),
|
|
123
|
+
)
|
|
124
|
+
return _linear_interpolation_nd_real(x, size, dims, align_corners)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _nearest_interpolation_nd_real(
|
|
128
|
+
x: Tensor,
|
|
129
|
+
size: Sequence[int],
|
|
130
|
+
dims: Sequence[int] | None = None,
|
|
131
|
+
) -> Tensor:
|
|
132
|
+
size = tuple(size)
|
|
133
|
+
if dims is None:
|
|
134
|
+
if len(size) > x.ndim:
|
|
135
|
+
raise ValueError("len(size) must be <= x.ndim")
|
|
136
|
+
target_dims = tuple(range(x.ndim - len(size), x.ndim))
|
|
137
|
+
elif len(dims) != len(size):
|
|
138
|
+
raise ValueError("len(dims) must equal len(size)")
|
|
139
|
+
else:
|
|
140
|
+
target_dims = _normalize_dims(x.ndim, dims)
|
|
141
|
+
|
|
142
|
+
for i in range(0, len(target_dims), 3):
|
|
143
|
+
chunk_dims = target_dims[i : i + 3]
|
|
144
|
+
chunk_size = size[i : i + 3]
|
|
145
|
+
|
|
146
|
+
other_dims = tuple(dim for dim in range(x.ndim) if dim not in chunk_dims)
|
|
147
|
+
to_standard = (*other_dims, *chunk_dims)
|
|
148
|
+
from_standard = tuple(sorted(range(len(to_standard)), key=to_standard.__getitem__))
|
|
149
|
+
other_shape = tuple(x.shape[dim] for dim in other_dims)
|
|
150
|
+
|
|
151
|
+
x = x.permute(to_standard)
|
|
152
|
+
x = x.flatten(end_dim=len(other_dims) - 1).unsqueeze(0) if other_dims else x.unsqueeze(0).unsqueeze(0)
|
|
153
|
+
x = torch.nn.functional.interpolate(x, size=chunk_size, mode="nearest")
|
|
154
|
+
x = x.squeeze(0).unflatten(0, other_shape).permute(from_standard) if other_dims else x.squeeze(0).squeeze(0)
|
|
155
|
+
return x
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def nearest_interpolation_nd(
|
|
159
|
+
x: Tensor,
|
|
160
|
+
size: Sequence[int],
|
|
161
|
+
dims: Sequence[int] | None = None,
|
|
162
|
+
) -> Tensor:
|
|
163
|
+
"""Resize selected tensor dimensions using nearest-neighbor interpolation.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
x
|
|
168
|
+
Input tensor.
|
|
169
|
+
size
|
|
170
|
+
Target size for the interpolated dimensions.
|
|
171
|
+
dims
|
|
172
|
+
Dimensions to interpolate. If None, interpolates the last ``len(size)`` dimensions.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
Tensor with selected dimensions resized to ``size``.
|
|
177
|
+
"""
|
|
178
|
+
if len(size) == 0:
|
|
179
|
+
return x
|
|
180
|
+
if x.is_complex():
|
|
181
|
+
return torch.complex(
|
|
182
|
+
_nearest_interpolation_nd_real(x.real, size, dims),
|
|
183
|
+
_nearest_interpolation_nd_real(x.imag, size, dims),
|
|
184
|
+
)
|
|
185
|
+
return _nearest_interpolation_nd_real(x, size, dims)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _adjoint_linear_interpolation_nd_real(
|
|
189
|
+
x: Tensor,
|
|
190
|
+
input_size: Sequence[int],
|
|
191
|
+
dims: Sequence[int] | None = None,
|
|
192
|
+
align_corners: bool = False,
|
|
193
|
+
) -> Tensor:
|
|
194
|
+
input_size = tuple(input_size)
|
|
195
|
+
if dims is None:
|
|
196
|
+
if len(input_size) > x.ndim:
|
|
197
|
+
raise ValueError("len(input_size) must be <= x.ndim")
|
|
198
|
+
target_dims = tuple(range(x.ndim - len(input_size), x.ndim))
|
|
199
|
+
elif len(dims) != len(input_size):
|
|
200
|
+
raise ValueError("len(dims) must equal len(input_size)")
|
|
201
|
+
else:
|
|
202
|
+
target_dims = _normalize_dims(x.ndim, dims)
|
|
203
|
+
|
|
204
|
+
chunks = [(target_dims[i : i + 3], input_size[i : i + 3]) for i in range(0, len(target_dims), 3)]
|
|
205
|
+
for chunk_dims, chunk_input_size in reversed(chunks):
|
|
206
|
+
output_size = tuple(x.shape[dim] for dim in chunk_dims)
|
|
207
|
+
|
|
208
|
+
other_dims = tuple(dim for dim in range(x.ndim) if dim not in chunk_dims)
|
|
209
|
+
to_standard = (*other_dims, *chunk_dims)
|
|
210
|
+
from_standard = tuple(sorted(range(len(to_standard)), key=to_standard.__getitem__))
|
|
211
|
+
other_shape = tuple(x.shape[dim] for dim in other_dims)
|
|
212
|
+
|
|
213
|
+
x = x.permute(to_standard)
|
|
214
|
+
x = x.flatten(end_dim=len(other_dims) - 1).unsqueeze(0) if other_dims else x.unsqueeze(0).unsqueeze(0)
|
|
215
|
+
|
|
216
|
+
x = LINEAR_INTERPOLATION_ADJOINT_REGISTRY[len(chunk_input_size)](
|
|
217
|
+
x,
|
|
218
|
+
list(output_size),
|
|
219
|
+
[x.shape[0], x.shape[1], *chunk_input_size],
|
|
220
|
+
align_corners,
|
|
221
|
+
*([None] * len(chunk_input_size)),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
x = x.squeeze(0).unflatten(0, other_shape).permute(from_standard) if other_dims else x.squeeze(0).squeeze(0)
|
|
225
|
+
return x
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def adjoint_linear_interpolation_nd(
|
|
229
|
+
x: Tensor,
|
|
230
|
+
input_size: Sequence[int],
|
|
231
|
+
dims: Sequence[int] | None = None,
|
|
232
|
+
align_corners: bool = False,
|
|
233
|
+
) -> Tensor:
|
|
234
|
+
"""Apply the adjoint of ``linear_interpolation_nd``.
|
|
235
|
+
|
|
236
|
+
The adjoint is evaluated with PyTorch's native interpolation backward
|
|
237
|
+
kernels, not by running a dummy forward interpolation. Complex tensors are
|
|
238
|
+
handled component-wise.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
x
|
|
243
|
+
Tensor in the output space of ``linear_interpolation_nd``.
|
|
244
|
+
input_size
|
|
245
|
+
Size of the selected dimensions before interpolation.
|
|
246
|
+
dims
|
|
247
|
+
Dimensions that were interpolated. If None, uses the last ``len(input_size)`` dimensions.
|
|
248
|
+
align_corners
|
|
249
|
+
Geometric convention passed to ``torch.nn.functional.interpolate``.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
Tensor in the input space of ``linear_interpolation_nd``.
|
|
254
|
+
"""
|
|
255
|
+
if len(input_size) == 0:
|
|
256
|
+
return x
|
|
257
|
+
if x.is_complex():
|
|
258
|
+
return torch.complex(
|
|
259
|
+
_adjoint_linear_interpolation_nd_real(x.real, input_size, dims, align_corners),
|
|
260
|
+
_adjoint_linear_interpolation_nd_real(x.imag, input_size, dims, align_corners),
|
|
261
|
+
)
|
|
262
|
+
return _adjoint_linear_interpolation_nd_real(x, input_size, dims, align_corners)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _adjoint_nearest_interpolation_nd_real(
|
|
266
|
+
x: Tensor,
|
|
267
|
+
input_size: Sequence[int],
|
|
268
|
+
dims: Sequence[int] | None = None,
|
|
269
|
+
) -> Tensor:
|
|
270
|
+
input_size = tuple(input_size)
|
|
271
|
+
if dims is None:
|
|
272
|
+
if len(input_size) > x.ndim:
|
|
273
|
+
raise ValueError("len(input_size) must be <= x.ndim")
|
|
274
|
+
target_dims = tuple(range(x.ndim - len(input_size), x.ndim))
|
|
275
|
+
elif len(dims) != len(input_size):
|
|
276
|
+
raise ValueError("len(dims) must equal len(input_size)")
|
|
277
|
+
else:
|
|
278
|
+
target_dims = _normalize_dims(x.ndim, dims)
|
|
279
|
+
|
|
280
|
+
chunks = [(target_dims[i : i + 3], input_size[i : i + 3]) for i in range(0, len(target_dims), 3)]
|
|
281
|
+
for chunk_dims, chunk_input_size in reversed(chunks):
|
|
282
|
+
output_size = tuple(x.shape[dim] for dim in chunk_dims)
|
|
283
|
+
|
|
284
|
+
other_dims = tuple(dim for dim in range(x.ndim) if dim not in chunk_dims)
|
|
285
|
+
to_standard = (*other_dims, *chunk_dims)
|
|
286
|
+
from_standard = tuple(sorted(range(len(to_standard)), key=to_standard.__getitem__))
|
|
287
|
+
other_shape = tuple(x.shape[dim] for dim in other_dims)
|
|
288
|
+
|
|
289
|
+
x = x.permute(to_standard)
|
|
290
|
+
x = x.flatten(end_dim=len(other_dims) - 1).unsqueeze(0) if other_dims else x.unsqueeze(0).unsqueeze(0)
|
|
291
|
+
|
|
292
|
+
x = NEAREST_INTERPOLATION_ADJOINT_REGISTRY[len(chunk_input_size)](
|
|
293
|
+
x,
|
|
294
|
+
list(output_size),
|
|
295
|
+
[x.shape[0], x.shape[1], *chunk_input_size],
|
|
296
|
+
*([None] * len(chunk_input_size)),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
x = x.squeeze(0).unflatten(0, other_shape).permute(from_standard) if other_dims else x.squeeze(0).squeeze(0)
|
|
300
|
+
return x
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def adjoint_nearest_interpolation_nd(
|
|
304
|
+
x: Tensor,
|
|
305
|
+
input_size: Sequence[int],
|
|
306
|
+
dims: Sequence[int] | None = None,
|
|
307
|
+
) -> Tensor:
|
|
308
|
+
"""Apply the adjoint of ``nearest_interpolation_nd``.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
x
|
|
313
|
+
Tensor in the output space of ``nearest_interpolation_nd``.
|
|
314
|
+
input_size
|
|
315
|
+
Size of the selected dimensions before interpolation.
|
|
316
|
+
dims
|
|
317
|
+
Dimensions that were interpolated. If None, uses the last ``len(input_size)`` dimensions.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
Tensor in the input space of ``nearest_interpolation_nd``.
|
|
322
|
+
"""
|
|
323
|
+
if len(input_size) == 0:
|
|
324
|
+
return x
|
|
325
|
+
if x.is_complex():
|
|
326
|
+
return torch.complex(
|
|
327
|
+
_adjoint_nearest_interpolation_nd_real(x.real, input_size, dims),
|
|
328
|
+
_adjoint_nearest_interpolation_nd_real(x.imag, input_size, dims),
|
|
329
|
+
)
|
|
330
|
+
return _adjoint_nearest_interpolation_nd_real(x, input_size, dims)
|
|
331
|
+
|
|
332
|
+
|
|
26
333
|
def conv_nd(
|
|
27
334
|
x: Tensor,
|
|
28
335
|
weight: Tensor,
|
|
@@ -295,10 +602,8 @@ def pad_nd(
|
|
|
295
602
|
else:
|
|
296
603
|
if len(dims) != n_dims:
|
|
297
604
|
raise ValueError("len(dims) must equal len(pad) // 2")
|
|
298
|
-
target_dims =
|
|
605
|
+
target_dims = list(_normalize_dims(ndim, dims))
|
|
299
606
|
pad_pairs = [(pad[2 * i], pad[2 * i + 1]) for i in range(n_dims)]
|
|
300
|
-
if len(set(target_dims)) != len(target_dims):
|
|
301
|
-
raise ValueError("duplicate dimensions not supported")
|
|
302
607
|
|
|
303
608
|
if mode == "constant":
|
|
304
609
|
return _pad_constant(x, target_dims, pad_pairs, value)
|
|
@@ -340,7 +645,7 @@ def pad_or_crop_to_size(
|
|
|
340
645
|
else:
|
|
341
646
|
if len(dims) != len(size):
|
|
342
647
|
raise ValueError("len(dims) must equal len(size)")
|
|
343
|
-
target_dims =
|
|
648
|
+
target_dims = list(_normalize_dims(ndim, dims))
|
|
344
649
|
|
|
345
650
|
pad_list: list[int] = []
|
|
346
651
|
for dim, target in zip(target_dims, size, strict=True):
|
|
@@ -374,10 +679,6 @@ def _pad_non_constant(
|
|
|
374
679
|
pad_pairs: list[tuple[int, int]],
|
|
375
680
|
mode: str,
|
|
376
681
|
) -> Tensor:
|
|
377
|
-
for left, right in pad_pairs:
|
|
378
|
-
if left < 0 or right < 0:
|
|
379
|
-
raise ValueError(f"Negative padding is not supported for mode '{mode}'. Use mode='constant' for cropping.")
|
|
380
|
-
|
|
381
682
|
result = x
|
|
382
683
|
|
|
383
684
|
for i in range(0, len(target_dims), 3):
|
|
@@ -464,7 +765,7 @@ def adjoint_pad_nd(
|
|
|
464
765
|
else:
|
|
465
766
|
if len(dims) != n_dims:
|
|
466
767
|
raise ValueError("len(dims) must equal len(pad) // 2")
|
|
467
|
-
target_dims =
|
|
768
|
+
target_dims = list(_normalize_dims(ndim, dims))
|
|
468
769
|
|
|
469
770
|
if original_sizes is None:
|
|
470
771
|
original_sizes = []
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: torchnd
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: ND
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: ND operations for PyTorch
|
|
5
5
|
Author-email: Felix Zimmermann <fzimmermann89@gmail.com>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/fzimmermann89/torch_conv_nd
|
|
@@ -31,7 +31,9 @@ Requires-Dist: types-setuptools; extra == "dev"
|
|
|
31
31
|
|
|
32
32
|
# torchnd
|
|
33
33
|
|
|
34
|
-
N-dimensional
|
|
34
|
+
N-dimensional operations for PyTorch with flexible dimension specification.
|
|
35
|
+
|
|
36
|
+
`torchnd` provides convolution, transposed convolution, padding/cropping, linear interpolation, and corresponding adjoint operations for tensors with arbitrary dimension layouts. It supports complex tensors where applicable and uses native PyTorch 1D/2D/3D kernels whenever possible.
|
|
35
37
|
|
|
36
38
|
## Installation
|
|
37
39
|
|
|
@@ -112,6 +114,17 @@ cropped = pad_nd(x, pad=(-1, -1), dims=(0,)) # Negative values crop
|
|
|
112
114
|
unpadded = adjoint_pad_nd(padded, pad=(1, 1, 2, 2), dims=(-2, -1))
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
**Linear interpolation:**
|
|
118
|
+
```python
|
|
119
|
+
from torchnd import linear_interpolation_nd, adjoint_linear_interpolation_nd
|
|
120
|
+
|
|
121
|
+
x = torch.randn(2, 4, 64, 64)
|
|
122
|
+
low = linear_interpolation_nd(x, size=(32, 32), dims=(-2, -1))
|
|
123
|
+
|
|
124
|
+
# Adjoint of the interpolation operator
|
|
125
|
+
high_adjoint = adjoint_linear_interpolation_nd(low, input_size=(64, 64), dims=(-2, -1))
|
|
126
|
+
```
|
|
127
|
+
|
|
115
128
|
**Adjoint operators:**
|
|
116
129
|
```python
|
|
117
130
|
from torchnd import ConvNd
|
|
@@ -149,6 +162,18 @@ Pad or crop a tensor to a target size, centering the original content. Combines
|
|
|
149
162
|
|
|
150
163
|
Computes the adjoint of `pad_nd` via autograd. For a padding operator P, the adjoint P* satisfies the inner product identity: `<Px, y> = <x, P*y>` for all x, y. The adjoint unpads the tensor, summing contributions from padded regions back into the original shape. For constant/zeros mode, the adjoint is equivalent to cropping. For other modes, it correctly handles the adjoint of the boundary extension.
|
|
151
164
|
|
|
165
|
+
### linear_interpolation_nd
|
|
166
|
+
|
|
167
|
+
Resizes selected tensor dimensions using linear interpolation. The `dims` parameter specifies which dimensions are interpolated; if `None`, the last `len(size)` dimensions are used. For more than three interpolated dimensions, the operation is split into chunks of up to three dimensions to use PyTorch's native interpolation kernels.
|
|
168
|
+
|
|
169
|
+
Complex tensors are handled by applying interpolation to real and imaginary parts separately.
|
|
170
|
+
|
|
171
|
+
### adjoint_linear_interpolation_nd
|
|
172
|
+
|
|
173
|
+
Computes the adjoint of `linear_interpolation_nd`. The adjoint is evaluated with PyTorch's native interpolation backward kernels and satisfies the inner product identity `<Ax, y> = <x, A*y>` for the linear interpolation operator A.
|
|
174
|
+
|
|
175
|
+
The adjoint supports autograd, forward-mode AD, and `torch.func.vmap`.
|
|
176
|
+
|
|
152
177
|
### Adjoint convolution
|
|
153
178
|
|
|
154
179
|
The `ConvNd` and `ConvTransposeNd` modules provide `adjoint()` methods. For `ConvNd`, the adjoint is the transposed convolution with conjugated weights (for complex) or identical weights (for real). For `ConvTransposeNd`, the adjoint is the forward convolution with conjugated weights.
|
|
@@ -159,6 +184,8 @@ The adjoint satisfies the inner product identity: `<Ax, y> = <x, A*y>` where A i
|
|
|
159
184
|
|
|
160
185
|
For dimensions beyond 3D, `conv_nd` recursively decomposes the convolution into lower-dimensional operations. A 4D convolution becomes a sum of 3D convolutions applied to strided slices of the input.
|
|
161
186
|
|
|
187
|
+
For interpolation of more than three dimensions, `linear_interpolation_nd` applies native PyTorch interpolation to chunks of up to three dimensions. The adjoint applies the corresponding chunks in reverse order.
|
|
188
|
+
|
|
162
189
|
Complex convolution is handled by decomposing into real operations: `(a+bi)*(c+di) = (ac-bd) + (ad+bc)i`.
|
|
163
190
|
|
|
164
191
|
The implementation uses native PyTorch operations when possible (1D, 2D, 3D) and falls back to recursion only for higher dimensions, minimizing memory overhead with strided views.
|
|
@@ -5,7 +5,16 @@ from collections.abc import Iterator
|
|
|
5
5
|
import pytest
|
|
6
6
|
import torch
|
|
7
7
|
|
|
8
|
-
from torchnd import
|
|
8
|
+
from torchnd import (
|
|
9
|
+
adjoint_linear_interpolation_nd,
|
|
10
|
+
adjoint_nearest_interpolation_nd,
|
|
11
|
+
adjoint_pad_nd,
|
|
12
|
+
conv_nd,
|
|
13
|
+
linear_interpolation_nd,
|
|
14
|
+
nearest_interpolation_nd,
|
|
15
|
+
pad_nd,
|
|
16
|
+
pad_or_crop_to_size,
|
|
17
|
+
)
|
|
9
18
|
from torchnd.functional import CONV_REGISTRY, CONV_TRANSPOSE_REGISTRY
|
|
10
19
|
|
|
11
20
|
|
|
@@ -373,10 +382,12 @@ class TestPadNd:
|
|
|
373
382
|
pad_nd(torch.zeros(2, 2), **kwargs)
|
|
374
383
|
|
|
375
384
|
@pytest.mark.parametrize("mode", ["reflect", "replicate", "circular"])
|
|
376
|
-
def
|
|
377
|
-
"""Verify negative padding
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
def test_negative_padding_non_constant_crops(self, mode: str) -> None:
|
|
386
|
+
"""Verify negative padding works for non-constant modes."""
|
|
387
|
+
x = torch.arange(5).float()[None, None, :]
|
|
388
|
+
out = pad_nd(x, (-1, 1), dims=(2,), mode=mode)
|
|
389
|
+
expected = torch.nn.functional.pad(x, (-1, 1), mode=mode)
|
|
390
|
+
torch.testing.assert_close(out, expected)
|
|
380
391
|
|
|
381
392
|
def test_multi_dim_non_constant_chunking(self) -> None:
|
|
382
393
|
"""Verify correct handling of >3 dims requiring multiple pad calls."""
|
|
@@ -527,3 +538,219 @@ class TestAdjointPadNd:
|
|
|
527
538
|
result = adjoint_pad_nd(padded, pad, mode="reflect", dims=dims)
|
|
528
539
|
|
|
529
540
|
assert result.shape == (2, 4, 10, 10)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class TestLinearInterpolationNd:
|
|
544
|
+
"""Tests for linear_interpolation_nd and adjoint_linear_interpolation_nd."""
|
|
545
|
+
|
|
546
|
+
@pytest.mark.parametrize(
|
|
547
|
+
"shape, size, dims",
|
|
548
|
+
[
|
|
549
|
+
((2, 3, 16), (7,), (-1,)),
|
|
550
|
+
((2, 3, 16, 18), (7, 9), (-2, -1)),
|
|
551
|
+
((2, 3, 8, 10, 12), (4, 5, 6), (-3, -2, -1)),
|
|
552
|
+
],
|
|
553
|
+
)
|
|
554
|
+
def test_matches_torch_interpolate(
|
|
555
|
+
self,
|
|
556
|
+
shape: tuple[int, ...],
|
|
557
|
+
size: tuple[int, ...],
|
|
558
|
+
dims: tuple[int, ...],
|
|
559
|
+
) -> None:
|
|
560
|
+
"""Verify standard layouts match torch.nn.functional.interpolate."""
|
|
561
|
+
x = torch.randn(shape)
|
|
562
|
+
expected = torch.nn.functional.interpolate(
|
|
563
|
+
x,
|
|
564
|
+
size=size,
|
|
565
|
+
mode={1: "linear", 2: "bilinear", 3: "trilinear"}[len(size)],
|
|
566
|
+
align_corners=False,
|
|
567
|
+
)
|
|
568
|
+
result = linear_interpolation_nd(x, size, dims=dims)
|
|
569
|
+
torch.testing.assert_close(result, expected)
|
|
570
|
+
|
|
571
|
+
def test_arbitrary_dims(self) -> None:
|
|
572
|
+
"""Verify interpolation works when spatial dimensions are not trailing."""
|
|
573
|
+
x = torch.randn(2, 12, 3, 14)
|
|
574
|
+
result = linear_interpolation_nd(x, size=(6, 7), dims=(1, 3))
|
|
575
|
+
assert result.shape == (2, 6, 3, 7)
|
|
576
|
+
|
|
577
|
+
def test_complex(self) -> None:
|
|
578
|
+
"""Verify complex tensors are interpolated component-wise."""
|
|
579
|
+
x = torch.randn(2, 3, 12, 14, dtype=torch.complex64)
|
|
580
|
+
result = linear_interpolation_nd(x, size=(6, 7), dims=(-2, -1))
|
|
581
|
+
expected = torch.complex(
|
|
582
|
+
linear_interpolation_nd(x.real, size=(6, 7), dims=(-2, -1)),
|
|
583
|
+
linear_interpolation_nd(x.imag, size=(6, 7), dims=(-2, -1)),
|
|
584
|
+
)
|
|
585
|
+
torch.testing.assert_close(result, expected)
|
|
586
|
+
|
|
587
|
+
def test_more_than_three_dims(self) -> None:
|
|
588
|
+
"""Verify interpolation with more than three dimensions uses multiple backend calls."""
|
|
589
|
+
x = torch.randn(2, 3, 4, 5, 6)
|
|
590
|
+
result = linear_interpolation_nd(x, size=(3, 4, 5, 7), dims=(1, 2, 3, 4))
|
|
591
|
+
assert result.shape == (2, 3, 4, 5, 7)
|
|
592
|
+
|
|
593
|
+
@pytest.mark.parametrize(
|
|
594
|
+
"shape, size, dims, dtype",
|
|
595
|
+
[
|
|
596
|
+
((2, 3, 16), (7,), (-1,), torch.float32),
|
|
597
|
+
((2, 12, 3, 14), (6, 7), (1, 3), torch.float32),
|
|
598
|
+
((2, 3, 8, 10, 12), (4, 5, 6), (-3, -2, -1), torch.complex64),
|
|
599
|
+
((2, 3, 4, 5, 6), (4, 3, 4, 5), (1, 2, 3, 4), torch.float32),
|
|
600
|
+
],
|
|
601
|
+
)
|
|
602
|
+
def test_adjointness(
|
|
603
|
+
self,
|
|
604
|
+
shape: tuple[int, ...],
|
|
605
|
+
size: tuple[int, ...],
|
|
606
|
+
dims: tuple[int, ...],
|
|
607
|
+
dtype: torch.dtype,
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Verify <Ax, y> == <x, A^H y>."""
|
|
610
|
+
x = torch.randn(shape, dtype=dtype)
|
|
611
|
+
y_shape = list(shape)
|
|
612
|
+
normalized_dims = tuple(dim % len(shape) for dim in dims)
|
|
613
|
+
for dim, target in zip(normalized_dims, size, strict=True):
|
|
614
|
+
y_shape[dim] = target
|
|
615
|
+
y = torch.randn(y_shape, dtype=dtype)
|
|
616
|
+
|
|
617
|
+
ax = linear_interpolation_nd(x, size, dims=dims)
|
|
618
|
+
ahy = adjoint_linear_interpolation_nd(y, [shape[dim] for dim in normalized_dims], dims=dims)
|
|
619
|
+
|
|
620
|
+
lhs = torch.vdot(ax.flatten(), y.flatten())
|
|
621
|
+
rhs = torch.vdot(x.flatten(), ahy.flatten())
|
|
622
|
+
torch.testing.assert_close(lhs, rhs, rtol=1e-4, atol=1e-5)
|
|
623
|
+
|
|
624
|
+
def test_adjoint_is_differentiable(self) -> None:
|
|
625
|
+
"""Verify the adjoint operation is differentiable."""
|
|
626
|
+
y = torch.randn(2, 3, 6, 7, requires_grad=True)
|
|
627
|
+
result = adjoint_linear_interpolation_nd(y, input_size=(12, 14), dims=(-2, -1))
|
|
628
|
+
result.square().sum().backward()
|
|
629
|
+
assert y.grad is not None
|
|
630
|
+
|
|
631
|
+
def test_gradcheck(self) -> None:
|
|
632
|
+
"""Verify numerical gradients for interpolation and its adjoint."""
|
|
633
|
+
x = torch.randn(1, 2, 5, 6, dtype=torch.float64, requires_grad=True)
|
|
634
|
+
y = torch.randn(1, 2, 3, 4, dtype=torch.float64, requires_grad=True)
|
|
635
|
+
|
|
636
|
+
assert torch.autograd.gradcheck(lambda value: linear_interpolation_nd(value, size=(3, 4), dims=(-2, -1)), x)
|
|
637
|
+
assert torch.autograd.gradcheck(
|
|
638
|
+
lambda value: adjoint_linear_interpolation_nd(value, input_size=(5, 6), dims=(-2, -1)),
|
|
639
|
+
y,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
def test_forward_jvp(self) -> None:
|
|
643
|
+
"""Verify interpolation works with forward-mode AD."""
|
|
644
|
+
x = torch.randn(2, 3, 12, 14)
|
|
645
|
+
tangent = torch.randn_like(x)
|
|
646
|
+
primal, tangent_out = torch.func.jvp(
|
|
647
|
+
lambda y: linear_interpolation_nd(y, size=(6, 7), dims=(-2, -1)),
|
|
648
|
+
(x,),
|
|
649
|
+
(tangent,),
|
|
650
|
+
)
|
|
651
|
+
assert primal.shape == (2, 3, 6, 7)
|
|
652
|
+
torch.testing.assert_close(tangent_out, linear_interpolation_nd(tangent, size=(6, 7), dims=(-2, -1)))
|
|
653
|
+
|
|
654
|
+
def test_forward_vmap(self) -> None:
|
|
655
|
+
"""Verify interpolation works with vmap."""
|
|
656
|
+
x = torch.randn(5, 2, 3, 12, 14)
|
|
657
|
+
result = torch.func.vmap(lambda y: linear_interpolation_nd(y, size=(6, 7), dims=(-2, -1)))(x)
|
|
658
|
+
assert result.shape == (5, 2, 3, 6, 7)
|
|
659
|
+
|
|
660
|
+
def test_adjoint_jvp(self) -> None:
|
|
661
|
+
"""Verify the adjoint works with forward-mode AD."""
|
|
662
|
+
y = torch.randn(2, 3, 6, 7)
|
|
663
|
+
tangent = torch.randn_like(y)
|
|
664
|
+
primal, tangent_out = torch.func.jvp(
|
|
665
|
+
lambda x: adjoint_linear_interpolation_nd(x, input_size=(12, 14), dims=(-2, -1)),
|
|
666
|
+
(y,),
|
|
667
|
+
(tangent,),
|
|
668
|
+
)
|
|
669
|
+
assert primal.shape == (2, 3, 12, 14)
|
|
670
|
+
torch.testing.assert_close(
|
|
671
|
+
tangent_out,
|
|
672
|
+
adjoint_linear_interpolation_nd(tangent, input_size=(12, 14), dims=(-2, -1)),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def test_adjoint_vmap(self) -> None:
|
|
676
|
+
"""Verify the adjoint works with vmap."""
|
|
677
|
+
y = torch.randn(5, 2, 3, 6, 7)
|
|
678
|
+
result = torch.func.vmap(lambda x: adjoint_linear_interpolation_nd(x, input_size=(12, 14), dims=(-2, -1)))(y)
|
|
679
|
+
assert result.shape == (5, 2, 3, 12, 14)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class TestNearestInterpolationNd:
|
|
683
|
+
"""Tests for nearest_interpolation_nd and adjoint_nearest_interpolation_nd."""
|
|
684
|
+
|
|
685
|
+
@pytest.mark.parametrize(
|
|
686
|
+
"shape, size, dims",
|
|
687
|
+
[
|
|
688
|
+
((2, 3, 16), (7,), (-1,)),
|
|
689
|
+
((2, 3, 16, 18), (7, 9), (-2, -1)),
|
|
690
|
+
((2, 3, 8, 10, 12), (4, 5, 6), (-3, -2, -1)),
|
|
691
|
+
],
|
|
692
|
+
)
|
|
693
|
+
def test_matches_torch_interpolate(
|
|
694
|
+
self,
|
|
695
|
+
shape: tuple[int, ...],
|
|
696
|
+
size: tuple[int, ...],
|
|
697
|
+
dims: tuple[int, ...],
|
|
698
|
+
) -> None:
|
|
699
|
+
"""Verify standard layouts match torch.nn.functional.interpolate."""
|
|
700
|
+
x = torch.randn(shape)
|
|
701
|
+
expected = torch.nn.functional.interpolate(x, size=size, mode="nearest")
|
|
702
|
+
result = nearest_interpolation_nd(x, size, dims=dims)
|
|
703
|
+
torch.testing.assert_close(result, expected)
|
|
704
|
+
|
|
705
|
+
def test_arbitrary_dims(self) -> None:
|
|
706
|
+
"""Verify interpolation works when spatial dimensions are not trailing."""
|
|
707
|
+
x = torch.randn(2, 12, 3, 14)
|
|
708
|
+
result = nearest_interpolation_nd(x, size=(6, 7), dims=(1, 3))
|
|
709
|
+
assert result.shape == (2, 6, 3, 7)
|
|
710
|
+
|
|
711
|
+
def test_complex(self) -> None:
|
|
712
|
+
"""Verify complex tensors are interpolated component-wise."""
|
|
713
|
+
x = torch.randn(2, 3, 12, 14, dtype=torch.complex64)
|
|
714
|
+
result = nearest_interpolation_nd(x, size=(6, 7), dims=(-2, -1))
|
|
715
|
+
expected = torch.complex(
|
|
716
|
+
nearest_interpolation_nd(x.real, size=(6, 7), dims=(-2, -1)),
|
|
717
|
+
nearest_interpolation_nd(x.imag, size=(6, 7), dims=(-2, -1)),
|
|
718
|
+
)
|
|
719
|
+
torch.testing.assert_close(result, expected)
|
|
720
|
+
|
|
721
|
+
def test_more_than_three_dims(self) -> None:
|
|
722
|
+
"""Verify interpolation with more than three dimensions uses multiple backend calls."""
|
|
723
|
+
x = torch.randn(2, 3, 4, 5, 6)
|
|
724
|
+
result = nearest_interpolation_nd(x, size=(3, 4, 5, 7), dims=(1, 2, 3, 4))
|
|
725
|
+
assert result.shape == (2, 3, 4, 5, 7)
|
|
726
|
+
|
|
727
|
+
@pytest.mark.parametrize(
|
|
728
|
+
"shape, size, dims, dtype",
|
|
729
|
+
[
|
|
730
|
+
((2, 3, 16), (7,), (-1,), torch.float32),
|
|
731
|
+
((2, 12, 3, 14), (6, 7), (1, 3), torch.float32),
|
|
732
|
+
((2, 3, 8, 10, 12), (4, 5, 6), (-3, -2, -1), torch.complex64),
|
|
733
|
+
((2, 3, 4, 5, 6), (4, 3, 4, 5), (1, 2, 3, 4), torch.float32),
|
|
734
|
+
],
|
|
735
|
+
)
|
|
736
|
+
def test_adjointness(
|
|
737
|
+
self,
|
|
738
|
+
shape: tuple[int, ...],
|
|
739
|
+
size: tuple[int, ...],
|
|
740
|
+
dims: tuple[int, ...],
|
|
741
|
+
dtype: torch.dtype,
|
|
742
|
+
) -> None:
|
|
743
|
+
"""Verify <Ax, y> == <x, A^H y>."""
|
|
744
|
+
x = torch.randn(shape, dtype=dtype)
|
|
745
|
+
y_shape = list(shape)
|
|
746
|
+
normalized_dims = tuple(dim % len(shape) for dim in dims)
|
|
747
|
+
for dim, target in zip(normalized_dims, size, strict=True):
|
|
748
|
+
y_shape[dim] = target
|
|
749
|
+
y = torch.randn(y_shape, dtype=dtype)
|
|
750
|
+
|
|
751
|
+
ax = nearest_interpolation_nd(x, size, dims=dims)
|
|
752
|
+
ahy = adjoint_nearest_interpolation_nd(y, [shape[dim] for dim in normalized_dims], dims=dims)
|
|
753
|
+
|
|
754
|
+
lhs = torch.vdot(ax.flatten(), y.flatten())
|
|
755
|
+
rhs = torch.vdot(x.flatten(), ahy.flatten())
|
|
756
|
+
torch.testing.assert_close(lhs, rhs, rtol=1e-4, atol=1e-5)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
2
|
-
|
|
3
|
-
from torchnd.functional import adjoint_pad_nd, conv_nd, pad_nd, pad_or_crop_to_size
|
|
4
|
-
from torchnd.modules import ConvNd, ConvTransposeNd
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"adjoint_pad_nd",
|
|
8
|
-
"conv_nd",
|
|
9
|
-
"pad_nd",
|
|
10
|
-
"pad_or_crop_to_size",
|
|
11
|
-
"ConvNd",
|
|
12
|
-
"ConvTransposeNd",
|
|
13
|
-
"__version__",
|
|
14
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|