Repository: GPflow/GPflow

Issue:
A multioutput Kuf function throws an error
<!-- Lines like this are comments and will be invisible -->

# Bug

<!-- A clear and concise description of what the bug is. -->

The multioutput covariance function `Kuf` has several functions corresponding to different combinations of parameters. The function which accepts `FallbackSeparateIndependentInducingVariables` or `FallbackSharedIndependentInducingVariables` along with the `LinearCoregionalization` kernel throws an error.

## To reproduce

**Minimal, reproducible example**
<!-- We need to be able to reproduce the bug by simply copy and pasting your code -->
```python
import numpy as np
import gpflow
import gpflow.inducing_variables.multioutput as mf
import gpflow.kernels.multioutput as mk
from gpflow.covariances.multioutput import kufs as mo_kufs

inducing_variables = mf.FallbackSharedIndependentInducingVariables(
    gpflow.inducing_variables.InducingPoints(np.random.rand(1, 1)))
kernel = mk.LinearCoregionalization(make_kernels(Datum.L), Datum.W)
Kuf = mo_kufs.Kuf(inducing_variables, kernel, Datum.Xnew)
```

**Stack trace, or error message**
```
    @Kuf.register(
        (FallbackSeparateIndependentInducingVariables, FallbackSharedIndependentInducingVariables),
        LinearCoregionalization,
        object,
    )
    def _Kuf(
        inducing_variable: Union[
            SeparateIndependentInducingVariables, SharedIndependentInducingVariables
        ],
        kernel: LinearCoregionalization,
        Xnew: tf.Tensor,
    ):
        kuf_impl = Kuf.dispatch(type(inducing_variable), SeparateIndependent, object)
>       K = tf.transpose(kuf_impl(inducing_variable, kernel, Xnew), [1, 0, 2])  # [M, L, N]
E       TypeError: 'NoneType' object is not callable

../../../gpflow/covariances/multioutput/kufs.py:96: TypeError

```

## Expected behavior

The covariance should be computed.

## System information

* GPflow version: 2.1.4
* GPflow installed from: git commit 405eb97dd30cb43f02501b58b581f2608eb6e43e
* TensorFlow version: 2.3.0
* Python version: 3.7.5
* Operating system: Ubuntu 18.04.5 LTS

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:

Resolving patch:
diff --git a/gpflow/covariances/multioutput/kufs.py b/gpflow/covariances/multioutput/kufs.py
--- a/gpflow/covariances/multioutput/kufs.py
+++ b/gpflow/covariances/multioutput/kufs.py
@@ -80,23 +80,42 @@ def _Kuf(
     return tf.stack(Kufs, axis=0)  # [L, M, N]
 
 
-@Kuf.register(
-    (FallbackSeparateIndependentInducingVariables, FallbackSharedIndependentInducingVariables),
-    LinearCoregionalization,
-    object,
-)
-def _Kuf(
+def _fallback_Kuf(
+    kuf_impl,
     inducing_variable: Union[
         SeparateIndependentInducingVariables, SharedIndependentInducingVariables
     ],
     kernel: LinearCoregionalization,
     Xnew: tf.Tensor,
 ):
-    kuf_impl = Kuf.dispatch(type(inducing_variable), SeparateIndependent, object)
     K = tf.transpose(kuf_impl(inducing_variable, kernel, Xnew), [1, 0, 2])  # [M, L, N]
     return K[:, :, :, None] * tf.transpose(kernel.W)[None, :, None, :]  # [M, L, N, P]
 
 
+@Kuf.register(
+    FallbackSeparateIndependentInducingVariables, LinearCoregionalization, object,
+)
+def _Kuf(
+    inducing_variable: FallbackSeparateIndependentInducingVariables,
+    kernel: LinearCoregionalization,
+    Xnew: tf.Tensor,
+):
+    kuf_impl = Kuf.dispatch(SeparateIndependentInducingVariables, SeparateIndependent, object)
+    return _fallback_Kuf(kuf_impl, inducing_variable, kernel, Xnew)
+
+
+@Kuf.register(
+    FallbackSharedIndependentInducingVariables, LinearCoregionalization, object,
+)
+def _Kuf(
+    inducing_variable: FallbackSharedIndependentInducingVariables,
+    kernel: LinearCoregionalization,
+    Xnew: tf.Tensor,
+):
+    kuf_impl = Kuf.dispatch(SharedIndependentInducingVariables, SeparateIndependent, object)
+    return _fallback_Kuf(kuf_impl, inducing_variable, kernel, Xnew)
+
+
 @Kuf.register(SharedIndependentInducingVariables, LinearCoregionalization, object)
 def _Kuf(
     inducing_variable: SharedIndependentInducingVariables,


Test patch:
diff --git a/tests/gpflow/covariances/test_multioutput.py b/tests/gpflow/covariances/test_multioutput.py
--- a/tests/gpflow/covariances/test_multioutput.py
+++ b/tests/gpflow/covariances/test_multioutput.py
@@ -53,6 +53,11 @@ class Datum:
     mf.SeparateIndependentInducingVariables(make_ips(Datum.P)),
 ]
 
+multioutput_fallback_inducing_variable_list = [
+    mf.FallbackSharedIndependentInducingVariables(make_ip()),
+    mf.FallbackSeparateIndependentInducingVariables(make_ips(Datum.P)),
+]
+
 multioutput_kernel_list = [
     mk.SharedIndependent(make_kernel(), Datum.P),
     mk.SeparateIndependent(make_kernels(Datum.L)),
@@ -62,23 +67,48 @@ class Datum:
 
 @pytest.mark.parametrize("inducing_variable", multioutput_inducing_variable_list)
 @pytest.mark.parametrize("kernel", multioutput_kernel_list)
-def test_kuu(inducing_variable, kernel):
+def test_kuu_shape(inducing_variable, kernel):
     Kuu = mo_kuus.Kuu(inducing_variable, kernel, jitter=1e-9)
-    tf.linalg.cholesky(Kuu)
+    t = tf.linalg.cholesky(Kuu)
+
+    if isinstance(kernel, mk.SharedIndependent):
+        if isinstance(inducing_variable, mf.SeparateIndependentInducingVariables):
+            assert t.shape == (3, 10, 10)
+        else:
+            assert t.shape == (10, 10)
+    else:
+        assert t.shape == (2, 10, 10)
 
 
 @pytest.mark.parametrize("inducing_variable", multioutput_inducing_variable_list)
 @pytest.mark.parametrize("kernel", multioutput_kernel_list)
-def test_kuf(inducing_variable, kernel):
+def test_kuf_shape(inducing_variable, kernel):
     Kuf = mo_kufs.Kuf(inducing_variable, kernel, Datum.Xnew)
 
+    if isinstance(kernel, mk.SharedIndependent):
+        if isinstance(inducing_variable, mf.SeparateIndependentInducingVariables):
+            assert Kuf.shape == (3, 10, 100)
+        else:
+            assert Kuf.shape == (10, 100)
+    else:
+        assert Kuf.shape == (2, 10, 100)
+
+
+@pytest.mark.parametrize("inducing_variable", multioutput_fallback_inducing_variable_list)
+def test_kuf_fallback_shared_inducing_variables_shape(inducing_variable):
+    kernel = mk.LinearCoregionalization(make_kernels(Datum.L), Datum.W)
+    Kuf = mo_kufs.Kuf(inducing_variable, kernel, Datum.Xnew)
+
+    assert Kuf.shape == (10, 2, 100, 3)
+
 
 @pytest.mark.parametrize("fun", [mo_kuus.Kuu, mo_kufs.Kuf])
-def test_mixed_shared(fun):
+def test_mixed_shared_shape(fun):
     inducing_variable = mf.SharedIndependentInducingVariables(make_ip())
     kernel = mk.LinearCoregionalization(make_kernels(Datum.L), Datum.W)
     if fun is mo_kuus.Kuu:
         t = tf.linalg.cholesky(fun(inducing_variable, kernel, jitter=1e-9))
+        assert t.shape == (2, 10, 10)
     else:
         t = fun(inducing_variable, kernel, Datum.Xnew)
-        print(t.shape)
+        assert t.shape == (2, 10, 100)