Repository: kymatio/kymatio

Issue:
JTFS `meta['order']` contains an off-by-one path index
When doing JTFS with `format="joint"` it seems that there is an off-by-one index in `meta['order']`

```
import torch
from kymatio.torch import TimeFrequencyScattering

jtfs_kwargs = {'shape': 2**15, 'Q': (12, 1), 'J': 8, 'J_fr': 6, 'T': 2**6, "F": 1}
jtfs = TimeFrequencyScattering(**jtfs_kwargs)
Sx = jtfs(torch.zeros(2**15))
```

The above code produces an out size of `torch.Size([98, 64, 512])`, while there is a `98` in `.meta()['order']`.


For 3D aligned format, we skip order 0, for obvious reasons: https://github.com/kymatio/kymatio/blob/3f7d02518f111e7c127da8d97e65de5f1a492a81/kymatio/scattering1d/frontend/base_frontend.py#L643 

However, it seems we forgot to omit order 0 from meta in the case that `format=="joint" and out_type=="array"`: https://github.com/kymatio/kymatio/blob/3f7d02518f111e7c127da8d97e65de5f1a492a81/kymatio/scattering1d/frontend/base_frontend.py#L668

A simple condition to check the format and out_type fixes this:

```
 if len(path['n']) == 0:
        # If format='joint' and out_type='array' skip zeroth order
        if not (self.format == 'joint' and self.out_type == 'array'):
            # add order0 meta 
            ...
``` 
JTFS `frequency_averaging` produces IndexError
I have encountered an issue with frequency averaging in JTFS. I am attempting to average over one octave.

```
import torch
from kymatio.torch import TimeFrequencyScattering
jtfs_kwargs = {'shape': 2**15, 'T': 2**15, 'Q': (12, 1), 'J': 8, 'J_fr': 6, 'F': 12}
jtfs = TimeFrequencyScattering(**jtfs_kwargs, format="time")
jtfs(torch.randn(2**15))
```

Stack trace:
```
  File "/homes/cv300/venvs/dptm-wyn/lib/python3.8/site-packages/kymatio/scattering1d/core/timefrequency_scattering.py", line 445, in jtfs_average_and_format
    path = frequency_averaging(path, backend,
  File "/homes/cv300/venvs/dptm-wyn/lib/python3.8/site-packages/kymatio/scattering1d/core/timefrequency_scattering.py", line 378, in frequency_averaging
    S_c = backend.cdgmm(U_hat, phi_fr_f['levels'][k_in])
IndexError: list index out of range
```

Frequency averaging fails as the `phi_fr_f` is selected at a resolution that is out of bounds.

the input resolution is determined by `k_in = U_2['j_fr'][-1]`. somehow this `j_fr` is larger to the number of resolutions. In this case, `J_fr = 6`, `len(phi_fr_f['levels']) == 4` and the violating `j_fr = 4`

I do not know whether we have created an insufficient number of `phi_fr_f` resolutions. 

@lostanlen 


Discussion:

after an offline conversation with @cyrusvahidi , we have traced down the problem to line 376

https://github.com/kymatio/kymatio/blob/a175292e5cc8d8194f5fea86ed36116b66f72c31/kymatio/scattering1d/core/timefrequency_scattering.py#L370-L384

it should read as `k_in = min(U_2['j_fr'][-1], log2_F)`

Resolving patch:
diff --git a/kymatio/scattering1d/core/timefrequency_scattering.py b/kymatio/scattering1d/core/timefrequency_scattering.py
--- a/kymatio/scattering1d/core/timefrequency_scattering.py
+++ b/kymatio/scattering1d/core/timefrequency_scattering.py
@@ -373,7 +373,7 @@ def frequency_averaging(U_2, backend, phi_fr_f, oversampling_fr, average_fr):
             S_2_T = backend.average_global(U_2_T)
             n1_stride = U_2['n1_max']
         elif average_fr == 'local':
-            k_in = U_2['j_fr'][-1]
+            k_in = min(U_2['j_fr'][-1], log2_F)
             k_J = max(log2_F - k_in - oversampling_fr, 0)
             U_hat = backend.rfft(U_2_T)
             S_c = backend.cdgmm(U_hat, phi_fr_f['levels'][k_in])
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
@@ -666,10 +666,12 @@ def meta(self):
         meta = dict(key=[path['n'] for path in S], n=[], n_fr=[], order=[])
         for path in S:
             if len(path['n']) == 0:
-                # Zeroth order: no n1, no n_fr, no n2
-                meta['n'].append([np.nan, np.nan])
-                meta['n_fr'].append(np.nan)
-                meta['order'].append(0)
+                # If format='joint' and out_type='array' skip zeroth order
+                if not (self.format == 'joint' and self.out_type == 'array'):
+                    # Zeroth order: no n1, no n_fr, no n2
+                    meta['n'].append([np.nan, np.nan])
+                    meta['n_fr'].append(np.nan)
+                    meta['order'].append(0)
             else:
                 if len(path['n']) == 1:
                     # First order and format='joint': n=(n_fr,)


Test patch:
diff --git a/tests/scattering1d/test_timefrequency_scattering.py b/tests/scattering1d/test_timefrequency_scattering.py
--- a/tests/scattering1d/test_timefrequency_scattering.py
+++ b/tests/scattering1d/test_timefrequency_scattering.py
@@ -548,6 +548,10 @@ def test_jtfs_numpy_and_sklearn(frontend):
     Sx = S(x)
     assert Sx.ndim == 3
 
+    # format='joint' meta()
+    meta = S.meta()
+    assert len(meta["order"]) == Sx.shape[-3]
+
     # format='time' with global averaging
     S = TimeFrequencyScatteringNumPy(T="global", F=0, format="time", **kwargs)
     Sx = S(x)
@@ -586,4 +590,33 @@ def test_jtfs_torch_tf_frontends(frontend):
     assert S.F == (2**S.J_fr)
     Sx = S(x)
     assert isinstance(Sx, torch.Tensor) if frontend == "torch" else isinstance(Sx, tf.Tensor)
-    assert Sx.ndim == 3
\ No newline at end of file
+    assert Sx.ndim == 3
+
+
+frontends = ["torch", "tensorflow"]
+@pytest.mark.parametrize("frontend", frontends)
+def test_F(frontend):
+    """
+    Applies scattering on a stored signal with non-default F
+    """
+    kwargs = {"J": 8, "J_fr": 3, "shape": (8192,), "Q": 3}
+    x = torch.zeros(kwargs["shape"])
+    x[kwargs["shape"][0] // 2] = 1
+
+    # default F
+    jtfs0 = TimeFrequencyScattering(frontend="torch", format="joint", **kwargs)
+
+    # explicit F
+    jtfs1 = TimeFrequencyScattering(frontend="torch", format="joint", F=2**kwargs['J_fr'], **kwargs)
+
+    Sx0 = jtfs0(x)
+    Sx1 = jtfs1(x)
+    assert torch.equal(Sx0, Sx1)
+    assert Sx0.shape == Sx1.shape
+
+    """ non-default F smaller than 2**J_fr. This checks that the subsampling
+        index does not exceed log2_F and cause an IndexError at runtime (#976).
+    """
+    jtfs2 = TimeFrequencyScattering(frontend="torch", format="joint", F=2**kwargs['J_fr'] // 4, **kwargs)
+    Sx2 = jtfs2(x)
+    assert Sx0.shape[0] == Sx2.shape[0] and Sx0.shape[2] == Sx2.shape[2] and Sx0.shape[1] < Sx2.shape[1]
\ No newline at end of file