diff --git a/benchmarks/decomposition_methods.ipynb b/benchmarks/decomposition_methods.ipynb new file mode 100644 index 0000000000..b76117593e --- /dev/null +++ b/benchmarks/decomposition_methods.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 119, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from __future__ import annotations\n", + "\n", + "import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from tqdm import tqdm\n", + "\n", + "from river.decomposition import (\n", + " OnlineDMD,\n", + " OnlinePCA,\n", + " OnlineSVD,\n", + " OnlineSVDZhang,\n", + ")\n", + "from river.preprocessing import Hankelizer\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "# Set the random seed for reproducibility\n", + "seed = 42\n", + "np.random.seed(seed)\n", + "\n", + "# Step 1: Generate Gaussian noise with mean 0 and variance 1\n", + "gaussian_noise = np.random.normal(0, 1, (10000, 1))\n", + "\n", + "# Step 2: Generate exponentially increasing X from 0 to 10\n", + "steps = np.logspace(0, 1, 10)\n", + "X = np.concatenate([np.full(1000, exp_val) for exp_val in steps])[\n", + " :, np.newaxis\n", + "]\n", + "\n", + "# Step 3: Combine the Gaussian noise with the exponential increments\n", + "X = gaussian_noise + X\n", + "\n", + "# Display the result array\n", + "plt.plot(X)\n", + "plt.grid(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [], + "source": [ + "models = [\n", + " OnlineDMD(r=2, seed=seed),\n", + " OnlinePCA(n_components=2, seed=seed),\n", + " OnlineSVD(n_components=2, seed=seed),\n", + " OnlineSVDZhang(n_components=2, seed=seed),\n", + "]\n", + "n_feats_range = range(2, 20)\n", + "repeat = 5\n", + "iterations = len(models) * len(n_feats_range) * repeat * len(X)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "times_per_model_np = {model.__class__.__name__: [] for model in models}\n", + "times_per_model_pd = {model.__class__.__name__: [] for model in models}\n", + "\n", + "with tqdm(total=iterations, mininterval=10) as pbar:\n", + " for model in models:\n", + " for n_features in n_feats_range:\n", + " for X_iter, times_per_model_ in zip(\n", + " [X, pd.DataFrame(X).to_dict(orient=\"records\")],\n", + " [times_per_model_np, times_per_model_pd]\n", + " ):\n", + " pipeline = Hankelizer(n_features) | model.clone()\n", + " times = np.zeros(repeat)\n", + " for rep in range(repeat):\n", + " tic = time.time()\n", + " for x in X_iter:\n", + " pipeline.transform_one(x)\n", + " pipeline.learn_one(x)\n", + " pbar.update(1)\n", + " times[rep] = time.time() - tic\n", + " times_per_model_[model.__class__.__name__].append(times)\n", + "\n", + "df_times_per_model_np = pd.DataFrame(times_per_model_np, index=n_feats_range)\n", + "df_times_per_model_pd = pd.DataFrame(times_per_model_pd, index=n_feats_range)" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAHHCAYAAAC2rPKaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd3gU1frHP7N90xPSe6H3DiJFBERBsSACKsVesLfftVwVe7mWexV7wS6CNBUVUBQFpLcQQgJJICG9burW+f0xySZLEgghYQOcz/PMsztnzsy8Mzs78533vOc9kizLMgKBQCAQCARnISp3GyAQCAQCgUDQXgihIxAIBAKB4KxFCB2BQCAQCARnLULoCAQCgUAgOGsRQkcgEAgEAsFZixA6AoFAIBAIzlqE0BEIBAKBQHDWIoSOQCAQCASCsxYhdAQCgUAgEJy1CKFzFvPqq68SHx+PWq2mf//+7jbnnOGXX36hf//+GAwGJEmitLTU3SYJjsPcuXOJjY11KauoqODmm28mNDQUSZK47777TmqbqampXHTRRfj6+iJJEsuXL28ze88EFi5ciCRJZGRkuNuU084XX3xB9+7d0Wq1+Pn5udscAULonFbq/vx1k8FgoGvXrtx1113k5eW16b5Wr17NI488wvnnn8+nn37KCy+80KbbFzRNUVER11xzDUajkQULFvDFF1/g6enZZN266+FMJSMjA0mS+OOPP9xtSiNO1bYXXniBhQsXcscdd/DFF18wa9ask9rmnDlz2Lt3L88//zxffPEFgwcPbpUdgjOL5ORk5s6dS0JCAh9++CEffPBBu+xn48aNPP300+IlqoVo3G3AucgzzzxDXFwcNTU1/P3337z77rusWrWKxMREPDw82mQfv//+OyqVio8//hidTtcm2xScmK1bt1JeXs6zzz7L+PHj3W2OoJX8/vvvDB8+nKeeespZ1lLvRHV1NZs2beLxxx/nrrvuaicLOzazZs1ixowZ6PV6d5tyWvnjjz9wOBz897//pXPnzu22n40bNzJ//nzmzp0rvEYtQHh03MAll1zC9ddfz80338zChQu57777SE9PZ8WKFae87aqqKgDy8/MxGo1tJnJkWaa6urpNtnU2k5+fD+CWm0/db38sNpsNi8Vymq05s8nPz2/1b1hQUAC07TVQWVnZZttqDTU1NTgcjhbXV6vVzqbbcwl3/v/bAndfZ+2FEDodgAsvvBCA9PR0Z9mXX37JoEGDMBqNBAQEMGPGDDIzM13Wu+CCC+jduzfbt29n9OjReHh48NhjjyFJEp9++imVlZXOZrKFCxcCykPv2WefJSEhAb1eT2xsLI899hhms9ll27GxsVx66aX8+uuvDB48GKPRyPvvv88ff/yBJEl89913zJ8/n4iICLy9vbn66qspKyvDbDZz3333ERwcjJeXFzfccEOjbX/66adceOGFBAcHo9fr6dmzJ++++26j81Jnw99//83QoUMxGAzEx8fz+eefN6pbWlrK/fffT2xsLHq9nsjISGbPnk1hYaGzjtls5qmnnqJz587o9XqioqJ45JFHGtnXHIsXL3b+JoGBgVx//fUcPXrU5feYM2cOAEOGDEGSJObOnduibdexYsUKJk+eTHh4OHq9noSEBJ599lnsdrtLveZ++7rmlf/85z+8+eabzt85KSkJi8XCk08+yaBBg/D19cXT05NRo0axbt0653ZlWSY2NpbLL7+8kW01NTX4+vpy2223tfh4tm3bhiRJfPbZZ42W/frrr0iSxI8//ghAeXk59913n/M3DA4OZsKECezYsaPF+zsRy5cvp3fv3hgMBnr37s2yZctcltdd3+np6fz000/O/09LvTlPP/00MTExADz88MNIkuQS/7Nz504uueQSfHx88PLyYty4cfzzzz8u26hr0vzzzz+58847CQ4OJjIyssn95eXlodFomD9/fqNlBw4cQJIk3n77bQCKi4t56KGH6NOnD15eXvj4+HDJJZewe/fuJs/Bt99+yxNPPEFERAQeHh7s2rULSZJ44403Gu1r48aNSJLEN99843IMDc/byfyf9+zZw5gxYzAajURGRvLcc8/x6aeftui3mDt3Ll5eXhw9epQrrrgCLy8vgoKCeOihhxr9jyorK3nwwQeJiopCr9fTrVs3/vOf/yDL8nH30RSxsbFOD2BQUBCSJPH00087l//888+MGjUKT09PvL29mTx5Mvv27Wt03HPnziU+Ph6DwUBoaCg33ngjRUVFzjpPP/00Dz/8MABxcXEu12jd/7/uft+QY+15+umnkSSJpKQkrr32Wvz9/Rk5cqRzeUueQampqUydOpXQ0FAMBgORkZHMmDGDsrKykz5/7YlouuoAHDp0CIBOnToB8Pzzz/Pvf/+ba665hptvvpmCggLeeustRo8ezc6dO13eFoqKirjkkkuYMWMG119/PSEhIQwePJgPPviALVu28NFHHwEwYsQIAG6++WY+++wzrr76ah588EE2b97Miy++yP79+xvd9A8cOMDMmTO57bbbuOWWW+jWrZtz2YsvvojRaORf//oXBw8e5K233kKr1aJSqSgpKeHpp5/mn3/+YeHChcTFxfHkk08613333Xfp1asXU6ZMQaPR8MMPP3DnnXficDiYN2+eiw0HDx7k6quv5qabbmLOnDl88sknzJ07l0GDBtGrVy9ACRwdNWoU+/fv58Ybb2TgwIEUFhaycuVKsrKyCAwMxOFwMGXKFP7++29uvfVWevTowd69e3njjTdISUk5YbDowoULueGGGxgyZAgvvvgieXl5/Pe//2XDhg3O3+Txxx+nW7dufPDBB87myYSEhJO4EpT9eHl58cADD+Dl5cXvv//Ok08+iclk4tVXX3Wp29RvX8enn35KTU0Nt956K3q9noCAAEwmEx999BEzZ87klltuoby8nI8//piJEyeyZcsW+vfvjyRJXH/99bzyyisUFxcTEBDg3OYPP/yAyWTi+uuvb/HxDB48mPj4eL777junCKxj0aJF+Pv7M3HiRABuv/12lixZwl133UXPnj0pKiri77//Zv/+/QwcOPCkzmNTrF69mqlTp9KzZ09efPFFioqKuOGGG1xERI8ePfjiiy+4//77iYyM5MEHHwSUB1edp+Z4XHXVVfj5+XH//fczc+ZMJk2ahJeXFwD79u1j1KhR+Pj48Mgjj6DVann//fe54IIL+PPPPxk2bJjLtu68806CgoJ48sknm33TDgkJYcyYMXz33XcuzWygnF+1Ws20adMASEtLY/ny5UybNo24uDjy8vJ4//33GTNmDElJSYSHh7us/+yzz6LT6XjooYcwm810796d888/n6+++or777/fpe5XX32Ft7d3kwK5IS35Px89epSxY8ciSRKPPvoonp6efPTRRyfVDGa325k4cSLDhg3jP//5D2vXruW1114jISGBO+64A1BE/ZQpU1i3bh033XQT/fv359dff+Xhhx/m6NGjTQq64/Hmm2/y+eefs2zZMt599128vLzo27cvoAQoz5kzh4kTJ/Lyyy9TVVXFu+++y8iRI9m5c6dTDK9Zs4a0tDRuuOEGQkND2bdvHx988AH79u3j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D0888QQbN27k/PPP57333uO5555r8T7bG9F01cG46qqrUKvVzJ8/v9FbjizLLt0MT5ZJkyYBNHpg1Xk5Jk+e3Optt5S6N4SGx1ZWVsann37a6m1OnTqV3bt3N+o11nA/11xzDUePHuXDDz9sVKe6uvq4+SMGDx5McHAw7733nktX9J9//pn9+/e32Xlr6txYLBbeeeeddtv+5s2b2bRpU5P1Z82aRVJSEg8//DBqtbpJYdJSevToQZ8+fVi0aBGLFi0iLCzMRczZ7fZGLu/g4GDCw8NdznlhYSHJycknHRMQFhZG//79+eyzz1z2s2bNGpKSklp5VC1HrVZz0UUXsWLFCpfu0Xl5eXz99deMHDkSHx+fVm/fz8+PiRMn8t133/Htt9+i0+m44oorGtlw7D1l8eLFLikSWoJGo2HmzJl89913LFy4kD59+jh7F7UFEydOZNOmTezatctZVlxc7BIX1BZMmjQJu93u7H5fxxtvvIEkSS7iOjk5mSNHjrRqPxMnTsTHx4cXXngBq9XaaHldT6mm/p/Q+H4NOLOtHytofHx8CAwMZP369S7lJ3MPaekzyGQyucRegiJ6VCpVi1N2nC6ER6eDkZCQwHPPPcejjz5KRkYGV1xxBd7e3qSnp7Ns2TJuvfVWHnrooVZtu1+/fsyZM4cPPvjA2UyyZcsWPvvsM6644grGjh3bxkfTmIsuugidTsdll13GbbfdRkVFBR9++CHBwcHk5OS0apsPP/wwS5YsYdq0adx4440MGjSI4uJiVq5cyXvvvUe/fv2YNWsW3333Hbfffjvr1q3j/PPPx263k5yczHfffefMF9QUWq2Wl19+mRtuuIExY8Ywc+ZMZ/fy2NjYRl1tW8uIESPw9/dnzpw53HPPPUiSxBdffNFmbv1LL72UpUuXcuWVVzJ58mTS09N577336NmzJxUVFY3qT548mU6dOrF48WIuueQSgoODT2n/06dP58knn8RgMHDTTTe5NHWUl5cTGRnJ1VdfTb9+/fDy8mLt2rVs3bqV1157zVnv7bffZv78+axbt44LLrjgpPb/4osvMnnyZEaOHMmNN95IcXExb731Fr169Wry+Nua5557jjVr1jBy5EjuvPNONBoN77//PmazmVdeeeWUtz99+nSuv/563nnnHSZOnNgoad2ll17KM888ww033MCIESPYu3cvX331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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# [donotremove]\n", + "df_samples_per_second_np = df_times_per_model_np.map(\n", + " lambda x: len(X) / np.mean(x)\n", + ")\n", + "df_samples_per_second_pd = df_times_per_model_pd.map(\n", + " lambda x: len(X) / np.mean(x)\n", + ")\n", + "\n", + "ax = df_samples_per_second_np.add_prefix(\"np: \").plot(\n", + " grid=True,\n", + " logy=True,\n", + ")\n", + "ax.set_prop_cycle(None) # type: ignore\n", + "df_samples_per_second_pd.add_prefix(\"pd: \").plot(\n", + " grid=True,\n", + " logy=True,\n", + " style=\"--\",\n", + " ax=ax,\n", + ")\n", + "ax.set_xlabel(\"no. features\")\n", + "ax.set_ylabel(\"samples/second\")\n", + "_ = ax.set_title(\"Performance of 'array' vs. 'df' for varying no. features\")" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "OnlineDMD (np - pd) [samples/second] 241.793371\n", + "OnlinePCA (np - pd) [samples/second] 1444.972470\n", + "OnlineSVD (np - pd) [samples/second] -58.670012\n", + "OnlineSVDZhang (np - pd) [samples/second] 309.198108\n", + "dtype: float64" + ] + }, + "execution_count": 121, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# [donotremove]\n", + "# Regarding whole dataset, how much slower is pd comparing to np in abs\n", + "(df_samples_per_second_np.mean() - df_samples_per_second_pd.mean()).add_suffix(\n", + " \" (np - pd) [samples/second]\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "OnlineDMD (np - pd) [%] 0.024179\n", + "OnlinePCA (np - pd) [%] 0.144497\n", + "OnlineSVD (np - pd) [%] -0.005867\n", + "OnlineSVDZhang (np - pd) [%] 0.030920\n", + "dtype: float64" + ] + }, + "execution_count": 118, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# [donotremove]\n", + "# Regarding whole dataset, how much slower is pd comparing to np in %\n", + "(df_samples_per_second_np.mean() - df_samples_per_second_pd.mean()).add_suffix(\n", + " \" (np - pd) [%]\"\n", + ") / len(X)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/unreleased.md b/docs/unreleased.md new file mode 100644 index 0000000000..529739242e --- /dev/null +++ b/docs/unreleased.md @@ -0,0 +1,21 @@ +# Unreleased + +## drift + +- Added `FHDDM` drift detector. +- Added a `iter_polars` function to iterate over the rows of a polars DataFrame. + +## neighbors + +- Simplified `neighbors.SWINN` to avoid recursion limit and pickling issues. + +## decomposition + +- Added `decomposition.OnlineSVD` class to perform Singular Value Decomposition. +- Added `decomposition.OnlinePCA` class to perform Principal Component Analysis. +- Added `decomposition.OnlineDMD` class to perform Dynamic Mode Decomposition. +- Added `decomposition.OnlineDMDwC` class to perform Dynamic Mode Decomposition with Control. + +## preprocessing + +- Added `preprocessing.Hankelizer` class to perform Hankelization of data stream. diff --git a/river/compose/pipeline.py b/river/compose/pipeline.py index 2c894ede04..023723c73c 100644 --- a/river/compose/pipeline.py +++ b/river/compose/pipeline.py @@ -471,11 +471,11 @@ def learn_one(self, x: dict, y=None, **params): # Here the step is not a transformer, and it's supervised, such as a LinearRegression. # This is usually the last step of the pipeline. elif step._supervised: - step.learn_one(x=x, y=y) + step.learn_one(x=x, y=y, **params) # Here the step is not a transformer, and it's unsupervised, such as a KMeans. This # is also usually the last step of the pipeline. else: - step.learn_one(x=x) + step.learn_one(x=x, **params) def _transform_one(self, x: dict): """This methods takes care of applying the first n - 1 steps of the pipeline, which are diff --git a/river/decomposition/__init__.py b/river/decomposition/__init__.py new file mode 100644 index 0000000000..fd87840687 --- /dev/null +++ b/river/decomposition/__init__.py @@ -0,0 +1,16 @@ +"""Decomposition. + +""" +from __future__ import annotations + +from .odmd import OnlineDMD, OnlineDMDwC +from .opca import OnlinePCA +from .osvd import OnlineSVD, OnlineSVDZhang + +__all__ = [ + "OnlineSVD", + "OnlineSVDZhang", + "OnlineDMD", + "OnlineDMDwC", + "OnlinePCA", +] diff --git a/river/decomposition/odmd.py b/river/decomposition/odmd.py new file mode 100644 index 0000000000..5a10e283cc --- /dev/null +++ b/river/decomposition/odmd.py @@ -0,0 +1,1275 @@ +"""Online Dynamic Mode Decomposition (DMD) in [River API](riverml.xyz). + +This module contains the implementation of the Online DMD, Weighted Online DMD, +and DMD with Control algorithms. It is based on the paper by Zhang et al. [^1] +and implementation of authors available at +[GitHub](https://github.com/haozhg/odmd). However, this implementation provides +a more flexible interface aligned with River API covers and separates update +and revert methods to operate with Rolling and TimeRolling wrapers. + +TODO: + + - [x] Compute amlitudes of the singular values of the input matrix. + - [x] Benchmark on performance with np vs pd input + - [ ] Update prediction computation for continuous time + x(t) = Phi exp(diag(ln(Lambda) / dt) * t) Phi^+ x(0) (MIT lecture) + continuous time eigenvalues exp(Lambda * dt) (Zhang et al. 2019) + - [ ] Figure out how to use as both MiniBatchRegressor and MiniBatchTransformer + - [ ] Find out why some values of A change sign between consecutive updates + - [ ] Fix inconsistency in xi (amplitudes) computation + +References: + [^1]: Zhang, H., Clarence Worth Rowley, Deem, E.A. and Cattafesta, L.N. + (2019). Online Dynamic Mode Decomposition for Time-Varying Systems. Siam + Journal on Applied Dynamical Systems, 18(3), pp.1586-1609. + doi:[10.1137/18m1192329](https://doi.org/10.1137/18m1192329). +""" + +from __future__ import annotations + +from typing import Literal + +import numpy as np +import pandas as pd +import scipy as sp + +from river.base import MiniBatchRegressor, MiniBatchTransformer + +from .osvd import OnlineSVDZhang as OnlineSVD + +__all__ = [ + "OnlineDMD", + "OnlineDMDwC", +] + + +class OnlineDMD(MiniBatchRegressor, MiniBatchTransformer): + """Online Dynamic Mode Decomposition (DMD). + + This regressor is a class that implements online dynamic mode decomposition + The time complexity (multiply-add operation for one iteration) is O(4n^2), + and space complexity is O(2n^2), where n is the state dimension. + + This estimator supports learning with mini-batches with same time and space + complexity as the online learning. It can be used as Rolling or TimeRolling + estimator. + + OnlineDMD implements `transform_one` and `transform_many` methods like + unsupervised MiniBatchTransformer. In such case, we may use `learn_one` + without `y` and `learn_many` without `Y` to learn the model. + In that case OnlineDMD preserves previous snapshot and uses it as x while + current snapshot is used as y, therefore, being delayed by one sample. + + At time step t, define two matrices X(t) = [x(1),x(2),...,x(t)], + Y(t) = [y(1),y(2),...,y(t)], that contain all the past snapshot pairs, + where x(t), y(t) are the n dimensional state vector, y(t) = f(x(t)) is + the image of x(t), f() is the dynamics. + + Here, if the (discrete-time) dynamics are given by z(t) = f(z(t-1)), + then x(t), y(t) should be measurements correponding to consecutive + states z(t-1) and z(t). + + An exponential weighting factor can be used to place more weight on + recent data. + + Args: + r: Number of modes to keep. If 0 (default), all modes are kept. + w: Weighting factor in (0,1]. Smaller value allows more adpative + learning, but too small weighting may result in model identification + instability (relies only on limited recent snapshots). + initialize: number of snapshot pairs to initialize the model with. If 0 + the model will be initialized with random matrix A and P = \alpha I + where \alpha is a large positive scalar. If initialize is smaller + than the state dimension, it will be set to the state dimension and + raise a warning. Defaults to 1. + exponential_weighting: Whether to use exponential weighting in revert + seed: Random seed for reproducibility (initialize A with random values) + + Attributes: + m: state dimension x(t) as in z(t) = f(z(t-1)) or y(t) = f(t, x(t)) + n_seen: number of seen samples (read-only), reverted if windowed + feature_names_in_: list of feature names. Used for dict inputs. + A: DMD matrix, size n by n (non-Hermitian) + _P: inverse of covariance matrix of X (symmetric) + + Examples: + >>> import numpy as np + >>> import pandas as pd + >>> n = 101; freq = 2.; tspan = np.linspace(0, 10, n); dt = 0.1 + >>> a1 = 1; a2 = 1; phase1 = -np.pi; phase2 = np.pi / 2 + >>> w1 = np.cos(np.pi * freq * tspan) + >>> w2 = -np.sin(np.pi * freq * tspan) + >>> df = pd.DataFrame({'w1': w1[:-1], 'w2': w2[:-1]}) + + >>> model = OnlineDMD(r=2, w=0.1, initialize=0) + >>> X, Y = df.iloc[:-1], df.shift(-1).iloc[:-1] + + >>> for (_, x), (_, y) in zip(X.iterrows(), Y.iterrows()): + ... x, y = x.to_dict(), y.to_dict() + ... model.learn_one(x, y) + >>> eig, _ = np.log(model.eig[0]) / dt + >>> r, i = eig.real, eig.imag + >>> np.isclose(eig.real, 0.) + True + >>> np.isclose(eig.imag, np.pi * freq) + True + + >>> model.xi # TODO: verify the result + array([0.54244, 0.54244]) + + >>> from river.utils import Rolling + >>> model = Rolling(OnlineDMD(r=2, w=1.), 10) + >>> X, Y = df.iloc[:-1], df.shift(-1).iloc[:-1] + + >>> for (_, x), (_, y) in zip(X.iterrows(), Y.iterrows()): + ... x, y = x.to_dict(), y.to_dict() + ... model.update(x, y) + + >>> eig, _ = np.log(model.eig[0]) / dt + >>> r, i = eig.real, eig.imag + >>> np.isclose(eig.real, 0.) + True + >>> np.isclose(eig.imag, np.pi * freq) + True + + >>> np.isclose(model.truncation_error(X.values, Y.values), 0) + True + + >>> w_pred = model.predict_one(np.array([w1[-2], w2[-2]])) + >>> np.allclose(w_pred, [w1[-1], w2[-1]]) + True + + >>> w_pred = model.predict_many(np.array([1, 0]), 10) + >>> np.allclose(w_pred.T, [w1[1:11], w2[1:11]]) + True + + References: + [^1]: Zhang, H., Clarence Worth Rowley, Deem, E.A. and Cattafesta, L.N. + (2019). Online Dynamic Mode Decomposition for Time-Varying Systems. + Siam Journal on Applied Dynamical Systems, 18(3), pp.1586-1609. + doi:[10.1137/18m1192329](https://doi.org/10.1137/18m1192329). + """ + + def __init__( + self, + r: int = 0, + w: float = 1.0, + initialize: int = 1, + exponential_weighting: bool = False, + eig_rtol: float | None = None, + seed: int | None = None, + ) -> None: + self.r = int(r) + if self.r != 0: + # Forcing orthogonality makes the results more unstable + self._svd = OnlineSVD( + n_components=self.r, + seed=seed, + ) + self.w = float(w) + assert self.w > 0 and self.w <= 1 + self.initialize = int(initialize) + self.exponential_weighting = exponential_weighting + self.eig_rtol = eig_rtol + assert self.eig_rtol is None or 0.0 <= self.eig_rtol < 1.0 + self.seed = seed # used with sparse SVD, otherwise its deterministic + + np.random.seed(self.seed) + + self.m: int + self.n_seen: int = 0 + self.feature_names_in_: list[str] + self.A: np.ndarray + self._P: np.ndarray + self._Y: np.ndarray # for xi and modes computation + + self._A_last: np.ndarray + self._A_allclose: bool = False + self._n_cached: int = 0 # TODO: remove before merge + self._n_computed: int = 0 # TODO: remove before merge + + # Properties to be reset at each update + self._eig: tuple[np.ndarray, np.ndarray] | None = None + self._modes: np.ndarray | None = None + self._xi: np.ndarray | None = None + + @property + def eig(self) -> tuple[np.ndarray, np.ndarray]: + """Compute and return DMD eigenvalues and DMD modes at current step""" + if self._eig is None: + # TODO: need to check if SVD is initialized in case r < m. Otherwise, transformation will fail. + # TODO: explore faster ways to compute eig + # TODO: find out whether Phi should have imaginary part + Lambda, Phi = sp.linalg.eig(self.A, check_finite=False) + + sort_idx = np.argsort(Lambda)[::-1] + if not np.array_equal(sort_idx, range(len(Lambda))): + Lambda = Lambda[sort_idx] + Phi = Phi[:, sort_idx] + self._eig = Lambda, Phi + self._n_computed += 1 + return self._eig + + @property + def modes(self) -> np.ndarray: + """Reconstruct high dimensional discrete-time DMD modes""" + if self._modes is None: + _, Phi_comp = self.eig + if self.r < self.m: + # Exact DMD modes (Tu et al. (2016)) + # self._Y.T @ self._svd._Vt.T is increasingly more computationally expensive without rolling + # self._modes = ( + # self._Y.T + # @ self._svd._Vt.T # sign may change if sparse SVD is used + # @ np.diag(1 / self._svd._S) + # @ Phi_comp # sign may change if sparse EIG is used + # ) + + # Projected DMD modes (Schmid (2010)) - faster, not guaranteed + # self._modes = self._svd._U @ Phi_comp + # This regularization works much better than the above + # if high variance in svs of X + self._modes = ( + self._svd._U @ np.diag(1 / self._svd._S) @ Phi_comp + ) + else: + self._modes = Phi_comp + return self._modes + + @property + def xi(self) -> np.ndarray: + """Amlitudes of the singular values of the input matrix.""" + if self._xi is None: + Lambda, Phi = self.eig + # Compute Discrete temporal dynamics matrix (Vandermonde matrix). + C = np.vander(Lambda, self.n_seen, increasing=True) + # xi = self.Phi.conj().T @ self._Y @ np.linalg.pinv(self.C) + + from scipy.optimize import minimize + + def objective_function(x): + return np.linalg.norm( + self._Y[:, : self.r].T - Phi @ np.diag(x) @ C, "fro" + ) + 0.5 * np.linalg.norm(x, 1) + + # Minimize the objective function + xi = minimize(objective_function, np.ones(self.r)).x + self._xi = xi + return self._xi + + @property + def A_allclose(self) -> bool: + """Check if A has changed since last update of eigenvalues""" + if self.eig_rtol is None: + return False + return np.allclose( + np.abs(self._A_last[: self.A.shape[0], : self.A.shape[1]]), + np.abs(self.A), + rtol=self.eig_rtol, + ) + + def _init_update(self) -> None: + if self.r == 0: + self.r = self.m + if self.initialize > 0 and self.initialize < self.r: + self.initialize = self.r + + # Zhang (2019) suggests to initialize A with random values + self.A = np.eye(self.r) + self._A_last = self.A.copy() + self._X_init = np.empty((self.initialize, self.m)) + self._Y_init = np.empty((self.initialize, self.m)) + self._Y = np.empty((0, self.m)) + + def _truncate_w_svd( + self, + x: np.ndarray, + y: np.ndarray, + svd_modify: Literal["update", "revert"] | None = None, + ): + U_prev = self._svd._U + # We can update svd on x now without leaking new sample which is in y + # try: + if svd_modify == "update": + self._svd.update(x) + elif svd_modify == "revert": + self._svd.revert(x) + _U = self._svd._U + _UU = _U.T @ U_prev + x = x @ _U + # p != self.m and p == self.A.shape[0] in case of DMDwC + p = self.A.shape[0] + y = y @ _U[: y.shape[1], :p] + # Check if A is square + if self.A.shape[0] == self.A.shape[1]: + self.A = _UU @ self.A @ _UU.T + # If A is not square, it is called by DMDwC + else: + _UUp = _UU[:p, :p] + # _UUq = _UU[p:, p:] + # self.A = np.column_stack( + # (_UUp @ self.A[:, :p] @ _UUp.T, _UUp @ self.A[:, p:] @ _UUq.T) + # ) + self.A = _UUp @ self.A @ _UU.T + # Understand why we divide by w + self._P = np.linalg.inv(_UU @ np.linalg.inv(self._P) @ _UU.T) / self.w + + return x, y + + def _update_A_P( + self, X: np.ndarray, Y: np.ndarray, W: float | np.ndarray + ) -> None: + Xt = X.T + AX = self.A.dot(Xt) + PX = self._P.dot(Xt) + PXt = PX.T + Gamma = np.linalg.inv(W + X.dot(PX)) + # update A on new data + self.A += (Y.T - AX).dot(Gamma).dot(PXt) + # update P, group Px*Px' to ensure positive definite + self._P = (self._P - PX.dot(Gamma).dot(PXt)) / self.w + # TODO: understand why is this needed (tests fail when commented out) + # Any matrix congruent to a symmetric matrix is again symmetric: if X + # is a symmetric matrix, then so is A@X@A.T for any matrix A. + self._P = (self._P + self._P.T) / 2 + + # Reset properties + # TODO: explore what revert does with reseting properties + if not self.A_allclose: + self._eig = None + self._A_last = self.A.copy() + else: + self._n_cached += 1 + + self._modes = None + + def update( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + ) -> None: + """Update the DMD computation with a new pair of snapshots (x, y) + + Here, if the (discrete-time) dynamics are given by z(t) = f(z(t-1)), + then (x,y) should be measurements correponding to consecutive states + z(t-1) and z(t). + + Args: + x: 1D array, shape (m, ), x(t) as in y(t) = f(t, x(t)) + y: 1D array, shape (m, ), y(t) as in y(t) = f(t, x(t)) + """ + # If Hankelizer is used, we need to use DMD without y + if y is None: + if not hasattr(self, "_x_last"): + self._x_last = x + return + else: + y = x + x = self._x_last + self._x_last = y + + if isinstance(x, dict): + self.feature_names_in_ = list(x.keys()) + x = np.array(list(x.values()), ndmin=2) + x_ = x.reshape(1, -1) + if isinstance(y, dict): + assert self.feature_names_in_ == list(y.keys()) + y = np.array(list(y.values()), ndmin=2) + y_ = y.reshape(1, -1) + + # Initialize properties which depend on the shape of x + if self.n_seen == 0: + self.m = x_.shape[1] + self._init_update() + + # Collect buffer of past snapshots to compute modes and xi + if self._Y.shape[0] <= self.n_seen + 1: + self._Y = np.row_stack([self._Y, y_]) + if self._Y.shape[0] > self.n_seen + 1: + self._Y = self._Y[-(self.n_seen + 1) :, :] + + # Initialize A and P with first self.initialize snapshot pairs + if bool(self.initialize) and self.n_seen < self.initialize: + self._X_init[self.n_seen, :] = x_ + self._Y_init[self.n_seen, :] = y_ + if self.n_seen == self.initialize - 1: + self.learn_many(self._X_init, self._Y_init) + # revert the number of seen samples to avoid doubling + self.n_seen -= self._X_init.shape[0] + del self._X_init, self._Y_init + # Update incrementally if initialized + else: + if self.n_seen == 0: + epsilon = 1e-15 + alpha = 1.0 / epsilon + self._P = alpha * np.identity(self.r) + if self.r < self.m: + x_, y_ = self._truncate_w_svd(x_, y_, svd_modify="update") + + self._update_A_P(x_, y_, 1.0) + + self.n_seen += 1 + + def learn_one( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + ) -> None: + """Allias for update method.""" + self.update(x, y) + + def revert( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + ) -> None: + """Gradually forget the older snapshots and revert the DMD computation. + + Compatible with Rolling and TimeRolling wrappers. + + Args: + x: 1D array, shape (1, m), x(t) as in y(t) = f(t, x(t)) + y: 1D array, shape (1, m), y(t) as in y(t) = f(t, x(t)) + + TODO: + - [ ] it seems like this does not work as expected + """ + if self.n_seen < self.initialize: + raise RuntimeError( + f"Cannot revert {self.__class__.__name__} before " + "initialization. If used with Rolling or TimeRolling, window " + f"size should be increased to {self.initialize + 1 if y is None else 0}." + ) + if y is None: + if not hasattr(self, "_x_first"): + self._x_first = x + return + else: + y = x + x = self._x_first + self._x_first = y + + if isinstance(x, dict): + x = np.array(list(x.values())) + x_ = x.reshape(1, -1) + if isinstance(y, dict): + y = np.array(list(y.values())) + y_ = y.reshape(1, -1) + + if self.r < self.m: + x_, y_ = self._truncate_w_svd(x_, y_, svd_modify="revert") + + # Apply exponential weighting factor + if self.exponential_weighting: + weight = 1.0 / -(self.w**self.n_seen) + else: + weight = -1.0 + + self._update_A_P(x_, y_, weight) + + self.n_seen -= 1 + + def _update_many( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame, + ) -> None: + """Update the DMD computation with a new batch of snapshots (X,Y). + + This method brings no change in theoretical time and space complexity. + However, it allows parallel computing by vectorizing update in loop. + + Args: + X: The input snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + Y: The output snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + + TODO: + - [ ] find out why not equal to for loop update implementation + when weights are used + + """ + p = X.shape[0] + if self.exponential_weighting: + weights = np.sqrt(self.w) ** np.arange(p - 1, -1, -1) + else: + weights = np.ones(p) + # Zhang (2019): Gamma = (C^{-1} U^T P U )^{−1} ) + C_inv = np.diag(np.reciprocal(weights)) + + if isinstance(X, pd.DataFrame): + X_ = X.values + else: + X_ = X + if isinstance(Y, pd.DataFrame): + Y_ = Y.values + else: + Y_ = Y + if self.r < self.m: + X_, Y_ = self._truncate_w_svd(X_, Y_, svd_modify="update") + self._update_A_P(X_, Y_, C_inv) + + def update_many( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame | None = None, + ) -> None: + """Learn the OnlineDMD model using multiple snapshot pairs. + + Useful for initializing the model with a batch of snapshot pairs. + Otherwise, it is equivalent to calling update method in a loop. + + Args: + X: The input snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + Y: The output snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + """ + if Y is None: + if isinstance(X, pd.DataFrame): + Y = X.shift(-1).iloc[:-1] + X = X.iloc[:-1] + elif isinstance(X, np.ndarray): + Y = np.roll(X, -1)[:-1] + X = X[:-1] + + if isinstance(X, pd.DataFrame): + X = X.values + if isinstance(Y, pd.DataFrame): + Y = Y.values + + # necessary condition for over-constrained initialization + n = X.shape[0] + # Exponential weighting factor - older snapshots are weighted less + if self.exponential_weighting: + weights = (np.sqrt(self.w) ** np.arange(n - 1, -1, -1))[ + :, np.newaxis + ] + else: + weights = np.ones((n, 1)) + Xqhat, Yqhat = weights * X, weights * Y + + self.n_seen += n + + # Initialize A and P with first p snapshot pairs + if not hasattr(self, "_P"): + self.m = X.shape[1] + if self.r == 0: + self.r = self.m + + _rank_X = np.linalg.matrix_rank(X) + if not _rank_X >= self.r: + raise ValueError( + f"Failed rank(X) [{_rank_X}] >= n_modes [{self.r}].\n" + "Increase the number of snapshots (increase initialize " + f"[{self.initialize}] if learn_many was not called " + "directly) or reduce the number of modes." + ) + XX = Xqhat.T @ Xqhat + # TODO: think about using correlation matrix to avoid scaling issues + # https://stats.stackexchange.com/questions/12200/normalizing-variables-for-svd-pca + # std = np.sqrt(np.diag(XX)) + # XX = XX / np.outer(std, std) + # Perform truncated DMD + if self.r < self.m: + self._svd.learn_many(Xqhat) + _U, _S, _V = self._svd._U, self._svd._S, self._svd._Vt + + _m = Yqhat.shape[1] + _l = self.m - _m + + # DMDwC, A = U.T @ K @ U; B = U.T @ K [Proctor (2016)] + if _l != 0: + _UU = _U.T @ np.row_stack([_U[:_m], np.eye(_l, self.r)]) + # DMD, A = U.T @ K @ U + else: + _UU = np.eye(self.r) + + # TODO: Verify if equivalent to Proctor (2016). They compute U_hat from SVD(Y), we select the first r columns of U + self.A = ( + _U.T[:, : Yqhat.shape[1]] + @ Yqhat.T + @ _V.T + @ np.diag(1 / _S) + ) @ _UU + self._P = np.linalg.inv(_U.T @ XX @ _U) / self.w + # Perform exact DMD + else: + self.A = Yqhat.T.dot(np.linalg.pinv(Xqhat.T)) + self._P = np.linalg.inv(XX) / self.w + + self._A_last = self.A.copy() + # Store the last p snapshots for xi computation + self._Y = Yqhat + self.initialize = 0 + # Update incrementally if initialized + # Zhang (2019): "single rank-s update is roughly the same as applying + # the rank-1 formula s times" + else: + self._update_many(Xqhat, Yqhat) + if self._Y.shape[0] <= self.n_seen: + self._Y = np.row_stack([self._Y, Yqhat]) + if self._Y.shape[0] > self.n_seen: + self._Y = self._Y[-(self.n_seen) :, :] + + def learn_many( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame | None = None, + ) -> None: + """Allias for update_many method.""" + self.update_many(X, Y) + + def predict_one(self, x: dict | np.ndarray) -> np.ndarray: + """ + Predicts the next state given the current state. + + Parameters: + x: The current state. + + Returns: + np.ndarray: The predicted next state. + """ + # Map A back to original space + if self.r < self.m: + A = self._svd._U @ self.A @ self._svd._U.T + else: + A = self.A + mat = np.zeros((2, self.m)) + mat[0, :] = x if isinstance(x, np.ndarray) else list(x.values()) + for s in range(1, 2): + mat[s, :] = (A @ mat[s - 1, :]).real + return mat[-1, :] + + def predict_many(self, x: dict | np.ndarray, horizon: int) -> np.ndarray: + """ + Predicts multiple future values based on the given initial value. + + Args: + x: The initial value. + horizon (int): The number of future values to predict. + + Returns: + np.ndarray: An array containing the predicted future values. + + TODO: + - [ ] Align predict_many with river API + """ + # Map A back to original space + if self.r < self.m: + A = self._svd._U @ self.A @ self._svd._U.T + else: + A = self.A + mat = np.zeros((horizon + 1, self.m)) + mat[0, :] = x if isinstance(x, np.ndarray) else list(x.values()) + for s in range(1, horizon + 1): + mat[s, :] = (A @ mat[s - 1, :]).real + return mat[1:, :] + + def forecast(self, horizon: int, xs: list[dict] | None = None) -> list: + x = self._x_last + if not hasattr(self, "m"): + self.m = len(x) + # Map A back to original space + if self.r < self.m: + if hasattr(self._svd, "_U"): + A = self._svd._U @ self.A @ self._svd._U.T + else: + return np.zeros((horizon, 1)).flatten().tolist() + else: + A = self.A + mat = np.zeros((horizon + 1, self.m)) + mat[0, :] = x if isinstance(x, np.ndarray) else list(x.values()) + for s in range(1, horizon + 1): + mat[s, :] = (A @ mat[s - 1, :]).real + return mat[1:, -1].flatten().tolist() + + def truncation_error( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame, + ) -> float: + """Compute the truncation error of the DMD model on the given data. + + Since this implementation computes exact DMD, the truncation error is relevant only for initialization. + + Args: + X: 2D array, shape (p, m), matrix [x(1),x(2),...x(p)] + Y: 2D array, shape (p, m), matrix [y(1),y(2),...y(p)] + + Returns: + float: Truncation error of the DMD model + """ + Y_hat = self.A @ X.T + return float(np.linalg.norm(Y - Y_hat.T) / np.linalg.norm(Y)) + + def transform_one(self, x: dict | np.ndarray) -> dict: + """ + Transforms the given input sample. + + Args: + x: The input to transform. + + Returns: + np.ndarray: The transformed input. + """ + if isinstance(x, dict): + x = np.array(list(x.values())) + if not hasattr(self, "A") or ( + hasattr(self, "_svd") and not hasattr(self._svd, "_U") + ): + return dict( + zip( + range(self.r), + np.zeros(self.r), + ) + ) + return dict(zip(range(self.r), x @ self.modes)) + + def transform_many( + self, X: np.ndarray | pd.DataFrame + ) -> np.ndarray | pd.DataFrame: + """ + Transforms the given input sequence. + + Args: + x: The input to transform. + + Returns: + np.ndarray: The transformed input. + """ + M = self.modes + return X @ M + + +class OnlineDMDwC(OnlineDMD): + """Online Dynamic Mode Decomposition (DMD) with Control. + + This regressor is a class that implements online dynamic mode decomposition + The time complexity (multiply-add operation for one iteration) is O(4n^2), + and space complexity is O(2n^2), where n is the state dimension. + + This estimator supports learning with mini-batches with same time and space + complexity as the online learning. + + At time step t, define three matrices X(t) = [x(1),x(2),...,x(t)], + Y(t) = [y(1),y(2),...,y(t)], U(t) = [U(1),U(2),...,U(t)] that contain all + the past snapshot pairs, where x(t), y(t) are the n dimensional state + vectors, and u(t) is m dimensional control input vector, given by + y(t) = f(x(t), u(t)). + + x(t), y(t) should be measurements correponding to consecutive states z(t-1) + and z(t). + + An exponential weighting factor can be used to place more weight on + recent data. + + Args: + B: control matrix, size n by m. If None, the control matrix will be + identified from the snapshots. Defaults to None. + p: truncation of states. If 0 (default), compute exact DMD. + q: truncation of control. If 0 (default), compute exact DMD. + w: weighting factor in (0,1]. Smaller value allows more adpative + learning, but too small weighting may result in model identification + instability (relies only on limited recent snapshots). + initialize: number of snapshot pairs to initialize the model with. If 0 + the model will be initialized with random matrix A and P = \alpha I + where \alpha is a large positive scalar. If initialize is smaller + than the state dimension, it will be set to the state dimension and + raise a warning. Defaults to 1. + exponential_weighting: whether to use exponential weighting in revert + seed: random seed for reproducibility (initialize A with random values) + + Attributes: + m: augumented state dimension. if B is None, m = x.shape[1], else m = x.shape[1] + u.shape[1] + n_seen: number of seen samples (read-only), reverted if windowed + A: DMD matrix, size n by n + _P: inverse of covariance matrix of X + + Examples: + >>> import numpy as np + >>> import pandas as pd + + >>> n = 101 + >>> freq = 2.0 + >>> tspan = np.linspace(0, 10, n) + >>> dt = 0.1 + >>> a1 = 1 + >>> a2 = 1 + >>> phase1 = -np.pi + >>> phase2 = np.pi / 2 + >>> w1 = np.cos(np.pi * freq * tspan) + >>> w2 = -np.sin(np.pi * freq * tspan) + >>> u_ = np.ones(n) + >>> u_[tspan > 5] *= 2 + >>> w1[tspan > 5] *= 2 + >>> w2[tspan > 5] *= 2 + >>> df = pd.DataFrame({"w1": w1[:-1], "w2": w2[:-1]}) + >>> U = pd.DataFrame({"u": u_[:-2]}) + + >>> model = OnlineDMDwC(p=2, q=1, w=0.1, initialize=4) + >>> X, Y = df.iloc[:-1], df.shift(-1).iloc[:-1] + + >>> for (_, x), (_, y), (_, u) in zip(X.iterrows(), Y.iterrows(), U.iterrows()): + ... x, y, u = x.to_dict(), y.to_dict(), u.to_dict() + ... model.learn_one(x, y, u) + >>> eig, _ = np.log(model.eig[0]) / dt + >>> r, i = eig.real, eig.imag + >>> np.isclose(eig.real, 0.0) + True + >>> np.isclose(eig.imag, np.pi * freq) + True + + Supports mini-batch learning: + >>> from river.utils import Rolling + + >>> model = Rolling(OnlineDMDwC(p=2, q=1, w=1.0), 10) + >>> X, Y = df.iloc[:-1], df.shift(-1).iloc[:-1] + + >>> for (_, x), (_, y), (_, u) in zip(X.iterrows(), Y.iterrows(), U.iterrows()): + ... x, y, u = x.to_dict(), y.to_dict(), u.to_dict() + ... model.update(x, y, u) + + >>> eig, _ = np.log(model.eig[0]) / dt + >>> r, i = eig.real, eig.imag + >>> np.isclose(eig.real, 0.0) + True + >>> np.isclose(eig.imag, np.pi * freq) + True + + # TODO: find out why not passing + # >>> np.isclose(model.truncation_error(X.values, Y.values, U.values), 0) + # True + + >>> w_pred = model.predict_one( + ... np.array([w1[-2], w2[-2]]), + ... np.array([u_[-2]]), + ... ) + >>> np.allclose(w_pred, [w1[-1], w2[-1]]) + True + + >>> w_pred = model.predict_one( + ... np.array([w1[-2], w2[-2]]), + ... np.array([u_[-2]]), + ... ) + >>> np.allclose(w_pred, [w1[-1], w2[-1]]) + True + + >>> w_pred = model.predict_many(np.array([1, 0]), np.ones((10, 1)), 10) + >>> np.allclose(w_pred.T, [w1[1:11], w2[1:11]]) + True + + References: + [^1]: Zhang, H., Clarence Worth Rowley, Deem, E.A. and Cattafesta, L.N. + (2019). Online Dynamic Mode Decomposition for Time-Varying Systems. + Siam Journal on Applied Dynamical Systems, 18(3), pp.1586-1609. + doi:[10.1137/18m1192329](https://doi.org/10.1137/18m1192329). + """ + + def __init__( + self, + B: np.ndarray | None = None, + p: int = 0, + q: int = 0, # TODO: fix case when q is 0 + w: float = 1.0, + initialize: int = 1, + exponential_weighting: bool = False, + eig_rtol: float | None = None, + seed: int | None = None, + ) -> None: + super().__init__( + p + q, + w, + initialize, + exponential_weighting, + eig_rtol, + seed, + ) + self.p = p + self.q = q + self.B = B + self.known_B = B is not None + self.l: int + + @property + def modes(self) -> np.ndarray: + """Reconstruct high dimensional DMD modes""" + if self._modes is None: + _, Phi = self.eig + if self.r < self.m: + # Sign of eigenvectors and singular vectors may change based on underlying algorithm initialization + # Proctor (2016) + # self._Y.T @ self._svd._Vt.T is increasingly more computationally expensive without rolling + self._modes = ( + self._Y.T + @ self._svd._Vt.T[:, : self.p] + @ np.diag(1 / self._svd._S[: self.p]) + @ Phi + ) + # Following has similar results to our modification + # self._modes = (self._Y.T @ self._svd._Vt.T @ np.diag(1/self._svd._S))[:, :self.p] @ Phi + + # This is faster but significantly alter results for OnlineDMDwC. + self._modes = (self._svd._U @ np.diag(1 / self._svd._S))[ + : self.m - self.l, : self.p + ] @ Phi + else: + self._modes = Phi + return self._modes + + @property + def xi(self) -> np.ndarray: + """Amlitudes of the singular values of the input matrix.""" + return np.linalg.pinv(self.modes) @ np.array( + list(self._x_first.values()) + ) + + def _init_update(self) -> None: + if not hasattr(self, "l"): + super()._init_update() + return + if self.p == 0: + self.p = self.m + if self.q == 0: + self.q = self.l + if self.known_B: + self.r = self.p + else: + self.r = self.p + self.q + # TODO: if p or q == 0 in __init__, we need to reinitialize SVD + self._svd = OnlineSVD( + n_components=self.r, + seed=self.seed, + ) + if self.initialize < self.r: + self.initialize = self.r + + self.A = np.eye(self.p) + self._A_last = self.A.copy() + if not self.known_B: + self.B = np.eye(self.p, self.q) + self._A_last = np.column_stack((self.A, self.B)) + self._U_init = np.zeros((self.initialize, self.l)) + self._X_init = np.empty((self.initialize, self.m)) + self._Y_init = np.empty((self.initialize, self.m)) + self._Y = np.empty((0, self.m)) + + def _reconstruct_AB(self): + # self.m stores augumented state dimension + _m = self.m - self.l if not self.known_B else self.m + if self.r < self.m: + A = ( + self._svd._U[:_m, : self.p] + @ self.A + @ self._svd._U[:_m, : self.p].T + ) + B = ( + self._svd._U[:_m, : self.p] + @ self.B + @ self._svd._U[-self.q :, -self.l :] + ) + else: + A = self.A + B = self.B + return A, B + + def update( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + u: dict | np.ndarray | None = None, + ) -> None: + """Update the DMD computation with a new pair of snapshots (x, y) + + Here, if the (discrete-time) dynamics are given by z(t) = f(z(t-1)), + then (x,y) should be measurements correponding to consecutive states + z(t-1) and z(t). + + Args: + x: 1D array, shape (n, ), x(t) as in y(t) = f(t, x(t)) + y: 1D array, shape (n, ), y(t) as in y(t) = f(t, x(t)) + u: 1D array, shape (m, ), u(t) as in y(t) = f(t, x(t), u(t)) + """ + if y is None: + if not hasattr(self, "_x_last"): + self._x_last = x + self._u_last = u + return + else: + y = x + x = self._x_last + self._x_last = y + _u_hold = u + u = self._u_last + self._u_last = _u_hold + + if isinstance(x, dict): + x = np.array(list(x.values())) + x = x.reshape(1, -1) + if isinstance(y, dict): + y = np.array(list(y.values())) + y = y.reshape(1, -1) + if isinstance(u, dict): + u = np.array(list(u.values())) + if isinstance(u, np.ndarray): + u = u.reshape(1, -1) + # Needed in case of recursive call from learn_many within parent class + if u is None: + super().update(x, y) + else: + if self.n_seen == 0: + self.m = x.shape[1] + self.l = u.shape[1] + self._init_update() + self.m += 0 if self.known_B else u.shape[1] + + if self.initialize and self.n_seen <= self.initialize - 1: + # Accumulate buffer of past snapshots for initialization + self._X_init[self.n_seen, :] = x + self._Y_init[self.n_seen, :] = y + self._U_init[self.n_seen, :] = u + # Run the initialization after collecting enough snapshots + if self.n_seen == self.initialize - 1: + self.learn_many(self._X_init, self._Y_init, self._U_init) + # Subtract the number of seen samples to avoid doubling + self.n_seen -= self._X_init.shape[0] + self.n_seen += 1 + + else: + if self.known_B and self.B is not None: + y = y - u @ self.B.T + else: + x = np.column_stack((x, u)) + if self.B is not None: # For correct type hinting + self.A = np.column_stack((self.A, self.B)) + super().update(x, y) + + # In case that learn_many was called, A is already square + if self.A.shape[0] < self.A.shape[1]: + self.B = self.A[: self.p, -self.q :] + self.A = self.A[: self.p, : -self.q] + + def learn_one( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + u: dict | np.ndarray | None = None, + ) -> None: + """Allias for OnlineDMDwC.update method.""" + return self.update(x, y, u) + + def revert( + self, + x: dict | np.ndarray, + y: dict | np.ndarray | None = None, + u: dict | np.ndarray | None = None, + ) -> None: + """Gradually forget the older snapshots and revert the DMD computation. + + Compatible with Rolling and TimeRolling wrappers. + + Args: + x: 1D array, shape (n, ), x(t) + y: 1D array, shape (n, ), y(t) + u: 1D array, shape (m, ), u(t) + """ + if u is None: + super().revert(x, y) + return + + if y is None: + if not hasattr(self, "_x_first"): + self._x_first = x + self._u_first = u + return + else: + y = x + x = self._x_first + self._x_first = y + _u_hold = u + u = self._u_first + self._u_first = _u_hold + + if isinstance(x, dict): + x = np.array(list(x.values())) + x = x.reshape(1, -1) + if isinstance(y, dict): + y = np.array(list(y.values())) + y = y.reshape(1, -1) + if isinstance(u, dict): + u = np.array(list(u.values())) + u = u.reshape(1, -1) + if self.known_B and self.B is not None: + y = y - u @ self.B.T + else: + x = np.column_stack((x, u)) + if self.B is not None: + self.A = np.column_stack((self.A, self.B)) + + super().revert(x, y) + + if not self.known_B: + self.B = self.A[: self.p, -self.q :] + self.A = self.A[: self.p, : -self.q] + + def _update_many( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame, + U: np.ndarray | pd.DataFrame | None = None, + ) -> None: + """Update the DMD computation with a new batch of snapshots (X,Y). + + This method brings no change in theoretical time and space complexity. + However, it allows parallel computing by vectorizing update in loop. + + Args: + X: The input snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + Y: The output snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + U: The control input snapshot matrix of shape (p, l), where p is the number of snapshots and p is the number of control inputs. + """ + if U is None: + super()._update_many(X, Y) + else: + if self.known_B: + Y = Y - self.B @ U + else: + X = np.column_stack((X, U)) + if self.n_seen == 0: + self.m = X.shape[1] + self.l = U.shape[1] + self._init_update() + if not self.known_B and self.B is not None: + self.A = np.column_stack((self.A, self.B)) + self.l = U.shape[1] + super()._update_many(X, Y) + + if not self.known_B: + self.B = self.A[:, -self.q :] + self.A = self.A[:, : -self.q] + + def learn_many( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame | None = None, + U: np.ndarray | pd.DataFrame | None = None, + ) -> None: + """Learn the OnlineDMDwC model using multiple snapshot pairs. + + Useful for initializing the model with a batch of snapshot pairs. + Otherwise, it is equivalent to calling update method in a loop. + + Args: + X: The input snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + Y: The output snapshot matrix of shape (p, m), where p is the number of snapshots and m is the number of features. + U: The output snapshot matrix of shape (p, l), where p is the number of snapshots and l is the number of control inputs. + """ + if U is None: + super().learn_many(X, Y) + return + + if isinstance(X, pd.DataFrame): + X = X.values + if isinstance(Y, pd.DataFrame): + Y = Y.values + if isinstance(U, pd.DataFrame): + U = U.values + + if Y is None: + Y = np.roll(X, -1)[:-1] + X = X[:-1] + U = U[:-1] + + if self.known_B and self.B is not None: + Y = Y - U @ self.B.T + else: + X = np.column_stack((X, U)) + if self.B is not None: # If learn_many is not called first + self.A = np.column_stack((self.A, self.B)) + + self.l = U.shape[1] + super().learn_many(X, Y) + + if self.p == 0: + self.p = self.m + if self.q == 0: + self.q = self.l + if not self.known_B: + self.B = self.A[: self.p, -self.q :] + self.A = self.A[: self.p, : -self.q] + + def predict_one( + self, x: dict | np.ndarray, u: dict | np.ndarray + ) -> np.ndarray: + """ + Predicts the next state given the current state. + + Parameters: + x: The current state. + u: The control input. + + Returns: + np.ndarray: The predicted next state. + """ + if isinstance(u, dict): + u = np.array(list(u.values())) + _m = len(x) + A, B = self._reconstruct_AB() + + mat = np.zeros((2, _m)) + mat[0, :] = x if isinstance(x, np.ndarray) else list(x.values()) + for s in range(1, 2): + action = (B @ u).real + # TODO: map A back to original space + mat[s, :] = (A @ mat[s - 1, :]).real + action + return mat[-1, :] + + def predict_many( + self, + x: dict | np.ndarray, + U: np.ndarray | pd.DataFrame, + horizon: int, + ) -> np.ndarray: + """ + Predicts multiple future values based on the given initial value. + + Args: + x: The initial value. + U: The control input matrix of shape (horizon, l), where l is the number of control inputs. + horizon (int): The number of future values to predict. + + Returns: + np.ndarray: An array containing the predicted future values. + + TODO: + - [ ] Align predict_many with river API + """ + if isinstance(U, pd.DataFrame): + U = U.values + _m = len(x) + A, B = self._reconstruct_AB() + + mat = np.zeros((horizon + 1, _m)) + mat[0, :] = x if isinstance(x, np.ndarray) else list(x.values()) + for s in range(1, horizon + 1): + action = (B @ U[s - 1, :]).real + mat[s, :] = (A @ mat[s - 1, :]).real + action + return mat[1:, :] + + def truncation_error( + self, + X: np.ndarray | pd.DataFrame, + Y: np.ndarray | pd.DataFrame, + U: np.ndarray | pd.DataFrame, + ) -> float: + """Compute the truncation error of the DMD model on the given data. + + Args: + X: 2D array, shape (n, m), matrix [x(1),x(2),...x(n)] + Y: 2D array, shape (n, m), matrix [y(1),y(2),...y(n)] + U: 2D array, shape (n, l), matrix [u(1),u(2),...u(n)] + + Returns: + float: Truncation error of the DMD model + """ + + A, B = self._reconstruct_AB() + Y_hat = A @ X.T + B @ U.T + return float(np.linalg.norm(Y - Y_hat.T) / np.linalg.norm(Y)) diff --git a/river/decomposition/opca.py b/river/decomposition/opca.py new file mode 100644 index 0000000000..9acdfd6904 --- /dev/null +++ b/river/decomposition/opca.py @@ -0,0 +1,200 @@ +"""Online Principal Component Analysis (PCA) in [River API](riverml.xyz). + +This module contains the implementation of the Online PCA algorithm. +It is based on the paper by Eftekhari et al. [^1] + +References: + [^1]: Eftekhari, A., Ongie, G., Balzano, L., Wakin, M. B. (2019). Streaming Principal Component Analysis From Incomplete Data. Journal of Machine Learning Research, 20(86), pp.1-62. url:http://jmlr.org/papers/v20/16-627.html. +""" +from __future__ import annotations + +from collections import deque + +import numpy as np + +from river.base import Transformer + +__all__ = [ + "OnlinePCA", +] + + +class OnlinePCA(Transformer): + """_summary_ + + Args: + n_components: Desired dimensionality of output data. The default value is useful for visualisation. + b: size of the blocks. Must be greater than or equal to n_components. + lambda_: tuning parameter + sigma: reject threshold + tau: reject threshold + + Attributes: + feature_names_in_: List of input features. + n_seen: Number of samples seen. + Y_k: Block of received data of size (n_features_in_, b). + S_hat: R-dimensional subspace with orthonormal basis (n_features_in_, n_components) + + Examples: + >>> import pandas as pd + >>> np.random.seed(0) + >>> m = 20 + >>> n = 80 + >>> mean = [5, 10, 15] + >>> covariance_matrix = [[1, 0.5, 0.3], + ... [0.5, 1, 0.2], + ... [0.3, 0.2, 1]] + >>> num_samples = 100 + >>> X = np.random.multivariate_normal(mean, covariance_matrix, num_samples) + >>> n_nans = 2 + >>> nan_indices = np.random.choice(range(X.shape[0]), size=n_nans, replace=False) + >>> X[nan_indices] = np.nan + >>> pca = OnlinePCA(n_components=2) + >>> for x in X[:50]: + ... pca.learn_one(x) + >>> pca.transform_one(X[-1, :]) + {0: -17.9652, 1: -0.8711} + + >>> pca = OnlinePCA(n_components=2, b=4) + >>> X = pd.DataFrame(X) + >>> for _, x in X.iloc[:50].iterrows(): + ... pca.learn_one(x.to_dict()) + >>> pca.transform_one(X.iloc[-1, :].to_dict()) + {0: -17.9470, 1: -1.0941} + + """ + + def __init__( + self, + n_components: int = 2, + b: int | None = None, + lambda_: float = 0.0, + sigma: float = 0.0, + tau: float = 0.0, + seed: int | None = None, + ): + self.n_components = int(n_components) + # Default maximizes the efficiency [Eftekhari, et al. (2019)] + if not b: + b = self.n_components + else: + b = int(b) + self.b = b + assert lambda_ >= 0 + self.lambda_ = lambda_ + assert sigma >= 0 + self.sigma = sigma + assert tau >= 0 + self.tau = tau + + self.feature_names_in_: list[str] + self.n_features_in_: int # n [Eftekhari, et al. (2019)] + self.n_seen: int = 0 # k [Eftekhari, et al. (2019)] + self.Y_k: deque + self.P_omega_k: deque + self.S_hat: np.ndarray + self.seed = seed + np.random.seed(self.seed) + + def learn_one(self, x: dict | np.ndarray): + """_summary_ + + Args: + x: Incomplete observation of data matrix. Accepts NaNs (n_features_in_,) + """ + if isinstance(x, dict): + if self.n_seen == 0: + self.feature_names_in_ = list(x.keys()) + else: + assert not set(self.feature_names_in_).difference( + set(x.keys()) + ) + x = np.array(list(x.values())) + # TODO: align with OnlineSVD + # x = x.reshape(1, -1) + + if self.n_seen == 0: + self.n_features_in_ = x.shape[0] + if self.n_components == 0: + self.n_components = self.n_features_in_ + # Make b feasible if not set and learn_one is called first + if not self.b: + self.b = self.n_components + self.Y_k = deque(maxlen=self.b) + self.P_omega_k = deque(maxlen=self.b) + # Initialize S_hat with random orthonormal matrix for transform_one + r_mat = np.random.randn(self.n_features_in_, self.n_components) + self.S_hat, _ = np.linalg.qr(r_mat) + + # Random index set over which s_t is observed + omega_t = ~np.isnan(x) # (n_features_in_,) + # TODO: find out whether correct + x = np.nan_to_num(x, nan=0.0) + # Projection onto coordinate set. Diagonal entry corresponding to the index set omega_t (n_features_in_, n_features_in_) + P_omega_t = np.diag(omega_t).astype(int) + self.Y_k.append(x) + self.P_omega_k.append(P_omega_t) + + if len(self.Y_k) == self.b: + # Reinitialize S_hat now when deque is full + if self.n_seen == self.b - 1: + # Let S_hat \in \mathbb{R}^{n \times b} be the + _, _, V = np.linalg.svd( + np.array(self.Y_k), full_matrices=False + ) + self.S_hat = V.T[:, : self.n_components] + else: + R_k = np.empty((self.n_features_in_, self.b)) + # range((self.n_seen - 1) * self.b + 1, self.n_seen * self.b) [Eftekhari, et al. (2019)] + for k, (y_t, P_omega_t) in enumerate( + zip(self.Y_k, self.P_omega_k) + ): + P_omega_t_comp = ( + np.identity(self.n_features_in_) - P_omega_t + ) + + I_r = np.identity(self.n_components) + S_hat_t = self.S_hat.T + R_k[:, k] = ( + y_t + + P_omega_t_comp + @ self.S_hat + @ np.linalg.pinv( + S_hat_t @ P_omega_t @ self.S_hat + + self.lambda_ * I_r + ) + @ S_hat_t + @ y_t + ) + U_r, sigma_r, _ = np.linalg.svd(R_k) + _sigma_below_thresh = ( + sigma_r[self.n_components - 1] < self.sigma + ) + if self.b > self.n_components: + _sigma_ratio_below_thresh = ( + sigma_r[self.n_components] + <= (1 + self.tau) * sigma_r[1] + ) + else: + _sigma_ratio_below_thresh = True + if ~(_sigma_below_thresh or _sigma_ratio_below_thresh): + self.S_hat = U_r[:, : self.n_components] + + self.Y_k.clear() # Non overlapping blocks + + self.n_seen += 1 + + def transform_one(self, x: dict | np.ndarray) -> dict: + if isinstance(x, dict): + x = np.array(list(x.values())) + # If transform one is called before any learning has been done + # TODO: consider raising an runtime error + if not hasattr(self, "S_hat"): + return dict( + zip( + range(self.n_components), + np.zeros(self.n_components), + ) + ) + x = x @ self.S_hat + return dict(zip(range(self.n_components), x)) diff --git a/river/decomposition/osvd.py b/river/decomposition/osvd.py new file mode 100644 index 0000000000..7e1e2eef78 --- /dev/null +++ b/river/decomposition/osvd.py @@ -0,0 +1,798 @@ +"""Online Singular Value Decomposition (SVD) in [River API](riverml.xyz). + +This module contains the implementation of the Online SVD algorithm. +It is based on the paper by Brand et al. [^1] + +References: + [^1]: Brand, M. (2006). Fast low-rank modifications of the thin singular value decomposition. Linear Algebra and its Applications, 415(1), pp.20-30. doi:[10.1016/j.laa.2005.07.021](https://doi.org/10.1016/j.laa.2005.07.021). + [^2]: Zhang, Y. (2022). An answer to an open question in the incremental SVD. doi:[10.48550/arXiv.2204.05398](https://doi.org/10.48550/arXiv.2204.05398). + [^3]: Zhang, Y. (2022). A note on incremental SVD: reorthogonalization. doi:[10.48550/arXiv.2204.05398](https://doi.org/10.48550/arXiv.2204.05398). +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import scipy as sp + +from river.base import MiniBatchTransformer + +__all__ = [ + "OnlineSVD", + "OnlineSVDZhang", +] + + +def test_orthonormality(vectors, tol=1e-12): # pragma: no cover + """ + Test orthonormality of a set of vectors. + + Parameters: + vectors : numpy.ndarray + Matrix where each column represents a vector + tol : float, optional + Tolerance for checking orthogonality and unit length + + Returns: + is_orthonormal : bool + True if vectors are orthonormal, False otherwise + """ + # Check unit length + norms = np.linalg.norm(vectors, axis=0) + is_unit_length = np.allclose(norms, 1, atol=tol) + + # Check orthogonality + inner_products = np.dot(vectors.T, vectors) + off_diagonal = inner_products - np.diag(np.diag(inner_products)) + is_orthogonal = np.allclose(off_diagonal, 0, atol=tol) + + # Check if both conditions are satisfied + is_orthonormal = is_unit_length and is_orthogonal + + return is_orthonormal + + +def _orthogonalize(U, S, Vt, tol=1e-12, solver="arpack", random_state=None): + """Orthogonalize the singular value decomposition. + + This function orthogonalizes the singular value decomposition by performing + a QR decomposition on the left and right singular vectors. + + TODO: verify if this is the correct way to orthogonalize the SVD. + [^3]: Zhang, Y. (2022). A note on incremental SVD: reorthogonalization. doi:[10.48550/arXiv.2204.05398](https://doi.org/10.48550/arXiv.2204.05398). + """ + n_components = S.shape[0] + # In house implementation of full reorthogonalization + # UQ, UR = np.linalg.qr(U, mode="complete") + # VQ, VR = np.linalg.qr(Vt, mode="complete") + # A = UR @ np.diag(S) @ VR + # tU, tS, tV = _svd(A, 0, None, solver, random_state) + # return UQ @ tU_, tSigma_, VQ @ tV_ + + # Zhang, Y. (2022) + if (U[:, -1].T @ U[:, 0] > tol).any(): + for i in range(n_components): + alpha = U[:, i : i + 1] # m x 1 + for j in range(i - 1): + beta = U[:, j] # m x 1 + U[:, i] = U[:, i] - (alpha.T @ beta) * beta + norm = np.linalg.norm(U[:, i]) + U[:, i] = U[:, i] / norm + return U, S, Vt + + +def _sort_svd(U, S, Vt): + """Sort the singular value decomposition in descending order. + + As sparse SVD does not guarantee the order of the singular values, we + need to sort the singular value decomposition in descending order. + """ + sort_idx = np.argsort(S)[::-1] + if not np.array_equal(sort_idx, range(len(S))): + S = S[sort_idx] + U = U[:, sort_idx] + Vt = Vt[sort_idx, :] + return U, S, Vt + + +def _truncate_svd(U, S, Vt, n_components): + """Truncate the singular value decomposition to the n components. + + Full SVD returns the full matrices U, S, and V in correct order. If the + result acqisition is faster than sparse SVD, we combine the results of + full SVD with truncation. + """ + U = U[:, :n_components] + S = S[:n_components] + Vt = Vt[:n_components, :] + return U, S, Vt + + +def _svd(A, n_components, tol=0.0, v0=None, solver=None, random_state=None): + """Compute the singular value decomposition of a matrix. + + This function computes the singular value decomposition of a matrix A. + If n_components < min(A.shape), the function uses sparse SVD for speed up. + """ + # TODO: sparse is slow if not n_components << min(A.shape) + # analyze performance benefits for various differencec between + # n_components and min(A.shape) + if 0 < n_components and n_components < min(A.shape): + U, S, Vt = sp.sparse.linalg.svds( + A, + k=n_components, + tol=tol, + v0=v0, + solver=solver, + random_state=random_state, + ) + U, S, Vt = _sort_svd(U, S, Vt) + else: + U, S, Vt = np.linalg.svd(A, full_matrices=False) + # # TODO: implement Optimal truncation if n_components is not set + # # Gavish, M., & Donoho, D. L. (2014). The optimal hard threshold for singular values is 4/sqrt(3). + # beta = A.shape[0] / A.shape[1] + # omega = 0.56 * beta**3 - 0.95 * beta**2 + 1.82 * beta + 1.43 + # n_components = sum(S > omega) + U, S, Vt = _truncate_svd(U, S, Vt, n_components) + return U, S, Vt + + +class OnlineSVD(MiniBatchTransformer): + """Online Singular Value Decomposition (SVD). + + Args: + n_components: Desired dimensionality of output data. The default value is useful for visualisation. + initialize: Number of initial samples to use for the initialization of the algorithm. The value must be greater than `n_components`. + force_orth: If True, the algorithm will force the singular vectors to be orthogonal. *Note*: Significantly increases the computational cost. + seed: Random seed. + + Attributes: + n_components: Desired dimensionality of output data. + initialize: Number of initial samples to use for the initialization of the algorithm. The value must be greater than `n_components`. + feature_names_in_: List of input features. + _U: Left singular vectors (n_features_in_, n_components). + _S: Singular values (n_components,). + _Vt: Right singular vectors (transposed) (n_components, n_seen). + + Examples: + >>> np.random.seed(0) + >>> r = 3 + >>> m = 4 + >>> n = 80 + >>> X = pd.DataFrame(np.linalg.qr(np.random.rand(n, m))[0]) + >>> svd = OnlineSVD(n_components=r, force_orth=False) + >>> svd.learn_many(X.iloc[: r * 2]) + >>> svd._U.shape == (m, r), svd._Vt.shape == (r, r * 2) + (True, True) + + >>> svd.transform_one(X.iloc[10].to_dict()) + {0: ...0.0494..., 1: ...0.0030..., 2: ...0.0111...} + + >>> for _, x in X.iloc[10:-1].iterrows(): + ... svd.learn_one(x.values.reshape(1, -1)) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0488..., 1: ...0.0613..., 2: ...0.1150...} + + >>> svd.update(X.iloc[-1].to_dict()) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0409..., 1: ...0.0336..., 2: ...0.1287...} + + For higher dimensional data and forced orthogonality, revert may not return us to the original state. + >>> svd.revert(X.iloc[-1].to_dict(), idx=-1) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0488..., 1: ...0.0613..., 2: ...0.1150...} + + >>> svd = OnlineSVD(n_components=0, initialize=3, force_orth=True) + >>> svd.learn_many(X.iloc[:30]) + + >>> svd.learn_many(X.iloc[30:60]) + >>> svd.transform_many(X.iloc[60:62]).abs() + 0 1 2 3 + 60 0.103403 0.134656 0.108399 0.125872 + 61 0.063485 0.023943 0.120235 0.088502 + + References: + [^1]: Brand, M. (2006). Fast low-rank modifications of the thin singular value decomposition. Linear Algebra and its Applications, 415(1), pp.20-30. doi:[10.1016/j.laa.2005.07.021](https://doi.org/10.1016/j.laa.2005.07.021). + """ + + def __init__( + self, + n_components: int = 2, + initialize: int = 0, + force_orth: bool = True, + solver="arpack", + seed: int | None = None, + ): + self.n_components = n_components + self.initialize = initialize + self.force_orth = force_orth + self.solver = solver + self.seed = seed + + np.random.seed(self.seed) + + self.n_features_in_: int + self.feature_names_in_: list + self.n_seen: int = 0 + + self._U: np.ndarray + self._S: np.ndarray + self._Vt: np.ndarray + + @classmethod + def _from_state( + cls: type[OnlineSVD], + U: np.ndarray, + S: np.ndarray, + Vt: np.ndarray, + force_orth: bool = True, + seed: int | None = None, + ): + new = cls( + n_components=S.shape[0], + initialize=0, + force_orth=force_orth, + seed=seed, + ) + new.n_features_in_ = U.shape[0] + new.n_seen = Vt.shape[1] + + new._U = U + new._S = S + new._Vt = Vt + + return new + + def _init_first_pass(self, x): + self.n_features_in_ = x.shape[1] + if self.n_components == 0: + self.n_components = self.n_features_in_ + self._X_init = np.empty((0, self.n_features_in_)) + if x.shape[0] == 1: + # Make initialize feasible if not set and learn_one is called first + if not self.initialize: + self.initialize = self.n_components + # Initialize _U with random orthonormal matrix for transform_one + r_mat = np.random.randn(self.n_features_in_, self.n_components) + self._U, _ = np.linalg.qr(r_mat) + + def update(self, x: dict | np.ndarray): + if isinstance(x, dict): + self.feature_names_in_ = list(x.keys()) + x = np.array(list(x.values()), ndmin=2) + if len(x.shape) == 1: + x = x.reshape(1, -1) # 1 x m + + if self.n_seen == 0: + self._init_first_pass(x) + + # Initialize if called without learn_many + if bool(self.initialize) and self.n_seen < self.initialize: + self._X_init = np.row_stack((self._X_init, x)) + if len(self._X_init) == self.initialize: + self.learn_many(self._X_init) + # learn many updated seen, we need to revert last sample which + # will be accounted for again at the end of update + self.n_seen -= x.shape[0] + else: + A = x.T # m x c + c = A.shape[1] + + Ut = self._U.T # r x m + M = Ut @ A # r x c + P = A - self._U @ M # m x c + # Results seems to be the same for non rank-increasing updates. + Po = np.linalg.qr(P)[0] + Pot = Po.T # c x m or m x m if m < c + R_A = Pot @ P # c x c + + # pad V with zeros to create place for new singular vector + # (could be omitted to preserve size of V) + _Vt = np.pad(self._Vt, ((0, 0), (0, c))) # r x n + c + nc = _Vt.shape[1] + B = np.zeros((nc, c)) # n + c x c + B[-c:, :] = 1.0 + N = _Vt @ B # r x c + V = _Vt.T # n + c x r + # Might be less numerically stable + # VVT = V @ _Vt # n + c x n + c + # Q = (np.eye(nc) - VVT) @ B # n + c x c + Q = B - V @ N # n + c x c + Qot = np.linalg.qr(Q)[0].T # c x n + c + # R_B = Q.T @ Q # c x c + + Z = np.zeros((c, self.n_components)) # c x r + K = np.block([[np.diag(self._S), M], [Z, R_A]]) # r + c x r + c + + U_, S_, Vt_ = _svd( + K, + self.n_components, + # v0=np.column_stack((self._U, Pot.T))[0,:], # N > M + v0=np.row_stack((_Vt, Qot))[:, 0], # N <= M + solver=self.solver, + random_state=self.seed, + ) # r + c x r; ...; r x r + c + + U_ = np.column_stack((self._U, Po)) @ U_ # m x r + Vt_ = Vt_ @ np.row_stack((_Vt, Qot)) # r x n + c + + if self.force_orth: + U_, S_, Vt_ = _orthogonalize(U_, S_, Vt_) + + self._U, self._S, self._Vt = U_, S_, Vt_ + + self.n_seen += x.shape[0] + + def revert(self, x: dict | np.ndarray, idx: int = 0): + c = 1 if isinstance(x, dict) else x.shape[0] + nc = self._Vt.shape[1] + B = np.zeros((nc, c)) # n + c x c + B[-c:] = np.identity(c) + # Schmid takes first c columns of Vt + # N = _Vt @ B # r x c + if idx >= 0: + N = self._Vt[:, idx : idx + c] # r x c + elif idx == -1: + N = self._Vt[:, -c:] # r x c + else: + N = self._Vt[:, -c + idx + 1 : idx + 1] # r x c + V = self._Vt.T # n + c x r + Q = B - V @ N # n + c x c + Qot = np.linalg.qr(Q)[ + 0 + ].T # c x n + c; Orthonormal basis of column space of q + + S_ = np.pad(np.diag(self._S), ((0, c), (0, c))) # r + c x r + c + # For full-rank SVD, this results in nn == 1. + NtN = N.T @ N # c x c + norm_n = np.sqrt(1.0 - NtN) # c x c + norm_n[np.isnan(norm_n)] = 0.0 + K = S_ @ ( + np.identity(S_.shape[0]) + - np.row_stack((N, np.zeros((c, c)))) @ np.row_stack((N, norm_n)).T + ) # r + c x r + c + U_, S_, Vt_ = _svd( + K, + self.n_components, + # Seems like this converges to different results + v0=np.row_stack((self._Vt, Qot))[:, 0], + solver=self.solver, + random_state=self.seed, + ) # r + c x r; ...; r x r + c + + # Since the update is not rank-increasing, we can skip computation of P + # otherwise we do U_ = np.column_stack((self._U, P)) @ U_ + U_ = self._U @ U_[: self.n_components, :] # m x r + + Vt_ = Vt_ @ np.row_stack((self._Vt, Qot))[:, :-c] # r x n + # Vt_ = Vt_[:, : self.n_components] @ self._Vt[:, :-c] + + if self.force_orth: # and not test_orthonormality(U_): + U_, S_, Vt_ = _orthogonalize(U_, S_, Vt_) + + self._U, self._S, self._Vt = U_, S_, Vt_ + self.n_seen -= c + + def learn_one(self, x: dict | np.ndarray): + """Allias for update method.""" + self.update(x) + + def learn_many(self, X: np.ndarray | pd.DataFrame): + if isinstance(X, pd.DataFrame): + self.feature_names_in_ = list(X.columns) + X = X.values + else: + self.feature_names_in_ = [str(i) for i in range(X.shape[0])] + + if self.n_seen == 0: + self._init_first_pass(X) + + if ( + hasattr(self, "_U") + and hasattr(self, "_S") + and hasattr(self, "_Vt") + ): + if X.shape[0] <= self.n_features_in_: + self.learn_one(X) + else: + for X_part in [ + X[i : i + self.n_features_in_] + for i in range(0, X.shape[0], self.n_features_in_) + ]: + self.learn_one(X_part) + + else: + assert np.linalg.matrix_rank(X.T) >= self.n_components + self._U, self._S, self._Vt = _svd( + X.T, + self.n_components, + solver=self.solver, + random_state=self.seed, + ) + + self.n_seen = X.shape[0] + + def transform_one(self, x: dict | np.ndarray) -> dict: + if isinstance(x, dict): + x = np.array(list(x.values())) + + # If transform one is called before any learning has been done + # TODO: consider raising an runtime error + if not hasattr(self, "_U"): + return dict( + zip( + range(self.n_components), + np.zeros(self.n_components), + ) + ) + + return dict(zip(range(self.n_components), x @ self._U)) + + def transform_many(self, X: np.ndarray | pd.DataFrame) -> pd.DataFrame: + # If transform one is called before any learning has been done + # TODO: consider raising an runtime error + if not hasattr(self, "_U"): + return pd.DataFrame( + np.zeros((X.shape[0], self.n_components)), + index=range(self.n_components), + ) + assert X.shape[1] == self.n_features_in_ + + X_ = X @ self._U + return pd.DataFrame(X_) + + +class OnlineSVDZhang(OnlineSVD): + """Online Singular Value Decomposition (SVD) using Zhang Algorithm. + + This OnlineSVD implementation handles reorthogonalization and rank-increasing updates automatically. + + Args: + n_components: Desired dimensionality of output data. The default value is useful for visualisation. + initialize: Number of initial samples to use for the initialization of the algorithm. The value must be greater than `n_components`. + rank_updates: If True, the algorithm will allow rank-increasing updates. *Note*: Significantly increases the computational cost. + seed: Random seed. + + Attributes: + n_components: Desired dimensionality of output data. + initialize: Number of initial samples to use for the initialization of the algorithm. The value must be greater than `n_components`. + feature_names_in_: List of input features. + _U: Left singular vectors (n_features_in_, n_components). + _S: Singular values (n_components,). + _Vt: Right singular vectors (transposed) (n_components, n_seen). + + Examples: + >>> np.random.seed(0) + >>> r = 3 + >>> m = 4 + >>> n = 80 + >>> X = pd.DataFrame(np.linalg.qr(np.random.rand(n, m))[0]) + >>> svd = OnlineSVDZhang(n_components=r, rank_updates=False) + >>> svd.learn_many(X.iloc[: r * 2]) + >>> svd._U.shape == (m, r), svd._Vt.shape == (r, r * 2) + (True, True) + + >>> svd.transform_one(X.iloc[10].to_dict()) + {0: ...0.0494..., 1: ...0.0030..., 2: ...0.0111...} + + >>> for _, x in X.iloc[10:-1].iterrows(): + ... svd.learn_one(x.values.reshape(1, -1)) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0488..., 1: ...0.0613..., 2: ...0.1150...} + + >>> svd.update(X.iloc[-1].to_dict()) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0409..., 1: ...0.0336..., 2: ...0.1287...} + + For higher dimensional data and forced orthogonality, revert may not return us to the original state. + >>> svd.revert(X.iloc[-1].to_dict(), idx=-1) + >>> svd.transform_one(X.iloc[0].to_dict()) + {0: ...0.0488..., 1: ...0.0613..., 2: ...0.1150...} + + >>> svd = OnlineSVDZhang(n_components=0, initialize=3, rank_updates=False) + >>> svd.learn_many(X.iloc[:30]) + + >>> svd.learn_many(X.iloc[30:60]) + + # TODO: Fix the problem related to ongoing batch updates + >>> svd.transform_many(X.iloc[60:62]).abs() + 0 1 2 3 + 60 0.103403 0.134656 0.108399 0.125872 + 61 0.063485 0.023943 0.120235 0.088502 + + References: + [^2]: Zhang, Y. (2022). An answer to an open question in the incremental SVD. doi:[10.48550/arXiv.2204.05398](https://doi.org/10.48550/arXiv.2204.05398). + """ + + def __init__( + self, + n_components: int = 2, + initialize: int = 0, + tol: float = 1e-12, + rank_updates: bool = False, + seed: int | None = None, + ): + super().__init__( + n_components=n_components, + initialize=initialize, + force_orth=False, + seed=seed, + ) + self.tol: float = tol + self.rank_updates = rank_updates + + self._V_buff: np.ndarray + self._U0: np.ndarray + self._q_u: int = 0 + self._q_r: int = 0 + self.W: np.ndarray + + @classmethod + def _from_state( + cls: type[OnlineSVDZhang], + U: np.ndarray, + S: np.ndarray, + V: np.ndarray, + rank_updates: bool = False, + seed: int | None = None, + ): + new = cls( + n_components=S.shape[0], + initialize=0, + rank_updates=rank_updates, + seed=seed, + ) + new.n_features_in_ = U.shape[0] + new.n_seen = V.shape[1] + + new._U = U + new._S = S + new._Vt = V + + new._V_buff = np.empty((new.n_components, 0)) + new._U0 = np.identity(new.n_components) + new.W = np.identity(new.n_features_in_) + + return new + + def _init_first_pass(self, x): + super()._init_first_pass(x) + self._V_buff = np.empty((self.n_components, 0)) + self._U0 = np.identity(self.n_components) + # TODO: Allow weighting specified by user + self.W = np.identity(self.n_features_in_) + + def update(self, x: dict | np.ndarray): + if isinstance(x, dict): + self.feature_names_in_ = list(x.keys()) + x = np.array(list(x.values()), ndmin=2) + if len(x.shape) == 1: + x = x.reshape(1, -1) + + if self.n_seen == 0: + self._init_first_pass(x) + + c = x.shape[0] + + # Initialize if called without learn_many + if bool(self.initialize) and self.n_seen < self.initialize: + self._X_init = np.row_stack((self._X_init, x)) + if len(self._X_init) == self.initialize: + self.learn_many(self._X_init) + # learn many updated seen, we need to revert last sample which + # will be accounted for again at the end of update + self.n_seen -= c + else: + if c > 1: + from warnings import warn + + warn( + "Calling update/learn_many with batches provides different results than incrementing one sample at the time." + ) + r = self.n_components + A = x.T # m x c + _U, _S, _V = self._U, self._S, self._Vt.T # m x r, r x 1, n x r + _Ut = _U.T # r x m + # Step 1: Calculate d, e, p + M = _Ut @ (self.W @ A) # r x c + P = A - _U @ M # m x c + PtP = P.T @ self.W @ P # c x c + PtP_cond = (PtP < 0.0).any() + if PtP_cond: + # Approx. 2x slower more stable solution for batched updates + Po = np.linalg.qr(P)[0] + Pot = Po.T # c x m + Ra = Pot @ P # c x c + else: + Ra = np.sqrt(PtP) # c x c + # Step 2: Check tolerance + if (Ra < self.tol).all(): # n_incr += c + self._q_u += c # 1 x 1 + self._V_buff = np.column_stack((self._V_buff, M)) # r x n_incr + else: + if self._q_u > 0: + # Step 7: Construct Y + Y = np.column_stack( + (np.diag(_S), self._V_buff) + ) # r x r + n_incr + # Step 8: Perform SVD on Y + UY, SY, VYt = np.linalg.svd( + Y, full_matrices=False + ) # r x r, r x 1, r x r + n_incr + VY = VYt.T # r + n_incr x r + # Step 9: Update U0, _S, _V + self._U0 = self._U0 @ UY # r x r + _S = SY # r x 1 + _V1 = VY[:r, :-1] # r x r + n_incr - 1 + _V2 = VY[r, :-1] # 1 x r + n_incr - 1 + _V = np.row_stack( + (_V @ _V1, _V2) + ) # n + 1 x r + n_incr - 1 + # Step 11: Calculate d + M = UY.T @ M # r x c + # Step 13: Normalize e + if not PtP_cond: + Po = P @ np.linalg.inv(Ra) # m x c + Pot = Po.T # c x m + # Step 14: Reorthogonalize if |e>W*_U(:, 1)| > tol + if (np.abs(Pot @ (self.W @ _U[:, 0])) > self.tol).any(): + Po = Po - _U @ (_Ut @ (self.W @ Po)) # m x c + Po = np.linalg.qr(Po)[0] + # Step 17: Construct Y + Y = np.block( + [ + [np.diag(_S), M], + [np.zeros((c, r)), Ra], + ] + ) # r + c x r + c + # Not using sp.sparse.linalg.svds for non-rank increasing + # updates as it is slower than np.linalg.svd + UY, SY, VYt = np.linalg.svd( + Y + ) # r + c x r + c, r + c x 1, r + c x r + c + VY = VYt.T # r + c x r + c + # Step 20: Update U0 + self._U0 = ( + np.block( + [ + [ + self._U0, + np.zeros((self._U0.shape[0], c)), + ], + [ + np.zeros((c, self._U0.shape[1])), + np.eye(c, c), + ], + ] + ) + @ UY + ) # r + c x r + c + _Ue = np.column_stack((_U, Po)) # m x k + c + # Step 19: Check if rank increasing + if self.rank_updates and SY[r] > self.tol: + # Step 20 - 21: Update _U, _S, _V + _U = _Ue @ self._U0 # m x r + c + _S = SY # r + c x c + _V1 = VY[:r, :] # r x r + c + _V2 = VY[r, :] # 1 x r + c + _V = np.row_stack((_V @ _V1, _V2)) # n + 1 x r + 1 + self._U0 = np.eye(r + 1) # r + 1 x r + 1 + else: + # Step 23 - 24: Update _U, _S, _V + _U = _Ue @ self._U0[:, :r] # m x r + _S = SY[:r] # r x 1 + V_1pad = VY.shape[1] - _V.shape[1] + _V = ( + np.block( + [ + [_V, np.zeros((_V.shape[0], V_1pad))], + [ + np.zeros((c, _V.shape[1])), + np.eye(c, V_1pad), + ], + ] + ) + @ VY[:, :r] + ) # n + 1 x r + self._U0 = np.eye(r) # r x r + + # Alg. 11 + # We note that the output of Algorithm 7 (11), V , may be not empty. This implies that the output of Algorithm 7 (11) is not the SVD of U. Hence we have to update the SVD for the vectors in V + # This step adds rows to _V to account for the ones buffered in V + if self._q_u > 0 and self._V_buff.shape[1] > 0: + # Step 2: Construct Y + Y = np.column_stack( + (np.diag(_S), self._V_buff) + ) # r x r + v_cols + # Step 3: Perform SVD on Y + UY, SY, VYt = np.linalg.svd(Y, full_matrices=False) + VY = VYt.T # r + 1 x r + 1 + # Step 4: Update _U, _S, _V + _U = _U @ UY + _S = SY + _V1 = VY[:r, :] + _V2 = VY[r : r + self._q_u + c - 1, :] + _V = np.row_stack((_V @ _V1, _V2)) + + self.n_components = _S.shape[0] + self._V_buff = np.empty((self.n_components, 0)) + self._q_u = 0 + self._U, self._S, self._Vt = _U, _S, _V.T + + self.n_seen += c + + def revert(self, x: dict | np.ndarray, idx: int = 0): + _U, _S, _V = self._U, self._S, self._Vt.T # m x r, r x 1, n + c x r + + c = 1 if isinstance(x, dict) else x.shape[0] + nc = self._Vt.shape[1] + # Step 1: Calculate N, Q, Qot + B = np.zeros((nc, c)) # n + c x c + B[-c:] = np.identity(c) + + if idx >= 0: + N = self._Vt[:, idx : idx + c] # r x c + elif idx == -1: + N = self._Vt[:, -c:] # r x c + else: + N = self._Vt[:, -c + idx + 1 : idx + 1] # r x c + Q = B - _V @ N # n + c x c + QtQ = Q.T @ Q + QtQ_cond = (QtQ < 0.0).any() + if QtQ_cond: + Qot = np.linalg.qr(Q)[0].T # c x n + c + Ra = Qot @ Q # c x c + else: + Qot = None + Ra = np.sqrt(Q.T @ Q) # c x c + # TODO: not activated at all, check why + if Ra.size > 0 and (Ra < self.tol).all(): + self._q_r += c + else: + if self._q_r > 0: + c += self._q_r + B = np.zeros((nc, c)) # n + c x c + B[-c:] = np.identity(c) + if idx >= 0: + N = self._Vt[:, idx : idx + c] # r x c + elif idx == -1: + N = self._Vt[:, -c:] # r x c + else: + N = self._Vt[:, -c + idx + 1 : idx + 1] # r x c + Q = B - _V @ N # n + c x c + Qot = None + # Step 13: Normalize Q + if Qot is None: + Qot = np.linalg.qr(Q)[ + 0 + ].T # c x n + c; Orthonormal basis of column space of q + # We do not touch original U therefore we leave reorthogonalization to update method :) + + S_ = np.pad(np.diag(_S), ((0, c), (0, c))) # r + c x r + c + # For full-rank SVD, this results in nn == 1. + NtN = N.T @ N # c x c + # TODO: validate if correct for c > 1 + norm_n = np.sqrt(1.0 - NtN) # c x c + norm_n[np.isnan(norm_n)] = 0.0 + K = S_ @ ( + np.identity(S_.shape[0]) + - np.row_stack((N, np.zeros((c, c)))) + @ np.row_stack((N, norm_n)).T + ) # r + c x r + c + # TODO: Maybe we can truncate and use full_matrices=True to get sqared Vt + U_, S_, Vt_ = np.linalg.svd(K, full_matrices=False) + + if self.rank_updates and S_[-1] <= self.tol: + self.n_components -= 1 + U_ = _U @ U_[: self.n_components, : self.n_components] # m x r + S_ = S_[: self.n_components] + Vt_ = ( + Vt_[: self.n_components, :] + @ np.row_stack((self._Vt, Qot))[:, :-c] + ) # r x n + + self._q_r = 0 + self._U, self._S, self._Vt = U_, S_, Vt_ + + self.n_seen -= c diff --git a/river/decomposition/test_odmd.py b/river/decomposition/test_odmd.py new file mode 100644 index 0000000000..39b091ff7e --- /dev/null +++ b/river/decomposition/test_odmd.py @@ -0,0 +1,176 @@ +"""Test conversion from river to scikit-learn API and back. + +Requires two modifications to river code: +1. change line 49 in river.compat.river_to_sklearn to +`SKLEARN_INPUT_Y_PARAMS = {"multi_output": True, "y_numeric": False}` +2. change line 194 in river.compat.river_to_sklearn to +`y_pred = np.empty(shape=(len(X), X.shape[1]))` +""" +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest +from scipy.integrate import odeint + +from river.decomposition.odmd import OnlineDMD +from river.utils import Rolling + +epsilon = 1e-1 + + +def dyn(x, t): + x1, x2 = x + dxdt = [(1 + epsilon * t) * x2, -(1 + epsilon * t) * x1] + return dxdt + + +# integrate from initial condition [1,0] +samples = 101 +tspan = np.linspace(0, 10, samples) +dt = 0.1 +x0 = [1, 0] +xsol = odeint(dyn, x0, tspan).T +# extract snapshots +X, Y = xsol[:, :-1].T, xsol[:, 1:].T +t = tspan[1:] +n, m = X.shape +A = np.empty((n, m, m)) +eigvals = np.empty((n, m), dtype=complex) +for k in range(n): + A[k, :, :] = np.array( + [[0, (1 + epsilon * t[k])], [-(1 + epsilon * t[k]), 0]] + ) + eigvals[k, :] = np.linalg.eigvals(A[k, :, :]) + + +def test_input_types(): + n_init = round(samples / 2) + + odmd1 = OnlineDMD() + + odmd1.learn_many(X[:n_init, :], Y[:n_init, :]) + for x, y in zip(X[n_init:, :], Y[n_init:, :]): + odmd1.learn_one(x, y) + + X_, Y_ = pd.DataFrame(X), pd.DataFrame(Y) + + odmd2 = OnlineDMD() + + odmd2.learn_many(X_.iloc[:n_init], Y_.iloc[:n_init]) + for x, y in zip(X_.iloc[n_init:].values, Y_.iloc[n_init:].values): + odmd2.learn_one(x, y) + + assert np.allclose(odmd1.A, odmd2.A) + + +def test_one_many_close(): + n_init = round(samples / 2) + + odmd1 = OnlineDMD() + odmd2 = OnlineDMD() + + odmd1.learn_many(X[:n_init, :], Y[:n_init, :]) + odmd2.learn_many(X[:n_init, :], Y[:n_init, :]) + + eig_o1 = np.log(np.linalg.eigvals(odmd1.A)) / dt + eig_o2 = np.log(np.linalg.eigvals(odmd2.A)) / dt + assert np.allclose(eig_o1, eig_o2) + + for x, y in zip(X[n_init:, :], Y[n_init:, :]): + odmd1.learn_one(x, y) + + odmd2.learn_many(X[n_init:, :], Y[n_init:, :]) + eig_o1 = np.log(np.linalg.eigvals(odmd1.A)) / dt + eig_o2 = np.log(np.linalg.eigvals(odmd2.A)) / dt + print(eig_o1, eig_o2) + assert np.allclose(eig_o1, eig_o2) + + +def test_errors_raised(): + odmd = OnlineDMD() + + with pytest.raises(Exception): + odmd._update_many(X, Y) + + rodmd = Rolling(OnlineDMD(), window_size=1) # type: ignore + with pytest.raises(Exception): + for x, y in zip(X, Y): + rodmd.update(x, y) + + +def test_allclose_unsupervised_supervised(): + m_u = OnlineDMD(r=2, w=0.1, initialize=0) + m_s = OnlineDMD(r=2, w=0.1, initialize=0) + + for x, y in zip(X, Y): + m_u.learn_one(x) + m_s.learn_one(x, y) + eig_u, _ = np.log(m_u.eig[0]) / dt + eig_s, _ = np.log(m_u.eig[0]) / dt + + assert np.allclose(eig_u, eig_s) + + +# TODO: test various combinations of truncated and exact state and control parts of DMDwC + +# Proctor et al. (2016) "Dynamic Mode Decomposition with Control" suggests that +# the DMDwC where B is unknown requires a second SVD computation for output +# space of Y. As the computation and updates of SVDs are expensive, we want to +# avoid this if possible. This test checks if the SVD of augumented state + +# control space is at least as close to SVD of original space than the SVD of +# the output space to the SVD of the original space. +def test_one_svd_is_enough(): + import numpy as np + import pandas as pd + import scipy as sp + np.random.seed(0) + + n = 101 + freq = 2.0 + tspan = np.linspace(0, 10, n) + w1 = np.cos(np.pi * freq * tspan) + w2 = -np.sin(np.pi * freq * tspan) + w3 = np.sin(2 * np.pi * freq * tspan) + u_ = np.ones(n) + u_[tspan > 5] *= 2 + w1[tspan > 5] *= 2 + w2[tspan > 5] *= 2 + w3[tspan > 5] *= 2 + df = pd.DataFrame({"w1": w1[:-1], "w2": w2[:-1], "w3": w3[:-1]}) + X, Y = df.iloc[:-1], df.shift(-1).iloc[:-1] + U = pd.DataFrame({"u": u_[:-2]}) + X_ = X.copy() + X_["u"] = U + + u_orig, s_orig, _ = sp.sparse.linalg.svds( + X.values.T, k=2, return_singular_vectors="u" + ) + u_aug, s_aug, _ = sp.sparse.linalg.svds( + X_.values.T, k=3, return_singular_vectors="u" + ) + u_out, s_out, _ = sp.sparse.linalg.svds( + Y.values.T, k=2, return_singular_vectors="u" + ) + + assert (np.abs(u_orig - u_aug[:3, :2]) <= np.abs(u_orig - u_out)).all() + assert (np.abs(s_orig - s_aug[:2]) <= np.abs(s_orig - s_out)).all() + +# TODO: find out why this test fails +# def test_allclose_weighted_true(): +# n_init = round(samples / 2) +# odmd = OnlineDMD(w=0.1) +# odmd.learn_many(X[:n_init, :], Y[:n_init, :]) + +# eigvals_online_ = np.empty((n, m), dtype=complex) +# for i, (x, y) in enumerate(zip(X, Y)): +# odmd.learn_one(x, y) +# eigvals_online_[i, :] = np.log(np.linalg.eigvals(odmd.A)) / dt + +# slope_eig_true = np.diff(eigvals)[n_init:, 0].mean() +# slope_eig_online = np.diff(eigvals_online_)[n_init:, 0].mean() +# np.allclose( +# slope_eig_true, +# slope_eig_online, +# atol=1e-4, +# ) diff --git a/river/decomposition/test_odmdwc.py b/river/decomposition/test_odmdwc.py new file mode 100644 index 0000000000..031e25eb9f --- /dev/null +++ b/river/decomposition/test_odmdwc.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import numpy as np +import pandas as pd + +from river.decomposition.odmd import OnlineDMD, OnlineDMDwC +from river.utils import Rolling + +T = 10 +t_diff = 0.01 +samples = int(T / t_diff) - 1 +time_space = np.linspace(0, T, num=samples + 1) + + +def omega(t): + return 1 + 0.1 * t + + +def u_t(x): + return K_prop * x + + +X = np.zeros((samples + 1, 2)) +X[0, :] = np.array([4, 7]) + +K_prop = -1 + +B = np.array([1, 0]) +U = np.zeros((samples + 1, 1)) + +i = 1 +true_eigs_ = [] +for k in np.linspace(t_diff, T, num=samples): + A_t = np.array([[t_diff, -omega(k)], [omega(k), 0.1 * t_diff]]) + true_eigs_.append(np.imag(np.log(np.linalg.eig(A_t)[0]))) + + control_input = np.matmul(B, u_t(X[i - 1]).T) * t_diff + U[i, :] = control_input + autonomous_state = np.matmul(X[i - 1, :], A_t) * t_diff + X[i - 1, :] + X[i, :] = autonomous_state + control_input + i += 1 + +true_eigs = np.vstack(true_eigs_) + +X = X[:-1, :] +Y = X[1:, :] +U = U[:-1, :] + + +def test_input_types(): + n_init = round(samples / 2) + + odmd1 = OnlineDMDwC(initialize=n_init) + + for x, y, u in zip(X, Y, U): + odmd1.learn_one(x, y, u) + + X_, Y_, U_ = pd.DataFrame(X), pd.DataFrame(Y), pd.DataFrame(U) + + odmd2 = OnlineDMDwC(initialize=n_init) + + for x, y, u in zip( + X_.to_dict(orient="records"), + Y_.to_dict(orient="records"), + U_.to_dict(orient="records"), + ): + odmd2.learn_one(x, y, u) + + assert np.allclose(odmd1.A, odmd2.A) + + +def test_dmdwc_variations(): + odmd = OnlineDMD(initialize=10) + odmdc_weight = OnlineDMDwC( + initialize=10, w=0.995, exponential_weighting=True + ) + odmdc_b = OnlineDMDwC(initialize=10, B=B.reshape(-1, 1)) + odmdc_window = Rolling(OnlineDMDwC(initialize=10), window_size=100) + odmdc_b_window = Rolling( + OnlineDMDwC(initialize=10, B=B.reshape(-1, 1)), window_size=100 + ) + + for x_, y_, u_ in zip(X, Y, U): + odmd.learn_one(x_, y_) + odmdc_weight.learn_one(x_, y_, u_) + odmdc_b.learn_one(x_, y_, u_) + odmdc_window.learn_one(x_, y_, u_) + odmdc_b_window.learn_one(x_, y_, u_) + + atol = np.abs(get_ct_eigs(odmd.A) - true_eigs[-1]) * 1.5 + eig_weight = get_ct_eigs(odmdc_weight.A) + assert np.allclose(eig_weight, true_eigs[-1], atol=atol) + eig_b = get_ct_eigs(odmdc_b.A) + assert np.allclose(eig_b, true_eigs[-1], atol=atol) + eig_window = get_ct_eigs(odmdc_window.A) + assert np.allclose(eig_window, true_eigs[-1], atol=atol) + eig_b_window = get_ct_eigs(odmdc_b_window.A) + assert np.allclose(eig_b_window, true_eigs[-1], atol=atol) + +def get_ct_eigs(A): + return np.imag(np.log(np.linalg.eigvals(A))) / t_diff + + +def test_close_learn_one_learn_many(): + pass diff --git a/river/preprocessing/__init__.py b/river/preprocessing/__init__.py index 458def0676..4a7a00544b 100644 --- a/river/preprocessing/__init__.py +++ b/river/preprocessing/__init__.py @@ -9,6 +9,7 @@ from __future__ import annotations from .feature_hasher import FeatureHasher +from .hankel import Hankelizer from .impute import PreviousImputer, StatImputer from .lda import LDA from .one_hot import OneHotEncoder @@ -31,6 +32,7 @@ "Binarizer", "FeatureHasher", "GaussianRandomProjector", + "Hankelizer", "LDA", "MaxAbsScaler", "MinMaxScaler", diff --git a/river/preprocessing/hankel.py b/river/preprocessing/hankel.py new file mode 100644 index 0000000000..0875cd1cc9 --- /dev/null +++ b/river/preprocessing/hankel.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from collections import deque +from typing import Literal + +from river.base import Transformer + +__all__ = ["Hankelizer"] + + +class Hankelizer(Transformer): + """Time Delay Embedding using Hankelization. + + Convert a time series into a time delay embedded Hankel vectors. + + Args: + w: The number of data snapshots to preserve + return_partial: Whether to return partial Hankel matrices when the + window is not full. Default "copy" fills missing with copies. + + Examples: + >>> h = Hankelizer(w=3) + >>> h.learn_one({"a": 1, "b": 2}) + >>> h.transform_one({"a": 1, "b": 2}) + {'a_0': 1, 'b_0': 2, 'a_1': 1, 'b_1': 2, 'a_2': 1, 'b_2': 2} + + >>> h = Hankelizer(w=3, return_partial=False) + >>> h.learn_one({"a": 1, "b": 2}) + >>> h.transform_one({"a": 1, "b": 2}) + Traceback (most recent call last): + ... + ValueError: The window is not full yet. Set `return_partial` to True ... + + >>> h = Hankelizer(w=3, return_partial=True) + >>> h.learn_one({"a": 1, "b": 2}) + >>> h.transform_one({"a": 1, "b": 2}) + {'a_0': nan, 'b_0': nan, 'a_1': nan, 'b_1': nan, 'a_2': 1, 'b_2': 2} + + Actually, transform_one does not care about the data as the learn should precede. + >>> h.learn_one({"a": 3, "b": 4}) + >>> h.transform_one({"a": 5, "b": 6}) + {'a_0': nan, 'b_0': nan, 'a_1': 1, 'b_1': 2, 'a_2': 3, 'b_2': 4} + >>> h._window + deque([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}], maxlen=3) + + Transform and learn in one go. + >>> h.learn_transform_one({"a": 5, "b": 6}) + {'a_0': 1, 'b_0': 2, 'a_1': 3, 'b_1': 4, 'a_2': 5, 'b_2': 6} + + TODO: + - [ ] Find out how to hankelize u while staying aligned with pipeline + """ + + def __init__( + self, w: int = 2, return_partial: bool | Literal["copy"] = "copy" + ): + self.w = w + self.return_partial = return_partial + + self._window: deque = deque(maxlen=self.w) + self.feature_names_in_: list[str] + self.n_features_in_: int + + def learn_one(self, x: dict): + if not hasattr(self, "feature_names_in_") and isinstance(x, dict): + self.feature_names_in_ = list(x.keys()) + self.n_features_in_ = len(x) + + self._window.append(x) + + def transform_one(self, x: dict): + if not isinstance(x, dict): + on_arrays = True + else: + on_arrays = False + # TODO: If called before learn_one, creates duplicate sample + _window = list(self._window) + w_past_current = len(_window) + if w_past_current == 0: + _window = [x] + # To avoid overflowing the window + w_past_current = 1 + if not self.return_partial and w_past_current < self.w: + raise ValueError( + "The window is not full yet. Set `return_partial` to True to return partial Hankel matrices." + ) + else: + n_missing = self.w - w_past_current + _window = [_window[0]] * (n_missing) + _window + if not self.return_partial == "copy": + for i in range(n_missing): + _window[i] = {k: float("nan") for k in _window[0]} + if on_arrays: + import numpy as np + + return np.array([v for d in _window for v in d]) + else: + return { + f"{k}_{i}": v + for i, d in enumerate(_window) + for k, v in d.items() + } + + def learn_transform_one(self, x: dict): + self.learn_one(x) + y = self.transform_one(x) + return y