├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ai4sci └── oml │ ├── __init__.py │ ├── _online_linear_model.py │ └── _online_model.py ├── assets ├── linear_time_varying.png ├── linear_time_varying_controlled.png ├── lorenz_control.png ├── lorenz_control_controlled.png ├── lorenz_state.png └── lorenz_state_controlled.png ├── demo ├── README.md ├── demo_linear_time_varying_system.ipynb ├── demo_lorenz.ipynb └── requirements.txt ├── setup.py └── tests ├── README.md ├── requirements.txt ├── test_online_linear_model.py └── test_online_model.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any directories, files, or patterns you don't want to be tracked by version control 2 | 3 | # matlab 4 | # OSX / *nix default autosave extension 5 | *.m~ 6 | 7 | # python 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # Other files 14 | tmp* 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hao Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | env: 2 | python3 -m venv ~/my-env 3 | source ~/my-env/bin/activate 4 | file $(which python) 5 | python --version 6 | dev: 7 | python -m pip install -U pip setuptools wheel pip-tools isort black flake8 pylint mypy 8 | 9 | install: 10 | python -m pip install -e . 11 | requirements: 12 | python -m piptools compile --no-emit-index-url 13 | # https://code.visualstudio.com/docs/python/linting 14 | lint: 15 | python -m flake8 . 16 | python -m pylint . 17 | python -m mypy . 18 | test: lint 19 | cd tests 20 | python -m pip install -r requirements.txt 21 | python -m pytest 22 | format: 23 | python -m isort . 24 | python -m black . 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oml 2 | [![License](https://img.shields.io/github/license/haozhg/oml)](https://github.com/haozhg/oml/blob/master/LICENSE) 3 | [![python version](https://img.shields.io/badge/python-3.7+-green)](https://docs.python.org/3.8/) 4 | [![pypi version](https://img.shields.io/badge/pypi-0.2.2-green)](https://pypi.org/project/ai4sci.oml/) 5 | [![Open In Collab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1dWeKuiEsVUjlNaKSFW6b7J-UyyFwov8C?usp=sharing) 6 | [![Downloads](https://pepy.tech/badge/ai4sci.oml)](https://pepy.tech/project/ai4sci.oml) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/haozhg/oml/pulls) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | 10 | AI4Science: Efficient data-driven Online Model Learning (OML) / system identification and control. The algorithm is proposed in this [paper](https://arxiv.org/abs/1707.02876.). 11 | 12 | To get started, 13 | ``` 14 | pip install ai4sci.oml --upgrade 15 | ``` 16 | This python package is based on the online dynamic mode decomposition algorithm, which is also available as a python package `pip install odmd --upgrade`, see [here](https://github.com/haozhg/odmd). 17 | 18 | ## Showcase: Lorenz system control 19 | [Lorenz system](https://en.wikipedia.org/wiki/Lorenz_system) is one of the most classical nonlinear dynamical systems. Here we show how the proposed algorithm can be used to controll that. For more details, see [demo](https://github.com/haozhg/oml/tree/master/demo). 20 | 21 | ### No control 22 | If there is no control, we can see the mysterious butterfly trajectory. It starts close to the bottom plane and enters into the butterfly wing region, then oscillates there. 23 | 24 |

25 | 26 | 27 |

28 | 29 | ### With control 30 | If we apply data-driven real-time closed loop control, it can be stabilized at an unstable fixed point (near the center of the butterfly wing). 31 | 32 |

33 | 34 | 35 |

36 | 37 | ## Highlights 38 | Here are some hightlights about this algorithm, and for more detail refer to this [paper](https://epubs.siam.org/doi/pdf/10.1137/18M1192329) 39 | 40 | - Efficient data-driven online linear/nonlinear model learning (system identification). Any nonlinear and/or time-varying system is locally linear, as long as the model is updated in real-time wrt to new measurements. 41 | - It finds the exact optimal solution (in the sense of least square error), without any approximation (unlike stochastic gradient descent). 42 | - It achieves theoretical optimal time and space complexity. 43 | - The time complexity (flops for one iteration) is O(n^2), where n is state dimension. This is much faster than standard algorithm O(n^2 * t), where t is the current time step (number of measurements). In online applications, t >> n and essentially will go to infinity. 44 | - The space complexity is O(n^2), which is far more efficient than standard algorithm O(n * t) (t >> n). 45 | - A weighting factor (in (0, 1]) can be used to place more weight on recent data, thus making the model more adaptive. 46 | - This local model can be used for short-horizon prediction and data-driven real-time closed loop control. 47 | - It has been successfully applied to flow separation control problem, and achived real-time closed loop control. See this [paper](https://doi.org/10.1017/jfm.2020.546) for details. 48 | 49 | ## Online model learning algorithm description 50 | This is a brief introduction to the algorithm. For full technical details, see this [paper](https://epubs.siam.org/doi/pdf/10.1137/18M1192329), and chapter 3 and chapter 7 of this [PhD thesis](http://arks.princeton.edu/ark:/88435/dsp0108612r49q). 51 | 52 | ### Unknown dynamical system 53 | Suppose we have a (discrete) nonlinear and/or time-varying [dynamical system](https://en.wikipedia.org/wiki/State-space_representation), and the state space representation is 54 | - x(t+1) = f(t, x(t), u(t)) 55 | - y(t) = g(t, x(t), u(t)) 56 | 57 | where t is (discrete) time, x(t) is state vector, u(t) is control (input) vector, y(t) is observation (output) vector. f(~, ~, ~) and g(~, ~, ~) are unknown vector-valued nonlinear functions. 58 | 59 | - It is assumed that we have measurements x(i), u(i), y(i) for i = 0,1,...t. 60 | - However, we do not know functions f and g. 61 | - We aim to learn a model for the unknown dynamical system from measurement data up to time t. 62 | - We want to the model to be updated efficiently in real-time as new measurement data becomes available. 63 | 64 | ### Online linear model learning 65 | We would like to learn an adaptive [linear model](https://en.wikipedia.org/wiki/State-space_representation) 66 | - x(t+1) = A x(t) + B u(t) 67 | - y(t) = C x(t) + D u(t) 68 | 69 | that fits/explains the observation optimally. By Taylor expansion approximation, any nonlinear and/or time-varying system is linear locally. There are many powerful tools for linear control, e.g, [Linear Quadratic Regulator](https://en.wikipedia.org/wiki/Linear%E2%80%93quadratic_regulator), [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). However, to accurately approximate the original (unknown) dynamical system, we need to update this linear model efficiently in real-time whenever new measurement becomes available. 70 | 71 | This problem can be formulated as an optimization problem, and at each time step t we need to solve a related but slightly different optimization problem. The optimal algorithm is achived through efficient reformulation of the problem. 72 | 73 | - `ai4sci.oml.OnlineLinearModel` class implements the optimal algorithm. 74 | 75 | ### Online nonlinear model learning 76 | If we need to fit a nonlinear model to the observed data, this algorithm also applies in this case. Keep in mind that linear adaptive model is good approximation as long as it is updated in real-time. Also, the choice of nonlinear form can be tricky. Based on Taylor expansion, if we add higher order nonlinearity (e.g., quadratic, cubic), the approximation can be more accurate. However, given the learned nonlinear model, it is still not easy to apply control. 77 | 78 | In particular, we want to fit a nonlinear model of this form 79 | - x(t+1) = F * phi(x(t), u(t)) 80 | - y(t) = G * psi(x(t), u(t)) 81 | 82 | where phi(~, ~) and psi(~, ~) are known vector-valued nonlinear functions (e.g, quadratic) that we specify, F and G are unknown matrices of proper size. 83 | 84 | - We aim to learn F and G from measurement data. 85 | - Notice that this model form is general, and encompass many systems such as Lorenze attractor, Logistic map, Auto-regressive model, polynomial systems. 86 | - F and G can be updated efficiently in real-time when new data comes in. 87 | 88 | This can also be formulated as the same optimization problem, and the same efficient algorithm works in this case. 89 | 90 | - `ai4sci.oml.OnlineModel` class implements the optimal algorithm. 91 | 92 | ## Use 93 | ### Install 94 | From PyPi 95 | ``` 96 | pip install ai4sci.oml --upgrade 97 | ``` 98 | 99 | From source 100 | ``` 101 | git clone https://github.com/haozhg/oml.git 102 | cd oml/ 103 | pip install -e . 104 | ``` 105 | 106 | ### Tests 107 | ``` 108 | cd tests/ 109 | python -m pytest . 110 | ``` 111 | 112 | ### Demo 113 | See `./demo` for python notebook to demo the algorithm for data-driven real-time closed loop control. 114 | - `demo_lorenz.ipynb` shows control of [Lorenz attractor](https://en.wikipedia.org/wiki/Lorenz_system). 115 | - `demo_online_linear_model.ipynb` shows control of an unstable linear time-varying system. 116 | 117 | ## Authors: 118 | Hao Zhang 119 | 120 | ## Reference 121 | If you you used these algorithms or this python package in your work, please consider citing 122 | 123 | ``` 124 | Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta. 125 | "Online dynamic mode decomposition for time-varying systems." 126 | SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609. 127 | ``` 128 | 129 | BibTeX 130 | ``` 131 | @article{zhang2019online, 132 | title={Online dynamic mode decomposition for time-varying systems}, 133 | author={Zhang, Hao and Rowley, Clarence W and Deem, Eric A and Cattafesta, Louis N}, 134 | journal={SIAM Journal on Applied Dynamical Systems}, 135 | volume={18}, 136 | number={3}, 137 | pages={1586--1609}, 138 | year={2019}, 139 | publisher={SIAM} 140 | } 141 | ``` 142 | 143 | ## Date created 144 | April 2017 145 | 146 | ## License 147 | MIT 148 | 149 | If you want to use this package, but find license permission an issue, pls contact me at `haozhang at alumni dot princeton dot edu`. 150 | 151 | ## Issues 152 | If there is any comment/suggestion, or if you find any bug, feel free to 153 | - create an issue [here](https://github.com/haozhg/oml/issues), and/or 154 | - fork this repo, make suggested changes, and create a pull request (merge from your fork to this repo). See [this](https://numpy.org/devdocs/dev/index.html#development-process-summary) as an example guidance for contribution and PRs. -------------------------------------------------------------------------------- /ai4sci/oml/__init__.py: -------------------------------------------------------------------------------- 1 | from ._online_model import OnlineModel 2 | from ._online_linear_model import OnlineLinearModel -------------------------------------------------------------------------------- /ai4sci/oml/_online_linear_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | from ._online_model import OnlineModel 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class OnlineLinearModel: 11 | """ 12 | # Unknown dynamical system 13 | Suppose we have a (discrete) nonlinear and/or time-varying [dynamical system](https://en.wikipedia.org/wiki/State-space_representation), 14 | and the state space representation is 15 | - x(t+1) = f(t, x(t), u(t)) 16 | - y(t) = g(t, x(t), u(t)) 17 | 18 | where t is (discrete) time, x(t) is state vector, u(t) is control (input) vector, y(t) is observation (output) vector. 19 | f(~, ~, ~) and g(~, ~, ~) are unknown vector-valued nonlinear functions. 20 | 21 | - It is assumed that we have measurements x(i), u(i), y(i) for i = 0,1,...t. 22 | - However, we do not know functions f and g. 23 | - We aim to learn a model for the unknown dynamical system from measurement data up to time t. 24 | - We want to the model to be updated efficiently in real-time as new measurement data becomes available. 25 | 26 | # Online linear model learning 27 | We would like to learn an adaptive [linear model](https://en.wikipedia.org/wiki/State-space_representation) 28 | - x(t+1) = A x(t) + B u(t) 29 | - y(t) = C x(t) + D u(t) 30 | 31 | that fits/explains the observation optimally. By Taylor expansion approximation, any nonlinear and/or 32 | time-varying system is linear locally. There are many powerful tools for linear control, 33 | e.g, [Linear Quadratic Regulator](https://en.wikipedia.org/wiki/Linear%E2%80%93quadratic_regulator), 34 | [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). 35 | However, to accurately approximate the original (unknown) dynamical system, we need to update this linear 36 | model efficiently in real-time whenever new measurement becomes available. 37 | 38 | This problem can be formulated as an optimization problem, and at each time step t we need to solve a 39 | related but slightly different optimization problem. The optimal algorithm is achived through efficient 40 | reformulation of the problem. 41 | """ 42 | def __init__(self, n: int, k: int, m: int = None, alpha: float = 0.9) -> None: 43 | """Online Linear Model 44 | Efficiently learn adaptive linear model from data in real-time 45 | 46 | Args: 47 | n (int): state dimension 48 | k (int): control (input) dimension 49 | m (int): observation (output) dimension 50 | alpha (float, optional): exponentiall weighting factor in system identification. Defaults to 0.9. 51 | """ 52 | # input check 53 | assert isinstance(n, int) and n >= 1 54 | assert isinstance(k, int) and k >= 1 55 | assert m is None or (isinstance(m, int) and m >= 1) 56 | alpha = float(alpha) 57 | assert alpha > 0 and alpha <= 1 58 | 59 | # set parameters 60 | self._n = n 61 | self._k = k 62 | self._m = m 63 | self._alpha = alpha 64 | 65 | # additional parameters 66 | self._t = 0 67 | self._tmin = 2 * max(self._n, self._n + self._k) 68 | if self._m: 69 | self._tmin = max(self._tmin, 2 * self._m) 70 | self._ready = False 71 | 72 | # initialize model 73 | self._f = OnlineModel(n + k, n, alpha) # encodes A and B, model for xn=f(x, u) 74 | if self._m: 75 | logger.info("Learn x(t+1) = A * x(t) + B * u(t), y(t) = C * x(t) + D * u(t)") 76 | self._g = OnlineModel(n + k, m, alpha) # encodes C and D, model for y=g(x, u) 77 | else: 78 | logger.info("No output eqution, only learn x(t+1) = A * x(t) + B * u(t)") 79 | 80 | def update(self, x: np.ndarray, u: np.ndarray, xn: np.ndarray, y: np.ndarray =None) -> None: 81 | """Update model wrt new measurement (state x, control u, next state xn, optinal observation y) 82 | 83 | Args: 84 | x (np.ndarray): state x(t), 1D array, shape (n, ) 85 | u (np.ndarray): control u(t), 1D array, shape (k, ) 86 | xn (np.ndarray): new state x(t+1), 1D array, shape (n, ) 87 | y (np.ndarray, optional): observation y(t), 1D array, shape (m, ). Defaults to None. 88 | """ 89 | # input check 90 | assert x.shape[0] == self._n 91 | assert u.shape[0] == self._k 92 | assert xn.shape[0] == self._n 93 | if y is None: 94 | assert self._m is None 95 | if self._m: 96 | assert y.shape[0] == self._m 97 | 98 | # update f 99 | w = np.concatenate((x, u)) 100 | self._f.update(w, xn) 101 | 102 | # update g if needed 103 | if self._m: 104 | self._g.update(w, y) 105 | 106 | # timestep 107 | self._t += 1 108 | 109 | # mark model as ready 110 | if self._t >= self._tmin: 111 | self._ready = True 112 | 113 | # no setter 114 | @property 115 | def A(self) -> np.ndarray: 116 | """A in state space representation 117 | 118 | Returns: 119 | np.ndarray: [description] 120 | """ 121 | if not self._ready: 122 | logger.warning(f"Model not ready (have not seen enough data)!") 123 | return self._f.M[:, : self._n] 124 | 125 | @property 126 | def B(self) -> np.ndarray: 127 | """B in state space representation 128 | 129 | Returns: 130 | np.ndarray: [description] 131 | """ 132 | if not self._ready: 133 | logger.warning(f"Model not ready (have not seen enough data)!") 134 | return self._f.M[:, self._n :] 135 | 136 | @property 137 | def C(self) -> np.ndarray: 138 | """C in state space representation 139 | 140 | Raises: 141 | Exception: if no output eqution 142 | 143 | Returns: 144 | np.ndarray: [description] 145 | """ 146 | if not self._m: 147 | raise Exception(f"No output eqution!") 148 | if not self._ready: 149 | logger.warning(f"Model not ready (have not seen enough data)!") 150 | return self._g.M[:, : self._n] 151 | 152 | @property 153 | def D(self) -> np.ndarray: 154 | """D in state space representation 155 | 156 | Raises: 157 | Exception: if no output eqution 158 | 159 | Returns: 160 | np.ndarray: [description] 161 | """ 162 | if not self._m: 163 | raise Exception(f"No output eqution!") 164 | if not self._ready: 165 | logger.warning(f"Model not ready (have not seen enough data)!") 166 | return self._g.M[:, self._n :] 167 | 168 | @property 169 | def n(self) -> int: 170 | """State x(t) dimension as in state space representation 171 | 172 | Returns: 173 | int: [description] 174 | """ 175 | return self._n 176 | 177 | @property 178 | def k(self) -> int: 179 | """Control (input) u(t) dimension as in state space representation 180 | 181 | Returns: 182 | int: [description] 183 | """ 184 | return self._k 185 | 186 | @property 187 | def m(self) -> int: 188 | """Observation (output) y(t) dimension as in state space representation 189 | 190 | Returns: 191 | int: [description] 192 | """ 193 | return self._m 194 | 195 | @property 196 | def alpha(self) -> float: 197 | """Exponential weighting factor in (0, 1] 198 | Small value allows more adaptive learning and more forgetting 199 | 200 | Returns: 201 | float: [description] 202 | """ 203 | return self._alpha 204 | 205 | @property 206 | def t(self) -> int: 207 | """Total number measurements processed 208 | 209 | Returns: 210 | int: [description] 211 | """ 212 | return self._t 213 | 214 | @property 215 | def ready(self) -> bool: 216 | """If the model has seen enough data 217 | 218 | Returns: 219 | bool: [description] 220 | """ 221 | return self._ready -------------------------------------------------------------------------------- /ai4sci/oml/_online_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class OnlineModel: 9 | """OnlineModel is a class that implements online model learning 10 | The time complexity (multiply–add operation for one iteration) is O(4n^2), 11 | and space complexity is O(2n^2), where m is the state dimension. 12 | 13 | Algorithm description: 14 | If the dynamics is z(t) = f(z(t-1), u(t-1)), then we first choose a nonlinear 15 | observable phi(~, ~), and assume the model can be approximated by 16 | z(t) = M * phi(z(t-1), u(t-1)). 17 | 18 | Let x(t) = phi(z(t-1), u(t-1)), and y(t) = z(t). 19 | We would like to learn an adaptive linear model M (a matrix) st y(t) = M * x(t). 20 | The matrix M is updated recursively by efficient rank-1 updating online algrithm. 21 | An exponential weighting factor (alpha in (0, 1])) can be used to place more weight on 22 | recent data. 23 | 24 | At time step t, define two matrix X(t) = [x(1), x(2), ..., x(t)], 25 | Y(t) = [y(1), y(2),..., y(t)], that contain all the past snapshot pairs, 26 | where x(t), y(t) are the m dimensional state vector, y(t) = f(x(t)) is 27 | the image of x(t), f() is the dynamics. 28 | 29 | Here, if there is no control and dynamics is z(t) = f(z(t-1)), 30 | then x(t), y(t) should be measurements correponding to consecutive 31 | states z(t-1) and z(t). 32 | 33 | Usage: 34 | online_model = OnlineModel(n, m, alpha) 35 | online_model.initialize(X, Y) # optional 36 | online_model.update(x, y) 37 | 38 | properties: 39 | M: the model matrix (adaptive and updated online) as in y(t) = M * x(t) 40 | n: x(t) vector dimension, can be z(t-1) (for z(t) = f(z(t-1))) or phi(z(t-1), u(t-1)) (for z(t) = f(z(t-1), u(t-1))) 41 | state dimension + control dimension for linear system identification 42 | m: y(t) vector dimension, z(t) in z(t) = f(z(t-1), u(t-1)) or z(t) = f(z(t-1)) 43 | state dimension for linear system identification 44 | alpha: weighting factor in (0,1] 45 | t: number of snapshot pairs processed (i.e., current time step) 46 | 47 | methods: 48 | initialize(X, Y), initialize online model learning algorithm with first p 49 | snapshot pairs stored in (X, Y), this func call is optional 50 | update(x, y), update online adaptive model when new snapshot pair (x, y) 51 | becomes available 52 | 53 | Authors: 54 | Hao Zhang 55 | 56 | References: 57 | Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta. 58 | "Online dynamic mode decomposition for time-varying systems." 59 | SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609. 60 | """ 61 | def __init__(self, n: int, m: int, alpha: float = 0.9) -> None: 62 | """Creat an object for online model learning 63 | Usage: online_model = OnlineModel(n, m, alpha) 64 | 65 | Args: 66 | n (int): x dimension in model y(t) = M * x(t) 67 | m (int): y dimension in model y(t) = M * x(t) 68 | alpha (float, optional): exponential weighting factor in (0, 1], smaller values allows more adaptive learning. Defaults to 0.9. 69 | """ 70 | # input check 71 | assert isinstance(n, int) and n >= 1 72 | assert isinstance(m, int) and m >= 1 73 | alpha = float(alpha) 74 | assert alpha > 0 and alpha <= 1 75 | 76 | # initialize parameters 77 | self._n = n 78 | self._m = m 79 | self._alpha = alpha 80 | self._t = 0 81 | self._A = np.zeros([m, n]) 82 | self._P = np.zeros([n, n]) 83 | 84 | # initialize model 85 | self._initialize() 86 | self._ready = False 87 | 88 | def _initialize(self) -> None: 89 | """Initialize online model with epsilon small (1e-15) ghost snapshot pairs before t=0""" 90 | epsilon = 1e-15 91 | self._A = np.random.randn(self._m, self._n) 92 | self._P = np.identity(self._n) / epsilon 93 | 94 | def initialize(self, X: np.ndarray, Y: np.ndarray) -> None: 95 | """Initialize online model with first p (p >= max(n, m)) snapshot pairs stored in (X, Y) 96 | (x(t), y(t)) as in model y(t) = M * x(t) 97 | Usage: online_model.initialize(X, Y) # this step is optional 98 | 99 | Args: 100 | X (np.ndarray): 2D array, shape (n, p), matrix [x(0), x(1), ..., x(n)] 101 | Y (np.ndarray): 2D array, shape (m, p), matrix [y(0), y(1), ..., y(n)] 102 | """ 103 | # input check 104 | assert X is not None and Y is not None 105 | X, Y = np.array(X).reshape(self._n, -1), np.array(Y).reshape(self._m, -1) 106 | assert X.shape[1] == Y.shape[1] 107 | 108 | # necessary condition for over-constrained initialization 109 | p = X.shape[1] 110 | assert p >= max(self._n, self._m) 111 | assert np.linalg.matrix_rank(X) == min(self._n, p) 112 | 113 | # initialize with weighted snapshots 114 | weight = np.sqrt(self._alpha) ** range(p - 1, -1, -1) 115 | Xhat, Yhat = weight * X, weight * Y 116 | self._A = Yhat.dot(np.linalg.pinv(Xhat)) 117 | self._P = np.linalg.inv(Xhat.dot(Xhat.T)) / self._alpha 118 | self._t += p 119 | 120 | def update(self, x: np.ndarray, y: np.ndarray) -> None: 121 | """Update the Model with a new pair of snapshots (x, y) 122 | y = f(x) is the dynamics/model 123 | Here, if the (discrete-time) dynamics are given by z(t) = f(z(t-1)), 124 | then (x,y) should be measurements correponding to consecutive states 125 | z(t-1) and z(t). 126 | Usage: online_model.update(x, y) 127 | 128 | Args: 129 | x (np.ndarray): 1D array, shape (n, ), x(t) in model y(t) = M * x(t) 130 | y (np.ndarray): 1D array, shape (m, ), y(t) in model y(t) = M * x(t) 131 | """ 132 | # input check 133 | assert x is not None and y is not None 134 | x, y = np.array(x).reshape(-1), np.array(y).reshape(-1) 135 | assert x.shape[0] == self._n 136 | assert y.shape[0] == self._m 137 | 138 | # compute P*x matrix vector product beforehand 139 | Px = self._P.dot(x) 140 | # compute gamma 141 | gamma = 1.0 / (1 + x.T.dot(Px)) 142 | # update A 143 | self._A += np.outer(gamma * (y - self._A.dot(x)), Px) 144 | # update P, group Px*Px' to ensure positive definite 145 | self._P = (self._P - gamma * np.outer(Px, Px)) / self._alpha 146 | # ensure P is SPD by taking its symmetric part 147 | self._P = (self._P + self._P.T) / 2 148 | 149 | # time step + 1 150 | self._t += 1 151 | 152 | # mark model as ready 153 | if self._t >= 2 * max(self._m, self._n): 154 | self._ready = True 155 | 156 | # ready only properties 157 | @property 158 | def M(self) -> np.ndarray: 159 | """Matrix in model y(t) = M * x(t) 160 | 161 | Returns: 162 | np.ndarray: [description] 163 | """ 164 | if not self._ready: 165 | logger.warning(f"Model not ready (have not seen enough data)!") 166 | return self._A 167 | 168 | @property 169 | def m(self) -> int: 170 | """y dimension in model y(t) = M * x(t) 171 | 172 | Returns: 173 | int: [description] 174 | """ 175 | return self._m 176 | 177 | @property 178 | def n(self) -> int: 179 | """x dimension in model y(t) = M * x(t) 180 | 181 | Returns: 182 | int: [description] 183 | """ 184 | return self._n 185 | 186 | @property 187 | def alpha(self) -> float: 188 | """Exponential weighting factor in (0, 1] 189 | Small value allows more adaptive learning and more forgetting 190 | 191 | Returns: 192 | float: [description] 193 | """ 194 | return self._alpha 195 | 196 | @property 197 | def t(self) -> int: 198 | """Total number measurements processed 199 | y(i) = M * x(i) 200 | (x(i), y(i)), i = 0,1,...,t 201 | 202 | Returns: 203 | int: [description] 204 | """ 205 | return self._t 206 | 207 | @property 208 | def ready(self) -> bool: 209 | """If the model has seen enough data 210 | 211 | Returns: 212 | bool: [description] 213 | """ 214 | return self._ready -------------------------------------------------------------------------------- /assets/linear_time_varying.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/linear_time_varying.png -------------------------------------------------------------------------------- /assets/linear_time_varying_controlled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/linear_time_varying_controlled.png -------------------------------------------------------------------------------- /assets/lorenz_control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/lorenz_control.png -------------------------------------------------------------------------------- /assets/lorenz_control_controlled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/lorenz_control_controlled.png -------------------------------------------------------------------------------- /assets/lorenz_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/lorenz_state.png -------------------------------------------------------------------------------- /assets/lorenz_state_controlled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haozhg/oml/6008a5d3223ebcce3aff4ea066042d8e23402edb/assets/lorenz_state_controlled.png -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## Data-driven closed loop real-time control 2 | This directory contains examples of the application of this algorithm to data-driven real-time closed loop control. 3 | - Linear time-varying system, `demo_linear_time_varying_system.ipynb`, can also run it in [google colab](https://colab.research.google.com/drive/1b4-wEfqXhY4QWeU7M7rmEMXwmJwZvsfU?usp=sharing) 4 | - Lorenz system, `demo_lorenz.ipynb`, can also run it in [google colab](https://colab.research.google.com/drive/1dWeKuiEsVUjlNaKSFW6b7J-UyyFwov8C?usp=sharing) 5 | -------------------------------------------------------------------------------- /demo/demo_linear_time_varying_system.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"\"\"\n", 8 | "An example to demonstrate online linear system identification\n", 9 | "\n", 10 | "We demonstrate the use of OnlineLinearModel class with a simple linear system.\n", 11 | "Take a 2D time-varying system dx/dt=A(t)x(t)+B(t)u(t), where A(t) and B(t)\n", 12 | "are slowly varying with time. In particular, we take A(t)=(1+eps\\*t)\\*A,\n", 13 | "B(t)=(1+eps\\*t)\\*B, and eps = 0.1 is small. It is discretize with\n", 14 | "time step dt = 0.1. Denote the discrete system as x(k+1)=A(k)x(k)+\n", 15 | "B(k)u(k).\n", 16 | "\n", 17 | "At time step t+1, we need to include new snapshot pair x(t), u(t), x(t+1).\n", 18 | "We would like to update the adaptive model in real-time\n", 19 | "\n", 20 | "Authors: \n", 21 | "Hao Zhang\n", 22 | "\n", 23 | "References:\n", 24 | "Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta.\n", 25 | "\"Online dynamic mode decomposition for time-varying systems.\"\n", 26 | "SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609.\n", 27 | "\n", 28 | "Created:\n", 29 | "June 2017.\n", 30 | "\"\"\"" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 7, 36 | "metadata": { 37 | "ExecuteTime": { 38 | "end_time": "2022-10-20T22:53:15.751498Z", 39 | "start_time": "2022-10-20T22:53:15.587128Z" 40 | } 41 | }, 42 | "outputs": [ 43 | { 44 | "name": "stdout", 45 | "output_type": "stream", 46 | "text": [ 47 | "/Users/haozhang/python-venvs/oml-reorg/bin/python: Mach-O 64-bit executable x86_64\r\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "!file $(which python)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 8, 58 | "metadata": { 59 | "ExecuteTime": { 60 | "end_time": "2022-10-20T22:53:16.348831Z", 61 | "start_time": "2022-10-20T22:53:16.199174Z" 62 | } 63 | }, 64 | "outputs": [ 65 | { 66 | "name": "stdout", 67 | "output_type": "stream", 68 | "text": [ 69 | "Python 3.8.5\r\n" 70 | ] 71 | } 72 | ], 73 | "source": [ 74 | "!python --version" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 9, 80 | "metadata": { 81 | "ExecuteTime": { 82 | "end_time": "2022-10-20T22:53:18.233275Z", 83 | "start_time": "2022-10-20T22:53:17.151364Z" 84 | } 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "!python -m pip install -Uqqq pip setuptools wheel" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 15, 94 | "metadata": { 95 | "ExecuteTime": { 96 | "end_time": "2022-10-20T22:54:18.035936Z", 97 | "start_time": "2022-10-20T22:54:17.896542Z" 98 | } 99 | }, 100 | "outputs": [ 101 | { 102 | "name": "stdout", 103 | "output_type": "stream", 104 | "text": [ 105 | "README.md demo_lorenz.ipynb\r\n", 106 | "demo_linear_time_varying_system.ipynb requirements.txt\r\n" 107 | ] 108 | } 109 | ], 110 | "source": [ 111 | "!ls" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 16, 117 | "metadata": { 118 | "ExecuteTime": { 119 | "end_time": "2022-10-20T22:54:24.872985Z", 120 | "start_time": "2022-10-20T22:54:24.732583Z" 121 | } 122 | }, 123 | "outputs": [ 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "ai4sci.oml\r\n", 129 | "numpy\r\n", 130 | "scipy\r\n", 131 | "matplotlib\r\n", 132 | "control\r\n", 133 | "slycot" 134 | ] 135 | } 136 | ], 137 | "source": [ 138 | "!cat requirements.txt" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 17, 144 | "metadata": { 145 | "ExecuteTime": { 146 | "end_time": "2022-10-20T22:54:26.445102Z", 147 | "start_time": "2022-10-20T22:54:25.697720Z" 148 | } 149 | }, 150 | "outputs": [ 151 | { 152 | "name": "stdout", 153 | "output_type": "stream", 154 | "text": [ 155 | "Requirement already satisfied: ai4sci.oml in /Users/haozhang/Local/open-source/oml (from -r requirements.txt (line 1)) (0.2.2)\n", 156 | "Requirement already satisfied: numpy in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 2)) (1.23.4)\n", 157 | "Requirement already satisfied: scipy in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 3)) (1.9.3)\n", 158 | "Requirement already satisfied: matplotlib in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 4)) (3.6.1)\n", 159 | "Requirement already satisfied: control in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 5)) (0.9.2)\n", 160 | "Requirement already satisfied: slycot in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 6)) (0.5.0)\n", 161 | "Requirement already satisfied: python-dateutil>=2.7 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (2.8.2)\n", 162 | "Requirement already satisfied: pyparsing>=2.2.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (3.0.9)\n", 163 | "Requirement already satisfied: cycler>=0.10 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (0.11.0)\n", 164 | "Requirement already satisfied: pillow>=6.2.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (9.2.0)\n", 165 | "Requirement already satisfied: kiwisolver>=1.0.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (1.4.4)\n", 166 | "Requirement already satisfied: fonttools>=4.22.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (4.37.4)\n", 167 | "Requirement already satisfied: packaging>=20.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (21.3)\n", 168 | "Requirement already satisfied: contourpy>=1.0.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (1.0.5)\n", 169 | "Requirement already satisfied: six>=1.5 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from python-dateutil>=2.7->matplotlib->-r requirements.txt (line 4)) (1.16.0)\n" 170 | ] 171 | } 172 | ], 173 | "source": [ 174 | "!python -m pip install -r requirements.txt" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 11, 180 | "metadata": { 181 | "ExecuteTime": { 182 | "end_time": "2022-10-20T22:53:21.040240Z", 183 | "start_time": "2022-10-20T22:53:20.535533Z" 184 | } 185 | }, 186 | "outputs": [ 187 | { 188 | "name": "stdout", 189 | "output_type": "stream", 190 | "text": [ 191 | "ai4sci.oml 0.2.2 /Users/haozhang/Local/open-source/oml\r\n" 192 | ] 193 | } 194 | ], 195 | "source": [ 196 | "!pip list | grep ai4sci.oml" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 13, 202 | "metadata": { 203 | "ExecuteTime": { 204 | "end_time": "2022-10-20T22:53:39.169234Z", 205 | "start_time": "2022-10-20T22:53:39.139049Z" 206 | } 207 | }, 208 | "outputs": [ 209 | { 210 | "ename": "ModuleNotFoundError", 211 | "evalue": "No module named 'ai4sci'", 212 | "output_type": "error", 213 | "traceback": [ 214 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 215 | "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 216 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 27\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 29\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mai4sci\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moml\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mOnlineLinearModel\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 30\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mcontrol\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mlqr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mStateSpace\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mctrb\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 217 | "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'ai4sci'" 218 | ] 219 | } 220 | ], 221 | "source": [ 222 | "\"\"\"\n", 223 | "An example to demonstrate online linear system identification\n", 224 | "\n", 225 | "We demonstrate the use of OnlineLinearModel class with a simple linear system.\n", 226 | "Take a 2D time-varying system dx/dt=A(t)x(t)+B(t)u(t), where A(t) and B(t)\n", 227 | "are slowly varying with time. In particular, we take A(t)=(1+eps*t)*A,\n", 228 | "B(t)=(1+eps*t)*B, and eps = 0.1 is small. It is discretize with\n", 229 | "time step dt = 0.02. Denote the discrete system as x(k+1)=A(k)x(k)+\n", 230 | "B(k)u(k).\n", 231 | "\n", 232 | "At time step k+1, we need to include new snapshot pair x(k), u(k), x(k+1).\n", 233 | "We would like to update the adaptive model in real-time\n", 234 | "\n", 235 | "Authors: \n", 236 | "Hao Zhang\n", 237 | "\n", 238 | "References:\n", 239 | "Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta.\n", 240 | "\"Online dynamic mode decomposition for time-varying systems.\"\n", 241 | "SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609.\n", 242 | "\n", 243 | "Created:\n", 244 | "June 2017.\n", 245 | "\"\"\"\n", 246 | "\n", 247 | "import random\n", 248 | "import matplotlib.pyplot as plt\n", 249 | "import numpy as np\n", 250 | "from ai4sci.oml import OnlineLinearModel\n", 251 | "from control import lqr, StateSpace, ctrb\n", 252 | "\n", 253 | "random.seed(20210220)\n", 254 | "np.random.seed(20210220)\n", 255 | "\n", 256 | "# define dynamics, negative damping -> unstable\n", 257 | "# https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator\n", 258 | "n = 2 # state dimension\n", 259 | "k = 1 # control dimension\n", 260 | "A = np.array([[0, 1], [-1, 0.2]])\n", 261 | "B = np.array([[0], [1]])\n", 262 | "C = np.eye(n)\n", 263 | "D = np.zeros((n, k))\n", 264 | "\n", 265 | "# check poles\n", 266 | "sys = StateSpace(A, B, np.eye(n), np.zeros((n, k)))\n", 267 | "print(f\"open loop poles: {sys.pole()}\")\n", 268 | "\n", 269 | "# check controllability\n", 270 | "Ctrb = ctrb(A, B)\n", 271 | "assert np.linalg.matrix_rank(Ctrb) == n\n", 272 | "\n", 273 | "# slowly time-varying dynamics\n", 274 | "def dyn(t, x, u, epsilon=1e-3):\n", 275 | " At = (1 + epsilon * t) * A\n", 276 | " Bt = (1 + epsilon * t) * B\n", 277 | " dxdt = At.dot(x) + Bt.dot(u)\n", 278 | " return dxdt\n", 279 | "\n", 280 | "# set up simulation parameter\n", 281 | "dt = 0.02\n", 282 | "tmax, tc = 20, 0.5\n", 283 | "T, kc = int(tmax / dt), int(tc / dt)\n", 284 | "tspan = np.linspace(0, tmax, T + 1)\n", 285 | "\n", 286 | "# online linear system identification setup\n", 287 | "alpha = 0.01 ** (1.0 / kc) # 99% decay after kc samples\n", 288 | "olm = OnlineLinearModel(n, k, None, alpha)\n", 289 | "\n", 290 | "# store data mtrices\n", 291 | "x = np.zeros([n, T])\n", 292 | "u = np.zeros([k, T])\n", 293 | "\n", 294 | "# initial condition, state and control\n", 295 | "x0 = np.array([1, 0])\n", 296 | "u0 = 0\n", 297 | "\n", 298 | "# uncontrolled system\n", 299 | "x[:, 0] = x0\n", 300 | "u[:, 0] = u0\n", 301 | "for t in range(1, T):\n", 302 | " # forward the system for one step\n", 303 | " x[:, t] = x[:, t - 1] + dt * dyn(t * dt, x[:, t - 1], u[:, t - 1])\n", 304 | " u[:, t] = 0\n", 305 | "\n", 306 | "def plot_state_control(x, u):\n", 307 | " plt.rcParams['figure.dpi'] = 100\n", 308 | " fig, axs = plt.subplots(2, figsize=(10, 6))\n", 309 | " fig.suptitle('State and control')\n", 310 | " axs[0].plot(tspan[1:], x[0, :], \"b-\", linewidth=1.0, label=\"State $x_1(t)$\")\n", 311 | " axs[0].plot(tspan[1:], x[1, :], \"g-\", linewidth=1.0, label=\"State $x_2(t)$\")\n", 312 | " axs[0].legend(loc=\"best\", fontsize=12, shadow=True)\n", 313 | " axs[0].grid()\n", 314 | "\n", 315 | " axs[1].plot(tspan[1:], u.reshape(-1), \"r-\", linewidth=1.0, label=\"Control $u(t)$\")\n", 316 | " axs[1].legend(loc=\"best\", fontsize=12, shadow=True)\n", 317 | " axs[1].grid()\n", 318 | " axs[1].set_xlabel(\"Time\", fontsize=12)\n", 319 | "\n", 320 | "plot_state_control(x, u)" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 14, 326 | "metadata": { 327 | "ExecuteTime": { 328 | "end_time": "2022-10-20T22:53:39.611008Z", 329 | "start_time": "2022-10-20T22:53:39.581041Z" 330 | } 331 | }, 332 | "outputs": [ 333 | { 334 | "ename": "NameError", 335 | "evalue": "name 'x' is not defined", 336 | "output_type": "error", 337 | "traceback": [ 338 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 339 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 340 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# control system simulation\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;31m# initial condition, state and control\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0marray\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mT\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 341 | "\u001b[0;31mNameError\u001b[0m: name 'x' is not defined" 342 | ] 343 | } 344 | ], 345 | "source": [ 346 | "# control system simulation\n", 347 | "# initial condition, state and control\n", 348 | "x[:, 0] = np.array([1, 0])\n", 349 | "u[:, 0] = 0\n", 350 | "for t in range(1, T):\n", 351 | " # forward the system for one step\n", 352 | " x[:, t] = x[:, t - 1] + dt * dyn(t * dt, x[:, t - 1], u[:, t - 1])\n", 353 | " # use new measurement to update online system identification\n", 354 | " olm.update(x[:, t - 1], u[:, t - 1], x[:, t])\n", 355 | " # apply control if we have collected enough data\n", 356 | " if t > 4 * max(n, n + k):\n", 357 | " # convert to continuous representation\n", 358 | " Ac = (olm.A - np.eye(n)) / dt\n", 359 | " Bc = olm.B / dt\n", 360 | " # get LQR gain, output = state\n", 361 | " sys = StateSpace(Ac, Bc, C, D, dt=0)\n", 362 | " K, S, E = lqr(sys, np.eye(n), np.eye(k))\n", 363 | " # apply control\n", 364 | " u[:, t] = -1.0 * K.dot(x[:, t])\n", 365 | " # clip control\n", 366 | " u[:, t] = np.clip(u[:, t], -10, 10)\n", 367 | " # smooth control\n", 368 | " w = 0.5\n", 369 | " u[:, t] = (1 - w) * u[:, t - 1] + w * u[:, t]\n", 370 | " # show progress\n", 371 | " if t % 100 == 0:\n", 372 | " print(f\"x(t)={x[:, t]}\")\n", 373 | " print(f\"u(t)={u[:, t]}\")\n", 374 | " print(f\"Ac={Ac}\")\n", 375 | " print(f\"Bc={Bc}\")\n", 376 | " print(f\"K={K}\")\n", 377 | " # random small perturbation if not enough data yet\n", 378 | " else:\n", 379 | " u[:, t] = 1e-1 * np.random.randn()\n", 380 | "\n", 381 | "plot_state_control(x, u)" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": null, 387 | "metadata": {}, 388 | "outputs": [], 389 | "source": [] 390 | } 391 | ], 392 | "metadata": { 393 | "kernelspec": { 394 | "display_name": "Python 3", 395 | "language": "python", 396 | "name": "python3" 397 | }, 398 | "language_info": { 399 | "codemirror_mode": { 400 | "name": "ipython", 401 | "version": 3 402 | }, 403 | "file_extension": ".py", 404 | "mimetype": "text/x-python", 405 | "name": "python", 406 | "nbconvert_exporter": "python", 407 | "pygments_lexer": "ipython3", 408 | "version": "3.8.5" 409 | }, 410 | "toc": { 411 | "base_numbering": 1, 412 | "nav_menu": {}, 413 | "number_sections": true, 414 | "sideBar": true, 415 | "skip_h1_title": false, 416 | "title_cell": "Table of Contents", 417 | "title_sidebar": "Contents", 418 | "toc_cell": false, 419 | "toc_position": {}, 420 | "toc_section_display": true, 421 | "toc_window_display": false 422 | }, 423 | "varInspector": { 424 | "cols": { 425 | "lenName": 16, 426 | "lenType": 16, 427 | "lenVar": 40 428 | }, 429 | "kernels_config": { 430 | "python": { 431 | "delete_cmd_postfix": "", 432 | "delete_cmd_prefix": "del ", 433 | "library": "var_list.py", 434 | "varRefreshCmd": "print(var_dic_list())" 435 | }, 436 | "r": { 437 | "delete_cmd_postfix": ") ", 438 | "delete_cmd_prefix": "rm(", 439 | "library": "var_list.r", 440 | "varRefreshCmd": "cat(var_dic_list()) " 441 | } 442 | }, 443 | "types_to_exclude": [ 444 | "module", 445 | "function", 446 | "builtin_function_or_method", 447 | "instance", 448 | "_Feature" 449 | ], 450 | "window_display": false 451 | } 452 | }, 453 | "nbformat": 4, 454 | "nbformat_minor": 4 455 | } 456 | -------------------------------------------------------------------------------- /demo/demo_lorenz.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"\"\"\n", 8 | "An example to demonstrate online control of lorenz system\n", 9 | "\n", 10 | "Authors: \n", 11 | "Hao Zhang\n", 12 | "\n", 13 | "References:\n", 14 | "Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta.\n", 15 | "\"Online dynamic mode decomposition for time-varying systems.\"\n", 16 | "SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609.\n", 17 | "\n", 18 | "Created:\n", 19 | "June 2017.\n", 20 | "\"\"\"" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "metadata": { 27 | "ExecuteTime": { 28 | "end_time": "2022-10-20T22:54:06.411809Z", 29 | "start_time": "2022-10-20T22:54:06.248173Z" 30 | } 31 | }, 32 | "outputs": [ 33 | { 34 | "name": "stdout", 35 | "output_type": "stream", 36 | "text": [ 37 | "/Users/haozhang/python-venvs/oml-reorg/bin/python: Mach-O 64-bit executable x86_64\r\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "!file $(which python)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": { 49 | "ExecuteTime": { 50 | "end_time": "2022-10-20T22:54:06.749849Z", 51 | "start_time": "2022-10-20T22:54:06.600938Z" 52 | } 53 | }, 54 | "outputs": [ 55 | { 56 | "name": "stdout", 57 | "output_type": "stream", 58 | "text": [ 59 | "Python 3.8.5\r\n" 60 | ] 61 | } 62 | ], 63 | "source": [ 64 | "!python --version" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 6, 70 | "metadata": { 71 | "ExecuteTime": { 72 | "end_time": "2022-10-20T22:54:29.821587Z", 73 | "start_time": "2022-10-20T22:54:28.647668Z" 74 | } 75 | }, 76 | "outputs": [], 77 | "source": [ 78 | "!python -m pip install -Uqqq pip setuptools wheel" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 7, 84 | "metadata": { 85 | "ExecuteTime": { 86 | "end_time": "2022-10-20T22:54:29.963607Z", 87 | "start_time": "2022-10-20T22:54:29.829432Z" 88 | } 89 | }, 90 | "outputs": [ 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "README.md demo_lorenz.ipynb\r\n", 96 | "demo_linear_time_varying_system.ipynb requirements.txt\r\n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "!ls" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 8, 107 | "metadata": { 108 | "ExecuteTime": { 109 | "end_time": "2022-10-20T22:54:30.103061Z", 110 | "start_time": "2022-10-20T22:54:29.971794Z" 111 | } 112 | }, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "ai4sci.oml\r\n", 119 | "numpy\r\n", 120 | "scipy\r\n", 121 | "matplotlib\r\n", 122 | "control\r\n", 123 | "slycot" 124 | ] 125 | } 126 | ], 127 | "source": [ 128 | "!cat requirements.txt" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 9, 134 | "metadata": { 135 | "ExecuteTime": { 136 | "end_time": "2022-10-20T22:54:31.074287Z", 137 | "start_time": "2022-10-20T22:54:30.329666Z" 138 | } 139 | }, 140 | "outputs": [ 141 | { 142 | "name": "stdout", 143 | "output_type": "stream", 144 | "text": [ 145 | "Requirement already satisfied: ai4sci.oml in /Users/haozhang/Local/open-source/oml (from -r requirements.txt (line 1)) (0.2.2)\n", 146 | "Requirement already satisfied: numpy in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 2)) (1.23.4)\n", 147 | "Requirement already satisfied: scipy in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 3)) (1.9.3)\n", 148 | "Requirement already satisfied: matplotlib in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 4)) (3.6.1)\n", 149 | "Requirement already satisfied: control in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 5)) (0.9.2)\n", 150 | "Requirement already satisfied: slycot in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from -r requirements.txt (line 6)) (0.5.0)\n", 151 | "Requirement already satisfied: fonttools>=4.22.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (4.37.4)\n", 152 | "Requirement already satisfied: cycler>=0.10 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (0.11.0)\n", 153 | "Requirement already satisfied: python-dateutil>=2.7 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (2.8.2)\n", 154 | "Requirement already satisfied: pyparsing>=2.2.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (3.0.9)\n", 155 | "Requirement already satisfied: contourpy>=1.0.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (1.0.5)\n", 156 | "Requirement already satisfied: packaging>=20.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (21.3)\n", 157 | "Requirement already satisfied: kiwisolver>=1.0.1 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (1.4.4)\n", 158 | "Requirement already satisfied: pillow>=6.2.0 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from matplotlib->-r requirements.txt (line 4)) (9.2.0)\n", 159 | "Requirement already satisfied: six>=1.5 in /Users/haozhang/python-venvs/oml-reorg/lib/python3.8/site-packages (from python-dateutil>=2.7->matplotlib->-r requirements.txt (line 4)) (1.16.0)\n" 160 | ] 161 | } 162 | ], 163 | "source": [ 164 | "!python -m pip install -r requirements.txt" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 10, 170 | "metadata": { 171 | "ExecuteTime": { 172 | "end_time": "2022-10-20T22:54:31.965529Z", 173 | "start_time": "2022-10-20T22:54:31.352735Z" 174 | } 175 | }, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "ai4sci.oml 0.2.2 /Users/haozhang/Local/open-source/oml\r\n" 182 | ] 183 | } 184 | ], 185 | "source": [ 186 | "!pip list | grep ai4sci.oml" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "Uncontrolled system simulation\n", 194 | "- classical butterfly trajectory" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 11, 200 | "metadata": { 201 | "ExecuteTime": { 202 | "end_time": "2022-10-20T22:54:38.652620Z", 203 | "start_time": "2022-10-20T22:54:38.373461Z" 204 | } 205 | }, 206 | "outputs": [ 207 | { 208 | "ename": "ModuleNotFoundError", 209 | "evalue": "No module named 'ai4sci'", 210 | "output_type": "error", 211 | "traceback": [ 212 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 213 | "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 214 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mai4sci\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moml\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mOnlineLinearModel\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 20\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mcontrol\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mlqr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mStateSpace\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mctrb\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 21\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 215 | "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'ai4sci'" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "\"\"\"\n", 221 | "An example to demonstrate online control of lorenz system\n", 222 | "\n", 223 | "Authors: \n", 224 | "Hao Zhang\n", 225 | "\n", 226 | "References:\n", 227 | "Zhang, Hao, Clarence W. Rowley, Eric A. Deem, and Louis N. Cattafesta.\n", 228 | "\"Online dynamic mode decomposition for time-varying systems.\"\n", 229 | "SIAM Journal on Applied Dynamical Systems 18, no. 3 (2019): 1586-1609.\n", 230 | "\n", 231 | "Created:\n", 232 | "June 2017.\n", 233 | "\"\"\"\n", 234 | "\n", 235 | "import random\n", 236 | "import matplotlib.pyplot as plt\n", 237 | "import numpy as np\n", 238 | "from ai4sci.oml import OnlineLinearModel\n", 239 | "from control import lqr, StateSpace, ctrb\n", 240 | "\n", 241 | "random.seed(20210220)\n", 242 | "np.random.seed(20210220)\n", 243 | "\n", 244 | "# define dynamics\n", 245 | "# https://en.wikipedia.org/wiki/Lorenz_system\n", 246 | "# Lorenz\n", 247 | "n = 3 # state dimension\n", 248 | "k = 3 # input dimension\n", 249 | "# output = state (full state observation)\n", 250 | "sigma = 10\n", 251 | "beta = 8.0 / 3.0\n", 252 | "rho = 28\n", 253 | "def dyn(x, u):\n", 254 | " assert x.shape == (n,)\n", 255 | " assert u.shape == (k,)\n", 256 | " f = np.zeros(n) # dx/dt = f(x, u)\n", 257 | " f[0] = sigma * (x[1] - x[0]) + u[0]\n", 258 | " f[1] = x[0] * (rho - x[2]) - x[1] + u[1]\n", 259 | " f[2] = x[0] * x[1] - beta * x[2] + u[2]\n", 260 | " return f\n", 261 | "\n", 262 | "# fixed point (unstable)\n", 263 | "phi = np.sqrt(beta * (rho - 1))\n", 264 | "xf1 = np.array([phi, phi, rho - 1])\n", 265 | "xf2 = np.array([-phi, -phi, rho - 1])\n", 266 | "xf3 = np.array([0, 0, 0])\n", 267 | "\n", 268 | "# set up simulation parameter\n", 269 | "dt = 0.01\n", 270 | "tmax, tc = 40, 0.4\n", 271 | "T, kc = int(tmax / dt), int(tc / dt)\n", 272 | "tspan = np.linspace(0, tmax, T + 1)\n", 273 | "\n", 274 | "# online linear system identification setup\n", 275 | "alpha = 0.01 ** (1.0 / kc) # 99% decay after kc samples\n", 276 | "olm = OnlineLinearModel(n, k, None, alpha)\n", 277 | "\n", 278 | "# store data mtrices\n", 279 | "x = np.zeros([n, T])\n", 280 | "u = np.zeros([k, T])\n", 281 | "\n", 282 | "# initial condition, state and control\n", 283 | "x0 = np.array([1, 1, 1])\n", 284 | "u0 = np.array([0, 0, 0])\n", 285 | "\n", 286 | "# initial condition\n", 287 | "x[:, 0] = x0\n", 288 | "u[:, 0] = u0\n", 289 | "\n", 290 | "# uncontrolled system simulation\n", 291 | "for t in range(1, T):\n", 292 | " # forward the system for one step\n", 293 | " x[:, t] = x[:, t - 1] + dt * dyn(x[:, t - 1], u[:, t - 1])\n", 294 | " u[:, t] = 0\n", 295 | "\n", 296 | "\n", 297 | "def plot_state(x):\n", 298 | " plt.rcParams['figure.dpi'] = 100\n", 299 | " fig = plt.figure(figsize=(6, 6))\n", 300 | " ax = fig.gca(projection=\"3d\")\n", 301 | " ax.plot(x[0, :], x[1, :], x[2, :])\n", 302 | " plt.title(\"State\")\n", 303 | " plt.draw()\n", 304 | " plt.grid()\n", 305 | " plt.show()\n", 306 | "\n", 307 | "\n", 308 | "def plot_control(u):\n", 309 | " plt.rcParams['figure.dpi'] = 100\n", 310 | " fig, axs = plt.subplots(3, figsize=(6, 6))\n", 311 | "\n", 312 | " axs[0].plot(tspan[1:], u[0, :], \"r-\", linewidth=1.0, label=\"$u_1(t)$\")\n", 313 | " axs[0].legend(loc=\"best\", fontsize=12, shadow=True)\n", 314 | " axs[0].grid()\n", 315 | " axs[0].set_title(\"Control\")\n", 316 | "\n", 317 | " axs[1].plot(tspan[1:], u[1, :], \"g-\", linewidth=1.0, label=\"$u_2(t)$\")\n", 318 | " axs[1].legend(loc=\"best\", fontsize=12, shadow=True)\n", 319 | " axs[1].grid()\n", 320 | "\n", 321 | " axs[2].plot(tspan[1:], u[2, :], \"b-\", linewidth=1.0, label=\"$u_3(t)$\")\n", 322 | " axs[2].legend(loc=\"best\", fontsize=12, shadow=True)\n", 323 | " axs[2].grid()\n", 324 | "\n", 325 | " axs[2].set_xlabel(\"Time\", fontsize=12)\n", 326 | "\n", 327 | "\n", 328 | "# plot state and control\n", 329 | "plot_state(x)\n", 330 | "plot_control(u)" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "metadata": {}, 336 | "source": [ 337 | "Controlled system simulation\n", 338 | "- Purely data-driven\n", 339 | "- Real-time model learning\n", 340 | "- Adaptive to new data\n", 341 | "- Closed loop control\n", 342 | "- Stabilizes system at unstable fixed point" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 12, 348 | "metadata": { 349 | "ExecuteTime": { 350 | "end_time": "2022-10-20T22:54:41.577450Z", 351 | "start_time": "2022-10-20T22:54:41.521448Z" 352 | } 353 | }, 354 | "outputs": [ 355 | { 356 | "ename": "NameError", 357 | "evalue": "name 'x0' is not defined", 358 | "output_type": "error", 359 | "traceback": [ 360 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 361 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 362 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;31m# initial condition, state and control\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mx0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 363 | "\u001b[0;31mNameError\u001b[0m: name 'x0' is not defined" 364 | ] 365 | } 366 | ], 367 | "source": [ 368 | "# controlled system simulation\n", 369 | "# we want to stabilize system at fixed point xf1\n", 370 | "# need to shift state by -xf1\n", 371 | "\n", 372 | "# initial condition, state and control\n", 373 | "x[:, 0] = x0\n", 374 | "u[:, 0] = u0\n", 375 | "\n", 376 | "# control system simulation\n", 377 | "for t in range(1, T):\n", 378 | " # forward the system for one step\n", 379 | " x[:, t] = x[:, t - 1] + dt * dyn(x[:, t - 1], u[:, t - 1])\n", 380 | " # use new measurement to update online system identification\n", 381 | " # try stabilize at xf1, shift state by -xf1\n", 382 | " olm.update(x[:, t - 1] - xf1, u[:, t - 1], x[:, t] - xf1)\n", 383 | " # apply control if we have collected enough data\n", 384 | " if t > 4 * max(n, n + k):\n", 385 | " # convert to continuous representation\n", 386 | " Ac = (olm.A - np.eye(n)) / dt\n", 387 | " Bc = olm.B / dt\n", 388 | " # get LQR gain, output = state\n", 389 | " sys = StateSpace(Ac, Bc, np.eye(n), np.zeros((n, k)), dt=0)\n", 390 | " K, S, E = lqr(sys, 2 * np.eye(n), np.eye(k))\n", 391 | " # apply control\n", 392 | " u[:, t] = -1.0 * K.dot(x[:, t] - xf1)\n", 393 | " # clip control\n", 394 | " u[:, t] = np.clip(u[:, t], -10, 10)\n", 395 | " # smooth control\n", 396 | " w = 0.5\n", 397 | " u[:, t] = (1 - w) * u[:, t - 1] + w * u[:, t]\n", 398 | " # show progress\n", 399 | " if t % 1000 == 0:\n", 400 | " print(f\"t={t * dt}\")\n", 401 | " print(f\"x(t)={x[:, t]}\")\n", 402 | " print(f\"xf1={xf1}\")\n", 403 | " print(f\"u(t)={u[:, t]}\")\n", 404 | " print(f\"Ac={Ac}\")\n", 405 | " print(f\"Bc={Bc}\")\n", 406 | " print(f\"K={K}\\n\")\n", 407 | " # random small perturbation if not enough data yet\n", 408 | " else:\n", 409 | " u[:, t] = 1e-1 * np.random.randn(k)\n", 410 | "\n", 411 | "plot_state(x)\n", 412 | "plot_control(u)\n", 413 | "assert np.linalg.norm(x[:, -1] - xf1) < 1e-1" 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": null, 419 | "metadata": {}, 420 | "outputs": [], 421 | "source": [] 422 | } 423 | ], 424 | "metadata": { 425 | "kernelspec": { 426 | "display_name": "Python 3", 427 | "language": "python", 428 | "name": "python3" 429 | }, 430 | "language_info": { 431 | "codemirror_mode": { 432 | "name": "ipython", 433 | "version": 3 434 | }, 435 | "file_extension": ".py", 436 | "mimetype": "text/x-python", 437 | "name": "python", 438 | "nbconvert_exporter": "python", 439 | "pygments_lexer": "ipython3", 440 | "version": "3.8.5" 441 | }, 442 | "toc": { 443 | "base_numbering": 1, 444 | "nav_menu": {}, 445 | "number_sections": true, 446 | "sideBar": true, 447 | "skip_h1_title": false, 448 | "title_cell": "Table of Contents", 449 | "title_sidebar": "Contents", 450 | "toc_cell": false, 451 | "toc_position": {}, 452 | "toc_section_display": true, 453 | "toc_window_display": false 454 | }, 455 | "varInspector": { 456 | "cols": { 457 | "lenName": 16, 458 | "lenType": 16, 459 | "lenVar": 40 460 | }, 461 | "kernels_config": { 462 | "python": { 463 | "delete_cmd_postfix": "", 464 | "delete_cmd_prefix": "del ", 465 | "library": "var_list.py", 466 | "varRefreshCmd": "print(var_dic_list())" 467 | }, 468 | "r": { 469 | "delete_cmd_postfix": ") ", 470 | "delete_cmd_prefix": "rm(", 471 | "library": "var_list.r", 472 | "varRefreshCmd": "cat(var_dic_list()) " 473 | } 474 | }, 475 | "types_to_exclude": [ 476 | "module", 477 | "function", 478 | "builtin_function_or_method", 479 | "instance", 480 | "_Feature" 481 | ], 482 | "window_display": false 483 | } 484 | }, 485 | "nbformat": 4, 486 | "nbformat_minor": 4 487 | } 488 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | ai4sci.oml 2 | numpy 3 | scipy 4 | matplotlib 5 | control 6 | slycot -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_namespace_packages 2 | 3 | # read readme as long description 4 | with open("README.md", "r") as f: 5 | long_description = f.read() 6 | 7 | # This call to setup() does all the work 8 | setup( 9 | name="ai4sci.oml", 10 | version="0.2.2", 11 | description="AI4Science: Efficient data-driven Online Model Learning (OML) / system identification and control", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/haozhg/oml", 15 | author="Hao Zhang", 16 | author_email="haozhang@alumni.princeton.edu", 17 | license="MIT", 18 | keywords=[ 19 | "machine-learning", 20 | "AI for Science", 21 | "data-driven modeling", 22 | "reduced-order-modeling", 23 | "dynamical systems", 24 | "control theory", 25 | "system identification", 26 | "online model learning", 27 | ], 28 | classifiers=[ 29 | "License :: OSI Approved :: MIT License", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.7", 32 | ], 33 | python_requires='>=3.7', 34 | packages=find_namespace_packages(include=["ai4sci.*"]), 35 | include_package_data=False, 36 | install_requires=["numpy"], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | ``` 3 | pip install ai4sci.oml 4 | pip install -r requirements.txt 5 | python -m pytest . 6 | ``` 7 | For demo of these algorithms on data-driven real-time closed loop control, see `/demo` directory. -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | ai4sci.oml 2 | pytest -------------------------------------------------------------------------------- /tests/test_online_linear_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from ai4sci.oml import OnlineLinearModel 5 | 6 | np.random.seed(20210218) 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def update(A, eps=1e-6): 12 | return A + eps * np.random.randn(*A.shape) 13 | 14 | 15 | def test_online_linear_model(): 16 | # n is state dimension 17 | for n in range(2, 10): 18 | k = n // 2 # control (input) dimension 19 | m = n // 2 # observation (output) dimension 20 | T = 16 * n # total number of measurements 21 | print(f"n={n}, k={k}, m={m}, T={T}") 22 | 23 | # true model, slowly varying in time 24 | A = np.random.randn(n, n) 25 | B = np.random.randn(n, k) 26 | C = np.random.randn(m, n) 27 | D = np.random.randn(m, k) 28 | 29 | # online linear model learning 30 | # no need to initialize 31 | olm = OnlineLinearModel(n, k, m, alpha=0.5) 32 | for t in range(T): 33 | # initial condition 34 | x = np.random.randn(n) 35 | u = np.random.randn(k) 36 | 37 | # state update 38 | xn = A.dot(x) + B.dot(u) 39 | y = C.dot(x) + D.dot(u) 40 | 41 | # update model est 42 | olm.update(x, u, xn, y) 43 | if olm.ready: 44 | assert np.linalg.norm(olm.A - A) / (n * n) < 1e-3 45 | assert np.linalg.norm(olm.B - B) / (n * k) < 1e-3 46 | assert np.linalg.norm(olm.C - C) / (m * n) < 1e-3 47 | assert np.linalg.norm(olm.D - D) / (m * k) < 1e-3 48 | 49 | # update time-varying model 50 | A = update(A) 51 | B = update(B) 52 | C = update(C) 53 | D = update(D) 54 | 55 | 56 | if __name__ == "__main__": 57 | test_online_linear_model() 58 | -------------------------------------------------------------------------------- /tests/test_online_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ai4sci.oml import OnlineModel 3 | 4 | np.random.seed(20210218) 5 | 6 | 7 | def update(A, eps=1e-6): 8 | return A + eps * np.random.randn(*A.shape) 9 | 10 | 11 | def test_online_model(): 12 | # m is y dimension 13 | for m in range(2, 10): 14 | n = 2 * m # x dimension 15 | T = 16 * m # total number of measurements 16 | print(f"m={m}, n={n}, T={T}") 17 | 18 | # true model, slowly varying in time 19 | A = np.random.randn(m, n) 20 | 21 | # online model learning 22 | # no need to initialize 23 | online_model = OnlineModel(n, m, alpha=0.5) 24 | for t in range(T): 25 | x = np.random.randn(n) 26 | y = A.dot(x) 27 | online_model.update(x, y) 28 | if online_model.ready: 29 | assert np.linalg.norm(online_model.M - A) / (m * n) < 1e-3 30 | 31 | # update time-varying model 32 | A = update(A) 33 | 34 | 35 | if __name__ == "__main__": 36 | test_online_model() 37 | --------------------------------------------------------------------------------