├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example1.png ├── example1.py ├── example2.png ├── example2.py ├── example3.png ├── example3.py ├── example4.png ├── example4.py ├── notebook_example_1.ipynb ├── notebook_example_2.ipynb ├── notebook_example_3.ipynb └── notebook_example_4.ipynb ├── setup.py ├── test ├── __init__.py ├── test_iddata.py ├── test_reference.py ├── test_utils.py └── test_vrft.py └── vrft ├── __init__.py ├── extended_tf.py ├── iddata.py ├── utils.py └── vrft_algo.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /dist/ 3 | /*.egg-info 4 | /build/ 5 | /.ipynb_checkpoints 6 | /examples/.ipynb_checkpoints -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 Alessio Russo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonVRFT Library - Virtual Reference Feedback Tuning - Version 0.0.6 2 | 3 | Virtual Reference Feedback Tuning (VRFT) Adaptive Control Library written in Python. Aim of this library is to provide an implementation of the VRFT (Virtual Reference Feedback Tuning) algorithm. 4 | 5 | You can find the package also at the following [link](https://pypi.org/project/pythonvrft/) 6 | 7 | _Author_: Alessio Russo (PhD Student at KTH - alessior@kth.se) 8 | 9 | _Contributors_: Alexander Berndt 10 | 11 | 12 | ![alt tag](https://github.com/rssalessio/PythonVRFT/blob/master/examples/example2.png) 13 | ## License 14 | 15 | Our code is released under the MIT license (refer to the [LICENSE](https://github.com/rssalessio/PythonVRFT/blob/master/LICENSE) file for details). 16 | 17 | ## Requirements 18 | 19 | To run the library you need atleast Python 3.5. 20 | 21 | Other dependencies: 22 | - NumPy (1.19.5) 23 | - SciPy (1.6.0) 24 | 25 | ## Installation 26 | 27 | - Install from source: git clone this repo and from the root folder execute the command ```pip install .``` 28 | 29 | ## Usage/Examples 30 | 31 | You can import the library by typing ```python import vrft``` in your code. 32 | 33 | To learn how to use the library, check the examples located in the examples/ folder. At the moment there are examples available. 34 | Check example3 to see usage of instrumental variables. 35 | 36 | In general the code has the following structure 37 | ```python 38 | from vrft import ExtendedTF # Discrete transfer function (inherits from the scipy.signal.dlti class) 39 | # Allows to sum/multiply/divide transfer functions and compute the feedback 40 | # loop 41 | from vrft import iddata # object used to store input/output data 42 | from vrft import compute_vrft # VRFT algorithm 43 | 44 | # Parameters 45 | dt = 0.1 # sampling time 46 | 47 | # Define a reference model 48 | ref_model = ExtendedTF([0.6], [1, -0.4], dt=dt) # Transfer function 0.6/(z-0.4) 49 | 50 | # Define pre-filter 51 | pre_filter = (1 - ref_model) * ref_model 52 | 53 | # Define control base (PI control) 54 | control = [ExtendedTF([1], [1, -1], dt=dt), # Transfer function 1/(z-1) 55 | ExtendedTF([1, 0], [1, -1], dt=dt)] # Transfer function z/(z-1) 56 | 57 | # Generate input/output data from a system 58 | u = .... # Generate input 59 | y = .... # measured output 60 | 61 | # Create an iddata object 62 | y0 = ... # initial conditions of the system (the length depends on the order of the reference model) 63 | data = iddata(y, u, dt, y0) 64 | 65 | # Compute VRFT 66 | # theta is the vector of parameters that parametrizes the control base 67 | # C is the final controller (computed as control.dot(theta)) 68 | theta, _, _, C = compute_vrft(data, ref_model, control, pre_filter) 69 | ``` 70 | 71 | ## Tests 72 | 73 | To execute tests run the following command from the root folder of the repo 74 | ```sh 75 | python -m unittest 76 | ``` 77 | 78 | ## Changelog 79 | 80 | - [**V. 0.0.2**][26.03.2017] Implement the basic VRFT algorithm (1 DOF. offline, linear controller, controller expressed as scalar product theta*f(z)) 81 | - [**V. 0.0.3**][05.01.2021] Code refactoring and conversion to Python 3; Removed support for Python Control library. 82 | - [**V. 0.0.5**][08.01.2021] Add Instrumental Variables (IVs) Support 83 | - [**In Progress**][07.01.2021-] Add Documentation and Latex formulas 84 | - [**TODO**] Add MIMO Support 85 | - [**TODO**] Generalize to other kind of controllers (e.g., neural nets) 86 | - [**TODO**] Add Cython support 87 | 88 | ## Citations 89 | 90 | If you find this code useful in your research, please, consider citing it: 91 | >@misc{pythonvrft, 92 | > author = {Alessio Russo}, 93 | > title = {Python VRFT Library}, 94 | > year = 2017, 95 | > doi = {}, 96 | > url = { https://github.com/rssalessio/PythonVRFT } 97 | >} 98 | 99 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 100 | 101 | -------------------------------------------------------------------------------- /examples/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssalessio/PythonVRFT/00a3f01d1f6a33198010d153379b5d754e6fe7b3/examples/example1.png -------------------------------------------------------------------------------- /examples/example1.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved. 2 | # This file is part of PythonVRFT. 3 | # PythonVRFT is free software: you can redistribute it and/or modify 4 | # it under the terms of the MIT License. You should have received a copy of 5 | # the MIT License along with PythonVRFT. 6 | # If not, see . 7 | # 8 | # Code author: [Alessio Russo - alessior@kth.se] 9 | # Last update: 10th January 2021, by alessior@kth.se 10 | # 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | 15 | from vrft import * 16 | 17 | # Example 1 18 | # ------------ 19 | # In this example we see how to apply VRFT to a simple SISO model 20 | # without any measurement noise. 21 | # Input data is generated using a square signal 22 | # 23 | 24 | #Generate time and u(t) signals 25 | t_start = 0 26 | t_end = 10 27 | t_step = 1e-2 28 | t = np.arange(t_start, t_end, t_step) 29 | u = np.ones(len(t)) 30 | u[200:400] = np.zeros(200) 31 | u[600:800] = np.zeros(200) 32 | 33 | #Experiment 34 | num = [0.5] 35 | den = [1, -0.9] 36 | sys = ExtendedTF(num, den, dt=t_step) 37 | t, y = scipysig.dlsim(sys, u, t) 38 | y = y.flatten() 39 | data = iddata(y, u, t_step, [0]) 40 | 41 | 42 | #Reference Model 43 | refModel = ExtendedTF([0.6], [1, -0.4], dt=t_step) 44 | 45 | #PI Controller 46 | base = [ExtendedTF([1], [1, -1], dt=t_step), 47 | ExtendedTF([1, 0], [1, -1], dt=t_step)] 48 | 49 | #Experiment filter 50 | pre_filter = refModel * (1 - refModel) 51 | 52 | #VRFT 53 | theta, r, loss, C = compute_vrft(data, refModel, base, pre_filter) 54 | 55 | #Obtained controller 56 | print("Controller: {}".format(C)) 57 | 58 | L = (C * sys).feedback() 59 | 60 | print("Theta: {}".format(theta)) 61 | print(scipysig.ZerosPolesGain(L)) 62 | 63 | #Analysis 64 | t = t[:len(r)] 65 | u = np.ones(len(t)) 66 | _, yr = scipysig.dlsim(refModel, u, t) 67 | _, yc = scipysig.dlsim(L, u, t) 68 | _, ys = scipysig.dlsim(sys, u, t) 69 | 70 | yr = np.array(yr).flatten() 71 | ys = np.array(ys).flatten() 72 | yc = np.array(yc).flatten() 73 | fig, ax = plt.subplots(4, sharex=True, figsize=(12,8), dpi= 100, facecolor='w', edgecolor='k') 74 | ax[0].plot(t, yr,label='Reference System') 75 | ax[0].plot(t, yc, label='CL System') 76 | ax[0].set_title('Systems response') 77 | ax[0].grid(True) 78 | ax[1].plot(t, ys, label='OL System') 79 | ax[1].set_title('OL Systems response') 80 | ax[1].grid(True) 81 | ax[2].plot(t, y[:len(r)]) 82 | ax[2].grid(True) 83 | ax[2].set_title('Experiment data') 84 | ax[3].plot(t, r) 85 | ax[3].grid(True) 86 | ax[3].set_title('Virtual Reference') 87 | 88 | # Now add the legend with some customizations. 89 | legend = ax[0].legend(loc='lower right', shadow=True) 90 | 91 | # The frame is matplotlib.patches.Rectangle instance surrounding the legend. 92 | frame = legend.get_frame() 93 | frame.set_facecolor('0.90') 94 | 95 | # Set the fontsize 96 | for label in legend.get_texts(): 97 | label.set_fontsize('large') 98 | 99 | for label in legend.get_lines(): 100 | label.set_linewidth(1.5) # the legend line width 101 | plt.show() 102 | -------------------------------------------------------------------------------- /examples/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssalessio/PythonVRFT/00a3f01d1f6a33198010d153379b5d754e6fe7b3/examples/example2.png -------------------------------------------------------------------------------- /examples/example2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved. 2 | # This file is part of PythonVRFT. 3 | # PythonVRFT is free software: you can redistribute it and/or modify 4 | # it under the terms of the MIT License. You should have received a copy of 5 | # the MIT License along with PythonVRFT. 6 | # If not, see . 7 | # 8 | # Code author: [Alessio Russo - alessior@kth.se] 9 | # Last update: 10th January 2021, by alessior@kth.se 10 | # 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import scipy.signal as scipysig 15 | from vrft import * 16 | 17 | # Example 2 18 | # ------------ 19 | # In this example we see how to apply VRFT to a simple SISO model 20 | # with colored measurement noise (no instrumental variables) 21 | # Input data is generated using random normal noise 22 | # 23 | 24 | def generateNoise(t): 25 | # Generate colored noise 26 | omega = 2*np.pi*100 27 | xi = 0.9 28 | dt = t[1] - t[0] 29 | noise = np.random.normal(0,0.1,t.size) 30 | tf = scipysig.TransferFunction([10*omega**2], [1, 2*xi*omega, omega**2]) 31 | # Second order system 32 | _, yn, _ = scipysig.lsim(tf, noise, t) 33 | return yn 34 | 35 | #Generate time and u(t) signals 36 | t_start = 0 37 | t_end = 10 38 | t_step = 1e-2 39 | t = np.arange(t_start, t_end, t_step) 40 | u = np.random.normal(size=t.size) 41 | 42 | #Experiment 43 | num = [0.5] 44 | den = [1, -0.9] 45 | sys = ExtendedTF(num, den, dt=t_step) 46 | t, y = scipysig.dlsim(sys, u, t) 47 | y = y.flatten() + generateNoise(t) 48 | data = iddata(y, u, t_step, [0]) 49 | 50 | 51 | #Reference Model 52 | refModel = ExtendedTF([0.6], [1, -0.4], dt=t_step) 53 | 54 | #PI Controller 55 | base = [ExtendedTF([1], [1, -1], dt=t_step), 56 | ExtendedTF([1, 0], [1, -1], dt=t_step)] 57 | 58 | #Experiment filter 59 | L = refModel * (1 - refModel) 60 | #VRFT 61 | theta, r, loss, C = compute_vrft(data, refModel, base, L) 62 | 63 | #Obtained controller 64 | print("Controller: {}".format(C)) 65 | 66 | L = (C * sys).feedback() 67 | 68 | print("Theta: {}".format(theta)) 69 | print(scipysig.ZerosPolesGain(L)) 70 | 71 | #Analysis 72 | t = t[:len(r)] 73 | u = np.ones(len(t)) 74 | _, yr = scipysig.dlsim(refModel, u, t) 75 | _, yc = scipysig.dlsim(L, u, t) 76 | _, ys = scipysig.dlsim(sys, u, t) 77 | 78 | yr = np.array(yr).flatten() 79 | ys = np.array(ys).flatten() 80 | yc = np.array(yc).flatten() 81 | fig, ax = plt.subplots(4, sharex=True, figsize=(12,8), dpi= 100, facecolor='w', edgecolor='k') 82 | ax[0].plot(t, yr,label='Reference System') 83 | ax[0].plot(t, yc, label='CL System') 84 | ax[0].set_title('Systems response') 85 | ax[0].grid(True) 86 | ax[1].plot(t, ys, label='OL System') 87 | ax[1].set_title('OL Systems response') 88 | ax[1].grid(True) 89 | ax[2].plot(t, y[:len(r)]) 90 | ax[2].grid(True) 91 | ax[2].set_title('Experiment data') 92 | ax[3].plot(t, r) 93 | ax[3].grid(True) 94 | ax[3].set_title('Virtual Reference') 95 | 96 | # Now add the legend with some customizations. 97 | legend = ax[0].legend(loc='lower right', shadow=True) 98 | 99 | # The frame is matplotlib.patches.Rectangle instance surrounding the legend. 100 | frame = legend.get_frame() 101 | frame.set_facecolor('0.90') 102 | 103 | # Set the fontsize 104 | for label in legend.get_texts(): 105 | label.set_fontsize('large') 106 | 107 | for label in legend.get_lines(): 108 | label.set_linewidth(1.5) # the legend line width 109 | plt.show() 110 | -------------------------------------------------------------------------------- /examples/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssalessio/PythonVRFT/00a3f01d1f6a33198010d153379b5d754e6fe7b3/examples/example3.png -------------------------------------------------------------------------------- /examples/example3.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved. 2 | # This file is part of PythonVRFT. 3 | # PythonVRFT is free software: you can redistribute it and/or modify 4 | # it under the terms of the MIT License. You should have received a copy of 5 | # the MIT License along with PythonVRFT. 6 | # If not, see . 7 | # 8 | # Code author: [Alessio Russo - alessior@kth.se] 9 | # Last update: 10th January 2021, by alessior@kth.se 10 | # 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import scipy.signal as scipysig 15 | from vrft import * 16 | 17 | # Example 3 18 | # ------------ 19 | # In this example we see how to apply VRFT to a simple SISO model 20 | # with measurement noise using instrumental variables 21 | # Input data is generated using random normal noise 22 | # 23 | 24 | #Generate time and u(t) signals 25 | t_start = 0 26 | t_end = 10 27 | dt = 1e-2 28 | t = np.array([i * dt for i in range(int(t_end/dt))]) 29 | 30 | #Experiment 31 | num = [0.5] 32 | den = [1, -0.9] 33 | sys = ExtendedTF(num, den, dt=dt) 34 | 35 | def generate_data(sys, u, t): 36 | t, y = scipysig.dlsim(sys, u, t) 37 | y = y.flatten() + 0.5 * np.random.normal(size = t.size) 38 | return iddata(y, u, dt, [0]) 39 | 40 | u = np.random.normal(size=t.size) 41 | data1 = generate_data(sys, u, t) 42 | data2 = generate_data(sys, u, t) 43 | data = [data1, data2] 44 | 45 | #Reference Model 46 | refModel = ExtendedTF([0.6], [1, -0.4], dt=dt) 47 | 48 | #PI Controller 49 | control = [ExtendedTF([1], [1, -1], dt=dt), 50 | ExtendedTF([1, 0], [1, -1], dt=dt)] 51 | 52 | #Experiment filter 53 | prefilter = refModel * (1 - refModel) 54 | 55 | # VRFT method with Instrumental variables 56 | theta_iv, r_iv, loss_iv, C_iv = compute_vrft(data, refModel, control, prefilter, iv=True) 57 | 58 | # VRFT method without Instrumental variables 59 | theta_noiv, r_noiv, loss_noiv, C_noiv = compute_vrft(data1, refModel, control, prefilter, iv=False) 60 | 61 | #Obtained controller 62 | print('------IV------') 63 | print("Loss: {}\nTheta: {}\nController: {}".format(loss_iv, theta_iv, C_iv)) 64 | print('------No IV------') 65 | print("Loss: {}\nTheta: {}\nController: {}".format(loss_noiv, theta_noiv, C_noiv)) 66 | 67 | 68 | # Closed loop system 69 | closed_loop_iv = (C_iv * sys).feedback() 70 | closed_loop_noiv = (C_noiv * sys).feedback() 71 | 72 | t = t[:len(r_iv)] 73 | u = np.ones(len(t)) 74 | 75 | _, yr = scipysig.dlsim(refModel, u, t) 76 | _, yc_iv = scipysig.dlsim(closed_loop_iv, u, t) 77 | _, yc_noiv = scipysig.dlsim(closed_loop_noiv, u, t) 78 | _, ys = scipysig.dlsim(sys, u, t) 79 | 80 | yr = yr.flatten() 81 | ys = ys.flatten() 82 | yc_noiv = yc_noiv.flatten() 83 | yc_iv = yc_iv.flatten() 84 | 85 | fig, ax = plt.subplots(4, sharex=True, figsize=(12,8), dpi= 100, facecolor='w', edgecolor='k') 86 | ax[0].plot(t, yr,label='Reference System') 87 | ax[0].plot(t, yc_iv, label='CL System - IV') 88 | ax[0].plot(t, yc_noiv, label='CL System - No IV') 89 | ax[0].set_title('CL Systems response') 90 | ax[0].grid(True) 91 | ax[1].plot(t, ys, label='OL System') 92 | ax[1].set_title('OL Systems response') 93 | ax[1].grid(True) 94 | ax[2].plot(t, data1.y[:len(r_iv)]) 95 | ax[2].grid(True) 96 | ax[2].set_title('Experiment data') 97 | ax[3].plot(t, r_iv) 98 | ax[3].grid(True) 99 | ax[3].set_title('Virtual Reference') 100 | 101 | # Now add the legend with some customizations. 102 | legend = ax[0].legend(loc='lower right', shadow=True) 103 | 104 | # The frame is matplotlib.patches.Rectangle instance surrounding the legend. 105 | frame = legend.get_frame() 106 | frame.set_facecolor('0.90') 107 | 108 | 109 | plt.show() 110 | -------------------------------------------------------------------------------- /examples/example4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssalessio/PythonVRFT/00a3f01d1f6a33198010d153379b5d754e6fe7b3/examples/example4.png -------------------------------------------------------------------------------- /examples/example4.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved. 2 | # This file is part of PythonVRFT. 3 | # PythonVRFT is free software: you can redistribute it and/or modify 4 | # it under the terms of the MIT License. You should have received a copy of 5 | # the MIT License along with PythonVRFT. 6 | # If not, see . 7 | # 8 | # Code author: [Alexander Berndt - alberndt@kth.se] 9 | # Last update: 10th January 2020, by alessior@kth.se 10 | # 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import scipy.signal as scipysig 15 | from vrft import * 16 | 17 | # Example 4 18 | # ------------ 19 | # In this example we see how to apply VRFT to a 20 | # more complex SISO model, specifically, the three-pulley 21 | # system analyzed in the original VRFT paper: 22 | # 23 | # "Virtual reference feedback tuning: 24 | # a direct method for the design offeedback controllers" 25 | # -- Campi et al. 2003 26 | # 27 | # As in Example 3, we consider the case of measurement 28 | # noise using instrumental variables. Input data is generated 29 | # using random normal noise 30 | 31 | dt = 0.05 32 | t_start = 0 33 | t_end = 10 34 | t = np.array([i * dt for i in range(int(t_end/dt))]) 35 | 36 | # Plant P(z) 37 | num_P = [0.28261, 0.50666] 38 | den_P = [1, -1.41833, 1.58939, -1.31608, 0.88642] 39 | sys = ExtendedTF(num_P, den_P, dt=dt) 40 | 41 | def generate_data(sys, u, t): 42 | t, y = scipysig.dlsim(sys, u, t) 43 | y = y.flatten() + 0.5 * np.random.normal(size = t.size) 44 | return iddata(y, u, dt, [0, 0, 0]) 45 | 46 | u = np.random.normal(size=t.size) 47 | data1 = generate_data(sys, u, t) 48 | data2 = generate_data(sys, u, t) 49 | data = [data1, data2] 50 | 51 | # Reference Model 52 | # z^-3 (1-alpha)^2 53 | # M(z) = --------------------- 54 | # (1 - alpha z^-1)^2 55 | # 56 | # with alpha = e^{-dt omega}, omega = 10 57 | # 58 | omega = 10 59 | alpha = np.exp(-dt*omega) 60 | num_M = [(1-alpha)**2] 61 | den_M = [1, -2*alpha, alpha**2, 0] 62 | refModel = ExtendedTF(num_M, den_M, dt=dt) 63 | 64 | # Controller C(z,O) where O is $\theta$ 65 | # 66 | # O_0 z^5 + O_1 z^4 + O_2 z^3 + O_3 z^2 + O_4 z^1 + O_5 67 | # C(z,O) = ------------------------------------------------------- 68 | # z^5 - z^4 69 | # 70 | control = [ExtendedTF([1, 0], [1, -1], dt=dt), 71 | ExtendedTF([1], [1, -1], dt=dt), 72 | ExtendedTF([1], [1, -1, 0], dt=dt), 73 | ExtendedTF([1], [1, -1, 0, 0], dt=dt), 74 | ExtendedTF([1], [1, -1, 0, 0, 0], dt=dt), 75 | ExtendedTF([1], [1, -1, 0, 0, 0, 0], dt=dt)] 76 | 77 | #Experiment filter 78 | # 79 | # L(z) = M(z) ( 1 - M(z) ) 80 | # 81 | prefilter = refModel * (1 - refModel) 82 | 83 | # VRFT method with Instrumental variables 84 | theta_iv, r_iv, loss_iv, C_iv = compute_vrft(data, refModel, control, prefilter, iv=True) 85 | 86 | # VRFT method without Instrumental variables 87 | theta_noiv, r_noiv, loss_noiv, C_noiv = compute_vrft(data1, refModel, control, prefilter, iv=False) 88 | 89 | # Obtained controller 90 | print('------IV------') 91 | print("Loss: {}\nTheta: {}\nController: {}".format(loss_iv, theta_iv, C_iv)) 92 | print('------No IV------') 93 | print("Loss: {}\nTheta: {}\nController: {}".format(loss_noiv, theta_noiv, C_noiv)) 94 | 95 | # Closed loop system 96 | closed_loop_iv = (C_iv * sys).feedback() 97 | closed_loop_noiv = (C_noiv * sys).feedback() 98 | 99 | t = t[:len(r_iv)] 100 | u = np.ones(len(t)) 101 | 102 | _, yr = scipysig.dlsim(refModel, u, t) 103 | _, yc_iv = scipysig.dlsim(closed_loop_iv, u, t) 104 | _, yc_noiv = scipysig.dlsim(closed_loop_noiv, u, t) 105 | _, ys = scipysig.dlsim(sys, u, t) 106 | 107 | yr = yr.flatten() 108 | ys = ys.flatten() 109 | yc_noiv = yc_noiv.flatten() 110 | yc_iv = yc_iv.flatten() 111 | 112 | fig, ax = plt.subplots(4, sharex=True, figsize=(12,8), dpi= 100, facecolor='w', edgecolor='k') 113 | ax[0].plot(t, yr,label='Reference System') 114 | ax[0].plot(t, yc_iv, label='CL System - IV') 115 | ax[0].plot(t, yc_noiv, label='CL System - No IV') 116 | ax[0].set_title('CL Systems response') 117 | ax[0].grid(True) 118 | ax[1].plot(t, ys, label='OL System') 119 | ax[1].set_title('OL Systems response') 120 | ax[1].grid(True) 121 | ax[2].plot(t, data1.y[:len(r_iv)]) 122 | ax[2].grid(True) 123 | ax[2].set_title('Experiment data') 124 | ax[3].plot(t, r_iv) 125 | ax[3].grid(True) 126 | ax[3].set_title('Virtual Reference') 127 | 128 | # Now add the legend with some customizations. 129 | legend = ax[0].legend(loc='lower right', shadow=True) 130 | 131 | # The frame is matplotlib.patches.Rectangle instance surrounding the legend. 132 | frame = legend.get_frame() 133 | frame.set_facecolor('0.90') 134 | 135 | plt.show() 136 | -------------------------------------------------------------------------------- /examples/notebook_example_1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## VRFT without measurement noise (no instrumental variables)" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 9, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "# Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved.\n", 17 | "# This file is part of PythonVRFT.\n", 18 | "# PythonVRFT is free software: you can redistribute it and/or modify\n", 19 | "# it under the terms of the MIT License. You should have received a copy of\n", 20 | "# the MIT License along with PythonVRFT.\n", 21 | "# If not, see .\n", 22 | "#\n", 23 | "# Code author: [Alessio Russo - alessior@kth.se]\n", 24 | "# Last update: 10th January 2021, by alessior@kth.se\n", 25 | "#\n", 26 | "\n", 27 | "# Example 1\n", 28 | "# ------------\n", 29 | "# In this example we see how to apply VRFT to a simple SISO model\n", 30 | "# without any measurement noise.\n", 31 | "# Input data is generated using a square signal\n", 32 | "#" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Load libraries" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 10, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "import numpy as np\n", 49 | "import matplotlib.pyplot as plt\n", 50 | "import scipy.signal as scipysig\n", 51 | "from vrft import *" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "### System, Reference Model and Control law" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 11, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "# System\n", 68 | "dt = 1e-2\n", 69 | "num = [0.5]\n", 70 | "den = [1, -0.9]\n", 71 | "sys = ExtendedTF(num, den, dt=dt)\n", 72 | "\n", 73 | "# Reference Model\n", 74 | "refModel = ExtendedTF([0.6], [1, -0.4], dt=dt)\n", 75 | "\n", 76 | "# Control law\n", 77 | "control = [ExtendedTF([1], [1, -1], dt=dt),\n", 78 | " ExtendedTF([1, 0], [1, -1], dt=dt)]\n" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "### Generate signals" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 12, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# Generate input siganl\n", 95 | "t_start = 0\n", 96 | "t_end = 10\n", 97 | "t = np.arange(t_start, t_end, dt)\n", 98 | "u = np.ones(len(t))\n", 99 | "u[200:400] = np.zeros(200)\n", 100 | "u[600:800] = np.zeros(200)\n", 101 | "\n", 102 | "# Open loop experiment\n", 103 | "t, y = scipysig.dlsim(sys, u, t)\n", 104 | "y = y.flatten()\n", 105 | "\n", 106 | "# Save data into an IDDATA Object with 0 initial condition\n", 107 | "# Length of the initial condition depends on the reference model\n", 108 | "data = iddata(y, u, dt, [0])" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "### VRFT" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 13, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "Loss: 4.4859502383167834e-30\n", 128 | "Theta: [-1.08 1.2 ]\n", 129 | "Controller: ExtendedTF(\n", 130 | "array([ 1.2 , -1.08]),\n", 131 | "array([ 1., -1.]),\n", 132 | "dt: 0.01\n", 133 | ")\n" 134 | ] 135 | } 136 | ], 137 | "source": [ 138 | "# VRFT Pre-filter\n", 139 | "prefilter = refModel * (1 - refModel)\n", 140 | "\n", 141 | "# VRFT method\n", 142 | "theta, r, loss, C = compute_vrft(data, refModel, control, prefilter)\n", 143 | "\n", 144 | "#Obtained controller\n", 145 | "print(\"Loss: {}\\nTheta: {}\\nController: {}\".format(loss, theta, C))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "### Verify performance" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 14, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "data": { 162 | "image/png": "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C3amowMAAACooUceeUQdOnRQSUmJNmzYoEcffVSfffaZPvroI8XGxtbqvQzD0HXXXad27dpp0aJFioiIUPv27astm5GRoREjRqhJkya69tpr1b59e+Xm5urnn3/Wm2++qZ07d6pVq1Z18Akre+yxxzR69GiNHDmyzt/bTCThJvJPRz+GxfwAAAAAGpcuXbropJNOkiQNGDBAHo9HDz74oFauXKlLL720Vu+VmZmp7OxsnX322Ro0aNBRyy5ZskQHDx7UihUr1Lp1a//xkSNH6qabbpLX6639h2nEmI5uIv9IuI1rIQAAAABqx5eQ79+/v9LxzZs3a/z48TrhhBOUmpqqYcOG6c033/Q//+CDD6p3796SpHvvvVdJSUnq27fvEc+TnZ0tu92u5s2bV/u83V6eVr7yyitKSkrSxo0bq5R5+OGH1bp1a2VmZkqSvv32W1155ZXq0aOH2rZtq169eumKK65QRkaGJCkpKUmFhYV66aWX/NPwL7jgAv/77du3T3//+9/Vu3dvtWnTRv369dNDDz2ksrIyf5n09HQlJSVp7ty5euKJJ9S3b1+1a9dOF1xwgX755Re53W7de++96tWrlzp37qxrrrlGWVlZR/6F1xGyPxPZVb4m3M5IOAAAAIBa2rlzpyRVmkb+6aef6rLLLlOvXr103333KSYmRq+//rquu+46FRUV6ZJLLtG4cePUrVs3TZgwQddcc43Gjh2r0NDQI56nT58+WrRokSZMmKBrr71WvXv3VnR0dJVy5557ru655x4tWrRIffr08R8vKyvT4sWLNXLkSLVs2VKFhYW69NJL1bp1a/3nP/9RixYttG/fPn322Wf+ddZvvfWWLrroIg0cOFBTpkyRJP859+3bp1GjRslut+uWW25RmzZttGnTJj366KNKT0/XnDlzKsW1aNEide3aVf/5z3+Um5urGTNmaPz48Tr55JMVEhKihx9+WLt27dLMmTN166236rnnnjum76OmSMJN5JuOLtaEAwAAAA3OMAwVl5kzlTosxF7rW6V5PB6VlZVVWhN+6qmnavjw4f4y06ZNU6dOnfTyyy8rJKQ83RsyZIgOHjyo++67TxdddJGSkpLk8ZQPCLZq1co/Kn4kY8eO1fr167VkyRJ9+OGHstls6tChg8444wxNmDBBKSkpksp3DL/88sv1xBNP6K677vKPnK9YsUKZmZm6+uqrJUnbtm1Tdna2HnroIZ199tn+85x77rn+v/fu3Vt2u13NmjWrEt9DDz2knJwcrVmzRsnJyZKkQYMGKSwsTDNnztSkSZPUqVMnf/mYmBgtXLjQP2J/8OBB3XnnnerQoYMWLVrkL7dt2zY9/fTTysvLq/YiQ10hCTeRwzcSbudrAAAAABpacZlXZ87dbMq5V086SeHO2g3GjR49utLjjh07auHChf5ke/v27dq2bZvuvPNOSao0NXvo0KF6//339csvv6hjx461Oq/NZtP999+vG2+8UR988IE2b96s9evX66mnntLzzz+vxYsXq3///pKk8ePH64knntCSJUt08803S5IWLlyorl276tRTT5UktW3bVrGxsbr33nu1b98+nXrqqZWS5j/y/vvva8CAAWrZsmWlz3jmmWdq5syZWrduXaX3Gzp0qD8Bl6QOHTpIks4666xK7+v7vezevVtdunSpza+oVsj+TGT33yecpfkAAAAAju6xxx5Tx44dlZ+frzfffFPPP/+8Jk2apCVLlkj6fW34zJkzNXPmzGrf4+DBg8d8/uTkZI0fP97/+M0339SkSZN09913a8WKFZKkFi1a6Nxzz9Xzzz+vG264QVu3btX69es1e/Zs/+tiYmK0fPlyPfroo7rvvvt06NAhJSQkaNy4cZoyZYqcTudR49i/f7/S0tIqbRJ3tM/4vzvH+6be/+9x33lLSkqOev7jRRJuIt/GbKwJBwAAABpeWIhdqyedZNq5a6tjx47+zdgGDhwoj8ejpUuX6u2339bo0aMVFxcnSbrxxhs1atSoat/jSLchOxbnnnuuHn/8cf3444+Vjk+cOFGvvPKK3nvvPa1Zs0ZNmjTR+eefX6lM165dNX/+fBmGoS1btuill17SI488orCwMN14441HPW9cXJy6du2qO+64o9rnExISju+D1TOScBP5d0d3HP1KDwAAAIC6Z7PZaj0l3Er+9a9/acWKFXrggQc0atQodejQQe3atdOWLVs0bdq0OjvP3r17q01sCwoKlJGRoZYtW1Y6fuKJJ6pPnz568skn9eOPP+ryyy9XREREte9ts9nUrVs3zZgxQy+99JK+++47/3Mul0tFRUVVXnPWWWdp9erVatOmTa3vj24FJOEm8o+EszEbAAAAgFqKjY3VDTfcoHvuuUevvfaaLrjgAt1///26/PLL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oKuvvtp/fMSIEbrhhhvk9XqP6/0bi8LCQkVERJgdBgAAlsBIOAAgoMyaNUs2m03z5s2rlIBLUkhIiObOnSubzab77rvvuM914MABSVJiYmK1z/vuA7pjxw6FhIRo1qxZVcp89NFHstlsevnllyVJ+/fv11//+lelpKTI5XKpRYsWGjhwoN5//31J0pAhQ/TOO+/ot99+k81m8//4lJaW6p577lGXLl38r7/66qu1f//+Sudt27atRo8erbffflu9evVSeHi4unbtqrfffltS+RT9rl27KjIyUn379tXGjRsrvf7XX3/VpZdeqqSkJLlcLiUkJGjo0KH6+uuvj/o7u+qqqxQVFaVvv/1Ww4cPV3R0tIYOHVqr2FevXq0hQ4aoWbNmCg8PV+vWrXXBBReosLDQ//u22WyaPXu27r33XrVu3VphYWHq06ePPvjggyoxffLJJxo6dKiio6MVERGhAQMG6J133qlUZtGiRbLZbFqzZo2uv/56NW/eXM2aNdP555+vjIyMWsVXm88KAGh8SMIBAAHD4/FozZo16tOnj5KTk6stk5KSot69e2v16tXyeDzHdb4+ffrI6XTq5ptv1pIlS7Rnz55qy7Vt21bnnnuu5s+fX+WcTzzxhJKSkjR27FhJ0hVXXKHXX39dd955p1atWqVnnnlGZ511lj/hnzt3rgYOHKiWLVtq3bp1/h9J8nq9GjNmjO677z6NGzdO77zzju677z6lpaVpyJAhKioqqnTuzZs3a9q0abr99tv16quvqkmTJjr//PM1ffp0PfPMM/rPf/6jJUuWKCcnR6NHj670+lGjRmnTpk2aPXu20tLSNG/ePPXq1UuHDh36w99baWmpzj33XJ155pl64403NGPGjBrHvmPHDp1zzjkKDQ3VggULtHLlSt13332KjIxUaWlpld/typUrNWfOHC1evFh2u10jR470/74k6cMPP9SZZ56pnJwcPfvss3rhhRcUHR2tP/3pT1q2bFmV2CdOnCin06mlS5dq9uzZWrt2rS6//HL/8zWJr7bfEwCgkTEAAAgQmZmZhiTj0ksvPWq5Sy65xJBk7N271zAMw1i4cKEhydiwYUOtz/nss88aUVFRhiRDkpGYmGhceeWVxkcffVSp3Jo1awxJxmuvveY/tnv3biMkJMSYMWOG/1hUVJQxZcqUo57znHPOMdq0aVPl+AsvvGBIMpYvX17p+IYNGwxJxty5c/3H2rRpY4SHhxu7du3yH/v666/9n6GgoMB//PXXXzckGW+++aZhGIaRlZVlSDLmzJlz1DirM378eEOSsWDBgmOK/ZVXXjEkGV9//fURz7F9+3ZDkpGUlGQUFRX5j+fm5hpxcXHGWWed5T926qmnGvHx8UZeXp7/WFlZmdG9e3cjOTnZ8Hq9hmH8XkcmTZpU6VyzZ882JBl79uypcXy1+Z4AAI0PI+EAgKBjVNz44/Bp3Mfqmmuu0a5du7R06VLddNNNSklJ0eLFizV48GA98MAD/nJDhgzRSSedpCeffNJ/bP78+bLZbPrrX//qP9a3b18tWrRI99xzjz7//HO53e4ax/L2228rNjZWf/rTn1RWVub/6dmzp1q2bKm1a9dWKt+zZ0+1atXK/7hr167+WA9fo+07/ttvv0mS4uLi1L59ez3wwAN6+OGH9dVXX9V6/fsFF1xwTLH37NlToaGh+utf/6rnnntOv/766xHPcf755yssLMz/2DfC/dFHH8nj8aigoEDr16/XhRdeqKioKH85h8OhK664Qrt27dLWrVsrvee5555b6fGJJ55Y6XdTk/hq+z0BABoXknAAQMBo3ry5IiIitH379qOW27FjhyIjIxUXF1cn523SpIn+/Oc/69FHH9X69ev1zTffKCEhQf/85z8rTc++6aab9MEHH2jr1q1yu916+umndeGFF6ply5b+MsuWLdP48eP1zDPPqH///oqLi9OVV16pzMzMP4xj7969OnTokEJDQ+V0Oiv9ZGZmKisrq1L5//38oaGhRz1eXFwsqfzixQcffKARI0Zo9uzZOvnkk9WiRQvddNNNysvL+8M4IyIiFBMTc0yxt2/fXu+//77i4+M1efJktW/fXu3bt9ejjz5a5TyH/14PP1ZaWqr8/HxlZ2fLMIxq1/QnJSVJ+n3dv0+zZs0qPfbdj9Y3hbwm8dX2ewIANC7sjg4ACBgOh0NnnHGGVq5cqV27dlW7LnzXrl3atGmTRo0aJYfDUS9xdOvWTZdeeqnmzJmjn376SX379pUkjRs3TrfffruefPJJnXrqqcrMzNTkyZMrvbZ58+aaM2eO5syZo507d+rNN9/UHXfcoX379mnlypVHPa9vs7AjlYuOjq6bDyipTZs2evbZZyVJP/30k1566SXdddddKi0t1fz584/62upmINQm9kGDBmnQoEHyeDzauHGjHn/8cU2ZMkUJCQm69NJL/eWqu3CRmZmp0NBQRUVFKSQkRHa7vdq1/L7N1po3b37Uz1KdP4qvIb8nAEDgYSQcABBQpk2bJsMwNGnSpCqboHk8Hl1//fUyDEN33HHHcZ/rwIEDVTYD8/nxxx8l/T6iKklhYWH+acoPP/ywevbsedTbpLVu3Vo33HCDhg0bpi+//NJ/3OVyVbt51+jRo3XgwAF5PB716dOnyk/nzp2P9aMeVadOnfSvf/1LPXr0qBRnbRxL7A6HQ/369fNP8f/fc7/66qv+0XtJysvL01tvvaVBgwbJ4XAoMjJS/fr106uvvlrp9+n1erV48WIlJyerU6dOx/R5jhafWd8TACAwMBIOAAgoAwcO1Jw5czRlyhSddtppuuGGG9S6dWvt3LlTTz75pNavX685c+ZowIABVV67evVq7dixo8rxUaNGVXsf6zVr1ujmm2/WZZddpgEDBqhZs2bat2+fXnjhBa1cuVJXXnllldH4SZMmafbs2dq0aZOeeeaZSs/l5OTojDPO0Lhx49SlSxdFR0drw4YNWrlypc4//3x/uR49eujVV1/VvHnz1Lt3b9ntdvXp00eXXnqplixZolGjRunmm29W37595XQ6tWvXLq1Zs0Zjxozx78J+PL755hvdcMMNuuiii9SxY0eFhoZq9erV+uabb4754kZNY58/f75Wr16tc845R61bt1ZxcbEWLFggSTrrrLMqvafD4dCwYcM0depUeb1e3X///crNzdWMGTP8ZWbNmqVhw4bpjDPO0G233abQ0FDNnTtX3333nV544YVa7xtQk/ga6nsCAAQoc/eFAwDg2Kxbt8648MILjYSEBCMkJMSIj483zj//fOOzzz6rUta38/WRfrZv317tOdLT041//etfxsCBA42WLVsaISEhRnR0tNGvXz/j8ccfN8rKyqp93ZAhQ4y4uDijsLCw0vHi4mLjuuuuM0488UQjJibGCA8PNzp37mxMnz690m7lBw8eNC688EIjNjbWsNlsxuH/XLvdbuPBBx80TjrpJCMsLMyIiooyunTpYlx77bXGzz//7C/Xpk0b45xzzqkSmyRj8uTJlY75dht/4IEHDMMwjL179xpXXXWV0aVLFyMyMtKIiooyTjzxROORRx454mf2GT9+vBEZGVntczWJfd26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TtUVV1yhPn36qH///nrqqae0c+dOXXfddWaHBgAAAADAMbNkEn7JJZfowIEDmjlzpvbs2aPu3btrxYoVatOmzR++1jAMSbWbk28Wt9utwsJC5ebmWnJKBUAdRSCgnsLqqKOwOuooAoHV66kv//Tlo0djM2pSKoDs2rVLKSkpZocBAAAAAGhk0tPTlZycfNQyQZeEe71eZWRkKDo6utrd1K3Et5N7enq6pXdyR+NFHUUgoJ7C6qijsDrqKAKB1eupYRjKy8tTUlKS7Pajb71myenox8Nut//hlQeriYmJsWRFAnyoowgE1FNYHXUUVkcdRSCwcj1t0qRJjcpZbnd0AAAAAACCFUk4AAAAAAANhCTcRC6XS9OnT5fL5TI7FKBa1FEEAuoprI46CqujjiIQBFM9DbqN2QAAqI1Fixbp6quvPuLza9as0ZAhQxouoFpau3atzjjjDMvHuWLFCn3xxRe66667jut9fJ9x7dq1tX7tf/7zH51wwgk677zzjisGAACOR9BtzAYAwLFYuHChunTpUuX4CSecYEI0NXfyySdr3bp1lo9zxYoVevLJJ487CT8e//nPf3ThhReShAMATEUSDgCApO7du6tPnz5mh1FjbrdbNptNMTExOvXUU80OBwAA1BBrwgEAqIEXX3xRNptNTzzxRKXj06dPl8PhUFpamiRpx44dstlsmj17tu699161bt1aYWFh6tOnjz744IMq7/vzzz9r3Lhxio+Pl8vlUteuXfXkk09WKrN27VrZbDY9//zzuvXWW9WqVSu5XC5t27bN/9zh07OvuuoqRUVF6ccff9SIESMUGRmpxMRE3XfffZKkzz//XKeddpoiIyPVqVMnPffcc1XiyszM1LXXXqvk5GSFhoYqNTVVM2bMUFlZmb+M77M++OCDevjhh5WamqqoqCj1799fn3/+eaV4fJ/JZrP5f3bs2HHE37dhGJo9e7batGmjsLAwnXzyyXr33XerlCsuLtatt96qnj17qkmTJoqLi1P//v31xhtvVCpns9lUUFCg5557zn9+39T2/fv3a9KkSTrhhBMUFRWl+Ph4nXnmmfr444+PGB8AAMeKkXAAACR5PJ5KCaZUnrg5HA5J0qWXXqoPP/xQt956q0499VT16dNHq1ev1j333KN//OMfGjZsWKXXPvHEE2rTpo3mzJkjr9er2bNna+TIkfrwww/Vv39/SdKWLVs0YMAAtW7dWg899JBatmyp9957TzfddJOysrI0ffr0Su85bdo09e/fX/Pnz5fdbld8fLwyMzOr/Txut1vnn3++rrvuOv3tb3/T0qVLNW3aNOXm5mr58uW6/fbblZycrMcff1xXXXWVunfvrt69e0sqT8D79u0ru92uO++8U+3bt9e6det0zz33aMeOHVq4cGGlcz355JPq0qWL5syZI0n697//rVGjRmn79u1q0qSJ/v3vf6ugoECvvPKK1q1b539dYmLiEb+PGTNmaMaMGZowYYIuvPBCpaen6y9/+Ys8Ho86d+7sL1dSUqKDBw/qtttuU6tWrVRaWqr3339f559/vhYuXKgrr7xSkrRu3TqdeeaZOuOMM/Tvf/9bkvz3mT148KCk8gsqLVu2VH5+vl577TUNGTJEH3zwgaXX2gMAApABAEAjtnDhQkNStT8Oh6NS2eLiYqNXr15GamqqsWXLFiMhIcEYPHiwUVZW5i+zfft2Q5KRlJRkFBUV+Y/n5uYacXFxxllnneU/NmLECCM5OdnIycmpdJ4bbrjBCAsLMw4ePGgYhmGsWbPGkGScfvrpVeL3PbdmzRr/sfHjxxuSjOXLl/uPud1uo0WLFoYk48svv/QfP3DggOFwOIypU6f6j1177bVGVFSU8dtvv1U614MPPmhIMr7//vtKn7VHjx6VfgdffPGFIcl44YUX/McmT55s1LTbkZ2dbYSFhRljx46tdPzTTz81JBmDBw8+4mvLysoMt9ttTJgwwejVq1el5yIjI43x48f/4fl97zF06NAqMQAAcLyYjg4AgKT//ve/2rBhQ6Wf9evXVyrjcrn00ksv6cCBAzr55JNlGIZeeOEF/2j54c4//3yFhYX5H0dHR+tPf/qTPvroI3k8HhUXF+uDDz7Q2LFjFRERobKyMv/PqFGjVFxcXGlKtyRdcMEFNf48NptNo0aN8j8OCQlRhw4dlJiYqF69evmPx8XFKT4+Xr/99pv/2Ntvv60zzjhDSUlJleIaOXKkJOnDDz+sdK5zzjmn0u/gxBNPlKRK71kb69atU3FxsS677LJKxwcMGKA2bdpUKf/yyy9r4MCBioqKUkhIiJxOp5599ln98MMPNT7n/PnzdfLJJyssLMz/Hh988EGt3gMAgJogCQcAQFLXrl3Vp0+fSj++6dmH69ChgwYNGuRPEo80pbply5bVHistLVV+fr4OHDigsrIyPf7443I6nZV+fMlzVlZWpdcfbfr2/4qIiKh0EUCSQkNDFRcXV6VsaGioiouL/Y/37t2rt956q0pc3bp1qzauZs2aVXrsu4drUVFRjeM93IEDByQd+Xd4uFdffVUXX3yxWrVqpcWLF2vdunXasGGDrrnmmkqf6WgefvhhXX/99erXr5+WL1+uzz//XBs2bNDZZ599zJ8BAIAjYU04AAC18Mwzz+idd95R37599cQTT+iSSy5Rv379qpSrbq12ZmamQkNDFRUVJafTKYfDoSuuuEKTJ0+u9lypqamVHttstrr5EH+gefPmOvHEE3XvvfdW+3xSUlK9nt+X1B/pd9i2bVv/48WLFys1NVXLli2r9PspKSmp8fkWL16sIUOGaN68eZWO5+Xl1TJyAAD+GEk4AAA19O233+qmm27SlVdeqaeffloDBgzQJZdcoq+++kpNmzatVPbVV1/VAw884B+NzsvL01tvvaVBgwbJ4XAoIiJCZ5xxhr766iudeOKJCg0NNeMjVWv06NFasWKF2rdvX+VzHavDR8fDw8OPWvbUU09VWFiYlixZUmkK/meffabffvutUhJus9kUGhpaKQHPzMyssju6L4bqRrZtNps/Pp9vvvlG69atU0pKSo0+HwAANUUSDgCApO+++67K7uiS1L59e7Vo0UIFBQW6+OKLlZqaqrlz5yo0NFQvvfSSTj75ZF199dV6/fXXK73O4XBo2LBhmjp1qrxer+6//37l5uZqxowZ/jKPPvqoTjvtNA0aNEjXX3+92rZtq7y8PG3btk1vvfWWVq9eXd8fu1ozZ85UWlqaBgwYoJtuukmdO3dWcXGxduzYoRUrVmj+/PlKTk6u1Xv26NFDknT//fdr5MiRcjgcR7z40LRpU91222265557NHHiRF100UVKT0/XXXfdVWU6+ujRo/Xqq69q0qRJ/l3U7777biUmJurnn3+uEsPatWv11ltvKTExUdHR0ercubNGjx6tu+++W9OnT9fgwYO1detWzZw5U6mpqdXWCQAAjgdJOAAAkq6++upqjz/99NOaOHGirrvuOu3cuVMbNmxQZGSkJKldu3Z65plndNFFF2nOnDmaMmWK/3U33HCDiouLddNNN2nfvn3q1q2b3nnnHQ0cONBf5oQTTtCXX36pu+++W//617+0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Z88excbGVlvmsssu00svvaRdu3bp3Xff1dVXX63t27erbdu2f/j+c+fOVUREhK666qo6jbs6ixYtqlFsvnI+DodDLVq00ODBg3X33XerY8eOx3T+Dz74QLfffrt++OEHFRYW6rXXXtN55513TO8FAEBjwEg4AKDRmTBhgoqLi7V06dJqn8/JydFrr72m0aNHKyEhQeecc47WrVunxMTEGr3/3LlztWjRojqMuO4sXLhQ69at0/vvv68bbrhBb775pk477TRlZ2fX+r0Mw9DFF18sp9OpN998U+vWrdPgwYPrIWoAAIJH0O2ODgDAHxk5cqSSkpK0YMECTZo0qcrzL7zwgoqKijRhwgRJUosWLdSiRYs/fN/CwkJFRETUebx1qXv37urTp48kaciQIfJ4PJo+fbpef/31SiPlNZGRkaGDBw9q7NixGjp0aJ3E53a7ZbPZFBJCFwUAEJwYCQcANDoOh0Pjx4/Xpk2b9O2331Z5fuHChUpMTNTIkSMllU/lttls2rFjh7/MkCFD1L17d3300UcaMGCAIiIidM0116ht27b6/vvv9eGHH8pms8lms/mniVf3PpK0du1a2Ww2rV271n8sLS1NY8aMUXJyssLCwtShQwdde+21ysrKqtPfhS8h37t3b6XjGzdu1Lnnnqu4uDiFhYWpV69eeumll/zP33XXXf5bgt5+++2VPqck/fzzzxo3bpzi4+PlcrnUtWtXPfnkk9V+7ueff1633nqrWrVqJZfLpW3btkmS3n//fQ0dOlQxMTGKiIjQwIED9cEHH1R6j7vuuks2m03ff/+9/vznP6tJkyZKSEjQNddco5ycnEplvV6vHn/8cfXs2VPh4eGKjY3VqaeeqjfffLNSuWXLlql///6KjIxUVFSURowYoa+++uoYfrsAAFRFEg4AaJSuueYa2Ww2LViwoNLxLVu26IsvvtD48ePlcDiO+h579uzR5ZdfrnHjxmnFihWaNGmSXnvtNbVr1069evXSunXrtG7dOr322mu1ju+XX35R//79NW/ePK1atUp33nmn1q9fr9NOO01ut7vW73ck27dvlyR16tTJf2zNmjUaOHCgDh06pPnz5+uNN95Qz549dckll/in2U+cOFGvvvqqJOnGG2+s9Dm3bNmiU045Rd99950eeughvf322zrnnHN00003acaMGVVimDZtmnbu3Kn58+frrbfeUnx8vBYvXqzhw4crJiZGzz33nF566SXFxcVpxIgRVRJxSbrg/9u78/io6nv/46/ZspIEQiALhADKvggmKIiAaxDqvpZWiq3l1lKtyLVWa2urv1au2lputaL0WrnKtaXVWqSiEhdABQoii+woS4AkBAhkJcks5/fHZCYJyYQEMnNmkvfz8eBB5mTO5AMeP3w/3/WWWxg4cCBvvvkmDz/8MK+//joPPPBAo/fcdddd3H///YwZM4bFixfz17/+leuvv75Rp8iTTz7JtGnTGDp0KH/729947bXXKC8vZ8KECWzfvv2c/q5FREQAMERERDqpSZMmGSkpKUZtba3/2n/+538agLF7927/tVdeecUAjH379jW6FzA+/PDDJp87bNgwY9KkSU2uN/c5hmEYH3/8sQEYH3/8cbNxejwew+l0GgcOHDAAY8mSJWf8zEA/e+3atYbT6TTKy8uN9957z0hLSzMmTpxoOJ1O/3sHDx5sjB49utE1wzCMa6+91khPTzfcbrdhGIaxb98+AzCeeeaZRu+bPHmy0bt3b6O0tLTR9XvvvdeIiYkxSkpKGv25J06c2Oh9lZWVRnJysnHdddc1uu52u40LLrjAuOiii/zXfvnLXxqA8fTTTzd676xZs4yYmBjD4/EYhmEYq1atMgDj0UcfDfh3lJ+fb9jtduO+++5rdL28vNxIS0szbr/99oD3ioiItJZGwkVEpNO6++67OXbsmH86ssvlYtGiRUyYMKFVu4V369aNK664IiixFRcXc88995CZmYndbsfhcJCVlQXAjh07zvpzx44di8PhICEhgWuuuYZu3bqxZMkS/xrsr776ip07d/Ltb38b8P6d+H5NnTqVwsJCdu3aFfDzq6ur+fDDD7npppuIi4trcn91dTVr165tdM8tt9zS6PXq1aspKSlhxowZje73eDxcc801rF+/nsrKykb3XH/99Y1ejxw5kurqaoqLiwF49913AfjRj34UMPb3338fl8vFd77znUY/NyYmhkmTJjVaLiAiInK2tOuJiIh0Wrfeeiv33Xcfr7zyCrfccgvLli3jyJEjPPXUU626v7W7pbeVx+MhNzeXgoICfvGLXzBixAji4+PxeDyMHTuWU6dOnfVnv/rqqwwZMoTy8nIWL17MSy+9xLRp0/xFqm9t+IMPPsiDDz7Y7Ge0tC79+PHjuFwunnvuOZ577rlW3X/636MvhltvvTXgzykpKSE+Pt7/unv37o2+7ztH1vd3dfToUWw2G2lpaQE/0/dzx4wZ0+z3rVaNXYiIyLlTES4iIp1WbGws06ZN409/+hOFhYX8+c9/JiEhgdtuu61V91ssljb9vJiYGABqamoaXT+9KN26dSubN29m4cKFzJgxw3/dt2HZuRgyZIh/M7bLL78ct9vN//zP//DGG29w6623kpKSAnjXad98883NfsagQYMCfn63bt2w2WxMnz494Khzv379Gr0+/e/RF8Nzzz3H2LFjm/2M1NTUgDE0p0ePHrjdboqKigJ2nvh+7htvvOGfdSAiItLeVISLiEindvfdd/Piiy/yzDPPsGzZMu66665zPmYsOjq62dFq3+7hW7ZsaVTInr47t68o9Y3m+rz00kvnFFdznn76ad58800ee+wxbr75ZgYNGsSAAQPYvHkzTz75ZJs/Ly4ujssvv5yNGzcycuRIoqKi2vwZ48ePp2vXrmzfvp177723zfc3Z8qUKcydO5f58+fzxBNPNPueyZMnY7fb+frrr5tMkRcREWkvKsJFRKRTy8nJYeTIkcybNw/DMPxng5+LESNG8Ne//pXFixfTv39/YmJiGDFiBGPGjGHQoEE8+OCDuFwuunXrxltvvcWnn37a6P7Bgwdz3nnn8fDDD2MYBsnJySxdupS8vLxzju103bp145FHHuGhhx7i9ddf58477+Sll15iypQpTJ48mbvuuotevXpRUlLCjh07+OKLL/j73//e4mf+93//N5deeikTJkzghz/8IX379qW8vJyvvvqKpUuX8tFHH7V4f5cuXXjuueeYMWMGJSUl3HrrrfTs2ZOjR4+yefNmjh49yvz589v055wwYQLTp0/n17/+NUeOHOHaa68lOjqajRs3EhcXx3333Uffvn154oknePTRR9m7d69/zfyRI0dYt24d8fHxze7uLiIi0hZa3CQiIp3e3XffjWEYDB06lIsvvvicP+/xxx9n0qRJzJw5k4suuojrrrsO8J5PvnTpUgYPHsw999zDd77zHaKjo3n++ecb3e9wOFi6dCkDBw7kBz/4AdOmTaO4uJgPPvjgnGNrzn333UefPn144okncLvdXH755axbt46uXbsye/ZsrrrqKn74wx/ywQcfcNVVV53x84YOHcoXX3zB8OHD+fnPf05u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TjQ7HBG/iooKLrzwQl544QV+/etfM2rUKObNm2d2WCIAPPzww3z22Wea7SZh69prryU1NZWXX37Zf+2WW24hLi6O1157zcTIRLwsFgtvvfUWN954I+AdBc/IyGD27Nn89Kc/BaCmpobU1FSeeuopfvCDH5gYbetpJDzEamtr2bBhA7m5uY2u5+bmsnr1apOiEmlZaWkpAMnJySZHItLYj370I77xjW9w1VVXmR2KSBNvv/02OTk53HbbbfTs2ZPRo0fzpz/9yeywRPwuvfRSPvzwQ3bv3g3A5s2b+fTTT5k6darJkYk0b9++fRQVFTWqpaKjo5k0aVJE1VJ2swPobI4dO4bb7SY1NbXR9dTUVIqKikyKSiQwwzCYM2cOl156KcOHDzc7HBG/v/71r2zYsIHPP//c7FBEmrV3717mz5/PnDlz+NnPfsa6dev48Y9/THR0NN/5znfMDk+En/70p5SWljJ48GBsNhtut5vf/OY3TJs2zezQRJrlq5eaq6UOHDhgRkhnRUW4SSwWS6PXhmE0uSYSDu699162bNnCp59+anYoIn4HDx7k/vvvZ/ny5cTExJgdjkizPB4POTk5PPnkkwCMHj2abdu2MX/+fBXhEhYWL17MokWLeP311xk2bBibNm1i9uzZZGRkMGPGDLPDEwko0mspFeEhlpKSgs1mazLqXVxc3KRHR8Rs9913H2+//TarVq2id+/eZocj4rdhwwaKi4vJzs72X3O73axatYrnn3+empoabDabiRGKQHp6OkOHDm10bciQIbz55psmRSTS2E9+8hMefvhhvvnNbwIwYsQIDhw4wNy5c1WES1hKS0sDvCPi6enp/uuRVktpTXiIRUVFkZ2dTV5eXqPreXl5XHLJJSZFJdKYYRjce++9/OMf/+Cjjz6iX79+Zock0siVV17Jl19+yaZNm/y/cnJy+Pa3v82mTZtUgEtYGD9+fJPjHXfv3k1WVpZJEYk0VlVVhdXauByw2Ww6okzCVr9+/UhLS2tUS9XW1rJy5cqIqqU0Em6COXPmMH36dHJychg3bhwLFiwgPz+fe+65x+zQRADvZlevv/46S5YsISEhwT9zIykpidjYWJOjE4GEhIQmexTEx8fTvXt37V0gYeOBBx7gkksu4cknn+T2229n3bp1LFiwgAULFpgdmggA1113Hb/5zW/o06cPw4YNY+PGjTz77LN873vfMzs06cQqKir46quv/K/37dvHpk2bSE5Opk+fPsyePZsnn3ySAQMGMGDAAJ588kni4uL41re+ZWLUbaMjykzywgsv8PTTT1NYWMjw4cP5/e9/r6OfJGwEWlPzyiuvcNddd4U2GJFWuuyyy3REmYSdf/3rXzzyyCPs2bOHfv36MWfOHGbOnGl2WCIAlJeX84tf/IK33nqL4uJiMjIymDZtGo899hhRUVFmhyed1IoVK7j88subXJ8xYwYLFy7EMAwef/xxXnrpJU6cOMHFF1/MH//4x4jqhFcRLiIiIiIiIhIiWhMuIiIiIiIiEiIqwkVERERERERCREW4iIiIiIiISIioCBcREREREREJERXhIiIiIiIiIiGiIlxEREREREQkRFSEi4iIiIiIiISIinARERERERGREFERLiIiIiIiIhIiKsJFREREREREQkRFuIiIiIiIiEiIqAgXERERERERCZH/D0u7GxmJcuMZAAAAAElFTkSuQmCC\n", 163 | "text/plain": [ 164 | "
" 165 | ] 166 | }, 167 | "metadata": {}, 168 | "output_type": "display_data" 169 | } 170 | ], 171 | "source": [ 172 | "# Closed loop system\n", 173 | "closed_loop = (C * sys).feedback()\n", 174 | "\n", 175 | "t = t[:len(r)]\n", 176 | "u = np.ones(len(t))\n", 177 | "\n", 178 | "_, yr = scipysig.dlsim(refModel, u, t)\n", 179 | "_, yc = scipysig.dlsim(closed_loop, u, t)\n", 180 | "_, ys = scipysig.dlsim(sys, u, t)\n", 181 | "\n", 182 | "yr = np.array(yr).flatten()\n", 183 | "ys = np.array(ys).flatten()\n", 184 | "yc = np.array(yc).flatten()\n", 185 | "fig, ax = plt.subplots(4, sharex=True, figsize=(12,8), dpi= 100, facecolor='w', edgecolor='k')\n", 186 | "ax[0].plot(t, yr,label='Reference System')\n", 187 | "ax[0].plot(t, yc, label='CL System')\n", 188 | "ax[0].set_title('Systems response')\n", 189 | "ax[0].grid(True)\n", 190 | "ax[1].plot(t, ys, label='OL System')\n", 191 | "ax[1].set_title('OL Systems response')\n", 192 | "ax[1].grid(True)\n", 193 | "ax[2].plot(t, y[:len(r)])\n", 194 | "ax[2].grid(True)\n", 195 | "ax[2].set_title('Experiment data')\n", 196 | "ax[3].plot(t, r)\n", 197 | "ax[3].grid(True)\n", 198 | "ax[3].set_title('Virtual Reference')\n", 199 | "\n", 200 | "# Now add the legend with some customizations.\n", 201 | "legend = ax[0].legend(loc='lower right', shadow=True)\n", 202 | "\n", 203 | "# The frame is matplotlib.patches.Rectangle instance surrounding the legend.\n", 204 | "frame = legend.get_frame()\n", 205 | "frame.set_facecolor('0.90')\n", 206 | "\n", 207 | "# Set the fontsize\n", 208 | "for label in legend.get_texts():\n", 209 | " label.set_fontsize('large')\n", 210 | "\n", 211 | "for label in legend.get_lines():\n", 212 | " label.set_linewidth(1.5) # the legend line width\n", 213 | "plt.show()\n" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": null, 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [] 222 | } 223 | ], 224 | "metadata": { 225 | "kernelspec": { 226 | "display_name": "Python 3", 227 | "language": "python", 228 | "name": "python3" 229 | }, 230 | "language_info": { 231 | "codemirror_mode": { 232 | "name": "ipython", 233 | "version": 3 234 | }, 235 | "file_extension": ".py", 236 | "mimetype": "text/x-python", 237 | "name": "python", 238 | "nbconvert_exporter": "python", 239 | "pygments_lexer": "ipython3", 240 | "version": "3.9.1" 241 | } 242 | }, 243 | "nbformat": 4, 244 | "nbformat_minor": 4 245 | } 246 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | this_directory = path.abspath(path.dirname(__file__)) 4 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 5 | long_description = f.read() 6 | 7 | 8 | setup(name = 'pythonvrft', 9 | version = '0.0.6', 10 | description = 'VRFT Python Library', 11 | long_description = long_description, 12 | long_description_content_type = 'text/markdown', 13 | keywords = ['VRFT', 'Virtual Reference Feedback Tuning', 14 | 'Data Driven Control', 'Adaptive Control'], 15 | url = 'https://github.com/rssalessio/PythonVRFT/', 16 | author = 'Alessio Russo', 17 | author_email = 'alessior@kth.se', 18 | license='GPL3', 19 | packages=['vrft', 'test'], 20 | zip_safe=False, 21 | install_requires = [ 22 | 'scipy', 23 | 'numpy', 24 | ], 25 | test_suite = 'nose.collector', 26 | test_requires = ['nose'], 27 | classifiers=[ 28 | "Programming Language :: Python :: 3", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | "Development Status :: 3 - Alpha", 32 | "Topic :: Scientific/Engineering" 33 | ], 34 | python_requires='>=3.5', 35 | ) 36 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssalessio/PythonVRFT/00a3f01d1f6a33198010d153379b5d754e6fe7b3/test/__init__.py -------------------------------------------------------------------------------- /test/test_iddata.py: -------------------------------------------------------------------------------- 1 | # test_iddata.py - Unittest for the iddata object 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | import numpy as np 15 | import scipy.signal as scipysig 16 | from unittest import TestCase 17 | from vrft.iddata import iddata 18 | from vrft.extended_tf import ExtendedTF 19 | 20 | 21 | class TestIDData(TestCase): 22 | def test_type(self): 23 | a = iddata(0.0, 0.0, 0.0, [0]) 24 | with self.assertRaises(ValueError): 25 | a.check() 26 | 27 | a = iddata(0.0, [1], 0.0, [0]) 28 | with self.assertRaises(ValueError): 29 | a.check() 30 | 31 | a = iddata(np.zeros(10), 1, 0.0, [0]) 32 | with self.assertRaises(ValueError): 33 | a.check() 34 | 35 | a = iddata([0 for i in range(10)], [0 for i in range(10)], 1.0, [0]) 36 | self.assertTrue(a.check()) 37 | 38 | a = iddata(np.zeros(10), np.zeros(10), 1.0, [0]) 39 | self.assertTrue(a.check()) 40 | 41 | def test_size(self): 42 | a = iddata(np.zeros(10), np.zeros(10), 0.0, [0]) 43 | self.assertEqual(len(a.y), 10) 44 | self.assertEqual(len(a.u), 10) 45 | self.assertEqual(len(a.y), len(a.u)) 46 | 47 | a = iddata([0 for i in range(10)], [1 for i in range(0,10)], 0.0, [0]) 48 | self.assertEqual(len(a.y), 10) 49 | self.assertEqual(len(a.u), 10) 50 | self.assertEqual(len(a.y), len(a.u)) 51 | 52 | a = iddata(np.zeros(10), np.zeros(9), 0.0, [0]) 53 | with self.assertRaises(ValueError): 54 | a.check() 55 | 56 | a = iddata(np.zeros(8), np.zeros(9), 0.0, [0]) 57 | with self.assertRaises(ValueError): 58 | a.check() 59 | 60 | 61 | def test_sampling_time(self): 62 | a = iddata(np.zeros(10), np.zeros(10), 0.0, [0]) 63 | with self.assertRaises(ValueError): 64 | a.check() 65 | 66 | a = iddata(np.zeros(10), np.zeros(10), 1e-9, [0]) 67 | with self.assertRaises(ValueError): 68 | a.check() 69 | 70 | a = iddata(np.zeros(10), np.zeros(10), -0.1, [0]) 71 | with self.assertRaises(ValueError): 72 | a.check() 73 | 74 | a = iddata(np.zeros(10), np.zeros(10), 0.1, [0]) 75 | self.assertTrue(a.check()) 76 | 77 | def test_copy(self): 78 | a = iddata(np.zeros(10), np.zeros(10), 0.1, [0]) 79 | b = a.copy() 80 | self.assertTrue(a.check()) 81 | self.assertTrue(b.check()) 82 | 83 | self.assertTrue(np.all(a.y == b.y)) 84 | self.assertTrue(np.all(a.u == b.u)) 85 | self.assertTrue(np.all(a.y0 == b.y0)) 86 | self.assertTrue(a.ts == b.ts) 87 | 88 | def test_filter(self): 89 | a = iddata(np.zeros(10), np.zeros(10), 0.1, [0]) 90 | L = scipysig.dlti([1], [1], dt=0.1) 91 | b = a.copy() 92 | a.filter(L) 93 | self.assertTrue(np.all(a.y == b.y)) 94 | self.assertTrue(np.all(a.u == b.u)) 95 | self.assertTrue(np.all(a.y0 == b.y0)) 96 | self.assertTrue(a.ts == b.ts) 97 | 98 | # Test more complex model 99 | dt = 0.05 100 | omega = 10 101 | alpha = np.exp(-dt * omega) 102 | num_M = [(1 - alpha) ** 2] 103 | den_M = [1, -2 * alpha, alpha ** 2, 0] 104 | refModel = ExtendedTF(num_M, den_M, dt=dt) 105 | 106 | a = iddata(np.ones(10), np.ones(10), 0.1, [0]) 107 | L = refModel * (1 - refModel) 108 | b = a.copy() 109 | a.filter(L) 110 | 111 | res = np.array([0, 0, 0, 0.15481812, 0.342622, 0.51348521, 112 | 0.62769493, 0.67430581, 0.66237955, 0.60937255]) 113 | 114 | self.assertTrue(np.allclose(a.y, res)) 115 | self.assertTrue(np.allclose(a.u, res)) 116 | self.assertTrue(np.all(a.u != b.u)) 117 | self.assertTrue(np.all(a.y != b.y)) 118 | self.assertTrue(np.all(a.y0 == b.y0)) 119 | self.assertTrue(a.ts == b.ts) 120 | 121 | def test_split(self): 122 | n = 9 123 | a = iddata(np.random.normal(size=n), np.random.normal(size=n), 0.1, [0]) 124 | 125 | b, c = a.split() 126 | n0 = len(a.y0) 127 | n1 = (n + n0) // 2 128 | 129 | self.assertTrue(b.y.size == c.y.size) 130 | self.assertTrue(b.u.size == c.u.size) 131 | self.assertTrue(b.ts == c.ts) 132 | self.assertTrue(b.ts == a.ts) 133 | self.assertTrue(np.all(b.y == a.y[:n1 - n0])) 134 | self.assertTrue(np.all(b.u == a.u[:n1 - n0])) 135 | self.assertTrue(np.all(b.y0 == a.y0)) 136 | 137 | self.assertTrue(np.all(c.y == a.y[n1:n])) 138 | self.assertTrue(np.all(c.u == a.u[n1:n])) 139 | self.assertTrue(np.all(c.y0 == a.y[n1 - n0:n1])) 140 | 141 | y0 = [-1, 2] 142 | a = iddata(np.random.normal(size=n), np.random.normal(size=n), 0.1, y0) 143 | n0 = len(y0) 144 | n1 = (n + n0) // 2 145 | b, c = a.split() 146 | 147 | self.assertTrue(b.y.size == c.y.size) 148 | self.assertTrue(b.u.size == c.u.size) 149 | self.assertTrue(b.ts == c.ts) 150 | self.assertTrue(b.ts == a.ts) 151 | self.assertTrue(np.all(b.y == a.y[:n1 - n0])) 152 | self.assertTrue(np.all(b.u == a.u[:n1 - n0])) 153 | self.assertTrue(np.all(b.y0 == a.y0)) 154 | 155 | self.assertTrue(np.all(c.y == a.y[n1:n-1])) 156 | self.assertTrue(np.all(c.u == a.u[n1:n-1])) 157 | self.assertTrue(np.all(c.y0 == a.y[n1 - n0:n1])) 158 | 159 | 160 | y0 = [-1, 2] 161 | n = 9 162 | a = iddata(np.random.normal(size=n), np.random.normal(size=n), 0.1, y0) 163 | n0 = len(y0) 164 | n -= 1 165 | n1 = (n + n0) // 2 166 | b, c = a.split() 167 | 168 | self.assertTrue(b.y.size == c.y.size) 169 | self.assertTrue(b.u.size == c.u.size) 170 | self.assertTrue(b.ts == c.ts) 171 | self.assertTrue(b.ts == a.ts) 172 | self.assertTrue(np.all(b.y == a.y[:n1 - n0])) 173 | self.assertTrue(np.all(b.u == a.u[:n1 - n0])) 174 | self.assertTrue(np.all(b.y0 == a.y0)) 175 | 176 | self.assertTrue(np.all(c.y == a.y[n1:n])) 177 | self.assertTrue(np.all(c.u == a.u[n1:n])) 178 | self.assertTrue(np.all(c.y0 == a.y[n1 - n0:n1])) 179 | 180 | y0 = [-1] 181 | n = 10 182 | a = iddata(np.random.normal(size=n), np.random.normal(size=n), 0.1, y0) 183 | n0 = len(y0) 184 | n -= 1 185 | n1 = (n + n0) // 2 186 | b, c = a.split() 187 | 188 | self.assertTrue(b.y.size == c.y.size) 189 | self.assertTrue(b.u.size == c.u.size) 190 | self.assertTrue(b.ts == c.ts) 191 | self.assertTrue(b.ts == a.ts) 192 | self.assertTrue(np.all(b.y == a.y[:n1 - n0])) 193 | self.assertTrue(np.all(b.u == a.u[:n1 - n0])) 194 | self.assertTrue(np.all(b.y0 == a.y0)) 195 | 196 | self.assertTrue(np.all(c.y == a.y[n1:n])) 197 | self.assertTrue(np.all(c.u == a.u[n1:n])) 198 | self.assertTrue(np.all(c.y0 == a.y[n1 - n0:n1])) -------------------------------------------------------------------------------- /test/test_reference.py: -------------------------------------------------------------------------------- 1 | # test_reference.py - Unittest for virtual reference algorithm 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | from unittest import TestCase 15 | import numpy as np 16 | import scipy.signal as scipysig 17 | from vrft.iddata import * 18 | from vrft.utils import * 19 | from vrft.vrft_algo import * 20 | 21 | 22 | class TestReference(TestCase): 23 | def test_virtualReference(self): 24 | # wrong system 25 | with self.assertRaises(ValueError): 26 | virtual_reference(1, 1, 0) 27 | 28 | # cant be constant the system 29 | with self.assertRaises(ValueError): 30 | virtual_reference([1],[1], 0) 31 | 32 | # cant be constant the system 33 | with self.assertRaises(ValueError): 34 | virtual_reference(np.array(2), np.array(3), 0) 35 | 36 | # wrong data 37 | with self.assertRaises(ValueError): 38 | virtual_reference([1], [1, 1], 0) 39 | 40 | t_start = 0 41 | t_end = 10 42 | t_step = 1e-2 43 | t = np.arange(t_start, t_end, t_step) 44 | u = np.ones(len(t)).tolist() 45 | 46 | num = [0.1] 47 | den = [1, -0.9] 48 | sys = scipysig.TransferFunction(num, den, dt=t_step) 49 | t,y = scipysig.dlsim(sys, u, t) 50 | y = y[:,0] 51 | data = iddata(y,u,t_step,[0,0]) 52 | 53 | # wrong initial conditions 54 | with self.assertRaises(ValueError): 55 | r, _ = virtual_reference(data, num, den) 56 | 57 | #test good data, first order 58 | data = iddata(y,u,t_step,[0]) 59 | 60 | r, _ = virtual_reference(data, num, den) 61 | 62 | for i in range(len(r)): 63 | self.assertTrue(np.isclose(r[i], u[i])) 64 | 65 | 66 | num = [0, 1-1.6+0.63] 67 | den = [1, -1.6, 0.63] 68 | sys = scipysig.TransferFunction(num, den, dt=t_step) 69 | t, y = scipysig.dlsim(sys, u, t) 70 | y = y[:,0] 71 | data = iddata(y,u,t_step,[0,0]) 72 | #test second order 73 | r, _ = virtual_reference(data, num, den) 74 | for i in range(len(r)): 75 | self.assertTrue(np.isclose(r[i], u[i])) 76 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | # test_utils.py - Unittest for utilities 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | from unittest import TestCase 15 | import numpy as np 16 | import scipy.signal as scipysig 17 | from vrft.utils import * 18 | from vrft.extended_tf import ExtendedTF 19 | from vrft.vrft_algo import virtual_reference 20 | from vrft.iddata import iddata 21 | 22 | 23 | class TestUtils(TestCase): 24 | def test_deconvolve(self): 25 | t_start = 0 26 | t_end = 10 27 | t_step = 1e-2 28 | t = np.arange(t_start, t_end, t_step) 29 | sys = ExtendedTF([0.5], [1, -0.9], dt=t_step) 30 | u = np.random.normal(size=t.size) 31 | _, y = scipysig.dlsim(sys, u, t) 32 | y = y[:, 0] 33 | data = iddata(y, u, t_step, [0]) 34 | r1, _ = virtual_reference(data, sys.num, sys.den) 35 | r2 = deconvolve_signal(sys, data.y) 36 | self.assertTrue(np.linalg.norm(r2-r1[:r2.size], np.infty) < 1e-3) 37 | 38 | 39 | def test_check_system(self): 40 | a = [1, 0, 1] 41 | b = [1, 0, 2] 42 | self.assertTrue(check_system(a,b)) 43 | 44 | b = [1, 0, 2, 4] 45 | self.assertTrue(check_system(a,b)) 46 | 47 | a = [1] 48 | self.assertTrue(check_system(a,b)) 49 | 50 | a = [1, 0, 1] 51 | b = [1,0] 52 | with self.assertRaises(ValueError): 53 | check_system(a,b) 54 | 55 | b = [1] 56 | with self.assertRaises(ValueError): 57 | check_system(a,b) 58 | 59 | def test_system_order(self): 60 | self.assertEqual(system_order(0, 0), (0, 0)) 61 | self.assertEqual(system_order(1, 0), (0, 0)) 62 | self.assertEqual(system_order([1],[1]), (0, 0)) 63 | self.assertEqual(system_order([1, 1],[1, 1]), (1,1)) 64 | self.assertEqual(system_order([1, 1, 3],[1, 1]), (2,1)) 65 | self.assertEqual(system_order([1, 1, 3],[1]), (2,0)) 66 | self.assertEqual(system_order([1, 1],[1, 1, 1]), (1,2)) 67 | self.assertEqual(system_order([1],[1, 1, 1]), (0,2)) 68 | self.assertEqual(system_order([0, 1],[1, 1, 1]), (0,2)) 69 | self.assertEqual(system_order([0,0,1],[1, 1, 1]), (0,2)) 70 | self.assertEqual(system_order([0,0,1],[0, 1, 1, 1]), (0,2)) 71 | self.assertEqual(system_order([0,0,1],[0, 0, 1, 1, 1]), (0,2)) 72 | -------------------------------------------------------------------------------- /test/test_vrft.py: -------------------------------------------------------------------------------- 1 | # test_vrft.py - Unittest for VRFT 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | from unittest import TestCase 15 | import numpy as np 16 | import scipy.signal as scipysig 17 | from vrft.iddata import * 18 | from vrft.vrft_algo import * 19 | from vrft.extended_tf import ExtendedTF 20 | 21 | 22 | class TestVRFT(TestCase): 23 | def test_vrft(self): 24 | t_start = 0 25 | t_step = 1e-2 26 | t_ends = [10, 10 + t_step] 27 | 28 | expected_theta = np.array([1.93220784, -1.05808206, 1.26623764, 0.0088772]) 29 | expected_loss = 0.00064687904235295 30 | 31 | for t_end in t_ends: 32 | t = np.arange(t_start, t_end, t_step) 33 | u = np.ones(len(t)).tolist() 34 | 35 | num = [0.1] 36 | den = [1, -0.9] 37 | sys = scipysig.TransferFunction(num, den, dt=t_step) 38 | t, y = scipysig.dlsim(sys, u, t) 39 | y = y[:,0] 40 | data = iddata(y,u,t_step,[0]) 41 | 42 | refModel = ExtendedTF([0.2], [1, -0.8], dt=t_step) 43 | prefilter = refModel * (1-refModel) 44 | 45 | control = [ExtendedTF([1], [1,0], dt=t_step), 46 | ExtendedTF([1], [1,0,0], dt=t_step), 47 | ExtendedTF([1], [1,0,0,0], dt=t_step), 48 | ExtendedTF([1, 0], [1,1], dt=t_step)] 49 | 50 | theta1, _, loss1, _ = compute_vrft(data, refModel, control, prefilter) 51 | theta2, _, loss2, _ = compute_vrft([data], refModel, control, prefilter) 52 | theta3, _, loss3, _ = compute_vrft([data, data], refModel, control, prefilter) 53 | 54 | self.assertTrue(np.isclose(loss1, loss2)) 55 | self.assertTrue(np.isclose(loss1, loss3)) 56 | self.assertTrue(np.linalg.norm(theta1-theta2)<1e-15) 57 | self.assertTrue(np.linalg.norm(theta1-theta3)<1e-15) 58 | self.assertTrue(np.linalg.norm(theta1-expected_theta, np.infty) < 1e-5) 59 | self.assertTrue(abs(expected_loss - loss1) < 1e-5) 60 | 61 | def test_iv(self): 62 | t_start = 0 63 | t_step = 1e-2 64 | t_ends = [10, 10 + t_step] 65 | 66 | 67 | for t_end in t_ends: 68 | t = np.arange(t_start, t_end, t_step) 69 | u = np.ones(len(t)).tolist() 70 | 71 | num = [0.1] 72 | den = [1, -0.9] 73 | sys = scipysig.TransferFunction(num, den, dt=t_step) 74 | _, y = scipysig.dlsim(sys, u, t) 75 | y = y.flatten() + 1e-2 * np.random.normal(size=t.size) 76 | data1 = iddata(y,u,t_step,[0]) 77 | 78 | _, y = scipysig.dlsim(sys, u, t) 79 | y = y.flatten() + 1e-2 * np.random.normal(size=t.size) 80 | data2 = iddata(y,u,t_step,[0]) 81 | 82 | 83 | refModel = ExtendedTF([0.2], [1, -0.8], dt=t_step) 84 | prefilter = refModel * (1-refModel) 85 | 86 | control = [ExtendedTF([1], [1,0], dt=t_step), 87 | ExtendedTF([1], [1,0,0], dt=t_step), 88 | ExtendedTF([1], [1,0,0,0], dt=t_step), 89 | ExtendedTF([1, 0], [1,1], dt=t_step)] 90 | 91 | with self.assertRaises(ValueError): 92 | compute_vrft(data1, refModel, control, prefilter, iv=True) 93 | 94 | compute_vrft([data1, data2], refModel, control, prefilter, iv=True) 95 | -------------------------------------------------------------------------------- /vrft/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 2 | # This file is part of PythonVRFT. 3 | # PythonVRFT is free software: you can redistribute it and/or modify 4 | # it under the terms of the MIT License. You should have received a copy of 5 | # the MIT License along with PythonVRFT. 6 | # If not, see . 7 | # 8 | # Code author: [Alessio Russo - alessior@kth.se] 9 | # Last update: 10th January 2021, by alessior@kth.se 10 | # 11 | 12 | from .iddata import * 13 | from .extended_tf import * 14 | from .utils import * 15 | from .vrft_algo import * 16 | 17 | __version__ = '0.0.6' 18 | __author__ = 'Alessio Russo' 19 | __contributors__ = ['Alexander Berndt'] 20 | __date__ = '09.01.2020' -------------------------------------------------------------------------------- /vrft/extended_tf.py: -------------------------------------------------------------------------------- 1 | # extended_tf.py - Extended definition of the discrete 2 | # transfer function implemented in scipy.signal. 3 | # Supports arithmetical operations between transfer function 4 | # and feedback loop computation 5 | # 6 | # Code author: [Alessio Russo - alessior@kth.se] 7 | # Last update: 10th January 2020, by alessior@kth.se 8 | # 9 | # Copyright (c) [2021] Alessio Russo [alessior@kth.se]. All rights reserved. 10 | # This file is part of PythonVRFT. 11 | # PythonVRFT is free software: you can redistribute it and/or modify 12 | # it under the terms of the MIT License. You should have received a copy of 13 | # the MIT License along with PythonVRFT. 14 | # If not, see . 15 | # 16 | 17 | from __future__ import division 18 | 19 | import numpy as np 20 | import scipy.signal as scipysig 21 | from scipy.signal.ltisys import TransferFunction as TransFun 22 | from numpy import polymul, polyadd 23 | 24 | 25 | class ExtendedTF(scipysig.ltisys.TransferFunctionDiscrete): 26 | """ 27 | Extended definition of the discrete transfer function implemented in scipy.signal. 28 | Supports arithmetical operations between transfer function and feedback loop 29 | computation 30 | """ 31 | 32 | def __init__(self, num: np.ndarray, den: np.ndarray, dt: float): 33 | self._dt = dt 34 | super().__init__(num, den, dt=dt) 35 | 36 | def __neg__(self): 37 | return ExtendedTF(-self.num, self.den, dt=self._dt) 38 | 39 | def __floordiv__(self, other): 40 | # can't make sense of integer division right now 41 | return NotImplemented 42 | 43 | def __mul__(self, other): 44 | if type(other) in [int, float]: 45 | return ExtendedTF(self.num*other, self.den, dt=self._dt) 46 | elif type(other) in [TransFun, ExtendedTF]: 47 | numer = polymul(self.num, other.num) 48 | denom = polymul(self.den, other.den) 49 | return ExtendedTF(numer, denom, dt=self._dt) 50 | 51 | def __truediv__(self, other): 52 | if type(other) in [int, float]: 53 | return ExtendedTF(self.num,self.den*other, dt=self._dt) 54 | if type(other) in [TransFun, ExtendedTF]: 55 | numer = polymul(self.num, other.den) 56 | denom = polymul(self.den, other.num) 57 | return ExtendedTF(numer, denom, dt=self._dt) 58 | 59 | def __rtruediv__(self, other): 60 | if type(other) in [int, float]: 61 | return ExtendedTF(other*self.den, self.num, dt=self._dt) 62 | if type(other) in [TransFun, ExtendedTF]: 63 | numer = polymul(self.den, other.num) 64 | denom = polymul(self.num, other.den) 65 | return ExtendedTF(numer, denom, dt=self._dt) 66 | 67 | def __add__(self,other): 68 | if type(other) in [int, float]: 69 | return ExtendedTF(polyadd(self.num, self.den*other), self.den, dt=self._dt) 70 | if type(other) in [TransFun, type(self)]: 71 | if len(self.den) == len(other.den) and np.all(self.den == other.den): 72 | numer = polyadd(self.num, other.num) 73 | denom = self.den 74 | else: 75 | numer = polyadd(polymul(self.num, other.den), polymul(self.den, other.num)) 76 | denom = polymul(self.den, other.den) 77 | return ExtendedTF(numer, denom, dt=self._dt) 78 | 79 | def __sub__(self, other): 80 | if type(other) in [int, float]: 81 | return ExtendedTF(polyadd(self.num, -self.den*other), self.den, dt=self._dt) 82 | if type(other) in [TransFun, type(self)]: 83 | if len(self.den) == len(other.den) and np.all(self.den == other.den): 84 | numer = polyadd(self.num, -other.num) 85 | denom = self.den 86 | else: 87 | numer = polyadd(polymul(self.num, other.den), -polymul(self.den, other.num)) 88 | denom = polymul(self.den, other.den) 89 | return ExtendedTF(numer, denom, dt=self._dt) 90 | 91 | def __rsub__(self, other): 92 | if type(other) in [int, float]: 93 | return ExtendedTF(polyadd(-self.num, self.den*other), self.den, dt=self._dt) 94 | if type(other) in [TransFun, type(self)]: 95 | if len(self.den) == len(other.den) and np.all(self.den == other.den): 96 | numer = polyadd(self.num, -other.num) 97 | denom = self.den 98 | else: 99 | numer = polyadd(polymul(self.num, other.den), -polymul(self.den, other.num)) 100 | denom = polymul(self.den, other.den) 101 | return ExtendedTF(numer, denom, dt=self._dt) 102 | 103 | def feedback(self): 104 | """ Computes T(z)/(1+T(z)) """ 105 | num = self.num 106 | den = self.den 107 | den = polyadd(num, den) 108 | self = ExtendedTF(num, den, dt=self.dt) 109 | return self 110 | 111 | __rmul__ = __mul__ 112 | __radd__ = __add__ 113 | 114 | 115 | -------------------------------------------------------------------------------- /vrft/iddata.py: -------------------------------------------------------------------------------- 1 | # iddata.py - iddata object definition 2 | # Analogous to the iddata object in Matlab sysid 3 | # 4 | # Code author: [Alessio Russo - alessior@kth.se] 5 | # Last update: 10th January 2021, by alessior@kth.se 6 | # 7 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 8 | # This file is part of PythonVRFT. 9 | # PythonVRFT is free software: you can redistribute it and/or modify 10 | # it under the terms of the MIT License. You should have received a copy of 11 | # the MIT License along with PythonVRFT. 12 | # If not, see . 13 | # 14 | 15 | import numpy as np 16 | import scipy.signal as scipysig 17 | from vrft.utils import filter_signal 18 | 19 | 20 | class iddata(object): 21 | """ 22 | iddata is a class analogous to the iddata object in Matlab 23 | It is used to save input/output data. 24 | 25 | @NOTE: y0, the initial conditions, are in general not used. 26 | The only reason to specify y0 is in case the system is non linear. 27 | In that case y0 needs to be specified (for the equilibria condition) 28 | """ 29 | 30 | def __init__(self, y: np.ndarray, 31 | u: np.ndarray, 32 | ts: float, 33 | y0: np.ndarray = None): 34 | """ 35 | Input/output data (suppors SISO systems only) 36 | Parameters 37 | ---------- 38 | y: np.ndarray 39 | Output data 40 | u: np.ndarray 41 | Input data 42 | ts: float 43 | sampling time 44 | y0: np.ndarray, optional 45 | Initial conditions 46 | """ 47 | if y is None: 48 | raise ValueError("Signal y can't be None.") 49 | if u is None: 50 | raise ValueError("Signal u can't be None.") 51 | if ts is None: 52 | raise ValueError("Sampling time ts can't be None.") 53 | 54 | self.y = np.array(y) if not isinstance(y, np.ndarray) else np.array([y]).flatten() 55 | self.u = np.array(u) if not isinstance(u, np.ndarray) else np.array([u]).flatten() 56 | self.ts = float(ts) 57 | 58 | if y0 is None: 59 | raise ValueError("y0: {} can't be None.".format(y0)) 60 | else: 61 | self.y0 = np.array(y0) if not isinstance(y0, np.ndarray) else np.array([y0]).flatten() 62 | if self.y0.size == 0 or self.y0.ndim == 0: 63 | raise ValueError("y0 can't be None.") 64 | 65 | 66 | def check(self): 67 | """ Checks validity of the data """ 68 | if (self.y.shape != self.u.shape): 69 | raise ValueError("Input and output size do not match.") 70 | 71 | if (np.isclose(self.ts, 0.0) == True): 72 | raise ValueError("Sampling time can not be zero.") 73 | 74 | if (self.ts < 0.0): 75 | raise ValueError("Sampling time can not be negative.") 76 | 77 | if (self.y0 is None): 78 | raise ValueError("Initial condition can't be zero") 79 | 80 | return True 81 | 82 | def copy(self): 83 | """ Returns a copy of the object """ 84 | return iddata(self.y, self.u, self.ts, self.y0) 85 | 86 | def filter(self, L: scipysig.dlti): 87 | """ Filters the data using the specified filter L(z) """ 88 | self.y = filter_signal(L, self.y) 89 | self.u = filter_signal(L, self.u) 90 | return self 91 | 92 | def split(self) -> tuple: 93 | """ Splits the dataset into two equal parts 94 | Used for the instrumental variable method 95 | """ 96 | n0 = self.y0.size if self.y0 is not None else 0 97 | n = self.y.size 98 | 99 | if (n + n0) % 2 != 0: 100 | print('iddata object has uneven data size. The last data point will be discarded') 101 | n -= 1 102 | 103 | # First dataset 104 | n1 = (n + n0) // 2 # floor division 105 | d1 = iddata(self.y[:n1 - n0], self.u[:n1 - n0], self.ts, self.y0) 106 | 107 | # Second dataset 108 | d2 = iddata(self.y[n1:n], self.u[n1:n], self.ts, self.y[n1 - n0:n1]) 109 | 110 | return (d1, d2) 111 | 112 | 113 | -------------------------------------------------------------------------------- /vrft/utils.py: -------------------------------------------------------------------------------- 1 | # utils.py - VRFT utility functions 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | from typing import overload 15 | import numpy as np 16 | import scipy.signal as scipysig 17 | 18 | 19 | def Doperator(p: int, q: int, x: float) -> np.ndarray: 20 | """ DOperator, used to compute the overall Toeplitz matrix """ 21 | D = np.zeros((p * q, q)) 22 | for i in range(q): 23 | D[i * p:(i + 1) * p, i] = x 24 | return D 25 | 26 | @overload 27 | def check_system(tf: scipysig.dlti) -> bool: 28 | """Returns true if a transfer function is causal 29 | Parameters 30 | ---------- 31 | tf : scipy.signal.dlti 32 | discrete time rational transfer function 33 | """ 34 | 35 | return check_system(tf.num, tf.den) 36 | 37 | def check_system(num: np.ndarray, den: np.ndarray) -> bool: 38 | """Returns true if a transfer function is causal 39 | Parameters 40 | ---------- 41 | num : np.ndarray 42 | numerator of the transfer function 43 | den : np.ndarray 44 | denominator of the transfer function 45 | 46 | """ 47 | try: 48 | M, N = system_order(num, den) 49 | except ValueError: 50 | raise 51 | 52 | if (N < M): 53 | raise ValueError("The system is not causal.") 54 | 55 | return True 56 | 57 | @overload 58 | def system_order(tf: scipysig.dlti) -> tuple: 59 | """Returns the order of the numerator and denominator 60 | of a transfer function 61 | Parameters 62 | ---------- 63 | tf : scipy.signal.dlti 64 | discrete time rational transfer function 65 | 66 | Returns 67 | ---------- 68 | (num, den): tuple 69 | Tuple containing the orders 70 | """ 71 | return system_order(tf.num, tf.den) 72 | 73 | def system_order(num: np.ndarray, den: np.ndarray) -> tuple: 74 | """Returns the order of the numerator and denominator 75 | of a transfer function 76 | Parameters 77 | ---------- 78 | num : np.ndarray 79 | numerator of the transfer function 80 | den : np.ndarray 81 | denominator of the transfer function 82 | 83 | Returns 84 | ---------- 85 | (num, den): tuple 86 | Tuple containing the orders 87 | """ 88 | den = den if isinstance(den, np.ndarray) else np.array([den]).flatten() 89 | num = num if isinstance(num, np.ndarray) else np.array([num]).flatten() 90 | 91 | if num.ndim == 0: 92 | num = np.expand_dims(num, axis=0) 93 | 94 | if den.ndim == 0: 95 | den = np.expand_dims(den, axis=0) 96 | 97 | return (np.poly1d(num).order, np.poly1d(den).order) 98 | 99 | def filter_signal(L: scipysig.dlti, x: np.ndarray, x0: np.ndarray = None) -> np.ndarray: 100 | """Filter data in an iddata object 101 | Parameters 102 | ---------- 103 | L : scipy.signal.dlti 104 | Discrete-time rational transfer function used to 105 | filter the signal 106 | x : np.ndarray 107 | Signal to filter 108 | x0 : np.ndarray, optional 109 | Initial conditions for L 110 | Returns 111 | ------- 112 | signal : iddata 113 | Filtered iddata object 114 | """ 115 | t_start = 0 116 | t_step = L.dt 117 | t_end = x.size * t_step 118 | 119 | t = np.arange(t_start, t_end, t_step) 120 | _, y = scipysig.dlsim(L, x, t, x0) 121 | return y.flatten() 122 | 123 | def deconvolve_signal(L: scipysig.dlti, x: np.ndarray) -> np.ndarray: 124 | """Deconvolve a signal x using a specified transfer function L(z) 125 | Parameters 126 | ---------- 127 | L : scipy.signal.dlti 128 | Discrete-time rational transfer function used to 129 | deconvolve the signal 130 | x : np.ndarray 131 | Signal to deconvolve 132 | 133 | Returns 134 | ------- 135 | signal : np.ndarray 136 | Deconvolved signal 137 | """ 138 | dt = L.dt 139 | impulse = scipysig.dimpulse(L)[1][0].flatten() 140 | idx1 = np.argwhere(impulse != 0)[0].item() 141 | idx2 = np.argwhere(np.isclose(impulse[idx1:], 0.) == True) 142 | idx2 = -1 if idx2.size == 0 else idx2[0].item() 143 | signal, _ = scipysig.deconvolve(x, impulse[idx1:idx2]) 144 | return signal[np.argwhere(impulse != 0)[0].item():] 145 | -------------------------------------------------------------------------------- /vrft/vrft_algo.py: -------------------------------------------------------------------------------- 1 | # vrft_algo.py - VRFT algorithm implementation 2 | # 3 | # Code author: [Alessio Russo - alessior@kth.se] 4 | # Last update: 10th January 2021, by alessior@kth.se 5 | # 6 | # Copyright (c) [2017-2021] Alessio Russo [alessior@kth.se]. All rights reserved. 7 | # This file is part of PythonVRFT. 8 | # PythonVRFT is free software: you can redistribute it and/or modify 9 | # it under the terms of the MIT License. You should have received a copy of 10 | # the MIT License along with PythonVRFT. 11 | # If not, see . 12 | # 13 | 14 | from typing import overload 15 | import numpy as np 16 | import scipy as sp 17 | import scipy.signal as scipysig 18 | 19 | from vrft.iddata import iddata 20 | from vrft.utils import system_order, check_system, \ 21 | filter_signal 22 | 23 | 24 | @overload 25 | def virtual_reference(data: iddata, L: scipysig.dlti) -> np.ndarray: 26 | """Compute virtual reference signal by performing signal deconvolution 27 | Parameters 28 | ---------- 29 | data : iddata 30 | iddata object containing data from experiments 31 | L : scipy.signal.dlti 32 | Discrete transfer function 33 | 34 | Returns 35 | ------- 36 | r : np.ndarray 37 | virtual reference signal 38 | """ 39 | return virtual_reference(data, L.num, L.den) 40 | 41 | 42 | def virtual_reference(data: iddata, num: np.ndarray, den: np.ndarray) -> np.ndarray: 43 | """Compute virtual reference signal by performing signal deconvolution 44 | Parameters 45 | ---------- 46 | data : iddata 47 | iddata object containing data from experiments 48 | num : np.ndarray 49 | numerator of a discrete transfer function 50 | phi2 : np.ndarray 51 | denominator of a discrete transfer function 52 | 53 | Returns 54 | ------- 55 | r : np.ndarray 56 | virtual reference signal 57 | """ 58 | try: 59 | check_system(num, den) 60 | except ValueError: 61 | raise ValueError('Error in check system') 62 | 63 | M, N = system_order(num, den) 64 | 65 | if (N == 0) and (M == 0): 66 | raise ValueError("The reference model can not be a constant.") 67 | 68 | data.check() 69 | offset_M = len(num) - M - 1 70 | offset_N = len(den) - N - 1 71 | 72 | lag = N - M # number of initial conditions 73 | y0 = data.y0 74 | 75 | if y0 is None: 76 | y0 = [0.] * lag 77 | 78 | if y0 is not None and (lag != len(y0)): 79 | raise ValueError("Wrong initial condition size.") 80 | 81 | reference = np.zeros_like(data.y) 82 | L = len(data.y) 83 | 84 | for k in range(0, len(data.y) + lag): 85 | left_side = 0 86 | r = 0 87 | 88 | start_i = 0 if k >= M else M - k 89 | start_j = 0 if k >= N else N - k 90 | 91 | for i in range(start_i, N + 1): 92 | index = k + i - N 93 | if (index < 0): 94 | left_side += den[offset_N + 95 | abs(i - N)] * y0[abs(index) - 1] 96 | else: 97 | left_side += den[offset_N + abs(i - N)] * ( 98 | data.y[index] if index < L else 0) 99 | 100 | for j in range(start_j, M + 1): 101 | index = k + j - N 102 | if (start_j != M): 103 | left_side += -num[offset_M + abs(j - M)] * reference[index] 104 | else: 105 | r = num[offset_M] 106 | 107 | if (np.isclose(r, 0.0) != True): 108 | reference[k - lag] = left_side / r 109 | else: 110 | reference[k - lag] = 0.0 111 | 112 | #add missing data..just copy last N-M points 113 | #for i in range(lag): 114 | # reference[len(self.data.y)+i-lag] =0 #reference[len(self.data.y)+i-1-lag] 115 | 116 | return reference[:-lag], len(reference[:-lag]) 117 | 118 | 119 | def compute_vrft_loss(data: iddata, phi: np.ndarray, theta: np.ndarray) -> float: 120 | z = np.dot(phi, theta.T).flatten() 121 | return np.linalg.norm(data.u[:z.size] - z) ** 2 / z.size 122 | 123 | def calc_minimum(u: np.ndarray, phi1: np.ndarray, 124 | phi2: np.ndarray = None) -> np.ndarray: 125 | """Compute least squares minimum 126 | Parameters 127 | ---------- 128 | u : np.ndarray 129 | Input signal 130 | phi1 : np.ndarray 131 | Regressor 132 | phi2 : np.ndarray, optional 133 | Second regressor (used only with instrumental variables) 134 | 135 | Returns 136 | ------- 137 | theta : np.ndarray 138 | Coefficients computed for the control basis 139 | """ 140 | phi2 = phi1 if phi2 is None else phi2 141 | return sp.linalg.solve(phi2.T @ phi1, phi2.T.dot(u)) 142 | 143 | def control_response(data: iddata, error: np.ndarray, control: list) -> np.ndarray: 144 | t_step = data.ts 145 | t = [i * t_step for i in range(len(error))] 146 | 147 | phi = [None] * len(control) 148 | for i, c in enumerate(control): 149 | _, y = scipysig.dlsim(c, error, t) 150 | phi[i] = y.flatten() 151 | 152 | phi = np.vstack(phi).T 153 | return phi 154 | 155 | def compute_vrft(data: iddata, refModel: scipysig.dlti, 156 | control: list, prefilter: scipysig.dlti = None, 157 | iv: bool = False): 158 | """Compute VRFT Controller 159 | Parameters 160 | ---------- 161 | data : iddata or list of iddata objects 162 | Data used to identify theta. If iv is set to true, 163 | then the algorithm expects a list of 2 iddata objects 164 | refModel : scipy.signal.dlti 165 | Discrete Transfer Function representing the reference model 166 | control : list 167 | list of discrete transfer functions, representing the control basis 168 | prefilter : scipy.signal.dlti, optional 169 | Filter used to pre-filter the data 170 | iv : bool, optiona; 171 | Instrumental variable option. If true, the instrumental variable will 172 | be constructed based on two iddata objets 173 | 174 | Returns 175 | ------- 176 | theta : np.ndarray 177 | Coefficients computed for the control basis 178 | r : np.ndarray 179 | Virtual reference signal 180 | loss: float 181 | VRFT loss 182 | final_control: scipy.signal.dlti 183 | Final controller 184 | """ 185 | 186 | # Check the data 187 | if not isinstance(data, iddata): 188 | if not isinstance(data, list): 189 | raise ValueError('data should be an iddata object or a list of iddata objects') 190 | else: 191 | if iv and len(data) != 2: 192 | raise ValueError('data should be a list of 2 iddata objects') 193 | 194 | for d in data: 195 | if not isinstance(d, iddata): 196 | raise ValueError('data should be a list of iddata objects') 197 | 198 | # Prefilter the data 199 | if prefilter is not None and isinstance(prefilter, scipysig.dlti): 200 | if isinstance(data, list): 201 | for i, d in enumerate(data): 202 | data[i] = d.copy().filter(prefilter) 203 | else: 204 | data = data.copy().filter(prefilter) 205 | 206 | 207 | if not iv: 208 | # No instrumental variable routine 209 | if isinstance(data, list): 210 | data = data[0] 211 | data.check() 212 | 213 | # Compute virtual reference 214 | r, n = virtual_reference(data, refModel.num, refModel.den) 215 | 216 | # Compute control response given the virtual reference 217 | phi = control_response(data, np.subtract(r, data.y[:n]), control) 218 | 219 | # Compute MSE minimizer 220 | theta = calc_minimum(data.u[:n], phi) 221 | else: 222 | # Instrumental variable routine 223 | 224 | # Retrieve the two datasets 225 | if isinstance(data, list): 226 | d1 = data[0] 227 | d2 = data[1] 228 | # check if the two datasets have same size 229 | if d1.y.size != d2.y.size: 230 | raise ValueError('The two datasets should have same size!') 231 | else: 232 | raise ValueError('To use IV the data should be a list of iddata objects') 233 | 234 | # Compute virtual reference 235 | r1, n1 = virtual_reference(d1, refModel.num, refModel.den) 236 | r2, n2 = virtual_reference(d2, refModel.num, refModel.den) 237 | 238 | # Compute control response 239 | phi1 = control_response(d1, np.subtract(r1, d1.y[:n1]), control) 240 | phi2 = control_response(d2, np.subtract(r2, d2.y[:n2]), control) 241 | 242 | # We use the first dataset to compute statistics (e.g. VRFT Loss) 243 | phi = phi1 244 | data = data[0] 245 | r = r1 246 | 247 | # Compute MSE minimizer 248 | theta = calc_minimum(data.u[:n1], phi1, phi2) 249 | 250 | # Compute VRFT loss 251 | loss = compute_vrft_loss(data, phi, theta) 252 | 253 | # Final controller 254 | final_control = np.dot(theta, control) 255 | 256 | return theta, r, loss, final_control 257 | --------------------------------------------------------------------------------