Repository: pyRiemann/pyRiemann

Issue:
kernel must be 'precomputed' or callable, is precomputed.
Given the model 
```
model = process.SVC(metric="logeuclid", kernel_fct="precomputed", C=0.1,
                                    shrinking=True, cache_size=1024,
                                    class_weight="balanced")
```

Then calling

`model.fit(X, y)`

returns 
`kernel must be 'precomputed' or callable, is precomputed.`

This is due to this piece of code in `pyriemann.classification.py`:

![image](https://github.com/pyRiemann/pyRiemann/assets/39137998/4b14584c-2840-4d87-8880-5c48b30b707b)

Which clearly doesn't check where `kernel_fct = "precomputed"`, but only if it's callable or None.

Hints from issue comments:
@qbarthelemy: Good catch! This problem will be solved with #272.

In the meantime you can use `kernel_fct=None` to achieve the same behavior.@qbarthelemy: Closed with #272

Solution patch:
diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst
index e34b5b3..7ea1a5f 100644
--- a/doc/whatsnew.rst
+++ b/doc/whatsnew.rst
@@ -28,6 +28,8 @@ v0.6.dev
 
 - Deprecate input ``covtest`` for predict of :class:`pyriemann.classification.KNearestNeighbor`, renamed into ``X``. :pr:`259` by :user:`qbarthelemy`
 
+- Correct check for `kernel_fct` param of :class:`pyriemann.classification.SVC`. :pr:`272` by :user:`qbarthelemy`
+
 v0.5 (Jun 2023)
 ---------------
 
diff --git a/pyriemann/classification.py b/pyriemann/classification.py
index 55d1d1a..768ba7c 100644
--- a/pyriemann/classification.py
+++ b/pyriemann/classification.py
@@ -583,16 +583,16 @@ class SVC(sklearnSVC):
 
     Parameters
     ----------
-    metric : {'riemann', 'euclid', 'logeuclid'}, default='riemann'
+    metric : {"riemann", "euclid", "logeuclid"}, default="riemann"
         Metric for kernel matrix computation.
     Cref : None | callable | ndarray, shape (n_channels, n_channels)
         Reference point for kernel matrix computation.
         If None, the mean of the training data according to the metric is used.
         If callable, the function is called on the training data to calculate
         Cref.
-    kernel_fct : None | 'precomputed' | callable
-        If 'precomputed', the kernel matrix for datasets X and Y is estimated
-        according to `pyriemann.utils.kernel(X, Y, Cref, metric)`.
+    kernel_fct : None | "precomputed" | callable
+        If None or "precomputed", the kernel matrix for datasets X and Y is
+        estimated according to `pyriemann.utils.kernel(X, Y, Cref, metric)`.
         If callable, the callable is passed as the kernel parameter to
         `sklearn.svm.SVC()` [2]_. The callable has to be of the form
         `kernel(X, Y, Cref, metric)`.
@@ -659,7 +659,7 @@ class SVC(sklearnSVC):
 
     def __init__(self,
                  *,
-                 metric='riemann',
+                 metric="riemann",
                  kernel_fct=None,
                  Cref=None,
                  C=1.0,
@@ -678,7 +678,7 @@ class SVC(sklearnSVC):
         self.metric = metric
         self.Cref_ = None
         self.kernel_fct = kernel_fct
-        super().__init__(kernel='precomputed',
+        super().__init__(kernel="precomputed",
                          C=C,
                          shrinking=shrinking,
                          probability=probability,
@@ -724,22 +724,24 @@ class SVC(sklearnSVC):
         elif isinstance(self.Cref, np.ndarray):
             self.Cref_ = self.Cref
         else:
-            raise TypeError(f'Cref must be np.ndarray, callable or None, is'
-                            f' {self.Cref}.')
+            raise TypeError(f"Cref must be np.ndarray, callable or None, is "
+                            f"{self.Cref}.")
 
     def _set_kernel(self):
         if callable(self.kernel_fct):
             self.kernel = functools.partial(self.kernel_fct,
                                             Cref=self.Cref_,
                                             metric=self.metric)
-
-        elif self.kernel_fct is None:
+        elif self.kernel_fct is None or (isinstance(self.kernel_fct, str) and
+                                         self.kernel_fct == "precomputed"):
             self.kernel = functools.partial(kernel,
                                             Cref=self.Cref_,
                                             metric=self.metric)
         else:
-            raise TypeError(f"kernel must be 'precomputed' or callable, is "
-                            f"{self.kernel}.")
+            raise TypeError(
+                "kernel_fct must be None, 'precomputed' or callable, is "
+                f"{self.kernel}."
+            )
 
 
 class MeanField(BaseEstimator, ClassifierMixin, TransformerMixin):


Test patch:
diff --git a/tests/test_classification.py b/tests/test_classification.py
index ed1114a..53e7cce 100644
--- a/tests/test_classification.py
+++ b/tests/test_classification.py
@@ -326,8 +326,7 @@ def test_svc_kernel_callable(get_mats, get_labels, metric):
     covmats = get_mats(n_matrices, n_channels, "spd")
     labels = get_labels(n_matrices, n_classes)
 
-    rsvc = SVC(kernel_fct=kernel,
-               metric=metric).fit(covmats, labels)
+    rsvc = SVC(kernel_fct=kernel, metric=metric).fit(covmats, labels)
     rsvc_1 = SVC(metric=metric).fit(covmats, labels)
     p1 = rsvc.predict(covmats[:-1])
     p2 = rsvc_1.predict(covmats[:-1])
@@ -338,18 +337,34 @@ def test_svc_kernel_callable(get_mats, get_labels, metric):
     SVC(kernel_fct=custom_kernel,
         metric=metric).fit(covmats, labels).predict(covmats[:-1])
 
+    # check if pickleable
+    pickle.dumps(rsvc)
+    pickle.dumps(rsvc_1)
+
+
+@pytest.mark.parametrize("kernel_fct", [None, "precomputed"])
+@pytest.mark.parametrize("metric", ["riemann", "euclid", "logeuclid"])
+def test_svc_kernel_precomputed(get_mats, get_labels, kernel_fct, metric):
+    n_matrices, n_channels, n_classes = 6, 3, 2
+    covmats = get_mats(n_matrices, n_channels, "spd")
+    labels = get_labels(n_matrices, n_classes)
+
+    SVC(kernel_fct=kernel_fct, metric=metric).fit(covmats, labels)
+
+
+def test_svc_kernel_error(get_mats, get_labels):
+    n_matrices, n_channels, n_classes = 4, 2, 2
+    covmats = get_mats(n_matrices, n_channels, "spd")
+    labels = get_labels(n_matrices, n_classes)
+
     def custom_kernel(X, Y, Cref):
         return np.ones((len(X), len(Y)))
     with pytest.raises(TypeError):
-        SVC(kernel_fct=custom_kernel, metric=metric).fit(covmats, labels)
+        SVC(kernel_fct=custom_kernel, metric="euclid").fit(covmats, labels)
 
     custom_kernel = np.array([1, 2])
     with pytest.raises(TypeError):
-        SVC(kernel_fct=custom_kernel, metric=metric).fit(covmats, labels)
-
-    # check if pickleable
-    pickle.dumps(rsvc)
-    pickle.dumps(rsvc_1)
+        SVC(kernel_fct=custom_kernel, metric="riemann").fit(covmats, labels)
 
 
 @pytest.mark.parametrize("method_label", ["sum_means", "inf_means"])