Repository: decargroup/pykoop

Issue:
Fix "inverse" Fourier transform in parameters and docs of RFF lifting functions
It should be the Fourier transform not the inverse one.

Discussion:

Patch:
diff --git a/CITATION.cff b/CITATION.cff
index ee217ef..5ab8fb4 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -10,6 +10,6 @@ authors:
     orcid: "https://orcid.org/0000-0002-1987-9268"
     affiliation: "McGill University"
 title: "decargroup/pykoop"
-version: v1.2.4
+version: v2.0.0
 doi: 10.5281/zenodo.5576490
 url: "https://github.com/decargroup/pykoop"
diff --git a/README.rst b/README.rst
index 70e9b34..bb68e51 100644
--- a/README.rst
+++ b/README.rst
@@ -221,7 +221,7 @@ If you use this software in your research, please cite it as below or see
         url={https://github.com/decargroup/pykoop},
         publisher={Zenodo},
         author={Steven Dahdah and James Richard Forbes},
-        version = {{v1.2.4}},
+        version = {{v2.0.0}},
         year={2022},
     }
 
diff --git a/pykoop/kernel_approximation.py b/pykoop/kernel_approximation.py
index 2712988..40adeff 100644
--- a/pykoop/kernel_approximation.py
+++ b/pykoop/kernel_approximation.py
@@ -93,9 +93,8 @@ class RandomFourierKernelApprox(KernelApproximation):
     n_features_out_ : int
         Number of features output. This attribute is not available in
         estimators from :mod:`sklearn.kernel_approximation`.
-    ift_ : scipy.stats.rv_continuous
-        Probability distribution corresponding to inverse Fourier transform of
-        chosen kernel.
+    ft_ : scipy.stats.rv_continuous
+        Probability distribution corresponding to Fourier transform of chosen kernel.
     random_weights_ : np.ndarray, shape (n_features, n_components)
         Random weights to inner-product with features.
     random_offsets_ : np.ndarray, shape (n_features, )
@@ -106,7 +105,7 @@ class RandomFourierKernelApprox(KernelApproximation):
     Generate random Fourier features from a Gaussian kernel
 
     >>> ka = pykoop.RandomFourierKernelApprox(
-    ...     kernel_or_ift='gaussian',
+    ...     kernel_or_ft='gaussian',
     ...     n_components=10,
     ...     shape=1,
     ...     random_state=1234,
@@ -117,12 +116,12 @@ class RandomFourierKernelApprox(KernelApproximation):
     array([...])
     """
 
-    _ift_lookup = {
+    _ft_lookup = {
         'gaussian': scipy.stats.norm,
         'laplacian': scipy.stats.cauchy,
         'cauchy': scipy.stats.laplace,
     }
-    """Lookup table for inverse Fourier transform of kernel.
+    """Lookup table for Fourier transform of kernel.
 
     Laplacian and Cauchy being swapped is not a typo. They are Fourier
     transforms of each other.
@@ -130,7 +129,7 @@ class RandomFourierKernelApprox(KernelApproximation):
 
     def __init__(
         self,
-        kernel_or_ift: Union[str, scipy.stats.rv_continuous] = 'gaussian',
+        kernel_or_ft: Union[str, scipy.stats.rv_continuous] = 'gaussian',
         n_components: int = 100,
         shape: float = 1,
         method: str = 'weight_offset',
@@ -140,19 +139,19 @@ class RandomFourierKernelApprox(KernelApproximation):
 
         Parameters
         ----------
-        kernel_or_ift : Union[str, scipy.stats.rv_continuous]
+        kernel_or_ft : Union[str, scipy.stats.rv_continuous]
             Kernel to approximate. Possible options are
 
-                - ``'gaussian'`` -- Gaussian kernel, with inverse Fourier
-                  transform :class:`scipy.stats.norm` (default),
-                - ``'laplacian'`` -- Laplacian kernel, with inverse Fourier
-                  transform :class:`scipy.stats.cauchy`, or
-                - ``'cauchy'`` -- Cauchy kernel, with inverse Fourier transform
-                  :class:`scipy.stats.laplace`.
+                - ``'gaussian'`` -- Gaussian kernel, with Fourier transform
+                   :class:`scipy.stats.norm` (default),
+                - ``'laplacian'`` -- Laplacian kernel, with Fourier transform
+                   :class:`scipy.stats.cauchy`, or
+                - ``'cauchy'`` -- Cauchy kernel, with Fourier transform
+                   :class:`scipy.stats.laplace`.
 
             Alternatively, a positive, shift-invariant kernel can be implicitly
-            specified by providing its inverse Fourier transform as a
-            univariate probability distribution subclassing
+            specified by providing its Fourier transform as a univariate
+            probability distribution subclassing
             :class:`scipy.stats.rv_continuous`.
 
         n_components : int
@@ -179,7 +178,7 @@ class RandomFourierKernelApprox(KernelApproximation):
         random_state : Union[int, np.random.RandomState, None]
             Random seed.
         """
-        self.kernel_or_ift = kernel_or_ift
+        self.kernel_or_ft = kernel_or_ft
         self.n_components = n_components
         self.shape = shape
         self.method = method
@@ -210,11 +209,11 @@ class RandomFourierKernelApprox(KernelApproximation):
             If any of the constructor parameters are incorrect.
         """
         X = sklearn.utils.validation.check_array(X)
-        # Set inverse Fourier transform
-        if isinstance(self.kernel_or_ift, str):
-            self.ift_ = self._ift_lookup[self.kernel_or_ift]
+        # Set Fourier transform
+        if isinstance(self.kernel_or_ft, str):
+            self.ft_ = self._ft_lookup[self.kernel_or_ft]
         else:
-            self.ift_ = self.kernel_or_ift
+            self.ft_ = self.kernel_or_ft
         # Validate input
         if self.n_components <= 0:
             raise ValueError('`n_components` must be positive.')
@@ -230,7 +229,7 @@ class RandomFourierKernelApprox(KernelApproximation):
         else:
             self.n_features_out_ = self.n_components
         # Generate random weights
-        self.random_weights_ = self.ift_.rvs(
+        self.random_weights_ = self.ft_.rvs(
             scale=1,
             size=(self.n_features_in_, self.n_components),
             random_state=self.random_state,
@@ -249,7 +248,7 @@ class RandomFourierKernelApprox(KernelApproximation):
         # Easiest way to make sure distribution is univariate is to check the
         # dimension of the output.
         if self.random_weights_.ndim != 2:
-            raise ValueError('`kernel_or_ift` must specify a univariate '
+            raise ValueError('`kernel_or_ft` must specify a univariate '
                              'probability distribution.')
         return self
 
diff --git a/pykoop/koopman_pipeline.py b/pykoop/koopman_pipeline.py
index d524e9a..3dafbb4 100644
--- a/pykoop/koopman_pipeline.py
+++ b/pykoop/koopman_pipeline.py
@@ -9,7 +9,6 @@ import numpy as np
 import sklearn.base
 import sklearn.exceptions
 import sklearn.metrics
-from deprecated import deprecated
 from matplotlib import pyplot as plt
 from scipy import linalg
 
@@ -2410,88 +2409,6 @@ class KoopmanPipeline(metaestimators._BaseComposition, KoopmanLiftingFn):
         score = scorer(self, X, None)
         return score
 
-    @deprecated('Use `predict_trajectory` instead')
-    def predict_multistep(self, X: np.ndarray) -> np.ndarray:
-        """Perform a multi-step prediction for the first state of each episode.
-
-        This function takes the first ``min_samples_`` states of the input,
-        along with all of its inputs, and predicts the next ``X.shape[0]``
-        states of the system. This action is performed on a per-episode basis.
-        The state features of ``X`` (other than the first ``min_samples_``
-        features) are not used at all.
-
-        If prediction fails numerically, missing predictions are filled with
-        ``np.nan``.
-
-        Parameters
-        ----------
-        X : np.ndarray
-            Data matrix.
-
-        Returns
-        -------
-        np.ndarray
-            Predicted data matrix.
-
-        Raises
-        ------
-        ValueError
-            If an episode is shorter than ``min_samples_``.
-
-        Warning
-        -------
-        Deprecated in favour of
-        :func:`pykoop.KoopmanPipeline.predict_trajectory`.
-
-        """
-        sklearn.utils.validation.check_is_fitted(self, 'regressor_fit_')
-        X = sklearn.utils.validation.check_array(X, **self._check_array_params)
-        # Split episodes
-        episodes = split_episodes(X, episode_feature=self.episode_feature_)
-        # Loop over episodes
-        predictions = []
-        for (i, X_i) in episodes:
-            # Check length of episode.
-            if X_i.shape[0] < self.min_samples_:
-                raise ValueError(f'Episode {i} has {X_i.shape[0]} samples but '
-                                 f'`min_samples_`={self.min_samples_} samples '
-                                 'are required.')
-            # Index where prediction blows up (if it does)
-            crash_index = None
-            # Extract initial state and input
-            x0 = X_i[:self.min_samples_, :self.n_states_in_]
-            u = X_i[:, self.n_states_in_:]
-            # Create array to hold predicted states
-            X_pred_i = np.zeros((X_i.shape[0], self.n_states_in_))
-            # Set the initial condition
-            X_pred_i[:self.min_samples_, :] = x0
-            # Predict all time steps
-            for k in range(self.min_samples_, X_i.shape[0]):
-                # Stack episode feature, previous predictions, and input
-                X_ik = combine_episodes(
-                    [(i,
-                      np.hstack((
-                          X_pred_i[(k - self.min_samples_):k, :],
-                          X_i[(k - self.min_samples_):k, self.n_states_in_:],
-                      )))],
-                    episode_feature=self.episode_feature_)
-                # Predict next step
-                try:
-                    X_pred_ik = self.predict(X_ik)[[-1], :]
-                except ValueError:
-                    crash_index = k
-                    break
-                # Extract data matrix from prediction
-                X_pred_i[[k], :] = split_episodes(
-                    X_pred_ik, episode_feature=self.episode_feature_)[0][1]
-            if crash_index is not None:
-                X_pred_i[crash_index:, :] = np.nan
-            predictions.append((i, X_pred_i))
-        # Combine episodes
-        X_p = combine_episodes(predictions,
-                               episode_feature=self.episode_feature_)
-        return X_p
-
     def predict_trajectory(
         self,
         X0_or_X: np.ndarray,
diff --git a/setup.py b/setup.py
index 30d562f..485c5d7 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ with open('README.rst', 'r') as f:
 
 setuptools.setup(
     name='pykoop',
-    version='1.2.4',
+    version='2.0.0',
     description=('Koopman operator identification library in Python, '
                  'compatible with `scikit-learn`'),
     long_description=readme,


Test patch:
diff --git a/.github/workflows/test-package-no-mosek.yml b/.github/workflows/test-package-no-mosek.yml
new file mode 100644
index 0000000..73c6b68
--- /dev/null
+++ b/.github/workflows/test-package-no-mosek.yml
@@ -0,0 +1,26 @@
+name: Test package (no MOSEK)
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ['3.8', '3.9', '3.10', '3.11']
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{matrix.python-version}}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{matrix.python-version}}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install -r requirements-dev.txt
+    - name: Test with pytest
+      run: |
+        pytest --exitfirst -k "not mosek"
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 087a2f8..b9c5bbf 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -10,7 +10,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+        python-version: ['3.12']
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python ${{matrix.python-version}}
diff --git a/tests/test_kernel_approximation.py b/tests/test_kernel_approximation.py
index bf9b380..ad714de 100644
--- a/tests/test_kernel_approximation.py
+++ b/tests/test_kernel_approximation.py
@@ -11,7 +11,7 @@ import pykoop
 @pytest.mark.parametrize('est, kern, X', [
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_only',
@@ -25,7 +25,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=3,
             method='weight_only',
@@ -39,7 +39,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_offset',
@@ -53,7 +53,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=1,
             method='weight_only',
@@ -67,7 +67,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=3,
             method='weight_only',
@@ -81,7 +81,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=1,
             method='weight_offset',
@@ -95,7 +95,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e5),
             shape=1,
             method='weight_only',
@@ -109,7 +109,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e5),
             shape=3,
             method='weight_only',
@@ -123,7 +123,7 @@ import pykoop
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e6),
             shape=1,
             method='weight_offset',
@@ -190,7 +190,7 @@ class TestKernelApproximation:
 @pytest.mark.parametrize('est, est_sk, X', [
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_only',
@@ -208,7 +208,7 @@ class TestKernelApproximation:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_offset',
@@ -253,7 +253,7 @@ class TestKernelApproximationSklearn:
 @pytest.mark.parametrize('est, X', [
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_only',
@@ -266,7 +266,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=3,
             method='weight_only',
@@ -279,7 +279,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='gaussian',
+            kernel_or_ft='gaussian',
             n_components=int(1e4),
             shape=1,
             method='weight_offset',
@@ -292,7 +292,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=1,
             method='weight_only',
@@ -305,7 +305,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=3,
             method='weight_only',
@@ -318,7 +318,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='laplacian',
+            kernel_or_ft='laplacian',
             n_components=int(1e5),
             shape=1,
             method='weight_offset',
@@ -331,7 +331,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e5),
             shape=1,
             method='weight_only',
@@ -344,7 +344,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e5),
             shape=3,
             method='weight_only',
@@ -357,7 +357,7 @@ class TestKernelApproximationSklearn:
     ),
     (
         pykoop.RandomFourierKernelApprox(
-            kernel_or_ift='cauchy',
+            kernel_or_ft='cauchy',
             n_components=int(1e6),
             shape=1,
             method='weight_offset',
diff --git a/tests/test_koopman_pipeline.py b/tests/test_koopman_pipeline.py
index d93e9d3..bf939a8 100644
--- a/tests/test_koopman_pipeline.py
+++ b/tests/test_koopman_pipeline.py
@@ -1097,8 +1097,6 @@ class TestEpisodeManipulation:
         np.testing.assert_allclose(X1s, X2s)
 
 
-@pytest.mark.filterwarnings(
-    'ignore:Call to deprecated method predict_multistep')
 @pytest.mark.parametrize(
     'kp',
     [
@@ -1186,8 +1184,6 @@ class TestPredictionNoInput:
         np.testing.assert_allclose(X_sim, X_sim_exp)
 
 
-@pytest.mark.filterwarnings(
-    'ignore:Call to deprecated method predict_multistep')
 @pytest.mark.parametrize(
     'kp',
     [
@@ -1381,41 +1377,6 @@ class TestPrediction:
             default_tolerance=dict(atol=1e-6, rtol=0),
         )
 
-    def test_predict_multistep(self, kp, mass_spring_damper_sine_input):
-        """Test :func:`predict_multistep` (deprecated)."""
-        msg = 'Test only works when there is no episode feature.'
-        assert (not mass_spring_damper_sine_input['episode_feature']), msg
-        # Extract initial conditions
-        x0 = pykoop.extract_initial_conditions(
-            mass_spring_damper_sine_input['X_train'],
-            kp.min_samples_,
-            n_inputs=mass_spring_damper_sine_input['n_inputs'],
-            episode_feature=mass_spring_damper_sine_input['episode_feature'],
-        )
-        # Extract input
-        u = pykoop.extract_input(
-            mass_spring_damper_sine_input['X_train'],
-            n_inputs=mass_spring_damper_sine_input['n_inputs'],
-            episode_feature=mass_spring_damper_sine_input['episode_feature'],
-        )
-        # Set up initial conditions and input
-        X_ic = np.zeros(mass_spring_damper_sine_input['X_train'].shape)
-        X_ic[:kp.min_samples_, :x0.shape[1]] = x0
-        X_ic[:, x0.shape[1]:] = u
-        # Predict using ``predict_multistep``
-        X_sim = kp.predict_multistep(X_ic)
-        # Predict manually
-        X_sim_exp = np.zeros(mass_spring_damper_sine_input['Xp_train'].shape)
-        X_sim_exp[:kp.min_samples_, :] = x0
-        for k in range(kp.min_samples_, u.shape[0]):
-            X = np.hstack((
-                X_sim_exp[(k - kp.min_samples_):k, :],
-                u[(k - kp.min_samples_):k, :],
-            ))
-            Xp = kp.predict(X)
-            X_sim_exp[[k], :] = Xp[[-1], :]
-        np.testing.assert_allclose(X_sim, X_sim_exp)
-
 
 @pytest.mark.parametrize(
     'X, episodes, episode_feature',