Repository: kymatio/kymatio

Issue:
Padding size problem
I was trying the Scattering2D on my data and it did not work. I had this error message: 
>RuntimeError: The filters are not compatible for multiplication!

While debugging, I found out that the padding does not work well with some input shapes. Here is a minimal example:

``` python
import torch
r = torch.randn(1, 1, 45, 45)

from kymatio import Scattering2D
scatterer = Scattering2D(J=3, shape=tuple(r.size()[2:]))

from kymatio.scattering2d.backend import fft
print(scatterer.Phi[0].size()) # size -> torch.Size([56, 56, 2])
print(fft(scatterer.pad(r), 'C2C').size()) # this is the 'U_0_c' size -> torch.Size([1, 1, 61, 61, 2])
print(scatterer.forward(r))
```

I have tried other matrix shapes and it seems that the `U_0_c` size, in axis `d=2,3`, is equal to the size of `Phi[0]`, in axis `d-2`, plus the rest of the Euclidian division of the original size, in axis `d-2`, on `2^J`. This causes the later uncatched exception.

Was this unintended? or was it the contrary (which implies that I have to choose a good `J` value)?

If it is the last case, how to choose `J` when I have no guaranty over the input sizes? 


Discussion:
Thanks for pointing this out. My bad. The padding process has some issues for a subset of input size.

Duplicate of #284 

Resolving patch:
diff --git a/kymatio/scattering2d/backend/backend_skcuda.py b/kymatio/scattering2d/backend/backend_skcuda.py
--- a/kymatio/scattering2d/backend/backend_skcuda.py
+++ b/kymatio/scattering2d/backend/backend_skcuda.py
@@ -1,6 +1,6 @@
 # Authors: Edouard Oyallon, Sergey Zagoruyko
 
-from collections import defaultdict, namedtuple
+from collections import namedtuple
 import torch
 from skcuda import cublas
 import cupy
@@ -38,22 +38,24 @@ def __init__(self, pad_size, pre_pad=False):
 
             Parameters
             ----------
-            pad_size : int
+            pad_size : list of 4 integers
                 size of padding to apply.
             pre_pad : boolean
                 if set to true, then there is no padding, one simply adds the imaginarty part.
         """
         self.pre_pad = pre_pad
-        self.padding_module = ReflectionPad2d(pad_size)
+        self.pad_size = pad_size
 
-    def __call__(self, input):
-        if(self.pre_pad):
-            output = input.new_zeros(input.size(0), input.size(1), input.size(2), input.size(3), 2)
-            output.narrow(output.ndimension()-1, 0, 1)[:] = input
-        else:
-            out_ = self.padding_module(input)
-            output = input.new_zeros(*(out_.size() + (2,)))
-            output.select(4, 0)[:] = out_
+        self.build()
+
+    def build(self):
+        self.padding_module = ReflectionPad2d(self.pad_size)
+
+    def __call__(self, x):
+        if not self.pre_pad:
+            x = self.padding_module(x)
+        output = x.new_zeros(x.shape + (2,))
+        output[...,0] = x
         return output
 
 def unpad(in_):
diff --git a/kymatio/scattering2d/backend/backend_torch.py b/kymatio/scattering2d/backend/backend_torch.py
--- a/kymatio/scattering2d/backend/backend_torch.py
+++ b/kymatio/scattering2d/backend/backend_torch.py
@@ -18,22 +18,24 @@ def __init__(self, pad_size, pre_pad=False):
 
             Parameters
             ----------
-            pad_size : int
+            pad_size : list of 4 integers
                 size of padding to apply.
             pre_pad : boolean
                 if set to true, then there is no padding, one simply adds the imaginarty part.
         """
         self.pre_pad = pre_pad
-        self.padding_module = ReflectionPad2d(pad_size)
+        self.pad_size = pad_size
 
-    def __call__(self, input):
-        if(self.pre_pad):
-            output = input.new_zeros(input.size(0), input.size(1), input.size(2), input.size(3), 2)
-            output.narrow(output.ndimension()-1, 0, 1)[:] = input
-        else:
-            out_ = self.padding_module(input)
-            output = input.new_zeros(*(out_.size() + (2,)))
-            output.select(4, 0)[:] = out_
+        self.build()
+
+    def build(self):
+        self.padding_module = ReflectionPad2d(self.pad_size)
+
+    def __call__(self, x):
+        if not self.pre_pad:
+            x = self.padding_module(x)
+        output = x.new_zeros(x.shape + (2,))
+        output[...,0] = x
         return output
 
 def unpad(in_):
diff --git a/kymatio/scattering2d/scattering2d.py b/kymatio/scattering2d/scattering2d.py
--- a/kymatio/scattering2d/scattering2d.py
+++ b/kymatio/scattering2d/scattering2d.py
@@ -98,10 +98,11 @@ def __init__(self, J, shape, L=8, max_order=2, pre_pad=False):
     def build(self):
         self.M, self.N = self.shape
         self.modulus = Modulus()
-        self.pad = Pad(2**self.J, pre_pad = self.pre_pad)
+        self.M_padded, self.N_padded = compute_padding(self.M, self.N, self.J)
+        # pads equally on a given side if the amount of padding to add is an even number of pixels, otherwise it adds an extra pixel
+        self.pad = Pad([(self.N_padded - self.N) // 2, (self.N_padded - self.N+1) // 2, (self.M_padded - self.M) // 2, (self.M_padded - self.M + 1) // 2], pre_pad=self.pre_pad)
         self.subsample_fourier = SubsampleFourier()
         # Create the filters
-        self.M_padded, self.N_padded = compute_padding(self.M, self.N, self.J)
         filters = filter_bank(self.M_padded, self.N_padded, self.J, self.L)
         self.Psi = filters['psi']
         self.Phi = [filters['phi'][j] for j in range(self.J)]


Test patch:
diff --git a/kymatio/scattering2d/tests/test_scattering2d.py b/kymatio/scattering2d/tests/test_scattering2d.py
--- a/kymatio/scattering2d/tests/test_scattering2d.py
+++ b/kymatio/scattering2d/tests/test_scattering2d.py
@@ -292,3 +292,24 @@ def test_scattering2d_errors():
     with pytest.raises(RuntimeError) as record:
         S(x)
     assert('Padded tensor must be of spatial size' in record.value.args[0])
+
+# Check that several input size works
+def test_input_size_agnostic():
+    for N in [31,32,33]:
+        for J in [2,4]:
+            scattering = Scattering2D(J, shape=(N, N))
+            x = torch.zeros(3,3,N,N)
+
+            if backend.NAME == 'skcuda':
+                x = x.cuda()
+                scattering.cuda()
+
+            S = scattering(x)
+            scattering = Scattering2D(J, shape=(N, N), pre_pad=True)
+            x = torch.zeros(3,3,scattering.M_padded, scattering.N_padded)
+
+            if backend.NAME == 'skcuda':
+                x = x.cuda()
+                scattering.cuda()
+
+            S = scattering(x)