Repository: kymatio/kymatio

Issue:
`calibrate_scattering_filters` is unnecessary
`calibrate_scattering_filters` is probably my least favorite function from `filter_bank.py`. It it called only ~once~ thrice, and has no units test. It has six inputs, seven (!) outputs, and only five lines of code. (and 50 lines of docs!)

In the spirit of #513, i suggest we remove it and have  `scattering_filter_factory` call `compute_filter_params` directly.


Discussion:

Resolving patch:
diff --git a/kymatio/scattering1d/filter_bank.py b/kymatio/scattering1d/filter_bank.py
--- a/kymatio/scattering1d/filter_bank.py
+++ b/kymatio/scattering1d/filter_bank.py
@@ -487,74 +487,6 @@ def compute_params_filterbank(sigma_min, Q, alpha, r_psi=math.sqrt(0.5)):
     return xi, sigma, j
 
 
-def calibrate_scattering_filters(J, Q, T, alpha, r_psi=math.sqrt(0.5), sigma0=0.1):
-    """
-    Calibrates the parameters of the filters used at the 1st and 2nd orders
-    of the scattering transform.
-
-    These filterbanks share the same low-pass filterbank, but use a
-    different Q: Q_1 = Q and Q_2 = 1.
-
-    The dictionaries for the band-pass filters have keys which are 2-tuples
-    of the type (j, n), where n is an integer >=0 counting the filters (for
-    identification purposes) and j is an integer >= 0 denoting the maximal
-    subsampling 2**j which can be performed on a signal convolved with this
-    filter without aliasing.
-
-    Parameters
-    ----------
-    J : int
-        maximal scale of the scattering (controls the number of wavelets)
-    Q : int
-        number of wavelets per octave for the first order
-    T : int
-        temporal support of low-pass filter, controlling amount of imposed
-        time-shift invariance and maximum subsampling
-    alpha : float, optional
-        tolerance factor for the aliasing after subsampling.
-        The larger alpha, the more conservative the value of maximal
-        subsampling is.
-    r_psi : float, optional
-        Should be >0 and <1. Controls the redundancy of the filters
-        (the larger r_psi, the larger the overlap between adjacent wavelets).
-        Defaults to sqrt(0.5)
-    sigma0 : float, optional
-        frequential width of the low-pass filter at scale J=0
-        (the subsequent widths are defined by sigma_J = sigma0 / 2^J).
-        Defaults to 1e-1
-
-    Returns
-    -------
-    sigma_low : float
-        frequential width of the low-pass filter
-    xi1 : dictionary
-        dictionary containing the center frequencies of the first order
-        filters. See above for a decsription of the keys.
-    sigma1 : dictionary
-        dictionary containing the frequential width of the first order
-        filters. See above for a description of the keys.
-    xi2 : dictionary
-        dictionary containing the center frequencies of the second order
-        filters. See above for a decsription of the keys.
-    sigma2 : dictionary
-        dictionary containing the frequential width of the second order
-        filters. See above for a description of the keys.
-    """
-    if Q < 1:
-        raise ValueError('Q should always be >= 1, got {}'.format(Q))
-
-    # lower bound of band-pass filter frequential widths:
-    # for default T = 2**(J), this coincides with sigma_low
-    sigma_min = sigma0 / math.pow(2, J)
-
-    xi1, sigma1, j1 = compute_params_filterbank(sigma_min, Q, alpha=alpha, r_psi=r_psi)
-    xi2, sigma2, j2 = compute_params_filterbank(sigma_min, 1, alpha=alpha, r_psi=r_psi)
-
-    # width of the low-pass filter
-    sigma_low = sigma0 / T
-    return sigma_low, xi1, sigma1, j1, xi2, sigma2, j2
-
-
 def scattering_filter_factory(J_support, J_scattering, Q, T, r_psi=math.sqrt(0.5),
                               normalize='l1',
                               max_subsampling=None, sigma0=0.1, alpha=5.,
@@ -648,8 +580,12 @@ def scattering_filter_factory(J_support, J_scattering, Q, T, r_psi=math.sqrt(0.5
     https://tel.archives-ouvertes.fr/tel-01559667
     """
     # compute the spectral parameters of the filters
-    sigma_low, xi1, sigma1, j1s, xi2, sigma2, j2s = calibrate_scattering_filters(J_scattering, Q, T, alpha=alpha,
-                                                                                 r_psi=r_psi, sigma0=sigma0)
+    sigma_min = sigma0 / math.pow(2, J_scattering)
+    xi1, sigma1, j1s = compute_params_filterbank(sigma_min, Q, alpha, r_psi)
+    xi2, sigma2, j2s = compute_params_filterbank(sigma_min, 1, alpha, r_psi)
+
+    # width of the low-pass filter
+    sigma_low = sigma0 / T
 
     # instantiate the dictionaries which will contain the filters
     phi_f = {}
diff --git a/kymatio/scattering1d/frontend/base_frontend.py b/kymatio/scattering1d/frontend/base_frontend.py
--- a/kymatio/scattering1d/frontend/base_frontend.py
+++ b/kymatio/scattering1d/frontend/base_frontend.py
@@ -40,6 +40,10 @@ def build(self):
         self.eps = 1e-7
         self.normalize = 'l1'
 
+        # check the number of filters per octave
+        if self.Q < 1:
+            raise ValueError('Q should always be >= 1, got {}'.format(self.Q))
+
         # check the shape
         if isinstance(self.shape, numbers.Integral):
             self.N = self.shape
@@ -95,7 +99,8 @@ def meta(self):
         meta : dictionary
             See the documentation for `compute_meta_scattering()`.
         """
-        return compute_meta_scattering(self.J, self.Q, self.T, max_order=self.max_order)
+        return compute_meta_scattering(
+            self.J, self.Q, self.T, self.max_order, self.r_psi, self.sigma0, self.alpha)
 
     def output_size(self, detail=False):
         """Get size of the scattering transform
@@ -114,9 +119,8 @@ def output_size(self, detail=False):
         size : int or tuple
             See the documentation for `precompute_size_scattering()`.
         """
-
-        return precompute_size_scattering(
-            self.J, self.Q, self.T, max_order=self.max_order, detail=detail)
+        return precompute_size_scattering(self.J, self.Q, self.T,
+            self.max_order, self.r_psi, self.sigma0, self.alpha, detail=detail)
 
     _doc_shape = 'N'
 
diff --git a/kymatio/scattering1d/utils.py b/kymatio/scattering1d/utils.py
--- a/kymatio/scattering1d/utils.py
+++ b/kymatio/scattering1d/utils.py
@@ -1,6 +1,6 @@
 import numpy as np
 import math
-from .filter_bank import calibrate_scattering_filters
+from .filter_bank import scattering_filter_factory, compute_params_filterbank
 
 def compute_border_indices(log2_T, J, i0, i1):
     """
@@ -70,7 +70,8 @@ def compute_padding(J_pad, N):
     return pad_left, pad_right
 
 
-def precompute_size_scattering(J, Q, T, max_order=2, detail=False):
+def precompute_size_scattering(
+        J, Q, T, max_order, r_psi, sigma0, alpha, detail=False):
     """Get size of the scattering transform
 
     The number of scattering coefficients depends on the filter
@@ -88,9 +89,20 @@ def precompute_size_scattering(J, Q, T, max_order=2, detail=False):
     T : int
         temporal support of low-pass filter, controlling amount of imposed
         time-shift invariance and maximum subsampling
-    max_order : int, optional
+    max_order : int
         The maximum order of scattering coefficients to compute.
-        Must be either equal to `1` or `2`. Defaults to `2`.
+        Must be either equal to `1` or `2`.
+    r_psi : float, optional
+        Should be >0 and <1. Controls the redundancy of the filters
+        (the larger r_psi, the larger the overlap between adjacent wavelets).
+    sigma0 : float
+        parameter controlling the frequential width of the
+        low-pass filter at J_scattering=0; at a an absolute J_scattering, it
+        is equal to sigma0 / 2**J_scattering.
+    alpha : float, optional
+        tolerance factor for the aliasing after subsampling.
+        The larger alpha, the more conservative the value of maximal
+        subsampling is.
     detail : boolean, optional
         Specifies whether to provide a detailed size (number of coefficient
         per order) or an aggregate size (total number of coefficients).
@@ -102,15 +114,16 @@ def precompute_size_scattering(J, Q, T, max_order=2, detail=False):
         integer. If `True`, returns a tuple of size `max_order` containing
         the number of coefficients in each order.
     """
-    sigma_low, xi1, sigma1, j1, xi2, sigma2, j2 = \
-        calibrate_scattering_filters(J, Q, T, alpha=5.)
+    sigma_min = sigma0 / math.pow(2, J)
+    xi1s, sigma1s, j1s = compute_params_filterbank(sigma_min, Q, alpha, r_psi)
+    xi2s, sigma2s, j2s = compute_params_filterbank(sigma_min, 1, alpha, r_psi)
 
     size_order0 = 1
-    size_order1 = len(xi1)
+    size_order1 = len(xi1s)
     size_order2 = 0
-    for n1 in range(len(xi1)):
-        for n2 in range(len(xi2)):
-            if j2[n2] > j1[n1]:
+    for n1 in range(len(xi1s)):
+        for n2 in range(len(xi2s)):
+            if j2s[n2] > j1s[n1]:
                 size_order2 += 1
     if detail:
         if max_order == 2:
@@ -124,7 +137,7 @@ def precompute_size_scattering(J, Q, T, max_order=2, detail=False):
             return size_order0 + size_order1
 
 
-def compute_meta_scattering(J, Q, T, max_order=2):
+def compute_meta_scattering(J, Q, T, max_order, r_psi, sigma0, alpha):
     """Get metadata on the transform.
 
     This information specifies the content of each scattering coefficient,
@@ -141,10 +154,20 @@ def compute_meta_scattering(J, Q, T, max_order=2):
     T : int
         temporal support of low-pass filter, controlling amount of imposed
         time-shift invariance and maximum subsampling
-
-    max_order : int, optional
+    max_order : int
         The maximum order of scattering coefficients to compute.
-        Must be either equal to `1` or `2`. Defaults to `2`.
+        Must be either equal to `1` or `2`.
+    r_psi : float, optional
+        Should be >0 and <1. Controls the redundancy of the filters
+        (the larger r_psi, the larger the overlap between adjacent wavelets).
+    sigma0 : float
+        parameter controlling the frequential width of the
+        low-pass filter at J_scattering=0; at a an absolute J_scattering, it
+        is equal to sigma0 / 2**J_scattering.
+    alpha : float, optional
+        tolerance factor for the aliasing after subsampling.
+        The larger alpha, the more conservative the value of maximal
+        subsampling is.
 
     Returns
     -------
@@ -170,8 +193,9 @@ def compute_meta_scattering(J, Q, T, max_order=2):
             The tuples indexing the corresponding scattering coefficient
             in the non-vectorized output.
     """
-    sigma_low, xi1s, sigma1s, j1s, xi2s, sigma2s, j2s = \
-        calibrate_scattering_filters(J, Q, T, alpha=5.)
+    sigma_min = sigma0 / math.pow(2, J)
+    xi1s, sigma1s, j1s = compute_params_filterbank(sigma_min, Q, alpha, r_psi)
+    xi2s, sigma2s, j2s = compute_params_filterbank(sigma_min, 1, alpha, r_psi)
 
     meta = {}
 


Test patch:
diff --git a/tests/scattering1d/test_filters_scattering1d.py b/tests/scattering1d/test_filters_scattering1d.py
--- a/tests/scattering1d/test_filters_scattering1d.py
+++ b/tests/scattering1d/test_filters_scattering1d.py
@@ -2,7 +2,7 @@
 Testing all functions in filters_bank
 """
 from kymatio.scattering1d.filter_bank import (adaptive_choice_P, periodize_filter_fourier, get_normalizing_factor,
-    compute_sigma_psi, compute_temporal_support, compute_xi_max, morlet_1d, calibrate_scattering_filters,
+    compute_sigma_psi, compute_temporal_support, compute_xi_max, morlet_1d,
     get_max_dyadic_subsampling, gauss_1d)
 import numpy as np
 import math
@@ -156,36 +156,6 @@ def test_gauss_1d():
     assert "should be non-negative" in ve.value.args[0]
 
 
-def test_calibrate_scattering_filters():
-    """
-    Various tests on the central frequencies xi and spectral width sigma
-    computed for the scattering filterbank
-    - Checks that all widths are > 0
-    - Check that sigma_low is smaller than all sigma2
-    """
-    J_range = np.arange(2, 11)
-    Q_range = np.arange(1, 21, dtype=int)
-    for J in J_range:
-        for Q in Q_range:
-            sigma_low, xi1, sigma1, j1, xi2, sigma2, j2 = \
-                calibrate_scattering_filters(J, Q, 2 ** J, alpha=5.)
-            # Check that all sigmas are > 0
-            assert sigma_low > 0
-            for sig in sigma1:
-                assert sig > 0
-            for sig in sigma2:
-                assert sig > 0
-            # check that sigma_low is smaller than all sigma2
-            for sig in sigma1:
-                assert sig >= sigma_low
-            for sig in sigma2:
-                assert sig >= sigma_low
-
-    with pytest.raises(ValueError) as ve:
-        calibrate_scattering_filters(J_range[0], 0.9, 2 ** J_range[0], alpha=5.)
-    assert "should always be >= 1" in ve.value.args[0]
-
-
 def test_compute_xi_max():
     """
     Tests that 0.25 <= xi_max(Q) <= 0.5, whatever Q
diff --git a/tests/scattering1d/test_torch_scattering1d.py b/tests/scattering1d/test_torch_scattering1d.py
--- a/tests/scattering1d/test_torch_scattering1d.py
+++ b/tests/scattering1d/test_torch_scattering1d.py
@@ -428,3 +428,15 @@ def test_T(device, backend):
     Sg1 = scattering1(x)
     assert torch.allclose(Sg0, Sx0)
     assert Sg1.shape == (Sg0.shape[0], Sg0.shape[1], Sg0.shape[2]*2**(sigma_low_scale_factor))
+
+@pytest.mark.parametrize("device", devices)
+@pytest.mark.parametrize("backend", backends)
+def test_Q(device, backend):
+    J = 3
+    length = 1024
+    shape = (length,)
+
+    with pytest.raises(ValueError) as ve:
+        _ = Scattering1D(
+            J, shape, Q=0.9, backend=backend, frontend='torch').to(device)
+    assert "Q should always be >= 1" in ve.value.args[0]