Repository: kymatio/kymatio

Issue:
Default arguments for 3D constructor
Currently, neither `L` or `sigma0` have default arguments, which is different from 1D and 2D. For `sigma0`, it makes sense to have a default value of `1`. For `L`, a popular value seems to be `3`. Do we want to fix these as defaults?
Remove kwargs to `forward` in `Scattering3D`
For consistency with 1D and 2D, we should have the keyword arguments be set as attributes instead of arguments to `forward`. Like in 1D, we can have these set in the constructor, if supplied.


Discussion:
yes
On it.
Yes, please
Ok. Will do.

Resolving patch:
diff --git a/examples/3d/compute_speed.py b/examples/3d/compute_speed.py
--- a/examples/3d/compute_speed.py
+++ b/examples/3d/compute_speed.py
@@ -101,14 +101,17 @@
     else:
         x = x.cpu()
 
-    scattering.forward(x, method='integral', integral_powers=integral_powers)
+    scattering.method = 'integral'
+    scattering.integral_powers = integral_powers
+
+    scattering.forward(x)
 
     if device == 'gpu':
         torch.cuda.synchronize()
 
     t_start = time.time()
     for _ in range(times):
-        scattering.forward(x, method='integral', integral_powers=integral_powers)
+        scattering.forward(x)
 
     if device == 'gpu':
         torch.cuda.synchronize()
diff --git a/examples/3d/scattering3d_qm7.py b/examples/3d/scattering3d_qm7.py
--- a/examples/3d/scattering3d_qm7.py
+++ b/examples/3d/scattering3d_qm7.py
@@ -206,22 +206,19 @@ def compute_qm7_solid_harmonic_scattering_coefficients(
         full_density_batch = generate_weighted_sum_of_gaussians(
                 grid, pos_batch, full_batch, sigma, cuda=cuda)
         full_order_0 = compute_integrals(full_density_batch, integral_powers)
-        full_scattering = scattering(
-                full_density_batch, max_order=2, method='integral',
-                integral_powers=integral_powers)
+        scattering.max_order = 2
+        scattering.method = 'integral'
+        scattering.integral_powers = integral_powers
+        full_scattering = scattering(full_density_batch)
 
         val_density_batch = generate_weighted_sum_of_gaussians(
                 grid, pos_batch, val_batch, sigma, cuda=cuda)
         val_order_0 = compute_integrals(val_density_batch, integral_powers)
-        val_scattering= scattering(
-                val_density_batch, max_order=2, method='integral',
-                integral_powers=integral_powers)
+        val_scattering= scattering(val_density_batch)
 
         core_density_batch = full_density_batch - val_density_batch
         core_order_0 = compute_integrals(core_density_batch, integral_powers)
-        core_scattering = scattering(
-                core_density_batch, max_order=2, method='integral',
-                integral_powers=integral_powers)
+        core_scattering = scattering(core_density_batch)
 
 
         order_0.append(
diff --git a/kymatio/scattering3d/scattering3d.py b/kymatio/scattering3d/scattering3d.py
--- a/kymatio/scattering3d/scattering3d.py
+++ b/kymatio/scattering3d/scattering3d.py
@@ -24,11 +24,38 @@ class Scattering3D(object):
         number of scales
     shape: tuple of int
         shape (M, N, O) of the input signal
-    L: int
-        number of l values
+    L: int, optional
+        Number of l values. Defaults to 3.
+    sigma_0: float, optional
+        Bandwidth of mother wavelet. Defaults to 1.
+    max_order: int, optional
+        The maximum order of scattering coefficients to compute. Must be
+        either 1 or 2. Defaults to 2.
+    rotation_covariant: bool, optional
+        if set to True the first order moduli take the form:
+
+        $\sqrt{\sum_m (x \star \psi_{j,l,m})^2)}$
+
+        if set to False the first order moduli take the form:
+
+        $x \star \psi_{j,l,m}$
+
+        The second order moduli change analogously
+        Defaut: True
+    method: string, optional
+        specifies the method for obtaining scattering coefficients
+        ("standard","local","integral"). Default: "standard"
+    points: array-like, optional
+        List of locations in which to sample wavelet moduli. Used when
+        method == 'local'
+    integral_powers: array-like
+        List of exponents to the power of which moduli are raised before
+        integration. Used with method == 'standard', method == 'integral'
 
     """
-    def __init__(self, J, shape, L, sigma_0):
+    def __init__(self, J, shape, L=3, sigma_0=1, max_order=2,
+                 rotation_covariant=True, method='standard', points=None,
+                 integral_powers=(0.5, 1., 2.)):
         super(Scattering3D, self).__init__()
         self.J = J
         self.shape = shape
@@ -37,6 +64,12 @@ def __init__(self, J, shape, L, sigma_0):
 
         self.is_cuda = False
 
+        self.max_order = max_order
+        self.rotation_covariant = rotation_covariant
+        self.method = method
+        self.points = points
+        self.integral_powers = integral_powers
+
         self.build()
 
     def build(self):
@@ -282,8 +315,7 @@ def _check_input(self, input_array):
         if (input_array.dim() != 4):
             raise (RuntimeError('Input tensor must be 4D'))
 
-    def forward(self, input_array, max_order=2, rotation_covariant=True,
-                method='standard', points=None, integral_powers=(.5, 1., 2.)):
+    def forward(self, input_array):
         """
         The forward pass of 3D solid harmonic scattering
 
@@ -291,30 +323,6 @@ def forward(self, input_array, max_order=2, rotation_covariant=True,
         ----------
         input_array: torch tensor 
             input of size (batchsize, M, N, O)
-        max_order: int, optional
-            The maximum order of scattering coefficients to compute. Must be
-            either 1 or 2. Defaults to 2.
-        rotation_covariant: bool, optional
-            if set to True the first order moduli take the form:
-
-            $\\sqrt{\\sum_m (x \\star \\psi_{j,l,m})^2)}$
-
-            if set to False the first order moduli take the form:
-
-            $x \\star \\psi_{j,l,m}$
-
-            The second order moduli change analogously
-            Defaut: True
-        method: string, optional
-            specifies the method for obtaining scattering coefficients
-            ("standard","local","integral"). Default: "standard"
-        points: array-like, optional
-            List of locations in which to sample wavelet moduli. Used when
-            method == 'local'
-
-        integral_powers: array-like
-            List of exponents to the power of which moduli are raised before
-            integration. Used with method == 'standard', method == 'integral'
 
         Returns
         -------
@@ -326,7 +334,7 @@ def forward(self, input_array, max_order=2, rotation_covariant=True,
             concatenated along the feature axis
         """
         self._check_input(input_array)
-        if rotation_covariant:
+        if self.rotation_covariant:
             convolution_and_modulus = (
                 self._rotation_covariant_convolution_and_modulus)
         else:
@@ -338,25 +346,26 @@ def forward(self, input_array, max_order=2, rotation_covariant=True,
         s_order_2 = []
         _input = to_complex(input_array)
 
-        method_args = dict(points=points, integral_powers=integral_powers)
+        method_args = dict(points=self.points,
+                           integral_powers=self.integral_powers)
 
         for l in range(self.L+1):
             s_order_1_l, s_order_2_l = [], []
             for j_1 in range(self.J+1):
                 conv_modulus = convolution_and_modulus(_input, l, j_1)
                 s_order_1_l.append(compute_scattering_coefs(
-                    conv_modulus, method, method_args, j_1))
-                if max_order == 1:
+                    conv_modulus, self.method, method_args, j_1))
+                if self.max_order == 1:
                     continue
                 for j_2 in range(j_1+1, self.J+1):
                     conv_modulus_2 = convolution_and_modulus(
                         conv_modulus, l, j_2)
                     s_order_2_l.append(compute_scattering_coefs(
-                        conv_modulus_2, method, method_args, j_2))
+                        conv_modulus_2, self.method, method_args, j_2))
             s_order_1.append(torch.stack([arr[..., 0] for arr in s_order_1_l], 1))
-            if max_order == 2:
+            if self.max_order == 2:
                 s_order_2.append(torch.stack([arr[..., 0] for arr in s_order_2_l], 1))
-        if max_order == 2:
+        if self.max_order == 2:
             return torch.cat(
                 [torch.stack(s_order_1, dim=2),
                 torch.stack(s_order_2, dim=2)], 1)


Test patch:
diff --git a/kymatio/scattering3d/tests/test_scattering3d.py b/kymatio/scattering3d/tests/test_scattering3d.py
--- a/kymatio/scattering3d/tests/test_scattering3d.py
+++ b/kymatio/scattering3d/tests/test_scattering3d.py
@@ -166,9 +166,10 @@ def test_against_standard_computations():
             x = x.cuda()
             scattering.cuda()
         order_0 = compute_integrals(x, integral_powers)
-        orders_1_and_2 = scattering(
-            x, max_order=2, method='integral',
-            integral_powers=integral_powers)
+        scattering.max_order = 2
+        scattering.method = 'integral'
+        scattering.integral_powers = integral_powers
+        orders_1_and_2 = scattering(x)
 
         # WARNING: These are hard-coded values for the setting J = 2.
         n_order_1 = 3
@@ -220,6 +221,10 @@ def test_solid_harmonic_scattering():
     x = generate_weighted_sum_of_gaussians(grid, centers, weights, sigma_gaussian)
     scattering = Scattering3D(J=J, shape=(M, N, O), L=L, sigma_0=sigma_0_wavelet)
 
+    scattering.max_order = 1
+    scattering.method = 'integral'
+    scattering.integral_powers = [1]
+
     for device in devices:
         if device == 'cpu':
             x = x.cpu()
@@ -228,7 +233,7 @@ def test_solid_harmonic_scattering():
             x = x.cuda()
             scattering.cuda()
 
-        s = scattering(x, max_order=1, method='integral', integral_powers=[1])
+        s = scattering(x)
 
         for j in range(J+1):
             sigma_wavelet = sigma_0_wavelet*2**j
@@ -249,7 +254,8 @@ def test_larger_scales():
         if not 'cpu' in devices:
             x = x.cuda()
             scattering.cuda()
-        Sx = scattering(x, method='integral')
+        scattering.method = 'integral'
+        Sx = scattering(x)
 
 def test_scattering_methods():
     shape = (32, 32, 32)
@@ -264,14 +270,19 @@ def test_scattering_methods():
         x = x.cuda()
         scattering.cuda()
 
-    Sx = scattering(x, method='standard')
-    Sx = scattering(x, method='standard', rotation_covariant=False)
+    scattering.method = 'standard'
+    Sx = scattering(x)
+    scattering.rotation_covariant = False
+    Sx = scattering(x)
 
     points = torch.zeros(1, 1, 3)
     points[0,0,:] = torch.tensor(shape)/2
 
-    Sx = scattering(x, method='local', points=points)
-    Sx = scattering(x, method='local', points=points, rotation_covariant=False)
+    scattering.method = 'local'
+    scattering.points = points
+    Sx = scattering(x)
+    scattering.rotation_covariant = False
+    Sx = scattering(x)
 
 def test_cpu_cuda():
     shape = (32, 32, 32)