Repository: Machine-Learning-Dynamical-Systems/kooplearn (language: python)

Issue:
updating fit_rand_reduced_rank_regression
Computing K_reg_inv with Cholesky and normalizing Omp by QR decomposition


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index d50b109..20f463e 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,12 @@
   <img src="logo.svg" alt="SVG Image" style="width:50%;"/>
 </p>
 
-# `kooplearn`: Learn Koopman and transfer operators of Dynamical Systems
+# Learn Koopman and Transfer operators for Dynamical Systems and Stochastic Processes
 
-`kooplearn` is a Python library designed for learning Koopman operators or transfer operators associated with dynamical systems. It provides an array of algorithms for model training and analysis, enabling users to perform forecasting, spectral decomposition, and modal decomposition based on the learned operator.
+`kooplearn` is a Python library designed for learning Koopman or Transfer operators associated with dynamical systems. Given a _nonlinear_ dynamical system $x_{t + 1} = S(x_{t})$, the **Koopman operator** provides a _global linearization_ of the dynamics by mapping it to a suitable space of observables $\mathcal{F}$. An observable is any (scalar) function of the state. The Koopman operator $\mathsf{K}$ is defined as $$(\mathsf{K}f)(x_{t}) = f(x_{t + 1}) := f \circ S (x_t) \qquad f \in \mathcal{F}.$$
+Similarly, given a stochastic process $X:= \{ X_{s} \colon s \in \mathbb{N}\}$, its **Transfer operator* returns the expected value of any observable forward in time. The Transfer operator $\mathsf{T}$ is defined as $$(\mathsf{T}f)(x) := \mathbb{E}\left[f(X_{t + 1}) \mid X_{t} = x \right] \qquad f \in \mathcal{F}.$$
+
+`kooplearn` provides a suite of algorithms for model training and analysis, enabling users to perform forecasting, spectral decomposition, and modal decomposition based on the learned operator.
 
 Please note that `kooplearn` is currently under active development, and while we are continuously adding new features and improvements, some parts of the library might still be a work in progress.
 
@@ -16,10 +19,17 @@ Please note that `kooplearn` is currently under active development, and while we
 - Perform modal decomposition for further analysis.
   
 ## Installation
+To install the core version of `kooplearn`, without optional dependencies, run
+```bash
+   pip install kooplearn
+```
+To install the full version of `kooplearn`, including Neural-Network models, and the dahsboard, run
+```bash
+   pip install "kooplearn[full]"
+```
+To install the development version of `kooplearn`, run
 ```bash
-pip install kooplearn
-# Or to install the latest version of kooplearn from git:
-pip install --upgrade git+https://github.com/CSML-IIT-UCL/kooplearn.git
+   pip install --upgrade git+https://github.com/Machine-Learning-Dynamical-Systems/kooplearn.git
 ```
 ## Contributing
 
@@ -41,5 +51,5 @@ This project is licensed under the [MIT License](LICENSE).
 
 ---
 
-We hope you find `kooplearn` useful for your dynamical systems analysis. If you encounter any issues or have suggestions for improvements, please don't hesitate to [raise an issue](https://github.com/CSML-IIT-UCL/kooplearn/issues). Happy coding!
+We hope you find `kooplearn` useful for your dynamical systems analysis. If you encounter any issues or have suggestions for improvements, please don't hesitate to [raise an issue](https://github.com/Machine-Learning-Dynamical-Systems/kooplearn/issues). Happy coding!
 
diff --git a/dev_docs/roadmap.md b/dev_docs/roadmap.md
index ed20afa..5d906e1 100644
--- a/dev_docs/roadmap.md
+++ b/dev_docs/roadmap.md
@@ -1,4 +1,13 @@
 # Roadmap
+#### Jan 24, 2024:
+Testing the old and new versions of the algorithm. Some observations:
+- At low (approaching machine precision) regularization strengths, or when the rank is too high, the computed estimator is highly unstable. As expected.
+- The old function `_fit_reduced_rank_regression_noreg` seem to be extremely well conditioned. Might be worth to retain it. 
+- Even with the numerical stabilization in place, if the rank is too high, the estimated models might differ.
+
+#### Jan 23, 2024:
+Working on RRR. Return by default the `svals_sq`, which are related to the spectral bias. Save it as as an attribute of the model. Extend the filtering mechanism to Rand-RRR and Nystroem-RRR. Add the possiblity to add a pre-computed cho_factor to the Rand-RRR for numerical efficiency. Use iterative methods otherwise.
+
 #### Oct 17, 2023:
 Added branch `in_out` in which I will work on serialization of each model, and more generally on every input-output utility which might be needed.
 
diff --git a/dev_docs/todo.md b/dev_docs/todo.md
index 19fb0cf..8c4f7ad 100644
--- a/dev_docs/todo.md
+++ b/dev_docs/todo.md
@@ -1,5 +1,38 @@
 > Do NOT delete completed tasks.
 
+## General
+- [ ] Do a complete pass on the documentation adding cross-links to class and attributes.
+- [ ] Rename Least Squares models from `*DMD` to `LeastSquares` and update docs accordingly
+
+## Documentation
+- [ ] Add a _Operators & Dynamical Systems_ documentation page replacing the blog post, for now.
+- [x] Add a Shared Bibliography file
+- [x] Document everything in `kooplearn.abc`.
+### Examples (tentative titles):
+> Add name of the author and link to its social media for each of the examples.
+- [ ] Large scale Kernel models with Nyström or Randomized techniques. (NoisyLogistic) - Giacomo
+- [ ] Ordered MNIST (All algorithms) - Pie
+- [ ] Sea temperature with Autoencoders (re-implementing Azencot's example) - Pie
+- [ ] ALA2 (VAMPNets, DPNets) - Pie
+- [ ] Regime switching - Grég
+
+## Models
+### NystromKDMD
+- [ ] Fix the low level algorithms for numerical stability & test
+- [ ] Use `kooplearn.models.KernelDMD` as an example to implement the Nystrom version. ~~Link to Falkon for a GPU implementation.~~
+- [ ] Check the role of Tikhnonv reg. in both PCR and RRR -- [Giacomo's implementation](https://github.com/Giodiro/NystromKoopman/tree/9463faf5dd6a7b7a5ccba31ebbd755fcfa91e20f/nyskoop/estimators) is not 100% consistent (PCR has no reg, while RRR has it) 
+
+### ExtendedDMD
+- [x] Add `risk` method.
+- [ ] Add power iteration QR normalization for the randomized RRR algorithms.
+- [x] Lift the constraint of having 2D observables in `modes` and `predict`
+- [x] Do as above for `KernelDMD`
+
+### ~~EncoderModel~~ DeepEDMD Model
+- [x] Rewrite it by subclassing `kooplearn.models.ExtendedDMD`
+- [ ] Implement the loading and saving utilities - pay attention to saving the feature map.
+- [x] Drop the requirements for the data to be of shape `[n_samples, n_features]`, and allow for a general `[n_samples, ...]`.
+
 ### Optimization
 - [ ] Rewrite `kooplearn._src.operator_regression` in C++/Cython
 
@@ -18,10 +51,6 @@
 - [ ] Finish the `modes` and `eig` methods.
 - [ ] Add  a test in `tests`
 
-### NystromKDMD
-- [ ] Fix the low level algorithms for numerical stability & test
-- [ ] Use `kooplearn.models.KernelDMD` as an example to implement the Nystrom version. Link to Falkon for a GPU implementation.
-
 ### Input-Output utilities
 - [x] Add Numpy utilities to convert from a trajectory to a context window **view**. This avoids unnecessary memory usage. 
 - [ ] Write a function to add `nan` padding for inference data (in which we do not know the target, nor the lookforward.)
@@ -31,11 +60,6 @@
 ### Datasets
 - [ ] The return of `Dataset.generate` should be a trajectory (as it is now, but double check).
 
-### ~~EncoderModel~~ DeepEDMD Model
-- [x] Rewrite it by subclassing `kooplearn.models.ExtendedDMD`
-- [ ] Implement the loading and saving utilities - pay attention to saving the feature map.
-- [x] Drop the requirements for the data to be of shape `[n_samples, n_features]`, and allow for a general `[n_samples, ...]`.
-
 ### Refactoring to the context-window data paradigm
 
 - [x] Take a decision on the name of the variables: current proposal is `data/contexts`, `lookback_len`.
@@ -64,18 +88,7 @@
 - [x] Update `test_edmd_estimators`, `test_kernel_estimators`.
 - [x] Add tests for the I/O utilities
 
-### Documentation
-- [x] Add a Shared Bibliography file
-- [x] Document everything in `kooplearn.abc`.
-- [ ] Do a complete pass on the documentation adding cross-links to class and attributes.
-
-### ExtendedDMD
-- [x] Add `risk` method.
-- [ ] Add power iteration QR normalization for the randomized RRR algorithms.
-- [x] Lift the constraint of having 2D observables in `modes` and `predict`
-- [x] Do as above for `KernelDMD`
-
-### DPNets
+### ~~DPNets~~ | LEGACY |
 - Implement different regularization functions:
   - [x] Frobenius.
   - [x] Von-Neumann divergence.
diff --git a/docs/api_reference/data.rst b/docs/api_reference/data.rst
index 20973a2..bd8c2c5 100644
--- a/docs/api_reference/data.rst
+++ b/docs/api_reference/data.rst
@@ -1,14 +1,28 @@
 .. _data_api:
 
-Data
+Data Utilities
 ========================
 
+Introduction
+------------
+
 In this page we document functions and classes related to the data-manipulation in Kooplearn. As explained in :ref:`kooplearn's data paradigm <kooplearn_data_paradigm>`, Kooplearn uses a context-based approach to represent dynamical systems data. A context window is just a (usually short) sequence of consecutive observations of the system, enclosing the 'past' in its *lookback window* and the 'future' in its *lookforward window*. Intuitively, everything in the lookback window is the information we need to provide, at inference time, to predict what is in the lookforward window. By using context windows we depart from the usual paradigm in supervised learning in which data is categorized into inputs and outputs. Rather, when studying dynamical system we find it more natural to conceive a "data point" as a context window containing the dynamical information at a given point in time. 
 
+Trajectories to Contexts
+------------------------
+
+The functions and classes documented in this page are used to convert raw data into context windows, and to convert context windows into datasets that can be used to train neural networks.
+
+Numpy
+~~~~~
+
 .. autofunction:: kooplearn.data.traj_to_contexts
 
-.. currentmodule:: kooplearn.nn
+PyTorch
+~~~~~~~
 
+.. currentmodule:: kooplearn.nn
+    
 .. autoclass:: kooplearn.nn.data.ContextsDataset
 
 .. autofunction:: kooplearn.nn.data.traj_to_contexts_dataset
diff --git a/docs/api_reference/feature_maps.rst b/docs/api_reference/feature_maps.rst
index 36638e0..d35d883 100644
--- a/docs/api_reference/feature_maps.rst
+++ b/docs/api_reference/feature_maps.rst
@@ -1,5 +1,5 @@
 Feature Maps
-=====================================
+============
 
 Introduction
 ------------
@@ -10,15 +10,18 @@ Feature maps can also be learned directly from data. To this end, Kooplearn expo
 
 .. currentmodule:: kooplearn.models.feature_maps
 
+Feature Maps
+------------
+
 Basic Feature Maps
--------------------------
+~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: IdentityFeatureMap
 
 .. autoclass:: ConcatenateFeatureMaps
 
 Deep-Learning Feature Maps
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 Kooplearn's deep learning components are implemented using `pytorch lightning <https://lightning.ai>`_ as it allows to easily set up complex machine learning pipelines in a modular way, and it comes with an extensive toolset out of the box. Broadly speaking, Kooplearn's implementation expect you to provide:
 
 1. One or more :code:`torch.nn.Module` objects defining the neural-network, along with the arguments passed at initialization.
diff --git a/docs/api_reference/models.rst b/docs/api_reference/models.rst
index dfbb59e..84fa17d 100644
--- a/docs/api_reference/models.rst
+++ b/docs/api_reference/models.rst
@@ -1,21 +1,31 @@
 Models
-=====================================
+======
+
+Least Squares Models
+--------------------
 
 .. currentmodule:: kooplearn.models
 
-Linear & Kernel models
-----------------------
+Ridge Regression
+~~~~~~~~~~~~~~~~
 
 .. autoclass:: ExtendedDMD
     :members:
 
+.. autoclass:: DMD
+
+Kernel Ridge
+~~~~~~~~~~~~
+
 .. autoclass:: KernelDMD
     :members:
 
-.. autoclass:: DMD
+.. autoclass:: NystroemKernel
+    :members:
 
 Deep-Learning models
 --------------------
+
 Kooplearn's deep learning components are implemented using `pytorch lightning <https://lightning.ai>`_ as it allows to easily set up complex machine learning pipelines in a modular way, and it comes with an extensive toolset out of the box. Broadly speaking, Kooplearn's implementation expect you to provide:
 
 1. One or more :code:`torch.nn.Module` objects defining the neural-network, along with the arguments passed at initialization.
@@ -24,12 +34,23 @@ Kooplearn's deep learning components are implemented using `pytorch lightning <h
 
 Kooplearn, in turn will handle the creation of a :code:`lightning.LightningModule` internally. Models are then fitted by calling the :code:`fit` method, which has roughly the same signature of :code:`lightning.Trainer().fit`, and accepts both `torch dataloaders <https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader>`_ and `lightning datamodules <https://lightning.ai/docs/pytorch/stable/data/datamodule.html>`_.
 
+Ridge Regression (NN features)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: DeepEDMD
 
+Dynamical Autoencoder
+~~~~~~~~~~~~~~~~~~~~~
+
 .. autoclass:: DynamicAE
     :members:
 
+Consistent Autoencoder
+~~~~~~~~~~~~~~~~~~~~~~
+
 .. autoclass:: ConsistentAE
     :members:
-    
+
+Bibliography
+------------
+
 .. footbibliography::
diff --git a/docs/bibliography.bib b/docs/bibliography.bib
index 6accc2f..2d74342 100644
--- a/docs/bibliography.bib
+++ b/docs/bibliography.bib
@@ -17,14 +17,15 @@ @inproceedings{Kostic2023SpectralRates
 url={https://openreview.net/forum?id=Lt3jqxsbVO}
 }
 
-@misc{Kostic2023DPNets,
-      title={Deep projection networks for learning time-homogeneous dynamical systems}, 
-      author={Vladimir R. Kostic and Pietro Novelli and Riccardo Grazzi and Karim Lounici and Massimiliano Pontil},
-      year={2023},
-      eprint={2307.09912},
-      archivePrefix={arXiv},
-      primaryClass={cs.LG}
+@inproceedings{
+Kostic2023DPNets,
+title={Learning invariant representations of time-homogeneous stochastic dynamical systems},
+author={Vladimir R. Kostic and Pietro Novelli and Riccardo Grazzi and Karim Lounici and Massimiliano Pontil},
+booktitle={The Twelfth International Conference on Learning Representations},
+year={2024},
+url={https://openreview.net/forum?id=twSnZwiOIm}
 }
+
 @inproceedings{Azencot2020CAE,
   title={Forecasting sequential data using consistent Koopman autoencoders},
   author={Azencot, Omri and Erichson, N Benjamin and Lin, Vanessa and Mahoney, Michael},
@@ -107,4 +108,15 @@ @article{Mardt2018
   author = {Andreas Mardt and Luca Pasquali and Hao Wu and Frank No{\'{e}}},
   title = {{VAMPnets} for deep learning of molecular kinetics},
   journal = {Nature Communications}
+}
+
+@misc{Turri2023,
+  doi = {10.48550/ARXIV.2312.17348},
+  url = {https://arxiv.org/abs/2312.17348},
+  author = {Turri,  Giacomo and Kostic,  Vladimir and Novelli,  Pietro and Pontil,  Massimiliano},
+  keywords = {Machine Learning (cs.LG),  Numerical Analysis (math.NA),  Machine Learning (stat.ML),  FOS: Computer and information sciences,  FOS: Computer and information sciences,  FOS: Mathematics,  FOS: Mathematics},
+  title = {A randomized algorithm to solve reduced rank operator regression},
+  publisher = {arXiv},
+  year = {2023},
+  copyright = {arXiv.org perpetual,  non-exclusive license}
 }
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index c44efcb..720fd2d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -45,11 +45,12 @@
 html_logo = "../logo.svg"
 
 html_theme_options = {
-    "repository_url": "https://github.com/CSML-IIT-UCL/kooplearn",
+    "repository_url": "https://github.com/Machine-Learning-Dynamical-Systems/kooplearn",
     "use_repository_button": True,  # add a 'link to repository' button
     "use_issues_button": False,  # add an 'Open an Issue' button
     "path_to_docs": ("docs"),
     "show_navbar_depth": 1,
+    "show_toc_level": 3,
 }
 
 bibtex_bibfiles = ["bibliography.bib"]
diff --git a/docs/getting_started.ipynb b/docs/getting_started.ipynb
index 3a44250..7498eed 100644
--- a/docs/getting_started.ipynb
+++ b/docs/getting_started.ipynb
@@ -4,8 +4,15 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "<a target=\"_blank\" href=\"https://colab.research.google.com/github/CSML-IIT-UCL/kooplearn/blob/3d4c517c29661d352d5c230c057a9b690a623eb7/docs/getting_started.ipynb\">\n",
+    "  <img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/>\n",
+    "</a> \n",
+    "\n",
     "# Getting started\n",
-    "Kooplearn is a machine learning library designed with a specific purpose in mind: learn Koopman/Transfer operators of dynamical systems from data. For a user-friendly introduction to the operatorial perspective on dynamical systems check out [this blog post [TODO]](). \n",
+    "\n",
+    "> _Author:_ Pietro Novelli — [@pie_novelli](https://twitter.com/pie_novelli)\n",
+    "\n",
+    "Kooplearn is a machine learning library designed for a specific purpose, that is learn Koopman/Transfer operators of dynamical systems from data. For a user-friendly introduction to the operatorial perspective on dynamical systems check out our [primer on Koopman and Transfer operators for dynamical systems](primer).\n",
     "\n",
     "In this article we will go through a typical training and inference pipeline in `kooplearn`, experimenting with the _noisy logistic map_, a one-dimensional dynamical system defined by the relation\n",
     "\n",
@@ -15,23 +22,36 @@
     "\n",
     "Here $\\xi_{t}$ is just a noise term with density $\\propto \\cos^{N}(x)$, $N$ being an even integer. {footcite:t}`Kostic2022` reported a full characterization of the transfer operator of the noisy logistic map. In particular, the transfer operator has rank $N + 1$, it is _non-normal_, and its eigenvalues and eigenfunctions can be computed exactly. In `kooplearn` we provide an implementation of the noisy logistic map in {class}`kooplearn.datasets.LogisticMap`.\n",
     "\n",
+    "## Setup"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import importlib\n",
+    "\n",
+    "for module in ['kooplearn', 'matplotlib']:\n",
+    "    try:\n",
+    "        importlib.import_module(module)\n",
+    "    except ImportError:\n",
+    "        %pip install {module}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "## Generating and splitting the data"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Train split has shape (50001, 1)\n",
-      "Test split has shape (1001, 1)\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "from kooplearn.datasets import LogisticMap\n",
     "\n",
@@ -73,7 +93,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -100,42 +120,15 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "The first 5 training points are\n",
-      "[[0.5       ]\n",
-      " [0.02461448]\n",
-      " [0.05297965]\n",
-      " [0.07993305]\n",
-      " [0.14713471]]\n",
-      "Their feature map is\n",
-      "[[ 1.          0.         -1.         -0.        ]\n",
-      " [ 1.         -0.95077105  0.80793118 -0.5855441 ]\n",
-      " [ 1.         -0.89404071  0.59861757 -0.17633624]\n",
-      " [ 1.         -0.84013391  0.41164997  0.14845172]\n",
-      " [ 1.         -0.70573058 -0.00388869  0.71121932]]\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "feature_map = ChebyshevPoly(max_order = 4)\n",
     "\n",
     "print(f\"The first 5 training points are\\n{datasets['train'][:5]}\\nTheir feature map is\\n{feature_map(datasets['train'][:5])}\")"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "```{footbibliography}\n",
-    "```"
-   ]
-  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -150,7 +143,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 36,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -171,18 +164,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 37,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Train contexts have shape (50000, 2, 1): 50000 contexts of length 2 with 1 features each\n",
-      "Test contexts have shape (1000, 2, 1): 1000 contexts of length 2 with 1 features each\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "from kooplearn.data import traj_to_contexts\n",
     "\n",
@@ -200,27 +184,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 38,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Fitted ExtendedDMD model. Lookback length set to 1\n"
-     ]
-    },
-    {
-     "data": {
-      "text/plain": [
-       "<kooplearn.models.edmd.ExtendedDMD at 0x28ca9f690>"
-      ]
-     },
-     "execution_count": 38,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "model.fit(contexts['train'])"
    ]
@@ -243,18 +209,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 39,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Prediction error for train: 0.0569\n",
-      "Prediction error for test: 0.0507\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "from sklearn.metrics import mean_squared_error\n",
     "\n",
@@ -278,7 +235,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 40,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -288,31 +245,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "ename": "NameError",
-     "evalue": "name 'true_eigs' is not defined",
-     "output_type": "error",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
-      "\u001b[1;32m/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb Cell 18\u001b[0m line \u001b[0;36m5\n\u001b[1;32m      <a href='vscode-notebook-cell:/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb#X23sZmlsZQ%3D%3D?line=0'>1</a>\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39mmatplotlib\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mpyplot\u001b[39;00m \u001b[39mas\u001b[39;00m \u001b[39mplt\u001b[39;00m\n\u001b[1;32m      <a href='vscode-notebook-cell:/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb#X23sZmlsZQ%3D%3D?line=2'>3</a>\u001b[0m fig, ax \u001b[39m=\u001b[39m plt\u001b[39m.\u001b[39msubplots(figsize\u001b[39m=\u001b[39m(\u001b[39m5\u001b[39m, \u001b[39m5\u001b[39m))\n\u001b[0;32m----> <a href='vscode-notebook-cell:/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb#X23sZmlsZQ%3D%3D?line=4'>5</a>\u001b[0m ax\u001b[39m.\u001b[39mscatter(true_eigs\u001b[39m.\u001b[39mreal, true_eigs\u001b[39m.\u001b[39mimag, label\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mTrue eigenvalues\u001b[39m\u001b[39m'\u001b[39m, marker\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mx\u001b[39m\u001b[39m'\u001b[39m, color\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39m#9f1853\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m      <a href='vscode-notebook-cell:/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb#X23sZmlsZQ%3D%3D?line=5'>6</a>\u001b[0m ax\u001b[39m.\u001b[39mscatter(pred_eigs\u001b[39m.\u001b[39mreal, pred_eigs\u001b[39m.\u001b[39mimag, label\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mPredicted eigenvalues\u001b[39m\u001b[39m'\u001b[39m, marker \u001b[39m=\u001b[39m \u001b[39m'\u001b[39m\u001b[39m.\u001b[39m\u001b[39m'\u001b[39m, color\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39m#0f62fe\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m      <a href='vscode-notebook-cell:/Users/pietronovelli/code_repos/kooplearn/docs/getting_started.ipynb#X23sZmlsZQ%3D%3D?line=6'>7</a>\u001b[0m \u001b[39m# Plot unit circle\u001b[39;00m\n",
-      "\u001b[0;31mNameError\u001b[0m: name 'true_eigs' is not defined"
-     ]
-    },
-    {
-     "data": {
-      "image/png": "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r9a9996rn/3sZ3r55Zc1d+7ckz6nz+dTbW3tiE+XYmTsWXzYN+/Ys/iwb96N1555fh8hAACnEz5rFABgGiEEAJhGCAEAphFCAIBpkyaEfLWTd172bPPmzVq8eLFmzpypmTNnKhgMfu8en668/q59o6mpSUlJSVqyZMn4LnAS8rpnhw4dUlVVlXJycuTz+XTeeefx7+hJ7Ft9fb3OP/98TZs2TYFAQMuXL9dXX301QatNvLfeekulpaWaNWuWkpKSTviZ1N/Yvn27LrvsMvl8Pp177rl65plnvJ/YTQJNTU0uNTXVPf300+5f//qXu+WWW9yMGTNcT0/PiPPfeecdl5KS4h566CH34YcfutWrV7upU6e6999/f4JXnjhe92zp0qWuoaHB7dq1y+3evdv99re/dRkZGe7f//73BK88sbzu2zf279/vcnNz3eLFi92vfvWriVnsJOF1zwYGBlxhYaG75ppr3Ntvv+3279/vtm/f7jo6OiZ45Ynldd+ee+455/P53HPPPef279/vXnvtNZeTk+OWL18+wStPnObmZrdq1Sr30ksvOUlu69atJ5zf2dnpzjjjDBcKhdyHH37oHn30UZeSkuJaWlo8nXdShHDBggWuqqoq9vPQ0JCbNWuWq6urG3H+9ddf76699tphY0VFRe53v/vduK5zMvG6Z9917NgxN336dPfss8+O1xInpXj27dixY27hwoXuySefdBUVFeZC6HXPHn/8cTd79mw3ODg4UUuclLzuW1VVlfvFL34xbCwUCrlFixaN6zonq5MJ4T333OMuvvjiYWNlZWWupKTE07kS/tToN1/tFAwGY2Mn89VO/3++9PVXO402/3QTz55915EjR3T06NEx//DaySzefbv//vuVlZWlm266aSKWOanEs2evvPKKiouLVVVVJb/fr7lz52r9+vUaGhqaqGUnXDz7tnDhQrW3t8eePu3s7FRzc7OuueaaCVnzD9FYtSDh3z4xUV/tdDqJZ8++a8WKFZo1a9Zxv0Sns3j27e2339ZTTz2ljo6OCVjh5BPPnnV2duof//iHbrzxRjU3N2vfvn264447dPToUdXW1k7EshMunn1bunSpent7dcUVV8g5p2PHjum2227TvffeOxFL/kEarQWRSERffvmlpk2bdlL3k/ArQky8DRs2qKmpSVu3blVaWlqilzNpHT58WMuWLdPmzZuVmZmZ6OX8YESjUWVlZemJJ55QQUGBysrKtGrVKjU2NiZ6aZPa9u3btX79ej322GPauXOnXnrpJW3btk3r1q1L9NJOewm/Ipyor3Y6ncSzZ994+OGHtWHDBr3xxhu69NJLx3OZk47Xffv444/1ySefqLS0NDYWjUYlSVOmTNHevXs1Z86c8V10gsXzu5aTk6OpU6cqJSUlNnbhhRcqHA5rcHBQqamp47rmySCefVuzZo2WLVumm2++WZJ0ySWXqL+/X7feeqtWrVo15l89dDoYrQXp6eknfTUoTYIrQr7aybt49kySHnroIa1bt04tLS0qLCyciKVOKl737YILLtD777+vjo6O2O26667TVVddpY6ODgUCgYlcfkLE87u2aNEi7du3L/Y/DZL00UcfKScnx0QEpfj27ciRI8fF7pv/mXB8JPSIxqwF3l7HMz6ampqcz+dzzzzzjPvwww/drbfe6mbMmOHC4bBzzrlly5a5lStXxua/8847bsqUKe7hhx92u3fvdrW1tSbfPuFlzzZs2OBSU1Pdiy++6D7//PPY7fDhw4l6CAnhdd++y+KrRr3uWVdXl5s+fbr7/e9/7/bu3eteffVVl5WV5R544IFEPYSE8LpvtbW1bvr06e6vf/2r6+zsdH//+9/dnDlz3PXXX5+ohzDhDh8+7Hbt2uV27drlJLmNGze6Xbt2uU8//dQ559zKlSvdsmXLYvO/efvEH//4R7d7927X0NDww337hHPOPfroo+7ss892qampbsGCBe6f//xn7J9deeWVrqKiYtj8F154wZ133nkuNTXVXXzxxW7btm0TvOLE87Jn55xzjpN03K22tnbiF55gXn/X/j+LIXTO+569++67rqioyPl8Pjd79mz34IMPumPHjk3wqhPPy74dPXrU3XfffW7OnDkuLS3NBQIBd8cdd7j//ve/E7/wBHnzzTdH/O/UN/tUUVHhrrzyyuOOyc/Pd6mpqW727NnuL3/5i+fz8jVMAADTEv43QgAAEokQAgBMI4QAANMIIQDANEIIADCNEAIATCOEAADTCCEAwDRCCAAwjRACAEwjhAAA0wghAMC0/wPPG/tz4omkYgAAAABJRU5ErkJggg==",
-      "text/plain": [
-       "<Figure size 500x500 with 1 Axes>"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
+   "outputs": [],
    "source": [
     "import matplotlib.pyplot as plt\n",
     "\n",
@@ -336,6 +271,21 @@
    "source": [
     "As we can see, the model predicts very well the largest eigenvalue $\\lambda_{1} = 1$, and fairly well a complex-conjugate pair of eigenvalues with real part $\\approx - 0.25$. On the other hand, the cluster of eigenvalues around $0$ is predicted quite poorly. This is not surprising, however, as the noisy logsitcic map is known to be non-normal. Non-normality implies that the the eigenvalues of the matrix are extremely sensitive to perturbations, meaning that even a small error in estimating the Transfer operator (see the small prediction error above), does not guarantee a good estimation of the eigenvalues."
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Bibliography"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "```{footbibliography}\n",
+    "```"
+   ]
   }
  ],
  "metadata": {
diff --git a/docs/guides/data_specs.md b/docs/guides/data_specs.md
index 289aaae..9ea9c34 100644
--- a/docs/guides/data_specs.md
+++ b/docs/guides/data_specs.md
@@ -1,5 +1,8 @@
 (kooplearn_data_paradigm)=
 # Kooplearn's data paradigm
+
+> _Author:_ Pietro Novelli — [@pie_novelli](https://twitter.com/pie_novelli)
+
 In this guide we describe kooplearn's data paradigm, which is based on context windows. 
 
 <p align = "center">
diff --git a/docs/guides/extending_kooplearn.md b/docs/guides/extending_kooplearn.md
index 41a7d9a..d3f6382 100644
--- a/docs/guides/extending_kooplearn.md
+++ b/docs/guides/extending_kooplearn.md
@@ -1,2 +1,4 @@
 (extending_jax)=
-# Extending `kooplearn`: writing a custom model in JAX+Flax
\ No newline at end of file
+# Extending `kooplearn`: writing a custom model in JAX+Flax
+
+> _Author:_ Pietro Novelli — [@pie_novelli](https://twitter.com/pie_novelli)
\ No newline at end of file
diff --git a/docs/guides/index.rst b/docs/guides/index.rst
index 3caa9a8..6aa8c30 100644
--- a/docs/guides/index.rst
+++ b/docs/guides/index.rst
@@ -5,5 +5,3 @@ Guides
     :maxdepth: 4
 
     data_specs.md
-    extending_kooplearn.md
-
diff --git a/docs/index.rst b/docs/index.rst
index 0a93bf0..d6c8681 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -58,25 +58,37 @@ Features
    .. grid-item::
       :columns: 12 12 12 6
 
-      .. card:: Neural operators
+      .. card:: Deep Learning models
          :class-card: sd-border-0
          :shadow: none
          :class-title: sd-fs-5
 
          .. div:: sd-font-normal
 
-            Kooplearn implements many neural-network models to learn the Koopman/transfer operators. Kooplearn's Deep-Learning models are based upon `Pytorch Lightning <https://lightning.ai/>`_ for fast and easy training on CPU, GPU, and multi-GPU systems. Kooplearn's functionality, however, is not bound to Pytorch in any way. In ":ref:`extending kooplearn <extending_jax>`", for example, we show how to use `JAX+Flax <https://flax.readthedocs.io/en/latest/>`_ to implement a custom model.
+            Kooplearn implements many neural-network models to learn the Koopman/transfer operators. Kooplearn's Deep-Learning models are based upon `Pytorch Lightning <https://lightning.ai/>`_ for fast and easy training on CPU, GPU, and multi-GPU systems. 
 
 ----
 
 Installation
 ^^^^^^^^^^^^
 
+To install the core version of ``kooplearn``, without optional dependencies, run
+
 .. code-block:: bash
 
    pip install kooplearn
-   # Or to install the latest version of kooplearn from git:
-   pip install --upgrade git+https://github.com/CSML-IIT-UCL/kooplearn.git
+
+To install the full version of ``kooplearn``, including Neural-Network models, and the dahsboard, run
+
+.. code-block:: bash
+
+   pip install "kooplearn[full]"
+
+To install the development version of ``kooplearn``, run
+
+.. code-block:: bash
+
+   pip install --upgrade git+https://github.com/Machine-Learning-Dynamical-Systems/kooplearn.git
 
 
 Learn more
@@ -98,12 +110,13 @@ Learn more
          :class-card: sd-text-black sd-bg-light
          :link: guides/index.html
 
-   .. grid-item::
-      :columns: 6 6 6 4
+   ..
+      .. grid-item::
+         :columns: 6 6 6 4
 
-      .. card:: :material-regular:`science;2em` Examples
-         :class-card: sd-text-black sd-bg-light
-         :link: examples/index.html
+         .. card:: :material-regular:`science;2em` Examples
+            :class-card: sd-text-black sd-bg-light
+            :link: examples/index.html
 
 
    .. grid-item::
@@ -118,8 +131,10 @@ Learn more
 .. toctree::
    :maxdepth: 1
    
+   primer
    getting_started
-   examples/index
+   .. 
+      examples/index
    model_zoo
    guides/index
    api_reference/index
diff --git a/docs/model_zoo.md b/docs/model_zoo.md
index 79daf9c..c774c6e 100644
--- a/docs/model_zoo.md
+++ b/docs/model_zoo.md
@@ -3,13 +3,14 @@
 
 | Model | Kooplearn Implementation | Notes |
 | --- | :---: | --- |
-| DMD | {class}`kooplearn.models.DMD` | | 
-| Extended DMD | {class}`kooplearn.models.ExtendedDMD` | | 
-| Kernel DMD | {class}`kooplearn.models.KernelDMD` | | 
-| Nyström Kernel DMD{footcite:p}`Meanti2023` | {class}`kooplearn.models.NystromKernelDMD` | | 
+| Ridge Regression {footcite:p}`Kostic2022`|  {class}`kooplearn.models.DMD` or <br>{class}`kooplearn.models.ExtendedDMD` | Full-rank models. Set `rank` to `None` at initialization. |
+| Principal Component Regression {footcite:p}`Kostic2022`|  {class}`kooplearn.models.DMD`,<br>{class}`kooplearn.models.ExtendedDMD`,<br>{class}`kooplearn.models.KernelDMD`, or <br>{class}`kooplearn.models.NystroemKernel` | Low-rank models. Set `reduced_rank` to `False` at initialization. |
+| Reduced Rank Regression {footcite:p}`Kostic2022`|  {class}`kooplearn.models.DMD`,<br>{class}`kooplearn.models.ExtendedDMD`,<br>{class}`kooplearn.models.KernelDMD`, or <br>{class}`kooplearn.models.NystroemKernel` | Optimal{footcite:p}`Kostic2022, Kostic2023SpectralRates` low-rank models. Set `reduced_rank` to `True` at initialization. |
+| Randomized Solver {footcite:p}`Kostic2022`|  {class}`kooplearn.models.DMD`,<br>{class}`kooplearn.models.ExtendedDMD`, or<br>{class}`kooplearn.models.KernelDMD` | Set `svd_solver` to `'randomized'` at initialization. |
+| Nyström Kernel Regression{footcite:p}`Meanti2023` | {class}`kooplearn.models.NystroemKernel` | |
 | Hankel DMD {footcite:p}`Arbabi2017` | {class}`kooplearn.models.DMD`<br>{class}`kooplearn.models.ExtendedDMD`<br>{class}`kooplearn.models.KernelDMD` | Hankel DMD with an history of $m$ steps is obtained by fitting these models with dataset of context length $m + 1$ |
-| Reduced Rank DMD {footcite:p}`Kostic2022`|  {class}`kooplearn.models.DMD`<br>{class}`kooplearn.models.ExtendedDMD`<br>{class}`kooplearn.models.KernelDMD` | Reduced rank estimators are obtained by setting the `reduced_rank` flag to `True` at initialization |
 | VAMPNets{footcite:p}`Mardt2018, Wu2019` | {class}`kooplearn.models.DeepEDMD` + {class}`kooplearn.models.feature_maps.VAMPNet` | |
+| DPNets{footcite:p}`Kostic2023DPNets` | {class}`kooplearn.models.DeepEDMD` + {class}`kooplearn.models.feature_maps.DPNet` | |
 | Dynamic AutoEncoder{footcite:p}`Lusch2018, Morton2018` | {class}`kooplearn.models.DynamicAE` | When `use_lstsq_for_evolution == True`, the linear evolution of the embedded state is given by a least square model as in {footcite:t}`Morton2018`.|
 | Consistent AutoEncoder{footcite:p}`Azencot2020CAE` | {class}`kooplearn.models.ConsistentAE` | |
 
diff --git a/docs/primer.md b/docs/primer.md
new file mode 100644
index 0000000..94ca619
--- /dev/null
+++ b/docs/primer.md
@@ -0,0 +1,13 @@
+(primer)=
+# Operators and dynamical systems
+
+> _Authors:_ Pietro Novelli — [@pie_novelli](https://twitter.com/pie_novelli) & Vladimir Kostic — [@vkostic30](https://twitter.com/vkostic30)
+
+### Definitions
+Given a _nonlinear_ dynamical system $x_{t + 1} = S(x_{t})$, the **Koopman operator** provides a _global linearization_ of the dynamics by mapping it to a suitable space of observables $\mathcal{F}$. An observable is any (scalar) function of the state. The Koopman operator $\mathsf{K}$ is defined as
+
+$$(\mathsf{K}f)(x_{t}) = f(x_{t + 1}) := f \circ S (x_t) \qquad f \in \mathcal{F}.$$
+
+Similarly, given a stochastic process $X:= \{ X_{s} \colon s \in \mathbb{N}\}$, its **Transfer operator** returns the expected value of any observable forward in time. The Transfer operator $\mathsf{T}$ is defined as
+
+$$(\mathsf{T}f)(x) := \mathbb{E}\left[f(X_{t + 1}) \mid X_{t} = x \right] \qquad f \in \mathcal{F}.$$
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
index d49a229..84fdb10 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -11,4 +11,4 @@ jupytext==1.13.8
 sphinxcontrib-bibtex
 docutils == 0.16
 matplotlib
-kooplearn [torch] == 1.0.0.dev1
\ No newline at end of file
+kooplearn [torch] == 1.0.4
\ No newline at end of file
diff --git a/examples/ala2/get_dataset.py b/examples/ala2/get_dataset.py
new file mode 100644
index 0000000..fc358f2
--- /dev/null
+++ b/examples/ala2/get_dataset.py
@@ -0,0 +1,51 @@
+import functools
+import os
+import pathlib
+import shutil
+from warnings import warn
+
+import requests
+from tqdm.auto import tqdm
+
+base_url = "http://ftp.imp.fu-berlin.de/pub/cmb-data/"
+files = [
+    "alanine-dipeptide-3x250ns-backbone-dihedrals.npz",
+    "alanine-dipeptide-3x250ns-heavy-atom-distances.npz",
+    "alanine-dipeptide-3x250ns-heavy-atom-positions.npz",
+]
+
+
+def download(url, filename):
+    # Adapted from https://stackoverflow.com/questions/37573483/progress-bar-while-download-file-over-http-with-requests
+    r = requests.get(url, stream=True, allow_redirects=True)
+    if r.status_code != 200:
+        r.raise_for_status()  # Will only raise for 4xx codes, so...
+        raise RuntimeError(f"Request to {url} returned status code {r.status_code}")
+    file_size = int(r.headers.get("Content-Length", 0))
+
+    path = pathlib.Path(filename).expanduser().resolve()
+    path.parent.mkdir(parents=True, exist_ok=True)
+
+    desc = "(Unknown total file size)" if file_size == 0 else ""
+    r.raw.read = functools.partial(
+        r.raw.read, decode_content=True
+    )  # Decompress if needed
+    with tqdm.wrapattr(r.raw, "read", total=file_size, desc=desc) as r_raw:
+        with path.open("wb") as f:
+            shutil.copyfileobj(r_raw, f)
+    return path
+
+
+if __name__ == "__main__":
+    example_path = pathlib.Path(__file__).parent.resolve()
+    data_path = os.path.join(example_path, "__data__/")
+    if not os.path.exists(data_path):
+        os.makedirs(data_path)
+    else:
+        warn(
+            "Warning: data folder already exists. Removing it and downloading new data."
+        )
+        shutil.rmtree(data_path)
+        # Remove existing files
+    for file in files:
+        download(base_url + file, os.path.join(data_path, file))
diff --git a/kooplearn/_src/linalg.py b/kooplearn/_src/linalg.py
index 05cfb7e..49d4187 100644
--- a/kooplearn/_src/linalg.py
+++ b/kooplearn/_src/linalg.py
@@ -205,11 +205,12 @@ def modified_QR(
         return Q[:, :effective_rank], R[:effective_rank]
 
 
-def _rank_reveal(
+def eigh_rank_reveal(
     values: np.ndarray,
     vectors: np.ndarray,
     rank: int,  # Desired rank
     rcond: Optional[float] = None,  # Threshold for the singular values
+    verbose: bool = True,
 ):
     if rcond is None:
         rcond = 10.0 * values.shape[0] * np.finfo(values.dtype).eps
@@ -221,24 +222,19 @@ def _rank_reveal(
     if all(_ftest):
         rsqrt_vals = (np.sqrt(values)) ** -1
     else:
+        first_invalid = np.argmax(
+            ~_ftest
+        )  # In the case of multiple occurrences of the maximum values, the indices corresponding to the first occurrence are returned.
+        _first_discarded_val = np.max(np.abs(values[first_invalid:]))
         values = values[_ftest]
         vectors = vectors[:, _ftest]
-        logger.warning(
-            f"The numerical rank of the result ({vectors.shape[1]}) is smaller than the desired rank ({rank}).\n {rank - vectors.shape[1]} degrees of freedom will be ignored."
-        )
+
+        if verbose:
+            logger.warning(
+                f"Warning: Discarted {rank - vectors.shape[1]} dimensions of the {rank} requested due to numerical instability. Consider decreasing the rank. The largest discarded value is: {_first_discarded_val:.3e}."
+            )
         # Compute stable sqrt
         rsqrt_vals = (np.sqrt(values)) ** -1
-        # Fill the missing values with zeroes
-        num_missing = rank - values.shape[0]
-        vectors = np.concatenate(
-            [vectors, np.zeros((vectors.shape[0], num_missing))], axis=1
-        )
-        values = np.concatenate([values, np.zeros(num_missing)])
-        rsqrt_vals = np.concatenate([rsqrt_vals, np.zeros(num_missing)])
-
-    assert vectors.shape[1] == rank
-    assert values.shape[0] == rank
-    assert rsqrt_vals.shape[0] == rank
     return vectors, values, rsqrt_vals
 
 
diff --git a/kooplearn/_src/operator_regression/dual.py b/kooplearn/_src/operator_regression/dual.py
index b0a4f3c..f16f0e9 100644
--- a/kooplearn/_src/operator_regression/dual.py
+++ b/kooplearn/_src/operator_regression/dual.py
@@ -2,17 +2,29 @@
 from typing import Optional
 
 import numpy as np
-from scipy.linalg import LinAlgError, eig, eigh, lstsq, pinvh
-from scipy.sparse.linalg import eigs, eigsh, lsqr
-from scipy.sparse.linalg._eigen.arpack.arpack import IterInv
+from scipy.linalg import cho_factor, cho_solve, eig, eigh, lstsq, qr
+from scipy.sparse.linalg import eigs, eigsh
 from sklearn.utils.extmath import randomized_svd
 
-from kooplearn._src.linalg import _rank_reveal, modified_QR, weighted_norm
+from kooplearn._src.linalg import eigh_rank_reveal, weighted_norm
 from kooplearn._src.utils import fuzzy_parse_complex, topk
 
 logger = logging.getLogger("kooplearn")
 
 
+def postprocess_UV(U, V, rank):
+    assert U.shape == V.shape
+    if U.shape[1] < rank:
+        logger.warning(
+            f"Warning: The fitting algorithm discarded {rank - U.shape[1]} dimensions of the {rank} requested out of numerical instabilities.\nThe rank attribute has been updated to {U.shape[1]}.\nConsider decreasing the rank parameter."
+        )
+    else:
+        # Assuming that everything is in decreasing order
+        U = U[:, :rank]
+        V = V[:, :rank]
+    return U, V
+
+
 def regularize(M: np.ndarray, reg: float):
     """Regularize a matrix by adding a multiple of the identity matrix to it.
     Args:
@@ -21,146 +33,190 @@ def regularize(M: np.ndarray, reg: float):
     Returns:
         np.ndarray: Regularized matrix.
     """
-    return M + reg * M.shape[0] * np.identity(M.shape[0], dtype=M.dtype)
+    return M + (M.shape[0] * reg) * np.identity(M.shape[0], dtype=M.dtype)
+
+
+def add_diagonal(M: np.ndarray, alpha: float):
+    np.fill_diagonal(M, M.diagonal() + alpha)
+
+
+# Reduced Rank Algorithms
+def _filter_reduced_rank_svals(values, vectors, rank):
+    eps = 2 * np.finfo(vectors.dtype).eps
+    # Filtering procedure.
+    # Create a mask which is True when the real part of the eigenvalue is negative or the imaginary part is nonzero
+    is_invalid = np.logical_or(np.abs(np.real(values)) <= eps, np.imag(values) != 0)
+    # Check if any is invalid take the first occurrence of a True value in the mask and filter everything after that
+    if np.any(is_invalid):
+        values = values[~is_invalid].real
+        vectors = vectors[:, ~is_invalid]
+
+    sort_perm = topk(values, len(values)).indices
+    values = values[sort_perm]
+    vectors = vectors[:, sort_perm]
+
+    # Assert that the eigenvectors do not have any imaginary part
+    assert np.all(
+        np.imag(vectors) == 0
+    ), "The eigenvectors should be real. Decrease the rank or increase the regularization strength."
+
+    # Take the real part of the eigenvectors
+    vectors = np.real(vectors)
+    values = np.real(values)
+    return values, vectors
 
 
 def fit_reduced_rank_regression(
-    K_X: np.ndarray,  # Kernel matrix of the input data
-    K_Y: np.ndarray,  # Kernel matrix of the output data
+    kernel_X: np.ndarray,  # Kernel matrix of the input data
+    kernel_Y: np.ndarray,  # Kernel matrix of the output data
     tikhonov_reg: float,  # Tikhonov regularization parameter, can be 0
     rank: int,  # Rank of the estimator
     svd_solver: str = "arnoldi",  # SVD solver to use. 'arnoldi' is faster but might be numerically unstable.
-    _return_singular_values: bool = False
-    # Whether to return the singular values of the projector. (Development purposes)
-) -> tuple[np.ndarray, np.ndarray] or tuple[np.ndarray, np.ndarray, np.ndarray]:
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
     if tikhonov_reg == 0.0:
         return _fit_reduced_rank_regression_noreg(
-            K_X,
-            K_Y,
-            rank,
-            svd_solver=svd_solver,
-            _return_singular_values=_return_singular_values,
+            kernel_X, kernel_Y, rank, svd_solver=svd_solver
         )
     else:
-        dim = K_X.shape[0]
-        rsqrt_dim = dim ** (-0.5)
-        # Rescaled Kernel matrices
-        K_Xn = K_X * rsqrt_dim
-        K_Yn = K_Y * rsqrt_dim
+        n_pts = kernel_X.shape[0]
+        eps = 1000.0 * np.finfo(kernel_X.dtype).eps
+        penalty = max(eps, tikhonov_reg) * n_pts
+        A = np.multiply(kernel_Y, n_pts ** (-0.5)) @ np.multiply(
+            kernel_X, n_pts ** (-0.5)
+        )
 
-        K = K_Yn @ K_Xn
+        add_diagonal(kernel_X, penalty)
         # Find U via Generalized eigenvalue problem equivalent to the SVD. If K is ill-conditioned might be slow.
         # Prefer svd_solver == 'randomized' in such a case.
         if svd_solver == "arnoldi":
             # Adding a small buffer to the Arnoldi-computed eigenvalues.
-            sigma_sq, U = eigs(K, rank + 3, regularize(K_X, tikhonov_reg))
+            _num_arnoldi_eigs = min(rank + 5, A.shape[0])
+            values, vectors = eigs(A, k=_num_arnoldi_eigs, M=kernel_X)
         else:  # 'full'
-            sigma_sq, U = eig(K, regularize(K_X, tikhonov_reg))
+            values, vectors = eig(A, kernel_X, overwrite_a=True, overwrite_b=True)
+        add_diagonal(kernel_X, -penalty)
 
-        max_imag_part = np.max(U.imag)
-        if max_imag_part >= 10.0 * U.shape[0] * np.finfo(U.dtype).eps:
+        values, vectors = _filter_reduced_rank_svals(values, vectors, rank)
+        # Compare the filtered eigenvalues with the regularization strength, and warn if there are any eigenvalues that are smaller than the regularization strength.
+        if not np.all(np.abs(values) >= tikhonov_reg):
             logger.warning(
-                f"The computed projector is not real. The Kernel matrix is severely ill-conditioned."
+                f"Warning: {(np.abs(values) < tikhonov_reg).sum()} out of the {len(values)} squared singular values are smaller than the regularization strength {tikhonov_reg:.2e}. Consider redudcing the regularization strength to avoid overfitting."
             )
-        U = np.real(U)
-        # Post-process U. Promote numerical stability via additional QR decoposition if necessary.
-        U = U[:, topk(sigma_sq.real, rank).indices]
-
-        norm_inducing_op = (K_Xn @ K_Xn.T) + tikhonov_reg * K_X
-        U, _, columns_permutation = modified_QR(
-            U, M=norm_inducing_op, column_pivoting=True
-        )
-        U = U[:, np.argsort(columns_permutation)]
-        if U.shape[1] < rank:
-            logger.warning(
-                f"The numerical rank of the projector is smaller than the selected rank ({rank}). {rank - U.shape[1]} "
-                f"degrees of freedom will be ignored."
-            )
-            _zeroes = np.zeros((U.shape[0], rank - U.shape[1]))
-            U = np.c_[U, _zeroes]
-            assert U.shape[1] == rank
-        V = K_X @ np.asfortranarray(U)
-        if _return_singular_values:
-            return U, V, sigma_sq
-        else:
-            return U, V
+        # Eigenvector normalization
+        kernel_X_vecs = np.dot(np.multiply(kernel_X, n_pts ** (-0.5)), vectors)
+        vecs_norms = np.sum(
+            kernel_X_vecs**2
+            + tikhonov_reg * kernel_X_vecs * vectors * (n_pts**0.5),
+            axis=0,
+        ) ** (0.5)
+        U = vectors / vecs_norms
+        # Ordering the results
+        sort_perm = topk(values, len(values)).indices
+        U = U[:, sort_perm]
+        V = kernel_X @ U
+        svals = np.flip(np.sort(np.abs(values))) ** 0.5
+        U, V = postprocess_UV(U, V, rank)
+        return U, V, svals
 
 
 def _fit_reduced_rank_regression_noreg(
-    K_X: np.ndarray,  # Kernel matrix of the input data
-    K_Y: np.ndarray,  # Kernel matrix of the output data
+    kernel_X: np.ndarray,  # Kernel matrix of the input data
+    kernel_Y: np.ndarray,  # Kernel matrix of the output data
     rank: int,  # Rank of the estimator
     svd_solver: str = "arnoldi",  # Solver for the generalized eigenvalue problem. 'arnoldi' or 'full'
-    _return_singular_values: bool = False
-    # Whether to return the singular values of the projector. (Development purposes)
-) -> tuple[np.ndarray, np.ndarray] or tuple[np.ndarray, np.ndarray, np.ndarray]:
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
     # Solve the Hermitian eigenvalue problem to find V
     logger.warning(
         "The least-squares solution (tikhonov_reg == 0) of the reduced rank problem in the kernel setting is computationally very inefficient. Consider adding a small regularization parameter."
     )
 
-    values_X, U_X = eigh(K_X)
-    U_X, _, _ = _rank_reveal(values_X, U_X, K_X.shape[0])
+    values_X, U_X = eigh(kernel_X)
+    U_X, _, _ = eigh_rank_reveal(values_X, U_X, kernel_X.shape[0], verbose=False)
     proj_X = U_X @ U_X.T
-    L = proj_X @ K_Y
+    L = proj_X @ kernel_Y
     if svd_solver != "full":
-        sigma_sq, V = eigs(L, rank + 3)
+        values, vectors = eigs(L, rank + 3)
     else:
-        sigma_sq, V = eig(L)
-
-    max_imag_part = np.max(V.imag)
-    if max_imag_part >= 10.0 * V.shape[0] * np.finfo(V.dtype).eps:
-        logger.warning(
-            f"The computed projector is not real. The Kernel matrix is severely ill-conditioned."
-        )
-    V = np.real(V)
-    V = V[:, topk(sigma_sq.real, rank).indices]
+        values, vectors = eig(L, overwrite_a=True)
+    values, V = _filter_reduced_rank_svals(values, vectors, rank)
     # Normalize V
     _V_norm = np.linalg.norm(V, ord=2, axis=0) / np.sqrt(V.shape[0])
-    rcond = 10.0 * K_X.shape[0] * np.finfo(K_X.dtype).eps
-    _inv_V_norm = np.where(_V_norm < rcond, 0.0, _V_norm**-1)
-    V = V @ np.diag(_inv_V_norm)
-
+    eps = 1000.0 * np.finfo(kernel_X.dtype).eps * V.shape[0]
+    _inv_V_norm = np.where(_V_norm < eps, 0.0, _V_norm**-1)
+    V = V / _inv_V_norm
     # Solve the least squares problem to determine U
-    if svd_solver != "full":
-        U = np.zeros_like(V)
-        for i in range(U.shape[1]):
-            U[:, i] = lsqr(K_X, V[:, i])[0]  # Not optimal with this explicit loop
-    else:
-        U = lstsq(K_X, V)[0]
-    if _return_singular_values:
-        return U, V, topk(sigma_sq.real, rank).values
+    U = lstsq(kernel_X, V)[0]
+    svals = np.flip(np.sort(np.abs(values))) ** 0.5
+    U, V = postprocess_UV(U, V, rank)
+    return U, V, svals
+
+
+def fit_nystroem_reduced_rank_regression(
+    kernel_X: np.ndarray,  # Kernel matrix of the input inducing points
+    kernel_Y: np.ndarray,  # Kernel matrix of the output inducing points
+    kernel_Xnys: np.ndarray,  # Kernel matrix between the input data and the input inducing points
+    kernel_Ynys: np.ndarray,  # Kernel matrix between the output data and the output inducing points
+    tikhonov_reg: float = 0.0,  # Tikhonov regularization parameter
+    rank: Optional[int] = None,  # Rank of the estimator
+    svd_solver: str = "arnoldi",  # Solver for the generalized eigenvalue problem. 'arnoldi' or 'full'
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+    dim = kernel_Xnys.shape[0]
+    num_centers = kernel_X.shape[0]
+
+    eps = 1000 * np.finfo(kernel_X.dtype).eps * num_centers
+    reg = max(eps, tikhonov_reg)
+
+    # LHS of the generalized eigenvalue problem
+    rsqrt_Mn = (num_centers * dim) ** -0.5
+    kernel_YX_nys = (rsqrt_Mn * kernel_Ynys.T) @ (rsqrt_Mn * kernel_Xnys)
+
+    _tmp_YX = lstsq(kernel_Y * (num_centers**-1), kernel_YX_nys)[0]
+    kernel_XYX = kernel_YX_nys.T @ _tmp_YX
+
+    # RHS of the generalized eigenvalue problem
+    kernel_Xnys_sq = (rsqrt_Mn * kernel_Xnys.T) @ (
+        kernel_Xnys * rsqrt_Mn
+    ) + reg * kernel_X * (num_centers**-1)
+
+    add_diagonal(kernel_Xnys_sq, eps)
+    A = lstsq(kernel_Xnys_sq, kernel_XYX)[0]
+    if svd_solver == "full":
+        values, vectors = eigh(
+            kernel_XYX, kernel_Xnys_sq
+        )  # normalization leads to needing to invert evals
+    elif svd_solver == "arnoldi":
+        _oversampling = max(10, 4 * int(np.sqrt(rank)))
+        _num_arnoldi_eigs = min(rank + _oversampling, kernel_X.shape[0])
+        values, vectors = eigs(kernel_XYX, k=_num_arnoldi_eigs, M=kernel_Xnys_sq)
     else:
-        return U, V
-
+        raise ValueError(f"Unknown svd_solver {svd_solver}")
+    add_diagonal(kernel_Xnys_sq, -eps)
 
-def fit_nystrom_reduced_rank_regression_tikhonov(
-    N_X: np.ndarray,  # Kernel matrix of the input inducing points
-    N_Y: np.ndarray,  # Kernel matrix of the output inducing points
-    KN_X: np.ndarray,  # Kernel matrix between the input data and the input inducing points
-    KN_Y: np.ndarray,  # Kernel matrix between the output data and the output inducing points
-    tikhonov_reg: float,  # Tikhonov regularization parameter
-    rank: int,  # Rank of the estimator
-) -> tuple[np.ndarray, np.ndarray]:
-    num_training_pts = KN_X.shape[0]
-    NKy_KNx = KN_Y.T @ KN_X
-    _B = lstsq(N_Y, NKy_KNx)[0]
-    NKN = NKy_KNx.T @ _B
-    G = KN_X.T @ KN_X + tikhonov_reg * num_training_pts * N_X
-    S, W = eigh(NKN, G)
-    # Low-rank projection
-    W = W[:, topk(S, rank).indices]
-    # Normalize the eigenvectors
-    W = W @ np.diag(weighted_norm(W, NKN) ** -1)
-    V = _B @ W
-    U = NKN @ W
-    U = lstsq(G, U)[0]
-    return U, V
+    values, vectors = _filter_reduced_rank_svals(values, vectors, rank)
+    # Compare the filtered eigenvalues with the regularization strength, and warn if there are any eigenvalues that are smaller than the regularization strength.
+    if not np.all(np.abs(values) >= tikhonov_reg):
+        logger.warning(
+            f"Warning: {(np.abs(values) < tikhonov_reg).sum()} out of the {len(values)} squared singular values are smaller than the regularization strength {tikhonov_reg:.2e}. Consider redudcing the regularization strength to avoid overfitting."
+        )
+    # Eigenvector normalization
+    normalization_csts = (
+        np.abs(np.sum(vectors.conj() * (kernel_XYX @ vectors), axis=0))
+    ) ** 0.5
+    has_small_norm = normalization_csts < 1000.0 * np.finfo(vectors.dtype).eps
+    vectors = vectors[:, ~has_small_norm] / normalization_csts[~has_small_norm]
+    # Ordering the results
+    sort_perm = topk(values, len(values)).indices
+    vectors = vectors[:, sort_perm]
+    U = A @ vectors
+    V = _tmp_YX @ vectors
+    svals = np.flip(np.sort(np.abs(values))) ** 0.5
+    U, V = postprocess_UV(U, V, rank)
+    return U.real, V.real, svals
 
 
 def fit_rand_reduced_rank_regression(
-    K_X: np.ndarray,  # Kernel matrix of the input data
-    K_Y: np.ndarray,  # Kernel matrix of the output data
+    kernel_X: np.ndarray,  # Kernel matrix of the input data
+    kernel_Y: np.ndarray,  # Kernel matrix of the output data
     tikhonov_reg: float,  # Tikhonov regularization parameter
     rank: int,  # Rank of the estimator
     n_oversamples: int,  # Number of oversamples
@@ -169,115 +225,167 @@ def fit_rand_reduced_rank_regression(
     rng_seed: Optional[
         int
     ] = None,  # Seed for the random number generator (for reproducibility)
-    _return_singular_values: bool = False
-    # Whether to return the singular values of the projector. (Development purposes)
-) -> tuple[np.ndarray, np.ndarray] or tuple[np.ndarray, np.ndarray, np.ndarray]:
-    dim = K_X.shape[0]
-    inv_dim = dim ** (-1.0)
-    alpha = dim * tikhonov_reg
-    tikhonov = np.identity(dim, dtype=K_X.dtype) * alpha
-    K_reg_inv = IterInv(K_X + tikhonov)
-    l = rank + n_oversamples
+    precomputed_cholesky=None,  # Precomputed Cholesky decomposition. Should be the output of cho_factor evaluated on the regularized kernel matrix.
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
     rng = np.random.default_rng(rng_seed)
+    dim = kernel_X.shape[0]
+    inv_dim = dim ** (-1.0)
+
+    penalty = dim * tikhonov_reg
+    add_diagonal(kernel_X, penalty)
+    if precomputed_cholesky is None:
+        cholesky_decomposition = cho_factor(kernel_X)
+    else:
+        cholesky_decomposition = precomputed_cholesky
+    add_diagonal(kernel_X, -penalty)
+
+    sketch_dimension = rank + n_oversamples
+
     if optimal_sketching:
-        Cov = inv_dim * K_Y
-        Om = rng.multivariate_normal(np.zeros(dim, dtype=K_X.dtype), Cov, size=l).T
+        Cov = inv_dim * kernel_Y
+        sketch = rng.multivariate_normal(
+            np.zeros(dim, dtype=kernel_Y.dtype), Cov, size=sketch_dimension
+        ).T
     else:
-        Om = rng.standard_normal(size=(dim, l))
+        sketch = rng.standard_normal(size=(dim, sketch_dimension))
 
     for _ in range(iterated_power):
         # Powered randomized rangefinder
-        Om = (inv_dim * K_Y) @ (Om - alpha * K_reg_inv @ Om)
-    KOm = K_reg_inv @ Om
-    KOmp = Om - alpha * KOm
-
-    F_0 = Om.T @ KOmp
-    F_1 = KOmp.T @ (inv_dim * (K_Y @ KOmp))
-
-    # Generation of matrices U and V.
-    try:
-        sigma_sq, Q = eigh(F_1, F_0)
-    except LinAlgError:
-        sigma_sq, Q = eig(pinvh(F_0) @ F_1)
-
-    Q_norm = np.sum(Q.conj() * (F_0 @ Q), axis=0)
-    Q = Q @ np.diag(Q_norm**-0.5)
-    _idxs = topk(sigma_sq.real, rank).indices
-    sigma_sq = sigma_sq.real
-
-    Q = Q[:, _idxs]
-    U = (dim**0.5) * np.asfortranarray(KOm @ Q)
-    V = (dim**0.5) * np.asfortranarray(KOmp @ Q)
-    if _return_singular_values:
-        return U.real, V.real, sigma_sq
-    else:
-        return U.real, V.real
+        sketch = (inv_dim * kernel_Y) @ (
+            sketch - penalty * cho_solve(cholesky_decomposition, sketch)
+        )
+        sketch, _ = qr(sketch, mode="economic")  # QR re-orthogonalization
+
+    kernel_X_sketch = cho_solve(cholesky_decomposition, sketch)
+    _M = sketch - penalty * kernel_X_sketch
+
+    F_0 = sketch.T @ sketch - penalty * (sketch.T @ kernel_X_sketch)  # Symmetric
+    F_0 = 0.5 * (F_0 + F_0.T)
+    F_1 = _M.T @ (inv_dim * (kernel_Y @ _M))
+
+    values, vectors = eig(lstsq(F_0, F_1)[0])
+
+    values, vectors = _filter_reduced_rank_svals(values, vectors, rank)
+    # Remove elements in the kernel of F_0
+    _threshold = 1000.0 * np.finfo(vectors.dtype).eps * vectors.shape[0]
+    relative_norm_sq = (
+        np.sum(vectors.conj() * (F_0 @ vectors), axis=0)
+        / np.linalg.norm(vectors, axis=0) ** 2
+    )
+    is_in_kernel = np.abs(relative_norm_sq) < _threshold
+    num_in_kernel = is_in_kernel.sum()
+    if num_in_kernel > 0:
+        # Logging. Print the number of discarded eigenvalues.
+        values = values[~is_in_kernel]
+        vectors = vectors[:, ~is_in_kernel]
+
+    normalization_csts = (np.sum(vectors.conj() * (F_0 @ vectors), axis=0).real) ** 0.5
+    vectors = vectors / normalization_csts
+
+    sort_perm = topk(values, len(values)).indices
+    vectors = vectors[:, sort_perm]
 
+    U = (dim**0.5) * kernel_X_sketch @ vectors
+    V = (dim**0.5) * _M @ vectors
 
+    svals = np.flip(np.sort(values)) ** 0.5
+    U, V = postprocess_UV(U, V, rank)
+    return U.real, V.real, svals
+
+
+# Principal Component Algorithms
 def fit_principal_component_regression(
-    K_X: np.ndarray,  # Kernel matrix of the input data
+    kernel_X: np.ndarray,  # Kernel matrix of the input data
     tikhonov_reg: float = 0.0,  # Tikhonov regularization parameter, can be zero
     rank: Optional[int] = None,  # Rank of the estimator
     svd_solver: str = "arnoldi",  # Solver for the generalized eigenvalue problem. 'arnoldi' or 'full'
 ) -> tuple[np.ndarray, np.ndarray]:
-    dim = K_X.shape[0]
-
-    if rank is None:
-        rank = dim
-    assert rank <= dim, f"Rank too high. The maximum value for this problem is {dim}"
-    reg_K_X = regularize(K_X, tikhonov_reg)
+    dim = kernel_X.shape[0]
+    add_diagonal(kernel_X, dim * tikhonov_reg)
     if svd_solver == "arnoldi":
-        values, vectors = eigsh(reg_K_X, k=rank + 3)
+        _num_arnoldi_eigs = min(rank + 5, kernel_X.shape[0])
+        values, vectors = eigsh(kernel_X, k=_num_arnoldi_eigs)
     elif svd_solver == "full":
-        values, vectors = eigh(reg_K_X)
+        values, vectors = eigh(kernel_X)
     else:
         raise ValueError(f"Unknown svd_solver {svd_solver}")
-    vectors, values, rsqrt_values = _rank_reveal(values, vectors, rank)
-    vectors = np.sqrt(dim) * vectors @ np.diag(rsqrt_values)
-    return vectors, vectors
+    add_diagonal(kernel_X, -dim * tikhonov_reg)
+
+    vectors, values, rsqrt_values = eigh_rank_reveal(values, vectors, rank)
+    Q = np.sqrt(dim) * vectors * (rsqrt_values)
+    kernel_X_eigvalsh = (np.flip(np.sort(np.abs(values))) / dim) ** 0.5
+    Q, Q = postprocess_UV(Q, Q, rank)
+    return Q, Q, kernel_X_eigvalsh
 
 
-def fit_nystrom_tikhonov(
-    N_X: np.ndarray,  # Kernel matrix of the input inducing points
-    N_Y: np.ndarray,  # Kernel matrix of the output inducing points
-    KN_X: np.ndarray,  # Kernel matrix between the input data and the input inducing points
-    KN_Y: np.ndarray,  # Kernel matrix between the output data and the output inducing points
+def fit_nystroem_principal_component_regression(
+    kernel_X: np.ndarray,  # Kernel matrix of the input inducing points
+    kernel_Y: np.ndarray,  # Kernel matrix of the output inducing points
+    kernel_Xnys: np.ndarray,  # Kernel matrix between the input data and the input inducing points
+    kernel_Ynys: np.ndarray,  # Kernel matrix between the output data and the output inducing points
     tikhonov_reg: float = 0.0,  # Tikhonov regularization parameter (can be 0)
-    # rank: Optional[int] = None,  # Rank of the estimator
-) -> tuple[np.ndarray, np.ndarray]:
-    # TODO Not using the Rank parameter fix it.
-    num_training_pts = KN_X.shape[0]
-    NKy_KNx = KN_Y.T @ KN_X
-    G = KN_X.T @ KN_X + tikhonov_reg * num_training_pts * N_X
-    U = lstsq(G, NKy_KNx)[0]
-    V = lstsq(N_Y, U)[0]
-    return U, V
+    rank: Optional[int] = None,  # Rank of the estimator
+    svd_solver: str = "arnoldi",  # Solver for the generalized eigenvalue problem. 'arnoldi' or 'full'
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+    dim = kernel_X.shape[0]
+    eps = 1000 * np.finfo(kernel_X.dtype).eps
+    reg = max(eps, tikhonov_reg)
+    kernel_Xnys_sq = kernel_Xnys.T @ kernel_Xnys
+    add_diagonal(kernel_X, reg * dim)
+    if svd_solver == "full":
+        values, vectors = eigh(
+            kernel_Xnys_sq, kernel_X
+        )  # normalization leads to needing to invert evals
+    elif svd_solver == "arnoldi":
+        _oversampling = max(10, 4 * int(np.sqrt(rank)))
+        _num_arnoldi_eigs = min(rank + _oversampling, kernel_X.shape[0])
+        values, vectors = eigsh(
+            kernel_Xnys_sq,
+            M=kernel_X,
+            k=_num_arnoldi_eigs,
+            which="LM",
+        )
+    else:
+        raise ValueError(f"Unknown svd_solver {svd_solver}")
+    add_diagonal(kernel_X, -reg * dim)
+    vectors, values, rsqrt_values = eigh_rank_reveal(values, vectors, rank)
+
+    U = np.sqrt(dim) * vectors * (rsqrt_values)
+    V = np.linalg.multi_dot([kernel_Ynys.T, kernel_Xnys, vectors])
+    V = lstsq(kernel_Y, V)[0]
+    V = np.sqrt(dim) * V * (rsqrt_values)
+
+    kernel_X_eigvalsh = (np.flip(np.sort(np.abs(values))) / kernel_Xnys.shape[0]) ** 0.5
+    U, V = postprocess_UV(U, V, rank)
+    return U, V, kernel_X_eigvalsh
 
 
 def fit_rand_principal_component_regression(
-    K_X: np.ndarray,  # Kernel matrix of the input data
+    kernel_X: np.ndarray,  # Kernel matrix of the input data
     tikhonov_reg: float,  # Tikhonov regularization parameter
     rank: int,  # Rank of the estimator
     n_oversamples: int,  # Number of oversamples
     iterated_power: int,  # Number of iterations for the power method
     rng_seed: Optional[int] = None,  # Seed for the random number generator
-    _return_singular_values: bool = False
-    # Whether to return the singular values of the projector. (Development purposes)
 ):
-    dim = K_X.shape[0]
+    dim = kernel_X.shape[0]
+    add_diagonal(kernel_X, dim * tikhonov_reg)
     vectors, values, _ = randomized_svd(
-        regularize(K_X, tikhonov_reg),
+        kernel_X,
         rank,
         n_oversamples=n_oversamples,
         n_iter=iterated_power,
         random_state=rng_seed,
     )
-    vectors, values, rsqrt_values = _rank_reveal(values, vectors, rank)
-    vectors = np.sqrt(dim) * vectors @ np.diag(rsqrt_values)
-    if _return_singular_values:
-        return vectors, vectors, values
-    else:
-        return vectors, vectors
+    add_diagonal(kernel_X, -dim * tikhonov_reg)
+
+    filtered_vectors, filtered_values, rsqrt_values = eigh_rank_reveal(
+        values, vectors, rank
+    )
+    Q = np.sqrt(dim) * filtered_vectors * (rsqrt_values)
+    kernel_X_eigvalsh = (np.flip(np.sort(np.abs(values))) / dim) ** 0.5
+    Q, Q = postprocess_UV(Q, Q, rank)
+    return Q, Q, kernel_X_eigvalsh
 
 
 def predict(
@@ -319,15 +427,16 @@ def estimator_eig(
     vl = vl[:, l_perm]
     values = values[r_perm]
 
+    rcond = 1000.0 * np.finfo(U.dtype).eps
     # Normalization in RKHS
     norm_r = weighted_norm(vr, W_X)
-    r_normr = np.where(norm_r == 0.0, 0.0, norm_r**-1)
-    vr = vr @ np.diag(r_normr)
+    norm_r = np.where(norm_r < rcond, np.inf, norm_r)
+    vr = vr / norm_r
 
     # Bi-orthogonality of left eigenfunctions
     norm_l = np.diag(np.linalg.multi_dot([vl.T, W_YX, vr]))
-    r_norm_l = np.where(np.abs(norm_l) == 0, 0.0, norm_l**-1)
-    vl = vl * r_norm_l
+    norm_l = np.where(np.abs(norm_l) < rcond, np.inf, norm_l)
+    vl = vl / norm_l
     return values, V @ vl, U @ vr
 
 
diff --git a/kooplearn/_src/operator_regression/primal.py b/kooplearn/_src/operator_regression/primal.py
index 1ebaf30..3f031cb 100644
--- a/kooplearn/_src/operator_regression/primal.py
+++ b/kooplearn/_src/operator_regression/primal.py
@@ -5,7 +5,7 @@
 from scipy.sparse.linalg import eigsh
 from sklearn.utils.extmath import randomized_svd
 
-from kooplearn._src.linalg import _rank_reveal, spd_neg_pow, weighted_norm
+from kooplearn._src.linalg import eigh_rank_reveal, spd_neg_pow, weighted_norm
 from kooplearn._src.utils import fuzzy_parse_complex, topk
 
 
@@ -50,7 +50,7 @@ def _fit_reduced_rank_regression_noreg(
         values, vectors = eigsh(_crcov, rank + 3)
     else:
         values, vectors = eigh(_crcov)
-    vectors, _vals, _ = _rank_reveal(values, vectors, rank)
+    vectors, _vals, _ = eigh_rank_reveal(values, vectors, rank)
     return rsqrt_C_X @ vectors
 
 
@@ -94,8 +94,6 @@ def fit_principal_component_regression(
     svd_solver: str = "arnoldi",  # SVD solver to use. Arnoldi is faster for low ranks.
 ):
     dim = C_X.shape[0]
-    if rank is None:
-        rank = dim
     assert rank <= dim, f"Rank too high. The maximum value for this problem is {dim}"
     reg_input_covariance = C_X + tikhonov_reg * np.identity(dim, dtype=C_X.dtype)
     if svd_solver == "arnoldi":
@@ -105,7 +103,7 @@ def fit_principal_component_regression(
     else:
         raise ValueError(f"Unknown svd_solver {svd_solver}")
 
-    vectors, _, rsqrt_evals = _rank_reveal(values, vectors, rank)
+    vectors, _, rsqrt_evals = eigh_rank_reveal(values, vectors, rank)
     return vectors @ np.diag(rsqrt_evals)
 
 
@@ -119,8 +117,6 @@ def fit_rand_principal_component_regression(
     rng_seed: Optional[int] = None,  # Random seed
 ):
     dim = C_X.shape[0]
-    if rank is None:
-        rank = dim
     assert rank <= dim, f"Rank too high. The maximum value for this problem is {dim}"
     reg_input_covariance = C_X + tikhonov_reg * np.identity(dim, dtype=C_X.dtype)
 
@@ -132,7 +128,7 @@ def fit_rand_principal_component_regression(
         random_state=rng_seed,
     )
 
-    vectors, _, rsqrt_evals = _rank_reveal(values, vectors, rank, rcond)
+    vectors, _, rsqrt_evals = eigh_rank_reveal(values, vectors, rank, rcond)
     return vectors @ np.diag(rsqrt_evals)
 
 
diff --git a/kooplearn/dashboard/__init__.py b/kooplearn/dashboard/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/kooplearn/dashboard/app.py b/kooplearn/dashboard/app.py
deleted file mode 100644
index 6d3aaa2..0000000
--- a/kooplearn/dashboard/app.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import argparse
-import pickle
-
-import dash_mantine_components as dmc
-from dash import Dash
-from sklearn.gaussian_process.kernels import DotProduct
-
-from kooplearn._src.dashboard.dash_components.callbacks import get_cb_functions
-from kooplearn._src.dashboard.dash_components.layout import header, plots_components
-from kooplearn._src.dashboard.visualizer import Visualizer
-from kooplearn.datasets import Mock
-
-APP_TITLE = "Koopman Modes Dashboard"
-DEBUG = True
-
-app = Dash(
-    "Koopman Modes Dashboard",
-    external_stylesheets=[
-        # include google fonts
-        "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap"
-    ],
-)
-
-
-def main():
-    parser = argparse.ArgumentParser(
-        description="A dashboard to explore the Koopman modes"
-    )
-    parser.add_argument(  # location of the koopman estimator
-        "--koopman", type=str, default=""
-    )
-
-    args = parser.parse_args()
-    if args.koopman == "":
-        # Tutorial mode
-        dataset = Mock(num_features=10, rng_seed=0)
-        _Z = dataset.sample(None, 100)
-        X, Y = _Z[:-1], _Z[1:]
-        operator = KernelReducedRank(DotProduct(), rank=10)
-        operator.fit(X, Y)
-    else:
-        with open(args.koopman, "rb") as file:
-            operator = pickle.load(file)
-
-    viz = Visualizer(operator)
-    frequencies = viz.infos["frequency"].unique()
-    frequencies = frequencies[frequencies > 0]
-    app.layout = dmc.MantineProvider(
-        theme={
-            "fontFamily": "'Inter', sans-serif",
-            "primaryColor": "indigo",
-            "colorScheme": "dark",
-            "components": {
-                "Button": {"styles": {"root": {"fontWeight": 400}}},
-                "Alert": {"styles": {"title": {"fontWeight": 500}}},
-                "AvatarGroup": {"styles": {"truncated": {"fontWeight": 500}}},
-            },
-        },
-        inherit=True,
-        withGlobalStyles=True,
-        withNormalizeCSS=True,
-        children=[header, plots_components(frequencies)],
-    )
-    callback_fn = get_cb_functions(viz)
-
-    app.run_server(debug=DEBUG)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/kooplearn/dashboard/dash_components/__init__.py b/kooplearn/dashboard/dash_components/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/kooplearn/dashboard/dash_components/callbacks.py b/kooplearn/dashboard/dash_components/callbacks.py
deleted file mode 100644
index ec0fddf..0000000
--- a/kooplearn/dashboard/dash_components/callbacks.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from functools import partial
-from typing import Optional
-
-from dash import Input, Output, callback
-
-from kooplearn._src.dashboard.visualizer import Visualizer
-
-
-def update_modes_visibility(viz: Visualizer, frequency_range: Optional[list] = None):
-    if frequency_range is None:
-        min_freq = viz.infos["frequency"].unique().min()
-        max_freq = viz.infos["frequency"].unique().max()
-    else:
-        min_freq = frequency_range[0]
-        max_freq = frequency_range[1]
-
-    fig_eig = viz.plot_eigs(min_freq, max_freq)
-    fig_freqs = viz.plot_freqs(min_freq, max_freq)
-    fig_modes = viz.plot_modes(index=None, min_freq=min_freq, max_freq=max_freq)
-    return fig_eig, fig_freqs, fig_modes
-
-
-def get_cb_functions(
-    viz: Visualizer,
-    eig_id="eig-plot",
-    freq_id="freq-plot",
-    modes_id="modes-plot",
-    frequency_range_id="freq_range_slider",
-):
-    update_modes_visibility_partial = partial(update_modes_visibility, viz)
-    return callback(
-        Output(eig_id, "figure"),
-        Output(freq_id, "figure"),
-        Output(modes_id, "figure"),
-        Input(frequency_range_id, "value"),
-    )(update_modes_visibility_partial)
diff --git a/kooplearn/dashboard/dash_components/layout.py b/kooplearn/dashboard/dash_components/layout.py
deleted file mode 100644
index 76c51cd..0000000
--- a/kooplearn/dashboard/dash_components/layout.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import dash_mantine_components as dmc
-import numpy as np
-from dash import Input, Output, callback, dcc, html
-from dash_iconify import DashIconify
-
-header = dmc.Header(
-    height=70,
-    px=25,
-    children=[
-        dmc.Stack(
-            justify="center",
-            style={"height": 70},
-            children=dmc.Grid(
-                children=[
-                    dmc.Col(
-                        span="content",
-                        children=[
-                            DashIconify(icon="mdi:frequency", width=40, height=40)
-                        ],
-                        pt=15,
-                    ),
-                    dmc.Col(
-                        [
-                            dmc.Title("Koopman Modes Dashboard", order=1),
-                        ],
-                        span="content",
-                        pt=12,
-                    ),
-                ],
-            ),
-        )
-    ],
-)
-
-
-def graph_component(name: str, id: str):
-    return html.Div(
-        [
-            dmc.Title(name, order=3, align="center"),
-            dmc.Center(
-                style={"width": "100%"},
-                children=[dcc.Graph(id=id, style={"align": "center"})],
-            ),
-        ]
-    )
-
-
-def frequency_range_selector(frequencies: list, id="freq_range_slider"):
-
-    max_freq = np.max(frequencies)
-    min_freq = np.min(frequencies)
-
-    freq_range = max_freq - min_freq
-
-    marks = [
-        {"value": round(i, 2), "label": str(round(i, 2)) + " Hz"} for i in frequencies
-    ]
-
-    marks.append({"value": 0, "label": "0 Hz"})
-
-    component = html.Div(
-        [
-            dmc.Title("Frequency range", align="center", order=4),
-            dmc.RangeSlider(
-                id=id,
-                min=min_freq - freq_range * 0.1,
-                max=max_freq + freq_range * 0.1,
-                minRange=freq_range * 0.01,
-                value=[min_freq, max_freq],
-                marks=marks,
-                step=freq_range * 0.01,
-                precision=2,
-                size="xl",
-                color="blue",
-                mt=10,
-                mb=35,
-            ),
-        ]
-    )
-    return component
-
-
-def plots_components(frequencies):
-    plots = dmc.Container(
-        size="lg",
-        style={"marginTop": 20},
-        children=[
-            frequency_range_selector(frequencies),
-            dmc.Grid(
-                children=[
-                    dmc.Col(
-                        [graph_component("Eigenvalues", "eig-plot")],
-                        span=6,
-                    ),
-                    dmc.Col(
-                        [graph_component("Frequencies", "freq-plot")],
-                        span=6,
-                    ),
-                    dmc.Col(
-                        [graph_component("Koopman Modes", "modes-plot")],
-                        span=12,
-                    ),
-                ]
-            ),
-        ],
-    )
-    return plots
diff --git a/kooplearn/dashboard/dash_server_GREG.py b/kooplearn/dashboard/dash_server_GREG.py
deleted file mode 100644
index 27888fb..0000000
--- a/kooplearn/dashboard/dash_server_GREG.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import argparse
-import pickle
-
-import numpy as np
-from dash import Dash, Input, Output, callback, dcc, html
-from sklearn.gaussian_process.kernels import DotProduct
-
-from kooplearn._src.dashboard.visualizer import Visualizer
-from kooplearn.datasets import Mock
-from kooplearn.models.kernel import KernelReducedRank
-
-#### WORK IN PROGRESS, FOR NOW USE THE visualizer.utils.py METHODS ####
-# Script for an interactive Dash html page using the visualisation methods of utils.py
-
-parser = argparse.ArgumentParser(
-    description="Dash web application for visualisation of a Koopman operator"
-)
-parser.add_argument(  # location of the koopman estimator
-    "--koopman", type=str, default=""
-)
-
-args = parser.parse_args()
-if args.koopman == "":
-    # tutorial mode
-    dataset = Mock(num_features=10, rng_seed=0)
-    _Z = dataset.sample(None, 100)
-    X, Y = _Z[:-1], _Z[1:]
-
-    operator = KernelReducedRank(DotProduct(), rank=10)
-    operator.fit(X, Y)
-else:
-    with open(args.koopman, "rb") as file:
-        operator = pickle.load(file)
-
-viz = Visualizer(operator)
-available_modes = viz.infos["eig_num"].unique().astype("str")
-available_modes = np.insert(available_modes, 0, "All")
-available_modes = np.insert(available_modes, 1, "Combined")
-
-frequencies = viz.infos["frequency"].unique()
-pos_frequencies = frequencies[frequencies > 0]
-frequency_dict = {i: str(round(i, 3)) for i in pos_frequencies}
-frequency_dict[0] = "0"
-
-app = Dash(__name__)
-
-app.layout = html.Div(
-    [
-        html.Div(
-            [dcc.Graph(id="eig-plot")],
-            style={"width": "30%", "display": "inline-block"},
-        ),
-        html.Div(
-            [dcc.Graph(id="freq-plot")],
-            style={"width": "49%", "display": "inline-block"},
-        ),
-        html.Div(
-            [
-                dcc.RangeSlider(
-                    min=-0.1,
-                    max=int(viz.infos["frequency"].max()) + 1,
-                    marks=frequency_dict,
-                    id="freq_range_slider",
-                ),
-                dcc.Input(id="Tmax", type="number", placeholder="input T max"),
-                dcc.Slider(min=0, max=1, step=1, id="T"),
-            ],
-            style={"width": "60%"},
-        ),
-        html.Div(
-            [
-                html.H4("Modes"),
-                dcc.Dropdown(available_modes, "All", id="modes_select"),
-                dcc.Graph(id="modes-plot"),
-                # html.H1("Prediction"),
-                # dcc.Graph(id='pred-plot', figure=viz.plot_preds())
-            ]
-        ),
-    ]
-)
-
-
-@callback(
-    Output("eig-plot", "figure"),
-    Output("freq-plot", "figure"),
-    Output("modes-plot", "figure"),
-    Output("T", "max"),
-    Input("freq_range_slider", "value"),
-    Input("Tmax", "value"),
-    Input("T", "value"),
-    Input("modes_select", "value"),
-)
-def update_modes_plots(value, Tmax, T, mode_selection):
-    if value is None:
-        min_freq = viz.infos["frequency"].unique().min()
-        max_freq = viz.infos["frequency"].unique().max()
-    else:
-        min_freq = value[0]
-        max_freq = value[1]
-
-    if T is None:
-        T = 1
-
-    fig_eig = viz.plot_eigs(min_freq, max_freq)
-    fig_freqs = viz.plot_freqs(min_freq, max_freq)
-
-    if mode_selection == "All":
-        fig_modes = viz.plot_modes(index=None, min_freq=min_freq, max_freq=max_freq)
-    elif mode_selection == "Combined":
-        fig_modes = viz.plot_combined_modes(T, min_freq, max_freq)
-    else:
-        fig_modes = viz.plot_modes(
-            index=[int(mode_selection)], min_freq=min_freq, max_freq=max_freq
-        )
-    # fig_pred = viz.plot_preds(operator.X_fit_[-1], 1, min_freq, max_freq)
-    return fig_eig, fig_freqs, fig_modes, Tmax
-
-
-# TODO:
-#  Predictions at t+n, where we can select n and filter by frequency
-#  Modes filtered by frequency
-
-# https://dash.plotly.com/dash-core-components/slider
-# https://plotly.com/python/filter/
-# https://stackoverflow.com/questions/45736656/how-to-use-a-button-to-trigger-callback-updates
-
-app.run_server(debug=True)
diff --git a/kooplearn/dashboard/utils.py b/kooplearn/dashboard/utils.py
deleted file mode 100644
index 08b5c94..0000000
--- a/kooplearn/dashboard/utils.py
+++ /dev/null
@@ -1,248 +0,0 @@
-import numpy as np
-import plotly.express as px
-import plotly.graph_objects as go
-from plotly.subplots import make_subplots
-
-PLOTLY_THEME = "plotly_dark"
-
-
-def create_plot_eigs(infos, min_freq=None, max_freq=None):
-    # infos is a dataframe with a 'eigval_real' and 'eigval_imag' columns
-    if min_freq is None:
-        min_freq = infos["frequency"].min()
-    if max_freq is None:
-        max_freq = infos["frequency"].max()
-    fig = go.Figure(
-        go.Scatter(
-            x=infos["eigval real"],
-            y=infos["eigval imag"],
-            mode="markers",
-            marker_size=5,
-            marker=dict(
-                size=10,
-                color=(
-                    (np.abs(infos.frequency) <= max_freq)
-                    & (np.abs(infos.frequency) >= min_freq)
-                ).astype("int"),
-                colorscale=[[0, "rgba(255,127,80, 0.01)"], [1, "rgba(255,127,80, 1)"]],
-            ),
-        )
-    )
-    fig.add_shape(
-        type="circle",
-        fillcolor="PaleTurquoise",
-        line_color="LightSeaGreen",
-        opacity=0.2,
-        layer="below",
-        x0=-1,
-        y0=-1,
-        x1=1,
-        y1=1,
-    )
-    fig.update_layout(
-        margin={"l": 10, "b": 10, "t": 40, "r": 20},
-        xaxis_title="Real Part",
-        yaxis_title="Imaginary Part",
-        width=400,
-        height=400,
-        # title='Eigenvalues',
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-def create_frequency_plot(infos, min_freq=None, max_freq=None):
-    # infos is a dataframe with 'modulus' and 'angles' columns
-    if min_freq is None:
-        min_freq = infos["frequency"].min()
-    if max_freq is None:
-        max_freq = infos["frequency"].max()
-    relevant_infos = infos[infos["frequency"] >= 0]
-    fig = go.Figure(
-        go.Scatter(
-            x=relevant_infos["frequency"],
-            y=relevant_infos["modulus"],  # plotting only positive frequencies
-            mode="markers",
-            marker_size=5,
-            marker=dict(
-                size=2,
-                color=(
-                    (np.abs(relevant_infos.frequency) <= max_freq)
-                    & (np.abs(relevant_infos.frequency) >= min_freq)
-                ).astype("int"),
-                colorscale=[[0, "rgba(255,127,80, 0.01)"], [1, "rgba(255,127,80, 1)"]],
-            ),
-        )
-    )
-    fig.update_layout(
-        xaxis_title="Frequency",
-        yaxis_title="Amplitude",
-        # title='Frequency spectrum',
-        margin={"l": 10, "b": 10, "t": 40, "r": 20},
-        autosize=False,
-        width=600,
-        height=400,
-        template=PLOTLY_THEME,
-    )
-    max_range = np.max(infos["frequency"])
-    fig.update_xaxes(
-        range=[-0.1 * max_range, 1.1 * max_range]
-    )  # Plotting with an outset for readability
-    # version with amplitude multiplied with mode modulus
-
-    return fig
-
-
-# TODO: function comparing koopman to fourier
-
-
-def create_plot_modes(infos, index=None, min_freq=None, max_freq=None):
-    # image is false if data one dimensional, true if two dimensional
-    # coordinates is hte position of each pixel if two dimensional
-    # index is None if all modes are to be plotted, an array if some nodes must plotted or an int
-    # if one mode must be plotted
-    if index is None:
-        index = infos["eig_num"].drop_duplicates().to_numpy()
-    n_plots = len(index)
-
-    if n_plots == 1:
-        mode = infos[infos["eig_num"] == index[0]]
-        freq = np.abs(mode["frequency"].to_numpy()[0])
-        is_selected = int((freq <= max_freq) & (freq >= min_freq))
-        opacity = is_selected + (1 - is_selected) * 0.2
-        fig = px.scatter(x=mode["x"], y=mode["mode"].to_numpy().real, opacity=opacity)
-        fig.update_layout(
-            xaxis_title="Variables", yaxis_title="Value of mode", template=PLOTLY_THEME
-        )
-        return fig
-
-    if n_plots % 2 == 0:
-        fig = make_subplots(
-            rows=n_plots // 2, cols=2, x_title="Variable", y_title="Signal"
-        )
-    else:
-        fig = make_subplots(rows=n_plots // 2 + 1, cols=2)
-    for i in index:
-        mode = infos[infos["eig_num"] == i]
-        freq = np.abs(mode["frequency"].to_numpy()[0])
-        is_selected = int((freq <= max_freq) & (freq >= min_freq))
-        opacity = is_selected + (1 - is_selected) * 0.2
-        fig.add_trace(
-            px.scatter(
-                x=mode["x"], y=mode["mode"].to_numpy().real, opacity=opacity
-            ).data[0],
-            col=i % 2 + 1,
-            row=i // 2 + 1,
-        )
-    fig.update_layout(
-        title="Mode decomposition",
-        # width=1100,
-        height=1100,
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-def create_2d_plot_modes(infos, index, min_freq, max_freq):
-    if index is None:
-        index = infos["eig_num"].drop_duplicates().to_numpy()
-    n_plots = len(index)
-    if n_plots % 2 == 0:
-        fig = make_subplots(rows=n_plots // 2, cols=2)
-    else:
-        fig = make_subplots(rows=n_plots // 2 + 1, cols=2)
-    for i in index:
-        freq = infos[infos["eig_num"] == i]["frequency"].to_numpy()[0]
-        is_selected = int((freq <= max_freq) & (freq >= min_freq))
-        opacity = is_selected + (1 - is_selected) * 0.2
-        fig.add_trace(
-            px.scatter(
-                infos[infos["eig_num"] == i],
-                x="x",
-                y="y",
-                color=infos["mode"].to_numpy().real,
-                opacity=opacity,
-            ),
-            col=i % 2,
-            row=i // 2,
-        )
-    fig.update_layout(
-        xaxis_title="x",
-        yaxis_title="y",
-        width=1100,
-        height=1100,
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-def create_combined_plot_modes(infos, T, min_freq, max_freq):
-    preds = np.zeros(infos.var_index.max() + 1)
-    for i in infos["eig_num"].unique():
-        mode = infos[infos["eig_num"] == i]
-        freq = mode["frequency"].to_numpy()[0]
-        if (np.abs(freq) > max_freq) or (
-            np.abs(freq) < min_freq
-        ):  # if frequency outside selection, don't add the mode
-            continue
-        eigs = mode["eigval real"].unique()[0] + mode["eigval imag"].unique()[0] * 1j
-        mode_value = mode["mode"].to_numpy()
-        # summing the mode
-        eigT = eigs**T
-        preds += (eigT * mode_value).real
-    fig = px.scatter(x=infos["x"].unique(), y=preds)
-    fig.update_layout(
-        xaxis_title="Variables",
-        yaxis_title="Value",
-        # width=1100,
-        height=1100,
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-def create_combined_2d_plot_modes(infos, T, min_freq, max_freq):
-    preds = np.zeros(infos.var_index.max() + 1)
-    for i in infos["eig_num"].unique():
-        mode = infos[infos["eig_num"] == i]
-        freq = infos["frequency"].to_numpy()[0]
-        if np.abs(freq) > max_freq or np.abs(freq) < min_freq:
-            continue
-        eigs = mode["eigval real"].unique()[0] + mode["eigval imag"].unique()[0] * 1j
-        mode_value = mode["mode"].to_numpy()
-        # summing the mode
-        eigT = eigs**T
-        preds += (eigT * mode_value).real
-    fig = px.scatter(x=infos["x"].unique(), y=infos["y"].unique(), color=preds)
-    fig.update_layout(
-        xaxis_title="Variables",
-        yaxis_title="Value",
-        # width=1100,
-        height=1100,
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-def map_predictions(preds, coordinates, height=False):
-    # coordinates is a list of x,y coordinates in np array format
-    if not height:
-        fig = px.scatter(x=coordinates[:, 0], y=coordinates[:, 1], color=preds)
-    else:
-        fig = px.scatter_3d(
-            x=coordinates[:, 0], y=coordinates[:, 1], z=preds, color=preds
-        )
-    return fig
-
-
-def create_plot_pred(infos, preds):
-    fig = px.line(y=preds, x=infos[infos["eig_num"] == 0]["x"])
-    fig.update_layout(
-        xaxis_title="Variables",
-        yaxis_title="Predicted value",
-        template=PLOTLY_THEME,
-    )
-    return fig
-
-
-# TODO: plot x= time, y= the mode frequencies, z = amplitudes
diff --git a/kooplearn/dashboard/visualizer.py b/kooplearn/dashboard/visualizer.py
deleted file mode 100644
index b5fe5e2..0000000
--- a/kooplearn/dashboard/visualizer.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import numpy as np
-import pandas
-
-from kooplearn._src.dashboard.utils import (
-    create_2d_plot_modes,
-    create_combined_2d_plot_modes,
-    create_combined_plot_modes,
-    create_frequency_plot,
-    create_plot_eigs,
-    create_plot_modes,
-    create_plot_pred,
-)
-from kooplearn.abc import BaseModel
-
-
-class Visualizer:
-    def __init__(self, koopman: BaseModel, deltat: float = 1):
-        self.operator = koopman
-        self.infos = None
-        self.eigs = None
-        self.compute_mode_info(deltat=deltat)
-
-    def compute_mode_info(self, deltat: float, xcoord=None, ycoord=None):
-        eigs = self.operator.eig()
-        modes = self.operator.modes([self.operator.X_fit_[-1]])
-        n_eigs = eigs.shape[0]
-        n_features = modes.shape[1]
-
-        # initiliasing the dataframe containing the information for every cell of every mode
-        self.infos = pandas.DataFrame()
-        self.infos["eig_num"] = np.repeat(
-            np.arange(n_eigs), n_features
-        )  # Identifying the mode number
-        self.infos["var_index"] = np.tile(
-            np.arange(n_features), n_eigs
-        )  # Unique identifier for each variable
-        if xcoord is None:  # If no specific coordinate is given, each dimensionis
-            self.infos["x"] = np.tile(
-                np.arange(n_features), n_eigs
-            )  # identified with its index in the observable array
-        else:  # Otherwise coordinates are saved for accurate plotting
-            self.infos["x"] = xcoord
-        if ycoord is not None:  # If y coordinates make sense, save them as well
-            self.infos["y"] = ycoord
-
-        # eigenvalue related information. This information is shared by every variable, hence the use of repeat
-        self.infos["eigval real"] = np.repeat(eigs.real, n_features)  # Real part
-        self.infos["eigval imag"] = np.repeat(eigs.imag, n_features)  # Imaginary part
-
-        self.infos["modulus"] = np.repeat(
-            np.abs(eigs) ** (1 / deltat), n_features
-        )  # Modulus of the mode
-        angles = np.log(eigs)
-        freqs = angles.imag / (2 * np.pi * deltat)
-        self.infos["frequency"] = np.repeat(freqs, n_features)  # Frequency of the mode
-
-        # mode specific information. This information is unique per mode and per variable
-        self.infos["mode"] = modes.flatten()  # Actual value of the mode
-        Z = modes * np.outer(
-            eigs, np.ones(n_features)
-        )  # Multiplying by the eigenvalue to recover the signal
-        Z = Z.flatten()  # Row-wise flattening of the modes matrix
-        self.infos["amplitude"] = np.abs(Z)  # Amplitude of the mode at every point
-        self.infos["phase"] = np.arctan(
-            Z.imag / Z.real
-        )  # Phase of the mode at every point
-
-    def save(self, filepath: str):
-        # self.operator.save(filepath)
-        pass
-
-    def load(self, filepath: str):
-        # self.operator.load(filepath)
-        pass
-
-    def plot_freqs(self, min_freq=None, max_freq=None):
-        fig = create_frequency_plot(self.infos, min_freq, max_freq)
-        return fig
-
-    def plot_eigs(self, min_freq=None, max_freq=None):
-        if not hasattr(self, "eigs"):
-            self.eigs = self.operator.eig()
-        fig = create_plot_eigs(self.infos, min_freq, max_freq)
-        return fig
-
-    def plot_modes(self, index=None, min_freq=None, max_freq=None):
-        # plot moduluses and arguments
-        if "y" in self.infos.columns:
-            fig = create_2d_plot_modes(self.infos, index, min_freq, max_freq)
-        else:
-            fig = create_plot_modes(self.infos, index, min_freq, max_freq)
-        return fig
-
-    def plot_combined_modes(self, T, min_freq=None, max_freq=None):
-        # real part
-        if "y" in self.infos.columns:
-            fig = create_combined_2d_plot_modes(self.infos, T, min_freq, max_freq)
-        else:
-            fig = create_combined_plot_modes(self.infos, T, min_freq, max_freq)
-        return fig
-
-    def plot_pred(self, X, T, min_freq=None, max_freq=None):
-        preds = self.operator.predict(X, T)
-        fig = create_plot_pred(self.infos, preds)
-        return fig
diff --git a/kooplearn/datasets/heteroscedastic.py b/kooplearn/datasets/heteroscedastic.py
index 06c95ab..22b9cca 100644
--- a/kooplearn/datasets/heteroscedastic.py
+++ b/kooplearn/datasets/heteroscedastic.py
@@ -1,11 +1,113 @@
 import numpy as np
+from typing import Optional
 
 from kooplearn.datasets.misc import DataGenerator
+from kooplearn.datasets.stochastic import DiscreteTimeDynamics
+
+class DiscreteBlackScholes(DiscreteTimeDynamics):
+    """
+    Discretised simulation of the Black Scholes model.
+
+    Args:
+        A: transition matrix of the drift of shape ''(d, d)''.
+        sigma1: covariance matrix of the increments of the prices brownian motion, shape is ''(d,d)''.
+        dt: size of the increments.
+
+    Attributes:
+       sample: samples the process over T iterations with starting point X0. 
+    """
+    def __init__(self, A:np.ndarray, sigma:np.ndarray, dt:float, rng_seed: Optional[int]=None):
+        self.dim = A.shape[0]
+        self.A = A
+        self.dt = dt
+        self.sigma = sigma
+        self.rng = np.random.default_rng(rng_seed)
+    
+    def _step(self, X:np.ndarray):
+        return X + self.A @ X *self.dt + np.sqrt(self.dt) * X * self.rng.multivariate_normal(np.zeros(self.dim), self.sigma)
+
+class DiscreteOhrnstein(DiscreteTimeDynamics):
+    """
+    Discretised simulation of the Ohrnstein-Ulhenbeck volatility model.
+
+    Args:
+        mu: mean of the volatility process, array of size ''d''.
+        beta: driving coefficient of the volatility process, matrix of shape ''(d,d)''.
+        sigma: covariance matrix of the increments of the volatility's volatility, matrix of shape ''(d,d)''.
+        dt: size of the increments.
+
+    Attributes:
+       sample: samples the process over T iterations with starting point X0. 
+    """
+    def __init__(self, mu:np.ndarray, beta:np.ndarray, sigma:np.ndarray, dt:float, rng_seed:Optional[int]=None):
+        self.dim = mu.shape[0]
+        self.beta = beta
+        self.mu = mu
+        self.sigma = sigma
+        self.dt = dt
+        self.rng = np.random.default_rng(rng_seed)
+
+    def _step(self, X:np.ndarray):
+        return X + self.beta @ (self.mu - X) * self.dt + np.sqrt(self.dt) * self.rng.multivariate_normal(np.zeros(self.dim), self.sigma)
+
+class DiscreteCIR(DiscreteOhrnstein):
+    """
+    Discretised simulation of the CIR volatility model.
+
+    Args:
+        mu: mean of the volatility process, array of size ''d''.
+        beta: driving coefficient of the volatility process, matrix of shape ''(d,d)''.
+        sigma: covariance matrix of the increments of the volatility's volatility, matrix of shape ''(d,d)''.
+        dt: size of the increments.
+
+    Attributes:
+       sample: samples the process over T iterations with starting point X0. 
+    """
+    def _step(self, X:np.ndarray):
+        return X + self.beta @ (self.mu - X)*self.dt + np.sqrt(self.dt)* np.sqrt(X) * self.rng.multivariate_normal(np.zeros(self.dim), self.sigma)
+
+# Heston like model (ohrnstein volatility)
+class DiscreteHeston(DiscreteTimeDynamics):
+    """
+    Discretised simulation of the Heston model.
+
+    Args:
+        A: transition matrix of the drift of shape ''(d, d)''.
+        sigma1: covariance matrix of the increments of the prices brownian motion, shape is ''(d,d)''.
+        mu: mean of the volatility process, array of size ''d''.
+        beta: driving coefficient of the volatility process, matrix of shape ''(d,d)''.
+        sigma2: covariance matrix of the increments of the volatility's volatility, matrix of shape ''(d,d)''.
+        dt: size of the increments.
+
+    Attributes:
+       sample: samples the process over T iterations with starting point X0. 
+    """
+    def __init__(self, A:np.ndarray, 
+                sigma1:np.ndarray,
+                mu:np.ndarray, beta:np.ndarray, 
+                sigma2:np.ndarray, 
+                nu0:np.ndarray, 
+                dt:float, 
+                rng_seed:Optional[int]=None):
+
+        self.A = A
+        self.dim = A.shape[0]
+        self.beta = beta
+        self.mu = mu
+        self.sigma1 = sigma1
+        self.sigma2 = sigma2
+        self.nu0 = nu0
+        self.dt = dt
+        self.rng = np.random.default_rng(rng_seed)
+    def _step(self, X:np.ndarray):
+        next_X = X + self.dt*self.A@X + np.sqrt(self.dt)*np.sqrt(self.nu0) * X * self.rng.multivariate_normal(np.zeros(self.dim), self.sigma2)
+        self.nu0 += self.dt*self.beta @ (self.mu - self.nu0) + np.sqrt(self.dt)* np.sqrt(self.nu0) * self.rng.multivariate_normal(np.zeros(self.dim), self.sigma1)
+        return next_X
 
 
 class Garch(DataGenerator):
     # one dimensional one lag garch model
-    def __init__(self, alpha, beta, alpha0=0.0):
+    def __init__(self, alpha:float, beta:float, alpha0:float=0.0):
         self.alpha = alpha
         self.beta = beta
         self.alpha0 = alpha0
diff --git a/kooplearn/models/__init__.py b/kooplearn/models/__init__.py
index 40f35d7..f8c9ff6 100644
--- a/kooplearn/models/__init__.py
+++ b/kooplearn/models/__init__.py
@@ -1,5 +1,6 @@
 from kooplearn.models.edmd import DMD, ExtendedDMD
 from kooplearn.models.kernel import KernelDMD
+from kooplearn.models.nystroem import NystroemKernel
 
 from kooplearn.models.deepedmd import DeepEDMD  # isort:skip
 
diff --git a/kooplearn/models/edmd.py b/kooplearn/models/edmd.py
index 99ae543..b059baa 100644
--- a/kooplearn/models/edmd.py
+++ b/kooplearn/models/edmd.py
@@ -29,7 +29,7 @@ class ExtendedDMD(BaseModel):
         reduced_rank (bool): If ``True`` initializes the reduced rank estimator introduced in :footcite:t:`Kostic2022`, if ``False`` initializes the classical principal component estimator.
         rank (int): Rank of the estimator. ``None`` returns the full rank estimator.
         tikhonov_reg (float): Tikhonov regularization coefficient. ``None`` is equivalent to ``tikhonov_reg = 0``, and internally calls specialized stable algorithms to deal with this specific case.
-        svd_solver (str): Solver used to perform the internal SVD calcuations. Currently supported: `full`, uses LAPACK solvers, `arnoldi`, uses ARPACK solvers, `randomized`, uses randomized SVD algorithms as described in :guilabel:`TODO - ADD REF`.
+        svd_solver (str): Solver used to perform the internal SVD calcuations. Currently supported: `full`, uses LAPACK solvers, `arnoldi`, uses ARPACK solvers, `randomized`, uses randomized SVD algorithms as described in :footcite:t:`Turri2023`.
         iterated_power (int): Number of power iterations when using a randomized algorithm (``svd_solver == 'randomized'``).
         n_oversamples (int): Number of oversamples when using a randomized algorithm (``svd_solver == 'randomized'``).
         rng_seed (int): Random Number Generator seed. Only used when ``svd_solver == 'randomized'``.  Defaults to ``None``, that is no explicit seed is setted.
@@ -159,7 +159,14 @@ def fit(self, data: np.ndarray, verbose: bool = True) -> ExtendedDMD:
                 vectors = primal.fit_principal_component_regression(
                     self.cov_X, self.tikhonov_reg, self.rank, self.svd_solver
                 )
+
         self.U = vectors
+        assert self.U.shape[1] <= self.rank
+        if self.U.shape[1] < self.rank:
+            logger.warning(
+                f"Warning: The fitting algorithm discarded {self.rank - self.U.shape[1]} dimensions of the {self.rank} requested out of numerical instabilities.\nThe rank attribute has been updated to {self.U.shape[1]}.\nConsider decreasing the rank parameter."
+            )
+            self.rank = self.U.shape[1]
 
         # Final Checks
         check_is_fitted(
@@ -318,7 +325,6 @@ def modes(
         phi_Xin = self.feature_map(X_inference)
 
         _gamma = primal.estimator_modes(self.U, self.cov_XY, phi_X, phi_Xin)
-
         expected_shape = (self.rank,) + expected_shape
         return np.matmul(_gamma, _obs).reshape(
             expected_shape
@@ -396,7 +402,7 @@ def _pre_fit_checks(self, data: np.ndarray) -> None:
         self.data_fit = data
 
         if self.rank is None:
-            self.rank = min(self.cov_X.shape[0], self.data_fit.shape[0])
+            self.rank = self.cov_X.shape[0]
             logger.info(f"Rank of the estimator set to {self.rank}")
 
         if hasattr(self, "_eig_cache"):
diff --git a/kooplearn/models/kernel.py b/kooplearn/models/kernel.py
index 1c067a8..79320e9 100644
--- a/kooplearn/models/kernel.py
+++ b/kooplearn/models/kernel.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import logging
 from typing import Callable, Optional
 
 import numpy as np
@@ -15,6 +16,8 @@
 from kooplearn._src.utils import ShapeError, check_contexts_shape, check_is_fitted
 from kooplearn.abc import BaseModel
 
+logger = logging.getLogger("kooplearn")
+
 
 class KernelDMD(BaseModel, RegressorMixin):
     """
@@ -26,7 +29,7 @@ class KernelDMD(BaseModel, RegressorMixin):
         reduced_rank (bool): If ``True`` initializes the reduced rank estimator introduced in :footcite:t:`Kostic2022`, if ``False`` initializes the classical principal component estimator.
         rank (int): Rank of the estimator. Defaults to 5.
         tikhonov_reg (float): Tikhonov regularization coefficient. ``None`` is equivalent to ``tikhonov_reg = 0``, and internally calls specialized stable algorithms to deal with this specific case.
-        svd_solver (str): Solver used to perform the internal SVD calcuations. Currently supported: `full`, uses LAPACK solvers, `arnoldi`, uses ARPACK solvers, `randomized`, uses randomized SVD algorithms as described in :guilabel:`TODO - ADD REF`.
+        svd_solver (str): Solver used to perform the internal SVD calcuations. Currently supported: `full`, uses LAPACK solvers, `arnoldi`, uses ARPACK solvers, `randomized`, uses randomized SVD algorithms as described in :footcite:t:`Turri2023`.
         iterated_power (int): Number of power iterations when using a randomized algorithm (``svd_solver == 'randomized'``).
         n_oversamples (int): Number of oversamples when using a randomized algorithm (``svd_solver == 'randomized'``).
         optimal_sketching (bool): Sketching strategy for the randomized solver. If `True` performs optimal sketching (computaitonally expensive but more accurate).
@@ -52,7 +55,7 @@ def __init__(
         reduced_rank: bool = True,
         rank: int = 5,
         tikhonov_reg: Optional[float] = None,
-        svd_solver: str = "full",
+        svd_solver: str = "arnoldi",
         iterated_power: int = 1,
         n_oversamples: int = 5,
         optimal_sketching: bool = False,
@@ -132,7 +135,7 @@ def fit(self, data: np.ndarray, verbose: bool = True) -> KernelDMD:
                         "tikhonov_reg must be specified when solver is randomized."
                     )
                 else:
-                    U, V = dual.fit_rand_reduced_rank_regression(
+                    U, V, spectral_bias = dual.fit_rand_reduced_rank_regression(
                         self.kernel_X,
                         self.kernel_Y,
                         self.tikhonov_reg,
@@ -143,7 +146,7 @@ def fit(self, data: np.ndarray, verbose: bool = True) -> KernelDMD:
                         rng_seed=self.rng_seed,
                     )
             else:
-                U, V = dual.fit_reduced_rank_regression(
+                U, V, spectral_bias = dual.fit_reduced_rank_regression(
                     self.kernel_X,
                     self.kernel_Y,
                     self.tikhonov_reg,
@@ -152,7 +155,7 @@ def fit(self, data: np.ndarray, verbose: bool = True) -> KernelDMD:
                 )
         else:
             if self.svd_solver == "randomized":
-                U, V = dual.fit_rand_principal_component_regression(
+                U, V, spectral_bias = dual.fit_rand_principal_component_regression(
                     self.kernel_X,
                     self.tikhonov_reg,
                     self.rank,
@@ -161,11 +164,13 @@ def fit(self, data: np.ndarray, verbose: bool = True) -> KernelDMD:
                     rng_seed=self.rng_seed,
                 )
             else:
-                U, V = dual.fit_principal_component_regression(
+                U, V, spectral_bias = dual.fit_principal_component_regression(
                     self.kernel_X, self.tikhonov_reg, self.rank, self.svd_solver
                 )
         self.U = U
         self.V = V
+        self.rank = U.shape[1]
+        self._spectral_biases = spectral_bias
 
         # Final Checks
         check_is_fitted(
@@ -387,6 +392,15 @@ def _pre_fit_checks(self, data: np.ndarray) -> None:
 
         if hasattr(self, "_eig_cache"):
             del self._eig_cache
+        self._check_rank(self.kernel_X.shape[0])
+
+    def _check_rank(self, n_samples):
+        if not isinstance(self.rank, int) or self.rank < 1:
+            raise ValueError("rank must be a positive integer.")
+        if self.rank > n_samples:
+            raise ValueError(
+                f"rank must be less than the number of samples ({n_samples})."
+            )
 
     def save(self, filename):
         """Serialize the model to a file.
diff --git a/kooplearn/models/nystroem.py b/kooplearn/models/nystroem.py
new file mode 100644
index 0000000..9208463
--- /dev/null
+++ b/kooplearn/models/nystroem.py
@@ -0,0 +1,427 @@
+from __future__ import annotations
+
+import logging
+from typing import Callable, Optional, Union
+
+import numpy as np
+from line_profiler import profile
+from sklearn.base import RegressorMixin
+from sklearn.gaussian_process.kernels import DotProduct, Kernel
+
+from kooplearn._src.operator_regression import dual
+from kooplearn._src.operator_regression.utils import (
+    contexts_to_markov_train_states,
+    parse_observables,
+)
+from kooplearn._src.serialization import pickle_load, pickle_save
+from kooplearn._src.utils import ShapeError, check_contexts_shape, check_is_fitted
+from kooplearn.abc import BaseModel
+
+logger = logging.getLogger("kooplearn")
+
+
+class NystroemKernel(BaseModel, RegressorMixin):
+    """
+    Nystroem-accelerated Kernel Least Squares (KernelDMD) Model.
+    Implements the KernelDMD estimators approximating the Koopman (deterministic systems) or Transfer (stochastic systems) operator following the approach described in :footcite:t:`Meanti2023`.
+
+    Args:
+        kernel (sklearn.gaussian_process.kernels.Kernel): sklearn Kernel object. Defaults to `DotProduct`.
+        reduced_rank (bool): If ``True`` initializes the reduced rank estimator introduced in :footcite:t:`Kostic2022`, if ``False`` initializes the classical principal component estimator.
+        rank (int): Rank of the estimator. Defaults to 5.
+        tikhonov_reg (float): Tikhonov regularization coefficient. ``None`` is equivalent to ``tikhonov_reg = 0``, and internally calls specialized stable algorithms to deal with this specific case.
+        svd_solver (str): Solver used to perform the internal SVD calcuations. Currently supported: `full`, uses LAPACK solvers, `arnoldi`, uses ARPACK solvers.
+        num_centers (int or float): Number of centers to select. If ``num_centers < 1``, selects ``int(num_centers * n_samples)`` centers. If ``num_centers >= 1``, selects ``int(num_centers)`` centers. Defaults to ``0.1``.
+        rng_seed (int): Random Number Generator seed. Only used when ``svd_solver == 'randomized'``.  Defaults to ``None``, that is no explicit seed is setted.
+
+    .. tip::
+
+        A powerful DMD variation proposed by :footcite:t:`Arbabi2017`, known as Hankel-DMD, evaluates the Koopman/Transfer estimators by stacking consecutive snapshots together in a Hankel matrix. When this model is fitted context windows of length > 2, the lookback window length is automatically set to ``context_len - 1``. Upon fitting, the whole lookback window is flattened and *concatenated* together before evaluating the kernel function, thus realizing an Hankel-KDMD estimator.
+
+    Attributes:
+        data_fit : Training data: array of context windows of shape ``(n_samples, context_len, *features_shape)``.
+        nys_centers_idxs : Indices of the selected centers.
+        kernel_X : Kernel matrix evaluated at the subsampled initial states, that is ``self.data_fit[self.nys_centers_idxs, :self.lookback_len, ...]`. Shape ``(n_centers, n_centers)``
+        kernel_Y : Kernel matrix evaluated at the subsampled evolved states, that is ``self.data_fit[self.nys_centers_idxs, 1:self.lookback_len + 1, ...]``. Shape ``(n_centers, n_centers)``
+        kernel_XY : Cross-kernel matrix between initial and evolved states. Shape ``(n_centers, n_centers)``.
+        U : Projection matrix of shape (n_centers, rank). The Koopman/Transfer operator is approximated as :math:`k(\cdot, X)U V^T k(\cdot, Y)` (see :footcite:t:`Meanti2023`).
+        V : Projection matrix of shape (n_centers, rank). The Koopman/Transfer operator is approximated as :math:`k(\cdot, X)U V^T k(\cdot, Y)` (see :footcite:t:`Meanti2023`).
+
+    """
+
+    def __init__(
+        self,
+        kernel: Kernel = DotProduct(),
+        reduced_rank: bool = True,
+        rank: int = 5,
+        tikhonov_reg: Optional[float] = None,
+        svd_solver: str = "arnoldi",
+        num_centers: Union[float, int] = 0.1,
+        rng_seed: Optional[int] = None,
+    ):
+        # Initial checks
+        if svd_solver not in ["full", "arnoldi"]:
+            raise ValueError(
+                "Invalid svd_solver. Allowed values are 'full' or 'arnoldi'."
+            )
+
+        self.rng_seed = rng_seed
+        self._kernel = kernel
+        if not isinstance(rank, int) or rank < 1:
+            raise ValueError("rank must be a positive integer.")
+
+        self.rank = rank
+        if tikhonov_reg is None:
+            self.tikhonov_reg = 0.0
+        else:
+            self.tikhonov_reg = tikhonov_reg
+        self.svd_solver = svd_solver
+        self.num_centers = num_centers
+
+        self.reduced_rank = reduced_rank
+        self._is_fitted = False
+        self._lookback_len = -1
+
+    @property
+    def lookback_len(self) -> bool:
+        return self._lookback_len
+
+    @property
+    def is_fitted(self) -> bool:
+        return self._is_fitted
+
+    def kernel(self, X: np.ndarray, Y: Optional[np.ndarray] = None) -> np.ndarray:
+        X = X.reshape(X.shape[0], -1)
+        if Y is not None:
+            Y = Y.reshape(Y.shape[0], -1)
+        return self._kernel(X, Y)
+
+    @profile
+    def fit(self, data: np.ndarray, verbose: bool = True) -> NystroemKernelLeastSquares:
+        """
+        Fits the KernelDMD model using either a randomized or a non-randomized algorithm, and either a full rank or a reduced rank algorithm,
+        depending on the parameters of the model.
+
+
+        .. attention::
+
+            If ``context_len = data.shape[1] > 2``, the attribute :attr:`lookback_len` will be automatically set to ``context_len - 1``. The data will be first flattened along the trailing dimensions before computing the kernel function. The pseudo-code of this operation is
+
+            .. code-block:: python
+
+                X = data[:, :self.lookback_len, ...] #Y = data[:, 1:self.lookback_len + 1, ...]
+                #Flatten the trailing dimensions
+                X = X.reshape(data.shape[0], -1)
+                self.kernel_X = self.kernel(X)
+
+        Args:
+            data (np.ndarray): Batch of context windows of shape ``(n_samples, context_len, *features_shape)``.
+
+        Returns:
+            The fitted estimator.
+        """
+        kernel_Xnys, kernel_Ynys = self._pre_fit_checks(data)
+        if self.reduced_rank:
+            U, V, spectral_bias = dual.fit_nystroem_reduced_rank_regression(
+                self.kernel_X,
+                self.kernel_Y,
+                kernel_Xnys,
+                kernel_Ynys,
+                self.tikhonov_reg,
+                self.rank,
+                self.svd_solver,
+            )
+        else:
+            U, V, spectral_bias = dual.fit_nystroem_principal_component_regression(
+                self.kernel_X,
+                self.kernel_Y,
+                kernel_Xnys,
+                kernel_Ynys,
+                self.tikhonov_reg,
+                self.rank,
+                self.svd_solver,
+            )
+
+        self.U = U
+        self.V = V
+        self.rank = U.shape[1]
+        self._spectral_biases = spectral_bias
+        # Final Checks
+        check_is_fitted(
+            self,
+            ["U", "V", "kernel_X", "kernel_Y", "kernel_YX", "data_fit", "lookback_len"],
+        )
+        self._is_fitted = True
+        if verbose:
+            print(
+                f"Fitted {self.__class__.__name__} model. Lookback length set to {self.lookback_len}"
+            )
+        return self
+
+    def risk(self, data: Optional[np.ndarray] = None) -> float:
+        """Risk of the estimator on the validation ``data``.
+
+        Args:
+            data (np.ndarray or None): Batch of context windows of shape ``(n_samples, context_len, *features_shape)``. If ``None``, evaluates the risk on the training data.
+
+        Returns:
+            Risk of the estimator, see Equation 11 of :footcite:p:`Kostic2022` for more details.
+        """
+        if data is not None:
+            check_contexts_shape(data, self.lookback_len)
+            data = np.asanyarray(data)
+            if data.shape[1] - 1 != self.lookback_len:
+                raise ShapeError(
+                    f"The  context length ({data.shape[1]}) of the validation data does not match the context length of the training data ({self.lookback_len + 1})."
+                )
+
+            X_val, Y_val = contexts_to_markov_train_states(data, self.lookback_len)
+            X_train, Y_train = contexts_to_markov_train_states(
+                self.data_fit, self.lookback_len
+            )
+            kernel_Yv = self.kernel(Y_val)
+            kernel_XXv = self.kernel(X_train, X_val)
+            kernel_YYv = self.kernel(Y_train, Y_val)
+        else:
+            kernel_Yv = self.kernel_Y
+            kernel_XXv = self.kernel_X
+            kernel_YYv = self.kernel_Y
+        return dual.estimator_risk(
+            kernel_Yv, self.kernel_Y, kernel_XXv, kernel_YYv, self.U, self.V
+        )
+
+    def predict(
+        self,
+        data: np.ndarray,
+        t: int = 1,
+        observables: Optional[Callable] = None,
+    ) -> np.ndarray:
+        """
+        Predicts the state or, if the system is stochastic, its expected value :math:`\mathbb{E}[X_t | X_0 = X]` after ``t`` instants given the initial conditions ``X = data[:, self.lookback_len:, ...]`` being the lookback slice of ``data``.
+
+        .. attention::
+
+            ``data.shape[1]`` must match the lookback length ``self.lookback_len``. Otherwise, an error is raised.
+
+        If ``observables`` are not ``None``, returns the analogue quantity for the observable instead.
+
+        Args:
+            data (numpy.ndarray): Initial conditions to predict. Array of context windows with shape ``(n_init_conditions, self.lookback_len, *self.data_fit.shape[2:])`` (see the note above).
+            t (int): Number of steps in the future to predict (returns the last one).
+            observables (callable or None): Callable or ``None``. If callable should map batches of states of shape ``(batch, *self.data_fit.shape[2:])`` to batches of observables ``(batch, *obs_features_shape)``.
+
+        Returns:
+           The predicted (expected) state/observable at time :math:`t`, shape ``(n_init_conditions, *obs_features_shape)``.
+        """
+        check_is_fitted(
+            self,
+            ["U", "V", "kernel_X", "kernel_Y", "kernel_YX", "data_fit", "lookback_len"],
+        )
+
+        _obs, expected_shape, X_inference, X_fit = parse_observables(
+            observables, data, self.data_fit, self.lookback_len
+        )
+
+        K_Xin_X = self.kernel(X_inference, X_fit[self.nys_centers_idxs])
+        return dual.predict(
+            t, self.U, self.V, self.kernel_YX, K_Xin_X, _obs[self.nys_centers_idxs]
+        ).reshape(expected_shape)
+
+    @profile
+    def eig(
+        self,
+        eval_left_on: Optional[np.ndarray] = None,
+        eval_right_on: Optional[np.ndarray] = None,
+    ):
+        """
+        Returns the eigenvalues of the Koopman/Transfer operator and optionally evaluates left and right eigenfunctions.
+
+        Args:
+            eval_left_on (numpy.ndarray or None): Array of context windows on which the left eigenfunctions are evaluated, shape ``(n_samples, *self.data_fit.shape[1:])``.
+            eval_right_on (numpy.ndarray or None): Array of context windows on which the right eigenfunctions are evaluated, shape ``(n_samples, *self.data_fit.shape[1:])``.
+
+        Returns:
+            (eigenvalues, left eigenfunctions, right eigenfunctions) - Eigenvalues of the Koopman/Transfer operator, shape ``(rank,)``. If ``eval_left_on`` or ``eval_right_on``  are not ``None``, returns the left/right eigenfunctions evaluated at ``eval_left_on``/``eval_right_on``: shape ``(n_samples, rank)``.
+        """
+
+        check_is_fitted(
+            self,
+            ["U", "V", "kernel_X", "kernel_Y", "kernel_YX", "lookback_len", "data_fit"],
+        )
+        if hasattr(self, "_eig_cache"):
+            w, vl, vr = self._eig_cache
+        else:
+            w, vl, vr = dual.estimator_eig(
+                self.U, self.V, self.kernel_X, self.kernel_YX
+            )
+            self._eig_cache = (w, vl, vr)
+
+        X_fit, Y_fit = contexts_to_markov_train_states(self.data_fit, self.lookback_len)
+
+        X_fit, Y_fit = X_fit[self.nys_centers_idxs], Y_fit[self.nys_centers_idxs]
+        if eval_left_on is None and eval_right_on is None:
+            # (eigenvalues,)
+            return w
+        elif eval_left_on is None and eval_right_on is not None:
+            # (eigenvalues, right eigenfunctions)
+            check_contexts_shape(
+                eval_right_on, self.lookback_len, is_inference_data=True
+            )
+            kernel_Xin_X_or_Y = self.kernel(eval_right_on, X_fit)
+            return w, dual.evaluate_eigenfunction(kernel_Xin_X_or_Y, vr)
+        elif eval_left_on is not None and eval_right_on is None:
+            # (eigenvalues, left eigenfunctions)
+            check_contexts_shape(
+                eval_left_on, self.lookback_len, is_inference_data=True
+            )
+            kernel_Xin_X_or_Y = self.kernel(eval_left_on, Y_fit)
+            return w, dual.evaluate_eigenfunction(kernel_Xin_X_or_Y, vl)
+        elif eval_left_on is not None and eval_right_on is not None:
+            # (eigenvalues, left eigenfunctions, right eigenfunctions)
+
+            check_contexts_shape(
+                eval_right_on, self.lookback_len, is_inference_data=True
+            )
+            check_contexts_shape(
+                eval_left_on, self.lookback_len, is_inference_data=True
+            )
+
+            kernel_Xin_X_or_Y_left = self.kernel(eval_left_on, Y_fit)
+            kernel_Xin_X_or_Y_right = self.kernel(eval_right_on, X_fit)
+            return (
+                w,
+                dual.evaluate_eigenfunction(kernel_Xin_X_or_Y_left, vl),
+                dual.evaluate_eigenfunction(kernel_Xin_X_or_Y_right, vr),
+            )
+
+    def modes(
+        self,
+        data: np.ndarray,
+        observables: Optional[Callable] = None,
+    ) -> np.ndarray:
+        """
+        Computes the mode decomposition of arbitrary observables of the Koopman/Transfer operator at the states defined by ``data``.
+
+        Informally, if :math:`(\\lambda_i, \\xi_i, \\psi_i)_{i = 1}^{r}` are eigentriplets of the Koopman/Transfer operator, for any observable :math:`f` the i-th mode of :math:`f` at :math:`x` is defined as: :math:`\\lambda_i \\langle \\xi_i, f \\rangle \\psi_i(x)`. See :footcite:t:`Kostic2022` for more details.
+
+        Args:
+            data (numpy.ndarray): Initial conditions to compute the modes on. See :func:`predict` for additional details.
+            observables (callable or None): Callable or ``None``. If callable should map batches of states of shape ``(batch, *self.data_fit.shape[2:])`` to batches of observables ``(batch, *obs_features_shape)``.
+
+        Returns:
+            Modes of the system at the states defined by ``data``. Array of shape ``(self.rank, n_states, ...)``.
+        """
+        check_is_fitted(
+            self, ["U", "V", "kernel_X", "kernel_YX", "lookback_len", "data_fit"]
+        )
+
+        _obs, expected_shape, X_inference, X_fit = parse_observables(
+            observables, data, self.data_fit, self.lookback_len
+        )
+
+        if hasattr(self, "_eig_cache"):
+            _, lv, rv = self._eig_cache
+        else:
+            _, lv, rv = dual.estimator_eig(
+                self.U, self.V, self.kernel_X, self.kernel_YX
+            )
+
+        K_Xin_X = self.kernel(X_inference, X_fit[self.nys_centers_idxs])
+        _gamma = dual.estimator_modes(K_Xin_X, rv, lv)
+
+        expected_shape = (self.rank,) + expected_shape
+        return np.matmul(_gamma, _obs[self.nys_centers_idxs]).reshape(
+            expected_shape
+        )  # [rank, num_initial_conditions, num_observables]
+
+    def svals(self):
+        """Singular values of the Koopman/Transfer operator.
+
+        Returns:
+            The estimated singular values of the Koopman/Transfer operator. Array of shape `(rank,)`.
+        """
+        check_is_fitted(self, ["U", "V", "kernel_X", "kernel_Y"])
+        return dual.svdvals(self.U, self.V, self.kernel_X, self.kernel_Y)
+
+    @profile
+    def _init_kernels(self, X: np.ndarray, Y: np.ndarray):
+        K_X = self.kernel(X)
+        K_Y = self.kernel(Y)
+        K_YX = self.kernel(Y, X)
+        return K_X, K_Y, K_YX
+
+    @profile
+    def _init_nys_kernels(
+        self, X: np.ndarray, Y: np.ndarray, X_nys: np.ndarray, Y_nys: np.ndarray
+    ):
+        K_X = self.kernel(X, X_nys)
+        K_Y = self.kernel(Y, Y_nys)
+        return K_X, K_Y
+
+    def _pre_fit_checks(self, data: np.ndarray) -> None:
+        """Performs pre-fit checks on the training data.
+
+        Use :func:`check_contexts_shape` to check and sanitize the input data, initialize the kernel matrices and saves the training data.
+
+        Args:
+            data (np.ndarray): Batch of context windows of shape ``(n_samples, context_len, *features_shape)``.
+        """
+        lookback_len = data.shape[1] - 1
+        check_contexts_shape(data, lookback_len)
+        data = np.asanyarray(data)
+        # Save the lookback length as a private attribute of the model
+        self._lookback_len = lookback_len
+        X_fit, Y_fit = contexts_to_markov_train_states(data, self.lookback_len)
+        # Perform random center selection
+        self.nys_centers_idxs = self.center_selection(X_fit.shape[0])
+        X_nys = X_fit[self.nys_centers_idxs]
+        Y_nys = Y_fit[self.nys_centers_idxs]
+
+        self.kernel_X, self.kernel_Y, self.kernel_YX = self._init_kernels(X_nys, Y_nys)
+        kernel_Xnys, kernel_Ynys = self._init_nys_kernels(X_fit, Y_fit, X_nys, Y_nys)
+        self.data_fit = data
+
+        if hasattr(self, "_eig_cache"):
+            del self._eig_cache
+        self._check_rank(self.kernel_X.shape[0])
+        return (
+            kernel_Xnys,
+            kernel_Ynys,
+        )  # Don't need to store them, as they only serve the purpose of fitting.
+
+    def _check_rank(self, n_samples):
+        if not isinstance(self.rank, int) or self.rank < 1:
+            raise ValueError("rank must be a positive integer.")
+        if self.rank > n_samples:
+            raise ValueError(
+                f"rank must be less than the number of samples ({n_samples})."
+            )
+
+    def center_selection(self, num_pts: int):
+        if self.num_centers < 1:
+            num_centers = int(self.num_centers * num_pts)
+        else:
+            num_centers = int(self.num_centers)
+        rng = np.random.default_rng(self.rng_seed)
+        rand_indices = rng.choice(num_pts, num_centers)
+        return rand_indices
+
+    def save(self, filename):
+        """Serialize the model to a file.
+
+        Args:
+            filename (path-like or file-like): Save the model to file.
+        """
+        pickle_save(self, filename)
+
+    @classmethod
+    def load(cls, filename):
+        """Load a serialized model from a file.
+
+        Args:
+            filename (path-like or file-like): Load the model from file.
+
+        Returns:
+            KernelDMD: The loaded model.
+        """
+        return pickle_load(cls, filename)
diff --git a/pyproject.toml b/pyproject.toml
index ea6a73a..cd65cd5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "kooplearn"  
-version = "1.0.2"  
+version = "1.0.4"  
 description = "A package to learn Koopman operators" 
 readme = "README.md"
 requires-python = ">=3.9"
@@ -25,14 +25,6 @@ torch = [
   "lightning",
   "pandas",
 ]
-
-visualizer = [
-  "plotly",
-  "dash",
-  "dash-mantine-components",
-  "dash-iconify",
-  "pandas",
-]
 docs = [
   "sphinx",
   "sphinx-book-theme",
@@ -43,15 +35,17 @@ docs = [
 ]
 dev = [
   "kooplearn[torch]",
-  "kooplearn[visualizer]",
   "kooplearn[docs]",
   "pytest",
   "black",
   "isort",
 ]
+full = [
+  "kooplearn[torch]",
+]
 
 [project.urls]  # Optional
-"Homepage" = "https://github.com/CSML-IIT-UCL/kooplearn"
+"Homepage" = "https://github.com/Machine-Learning-Dynamical-Systems/kooplearn"
 
 [tool.isort]
 # make it compatible with black


Test patch:
diff --git a/tests/dev_nystroem.ipynb b/tests/dev_nystroem.ipynb
new file mode 100644
index 0000000..ef4c817
--- /dev/null
+++ b/tests/dev_nystroem.ipynb
@@ -0,0 +1,398 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%load_ext autoreload\n",
+    "%load_ext line_profiler"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#Reload all modules (except those excluded by %aimport) every time before executing the Python code typed.\n",
+    "%autoreload 2 "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import functools\n",
+    "from time import perf_counter\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "import pnstyler.styler as styler\n",
+    "import ml_confs"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "configs = ml_confs.from_dict({\n",
+    "    'train_samples': 10000,\n",
+    "    'test_samples': 100,\n",
+    "})"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Adapted from https://realpython.com/python-timer/#creating-a-python-timer-decorator\n",
+    "def timer(func):\n",
+    "    @functools.wraps(func)\n",
+    "    def wrapper_timer(*args, **kwargs):\n",
+    "        tic = perf_counter()\n",
+    "        value = func(*args, **kwargs)\n",
+    "        toc = perf_counter()\n",
+    "        elapsed_time = toc - tic\n",
+    "        return value, elapsed_time\n",
+    "\n",
+    "    return wrapper_timer"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Data Generation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 110,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from kooplearn.datasets import Lorenz63, LogisticMap\n",
+    "\n",
+    "raw_data = Lorenz63().sample(X0 = np.ones(3), T=configs.train_samples + 1000 + configs.test_samples)\n",
+    "#raw_data = LogisticMap(N=20).sample(X0 = np.ones(1), T=configs.train_samples + 1000 + configs.test_samples)\n",
+    "mean = np.mean(raw_data, axis=0)\n",
+    "norm = np.max(np.abs(raw_data), axis=0)\n",
+    "# Data rescaling\n",
+    "data = raw_data - mean\n",
+    "data /= norm\n",
+    "\n",
+    "train_data = data[:configs.train_samples + 1]\n",
+    "test_data =  data[-configs.test_samples - 1:]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Estimator fitting"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 111,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from sklearn.gaussian_process.kernels import RBF\n",
+    "from scipy.spatial.distance import pdist\n",
+    "from kooplearn.models import KernelDMD\n",
+    "from kooplearn.models import NystroemKernel\n",
+    "from kooplearn.data import traj_to_contexts\n",
+    "\n",
+    "# Length scale of the kernel: median of the pairwise distances of the dataset\n",
+    "data_pdist = pdist(train_data)\n",
+    "kernel = RBF(length_scale=np.quantile(data_pdist, 0.5))\n",
+    "train_contexts = traj_to_contexts(train_data)\n",
+    "test_contexts = traj_to_contexts(test_data)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 112,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def runner(models, stop):\n",
+    "    results = {}\n",
+    "    for name, model in models.items():\n",
+    "        print(f'{name}')\n",
+    "        model, fit_time = timer(model.fit)(train_contexts[:stop], verbose=False)\n",
+    "        # One-step prediction\n",
+    "        X_pred = model.predict(test_contexts[:, :-1, ...])\n",
+    "        X_true = test_contexts[:, 1, ...]\n",
+    "        # Eigenvalues\n",
+    "        eigs = model.eig()\n",
+    "\n",
+    "        results[name] = {\n",
+    "            'model': model,\n",
+    "            'fit_time': fit_time,\n",
+    "            'train_risk': model.risk(),\n",
+    "            'rMSE_onestep': np.sqrt(np.mean((X_pred - X_true)**2)),\n",
+    "            'eigenvalues': eigs,\n",
+    "        }\n",
+    "    return results\n",
+    "    \n",
+    "def hausdorff_distance(eigs_1, eigs_2):\n",
+    "    dist = np.abs(eigs_1[:, None] - eigs_2[None, :])\n",
+    "    d1 = np.min(dist, axis=1).max()\n",
+    "    d2 = np.min(dist, axis=0).max()\n",
+    "    return max(d1, d2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Benchmarking"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 120,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "reduced_rank = True\n",
+    "rank = 25\n",
+    "num_centers = 250\n",
+    "tikhonov_reg = 1e-6"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 122,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\n",
+      "Training points: 1000\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n",
+      "\n",
+      "\n",
+      "Training points: 1291\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n",
+      "\n",
+      "\n",
+      "Training points: 1668\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n",
+      "\n",
+      "\n",
+      "Training points: 2154\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Warning: 1 out of the 32 squared singular values are smaller than the regularization strength 1.00e-06. Consider redudcing the regularization strength to avoid overfitting.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\n",
+      "Training points: 2782\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n",
+      "\n",
+      "\n",
+      "Training points: 3593\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Warning: 3 out of the 35 squared singular values are smaller than the regularization strength 1.00e-06. Consider redudcing the regularization strength to avoid overfitting.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\n",
+      "Training points: 4641\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n",
+      "\n",
+      "\n",
+      "Training points: 5994\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Warning: 4 out of the 36 squared singular values are smaller than the regularization strength 1.00e-06. Consider redudcing the regularization strength to avoid overfitting.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\n",
+      "Training points: 7742\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Warning: 4 out of the 36 squared singular values are smaller than the regularization strength 1.00e-06. Consider redudcing the regularization strength to avoid overfitting.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\n",
+      "Training points: 10000\n",
+      "KernelDMD\n",
+      "RandSVDKernelDMD\n",
+      "Nystroem\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Warning: 1 out of the 33 squared singular values are smaller than the regularization strength 1.00e-06. Consider redudcing the regularization strength to avoid overfitting.\n"
+     ]
+    }
+   ],
+   "source": [
+    "train_stops = np.logspace(3, 4, 10).astype(int)\n",
+    "results = []\n",
+    "for stop in train_stops:\n",
+    "    models = {\n",
+    "        'KernelDMD': KernelDMD(kernel=kernel, reduced_rank=reduced_rank, tikhonov_reg=tikhonov_reg, rank = rank, svd_solver='arnoldi'),\n",
+    "        'RandSVDKernelDMD': KernelDMD(kernel=kernel, reduced_rank=reduced_rank, svd_solver='randomized', tikhonov_reg=tikhonov_reg, rank = rank, rng_seed=5),\n",
+    "        'Nystroem': NystroemKernel(kernel=kernel, reduced_rank=reduced_rank, svd_solver='arnoldi', tikhonov_reg=tikhonov_reg, rank = rank, num_centers=num_centers, rng_seed=42),\n",
+    "    }\n",
+    "    print(f'\\n\\nTraining points: {stop}')\n",
+    "    results.append(runner(models, stop))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 123,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "timings = {name: [res[name]['fit_time'] for res in results] for name in models.keys()}\n",
+    "rMSE = {name: [res[name]['rMSE_onestep'] for res in results] for name in models.keys()}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 124,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "styler.set_color_palette('categorical_dark')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 125,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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",
+      "text/plain": [
+       "<Figure size 791.034x263.678 with 2 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "\n",
+    "fig, axes = plt.subplots(ncols=2, figsize=styler.figsize(width_to_height=3), dpi=144)\n",
+    "for name in models.keys():\n",
+    "    axes[0].plot(train_stops, rMSE[name], '.-', label=name)\n",
+    "    axes[1].plot(train_stops, timings[name], '.-', label=name)\n",
+    "\n",
+    "axes[0].set_title('rMSE')\n",
+    "axes[1].set_title('Training time (s)')\n",
+    "axes[1].legend(frameon = False, loc = 'upper left')\n",
+    "axes[1].set_yscale('log')\n",
+    "for ax in axes:\n",
+    "    ax.set_xscale('log')\n",
+    "    \n",
+    "    ax.set_xlabel('Training points')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "kooplearn",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.4"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/tests/test_estimator_scale_invariance.py b/tests/test_estimator_scale_invariance.py
index dbfb81c..8621804 100644
--- a/tests/test_estimator_scale_invariance.py
+++ b/tests/test_estimator_scale_invariance.py
@@ -58,10 +58,10 @@ def test_reduced_rank_tikhonov_dual_scale_invariance(
     K_Y = Y @ (Y.T)
 
     # Dual
-    U, V = dual.fit_reduced_rank_regression(
+    U, V, _ = dual.fit_reduced_rank_regression(
         K_X, K_Y, tikhonov_reg, rank, svd_solver=svd_solver
     )
-    U_scaled, V_scaled = dual.fit_reduced_rank_regression(
+    U_scaled, V_scaled, _ = dual.fit_reduced_rank_regression(
         scale_factor * K_X,
         scale_factor * K_Y,
         tikhonov_reg * scale_factor,
diff --git a/tests/test_kernel_estimators.py b/tests/test_kernel_estimators.py
index da2921e..f6fa198 100644
--- a/tests/test_kernel_estimators.py
+++ b/tests/test_kernel_estimators.py
@@ -68,7 +68,7 @@ def test_KernelDMD_fit_predict_eig_modes_save_load(
             X_pred = model.predict(test_data, observables=observables)
             assert X_pred.shape == (data.shape[0],) + data.shape[2:]
             modes = model.modes(test_data, observables=observables)
-            assert modes.shape == (rank,) + (data.shape[0],) + data.shape[2:]
+            assert modes.shape[1:] == (data.shape[0],) + data.shape[2:]
         else:
             Y = data[:, -1, ...]
             _dummy_vec = observables(Y)
@@ -77,11 +77,11 @@ def test_KernelDMD_fit_predict_eig_modes_save_load(
             X_pred = model.predict(test_data, observables=observables)
             assert X_pred.shape == _dummy_vec.shape
             modes = model.modes(test_data, observables=observables)
-            _target_shape = (rank,) + _dummy_vec.shape
-            assert modes.shape == _target_shape
+            _target_shape = _dummy_vec.shape
+            assert modes.shape[1:] == _target_shape
 
         vals, lv, rv = model.eig(eval_left_on=test_data, eval_right_on=test_data)
-        assert vals.shape[0] == rank
+        assert vals.shape[0] <= rank
         assert vals.ndim == 1
         tmp_path = Path(__file__).parent / f"tmp/model.bin"
         model.save(tmp_path)
diff --git a/tests/test_low_level_primal_dual_consistency.py b/tests/test_low_level_primal_dual_consistency.py
index 1d85bf8..e5876ed 100644
--- a/tests/test_low_level_primal_dual_consistency.py
+++ b/tests/test_low_level_primal_dual_consistency.py
@@ -33,7 +33,7 @@ def _compare_evd(evd_1: EigenDecomposition, evd_2: EigenDecomposition) -> bool:
     return True
 
 
-@pytest.mark.parametrize("tikhonov_reg", [0.0, 1e-3])
+@pytest.mark.parametrize("tikhonov_reg", [1e-3])
 @pytest.mark.parametrize("svd_solver", ["full", "arnoldi"])
 @pytest.mark.parametrize("dt", [1, 2, 3])
 def test_reduced_rank_tikhonov_primal_dual_consistency(dt, svd_solver, tikhonov_reg):
@@ -61,7 +61,7 @@ def test_reduced_rank_tikhonov_primal_dual_consistency(dt, svd_solver, tikhonov_
     K_testX = X_test @ (X.T)
 
     # Dual
-    U, V = dual.fit_reduced_rank_regression(
+    U, V, _ = dual.fit_reduced_rank_regression(
         K_X, K_Y, tikhonov_reg, rank, svd_solver=svd_solver
     )
     dual_predict = dual.predict(dt, U, V, K_YX, K_testX, Y)
@@ -101,7 +101,7 @@ def test_reduced_rank_tikhonov_primal_dual_consistency(dt, svd_solver, tikhonov_
 
 
 @pytest.mark.parametrize("tikhonov_reg", [0.0, 1e-3])
-@pytest.mark.parametrize("rank", [5, None])
+@pytest.mark.parametrize("rank", [5, 7])
 @pytest.mark.parametrize("svd_solver", ["full", "arnoldi"])
 @pytest.mark.parametrize("dt", [1, 2, 3])
 def test_tikhonov_primal_dual_consistency(dt, svd_solver, rank, tikhonov_reg):
@@ -128,7 +128,7 @@ def test_tikhonov_primal_dual_consistency(dt, svd_solver, rank, tikhonov_reg):
     K_testX = X_test @ (X.T)
 
     # Dual
-    U, V = dual.fit_principal_component_regression(
+    U, V, _ = dual.fit_principal_component_regression(
         K_X, tikhonov_reg, rank=rank, svd_solver=svd_solver
     )
 
diff --git a/tests/test_spectral_decomposition.py b/tests/test_spectral_decomposition.py
index f773f97..f3b763f 100644
--- a/tests/test_spectral_decomposition.py
+++ b/tests/test_spectral_decomposition.py
@@ -83,7 +83,7 @@ def test_dual_eig_decomposition(tikhonov_reg, svd_solver):
     K_YX = Y @ (X.T)
 
     # Dual
-    U, V = dual.fit_principal_component_regression(
+    U, V, _ = dual.fit_principal_component_regression(
         K_X, tikhonov_reg, rank=rank, svd_solver=svd_solver
     )
     eig, lv, rv = dual.estimator_eig(U, V, K_X, K_YX)