├── .github └── workflows │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── .testignore ├── 0_Getting_Started ├── Cheatsheet.ipynb ├── Extra Python.ipynb └── Notebook introduction.ipynb ├── 1_Dynamics ├── 1_Modelling │ └── Draining cup.ipynb ├── 2_Time_domain_simulation │ ├── Equation solving tools.ipynb │ ├── Fed batch bioreactor.ipynb │ ├── Mixing system.ipynb │ ├── Nonlinear CSTR.ipynb │ ├── Numeric representation.ipynb │ └── Read input from file.ipynb ├── 3_Linear_systems │ ├── Convolution.ipynb │ ├── Laplace transforms.ipynb │ ├── Linearisation.ipynb │ └── Visualising complex functions.ipynb ├── 4_First_and_second_order_system_dynamics │ ├── First order systems.ipynb │ ├── Second order systems.ipynb │ ├── Sinusoidal response.ipynb │ └── Standard process inputs.ipynb ├── 5_Complex_system_dynamics │ ├── Approximation.ipynb │ ├── Block diagram simplification.ipynb │ ├── Random response generator.ipynb │ └── Simulation of arbitrary transfer functions.ipynb ├── 6_Multivariable_system_representation │ ├── State space.ipynb │ └── Transfer function matrices.ipynb ├── 7_System_identification │ ├── Dynamic model parameter estimation.ipynb │ ├── Identifying discrete-time models.ipynb │ ├── Neural networks.ipynb │ └── Regression.ipynb ├── 8_Frequency_domain │ ├── Asymptotic Bode diagrams.ipynb │ ├── Fourier series.ipynb │ ├── Frequency response plots.ipynb │ └── Sound and frequency.ipynb └── 9_Sampled_systems │ ├── Aliasing.ipynb │ ├── Filtering.ipynb │ ├── The z domain and continuous systems.ipynb │ └── The z transform.ipynb ├── 2_Control ├── 1_Conventional_feedback_control │ ├── Closed loop controlled responses.ipynb │ ├── Control game.ipynb │ ├── Effect of Proportional Control.ipynb │ ├── PID controller step responses.ipynb │ └── controlgame.py ├── 2_Laplace_domain_analysis_of_control_systems │ ├── Root locus diagrams.ipynb │ ├── Stability analysis.ipynb │ └── SymPy Routh Array.ipynb ├── 3_PID_controller_design_tuning_and_troubleshooting │ ├── Direct synthesis PID design.ipynb │ ├── ITAE parameters for FOPDT system.ipynb │ └── Optimal control - minimal integral measures.ipynb ├── 4_Frequency_domain_analysis_of_control_systems │ ├── Frequency domain stability.ipynb │ └── RQ Freq basics.ipynb ├── 5_Advanced_control_methods │ └── Dead time compensation.ipynb ├── 6_Discrete_control_and_analysis │ ├── Dahlin controller.ipynb │ ├── Discrete PI.ipynb │ ├── Discrete control.ipynb │ ├── Noise models.ipynb │ └── Simple discrete controller simulation.ipynb ├── 7_Multivariable_control │ ├── Decoupling.ipynb │ ├── Eigenvalues and Eigenvectors.ipynb │ ├── Multivariable Pairing.ipynb │ ├── Multivariable closed loop transfer functions.ipynb │ ├── Multivariable stability analysis.ipynb │ └── Simple MPC.ipynb └── 8_Control_Practice │ └── Control valve design.ipynb ├── CITATION.cff ├── Function index.ipynb ├── LICENSE ├── Makefile ├── Modelica ├── Discrete.mo ├── FedBatchBioreactorModel.mo └── MixingSystem.mo ├── README.md ├── Simulation ├── Blocksim.ipynb ├── Classes.ipynb ├── Hybrid system simulation.ipynb ├── Object-Oriented simulation - Discrete.ipynb ├── Object-Oriented simulation.ipynb ├── Special functions in classes.ipynb └── Timing study.ipynb ├── TOC.ipynb ├── assets ├── 0.1.png ├── bigblockdiagram.png ├── blending_system.png ├── continuous_controller.ipe ├── continuous_controller.png ├── cstr.png ├── cup.png ├── data.csv ├── decoupling.png ├── example_6_1.xlsx ├── mimo2x2.png ├── mimo2x2_off_diagonal.png ├── mixing_tanks.png ├── mmult.gif ├── neuron.png ├── nn.png ├── rapmusic.wav ├── simple_feedback.png ├── smith.png ├── standard_feedback.png ├── tankdata.xlsx ├── tanksystem.ipe ├── tanksystem.png ├── transfer_function_block_diagram.png ├── weddingday.wav └── z_transform_integrated_first_order.png ├── conf.py ├── environment.yml ├── find_unlinked.py ├── function_list.py ├── index.rst ├── link.py ├── setup.py ├── sphinx_requirements.txt ├── tbcontrol ├── __init__.py ├── blocksim.py ├── conversion.py ├── fopdtitae.py ├── loops.py ├── numeric.py ├── plotting.py ├── responses.py ├── symbolic.py └── version.py ├── tclab ├── Continuous PID on TCLab.ipynb ├── FOPDT fit.ipynb ├── Frequency domain.ipynb ├── TCLab PID.ipynb ├── TCLab step test.ipynb └── pidgui.py ├── test_all_notebooks.py └── tests └── test_numeric.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | run-name: Run tests 3 | on: [push] 4 | jobs: 5 | Run-Tests: 6 | runs-on: "ubuntu-latest" 7 | defaults: 8 | run: 9 | shell: bash -l {0} 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: conda-incubator/setup-miniconda@v2 13 | with: 14 | activate-environment: dynamicscontrol 15 | environment-file: environment.yml 16 | python-version: 3.11 17 | auto-activate-base: false 18 | - run: pip install . 19 | - run: | 20 | pytest tests 21 | pytest --verbose test_all_notebooks.py 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | *~ 3 | __pycache__ 4 | .pytest_cache 5 | build 6 | dist 7 | _build 8 | *.egg-info 9 | .idea 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | 14 | # Build with Sphinx 15 | sphinx: 16 | configuration: conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: sphinx_requirements.txt 23 | 24 | formats: 25 | - epub 26 | - pdf 27 | -------------------------------------------------------------------------------- /.testignore: -------------------------------------------------------------------------------- 1 | tclab/* 2 | under_construction/* 3 | assets/* 4 | 2_Control/1_Conventional_feedback_control/Control game.ipynb 5 | -------------------------------------------------------------------------------- /1_Dynamics/5_Complex_system_dynamics/Random response generator.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Random response generator\n", 8 | "\n", 9 | "This sheet will generate random systems and show their step responses. See if you can predict the responses from the transfer functions and the poles and zeros." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "This notebook assumes version 0.8.0 of the control library or better" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "import control\n", 26 | "import numpy\n", 27 | "import matplotlib.pyplot as plt\n", 28 | "%matplotlib inline" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 2, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "def viz(order):\n", 38 | " # Not all random transfer functions work, so we generate one\n", 39 | " # and try to calculate the step response, only continuing when it works\n", 40 | " valid = False\n", 41 | " while not valid:\n", 42 | " coeffs = (numpy.random.random(order)*3).tolist() + [1]\n", 43 | " G = control.tf(1, coeffs)\n", 44 | "\n", 45 | " try:\n", 46 | " t, y = control.step_response(G)\n", 47 | " valid = True\n", 48 | " except ValueError:\n", 49 | " continue\n", 50 | "\n", 51 | " fig, [ax_complex, ax_time] = plt.subplots(1, 2, figsize=(10, 5))\n", 52 | " \n", 53 | " plt.sca(ax_complex)\n", 54 | " control.pzmap(G)\n", 55 | " ax_complex.axis('equal')\n", 56 | " ax_complex.axis([-5, 5, -5, 5])\n", 57 | " ax_complex.grid()\n", 58 | " ax_time.plot(t, y)\n", 59 | " ax_time.axhline(1)" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 3, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "from ipywidgets import interact" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 4, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "data": { 78 | "application/vnd.jupyter.widget-view+json": { 79 | "model_id": "c20086f3ed33484daddc0b03467c10dc", 80 | "version_major": 2, 81 | "version_minor": 0 82 | }, 83 | "text/plain": [ 84 | "interactive(children=(IntSlider(value=3, description='order', max=5, min=1), Output()), _dom_classes=('widget-…" 85 | ] 86 | }, 87 | "metadata": {}, 88 | "output_type": "display_data" 89 | }, 90 | { 91 | "data": { 92 | "text/plain": [ 93 | "" 94 | ] 95 | }, 96 | "execution_count": 4, 97 | "metadata": {}, 98 | "output_type": "execute_result" 99 | } 100 | ], 101 | "source": [ 102 | "interact(viz, order=(1, 5))" 103 | ] 104 | } 105 | ], 106 | "metadata": { 107 | "kernelspec": { 108 | "display_name": "Python 3", 109 | "language": "python", 110 | "name": "python3" 111 | }, 112 | "language_info": { 113 | "codemirror_mode": { 114 | "name": "ipython", 115 | "version": 3 116 | }, 117 | "file_extension": ".py", 118 | "mimetype": "text/x-python", 119 | "name": "python", 120 | "nbconvert_exporter": "python", 121 | "pygments_lexer": "ipython3", 122 | "version": "3.6.5" 123 | } 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 2 127 | } 128 | -------------------------------------------------------------------------------- /1_Dynamics/9_Sampled_systems/Aliasing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import numpy\n", 12 | "import matplotlib.pyplot as plt\n", 13 | "%matplotlib inline" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 5, 19 | "metadata": { 20 | "collapsed": true 21 | }, 22 | "outputs": [], 23 | "source": [ 24 | "from ipywidgets import interact, Checkbox" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 6, 30 | "metadata": { 31 | "collapsed": true 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "f = numpy.sin" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "Let's generate a signal and sample it" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 7, 48 | "metadata": { 49 | "collapsed": true 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "maxt = 100\n", 54 | "t = numpy.linspace(0, maxt, 1000)\n", 55 | "y = f(t)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 8, 61 | "metadata": { 62 | "collapsed": true 63 | }, 64 | "outputs": [], 65 | "source": [ 66 | "def show_sampled(T=6.7, show_f=True):\n", 67 | " t_sampled = numpy.arange(0, maxt, T)\n", 68 | " y_sampled = f(t_sampled)\n", 69 | "\n", 70 | " if show_f:\n", 71 | " plt.plot(t, y)\n", 72 | " plt.scatter(t_sampled, y_sampled)\n", 73 | " plt.axis([0, maxt, -1.1, 1.1])" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 9, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "data": { 83 | "image/png": "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\n", 84 | "text/plain": [ 85 | "" 86 | ] 87 | }, 88 | "metadata": {}, 89 | "output_type": "display_data" 90 | } 91 | ], 92 | "source": [ 93 | "interact(show_sampled, T=(0.1, 10), show_f=Checkbox());" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "The default sampling rate in the demo above illustrates the idea of *aliasing*, where a higher frequency sinusoid can masquerade as a lower frequency one. We can avoid this problem by ensuring that we sample at least twice per cycle for the highest frequency in the signal we are sampling. See the Wikipedia page on the [Nyquist-Shannon sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem) for more information." 101 | ] 102 | } 103 | ], 104 | "metadata": { 105 | "anaconda-cloud": {}, 106 | "kernelspec": { 107 | "display_name": "Python 3", 108 | "language": "python", 109 | "name": "python3" 110 | }, 111 | "language_info": { 112 | "codemirror_mode": { 113 | "name": "ipython", 114 | "version": 3 115 | }, 116 | "file_extension": ".py", 117 | "mimetype": "text/x-python", 118 | "name": "python", 119 | "nbconvert_exporter": "python", 120 | "pygments_lexer": "ipython3", 121 | "version": "3.6.2" 122 | } 123 | }, 124 | "nbformat": 4, 125 | "nbformat_minor": 2 126 | } 127 | -------------------------------------------------------------------------------- /2_Control/1_Conventional_feedback_control/Control game.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import matplotlib.pyplot as plt\n", 10 | "%matplotlib inline" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from controlgame import ControlGame" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 3, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "game = ControlGame(runtime=30) # seconds" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "# Instructions\n", 36 | "\n", 37 | "Run the cell below and click the \"run\" button. Then move the \"MV\" slider in a way which gets the controlled slider close to the setpoint. Your score increases more quickly when Controlled is near Setpoint. See how high your score can get by clicking run a couple of times. To see your performance graphed out, execute the next cell (`game.plot()`)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "data": { 47 | "application/vnd.jupyter.widget-view+json": { 48 | "model_id": "5e64a1c5f2f24e76ac8f073f8f20914f", 49 | "version_major": 2, 50 | "version_minor": 0 51 | }, 52 | "text/plain": [ 53 | "VBox(children=(HBox(children=(Button(description='Run', style=ButtonStyle()), Text(value='0', description='Sco…" 54 | ] 55 | }, 56 | "metadata": {}, 57 | "output_type": "display_data" 58 | } 59 | ], 60 | "source": [ 61 | "game.ui()" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 5, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "image/png": "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\n", 72 | "text/plain": [ 73 | "
" 74 | ] 75 | }, 76 | "metadata": { 77 | "needs_background": "light" 78 | }, 79 | "output_type": "display_data" 80 | } 81 | ], 82 | "source": [ 83 | "game.plot()" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 6, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "import scipy.signal" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 7, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "ts = game.ts" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 8, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "G = scipy.signal.lti(2, [2, 0])" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 9, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "import numpy" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 10, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "LIMIT = 100" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 11, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "def score(ts, sps, cvs):\n", 138 | " scores = 1 - numpy.minimum(numpy.abs(numpy.array(sps) - numpy.array(cvs)), LIMIT)/LIMIT\n", 139 | " \n", 140 | " score = sum(scores)\n", 141 | " \n", 142 | " return score\n", 143 | " " 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 12, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "def sim(ts, mvs):\n", 153 | " _, cvs, _ = scipy.signal.lsim(G, mvs, ts)\n", 154 | " \n", 155 | " return cvs" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 13, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "def objective(mvs):\n", 165 | " return -score(game.ts, game.sps, sim(game.ts, mvs))" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 15, 171 | "metadata": {}, 172 | "outputs": [ 173 | { 174 | "ename": "IndexError", 175 | "evalue": "index 0 is out of bounds for axis 0 with size 0", 176 | "output_type": "error", 177 | "traceback": [ 178 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 179 | "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", 180 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mobjective\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmvs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 181 | "\u001b[0;32m\u001b[0m in \u001b[0;36mobjective\u001b[0;34m(mvs)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mobjective\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmvs\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[0;32m----> 2\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0mscore\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msps\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmvs\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", 182 | "\u001b[0;32m\u001b[0m in \u001b[0;36msim\u001b[0;34m(ts, mvs)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0msim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmvs\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[0;32m----> 2\u001b[0;31m \u001b[0m_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcvs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mscipy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msignal\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlsim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmvs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mts\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 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcvs\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 183 | "\u001b[0;32m~/anaconda3/lib/python3.7/site-packages/scipy/signal/ltisys.py\u001b[0m in \u001b[0;36mlsim\u001b[0;34m(system, U, T, X0, interp)\u001b[0m\n\u001b[1;32m 1944\u001b[0m \u001b[0mxout\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mzeros\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mn_steps\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn_states\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msys\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1945\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1946\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mT\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[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1947\u001b[0m \u001b[0mxout\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[1;32m 1948\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mT\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[0;34m\u001b[0m\u001b[0m\n", 184 | "\u001b[0;31mIndexError\u001b[0m: index 0 is out of bounds for axis 0 with size 0" 185 | ] 186 | } 187 | ], 188 | "source": [ 189 | "objective(game.mvs)" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "import scipy.optimize" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "guesses = 1" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "bestmvs = game.mvs\n", 224 | "for i in range(guesses):\n", 225 | " sol = scipy.optimize.minimize(objective, bestmvs + 2*(numpy.random.rand(len(bestmvs))*2-1), bounds=[(-LIMIT, LIMIT)]*len(game.mvs))\n", 226 | " print('Score:', -sol.fun)\n", 227 | " bestmvs = sol.x\n", 228 | " bestmvs[numpy.abs(bestmvs)<10] = 0" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "bestcvs = sim(ts, bestmvs)" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "fig, (axmv, axcv) = plt.subplots(2, 1)\n", 247 | "axmv.plot(ts, bestmvs)\n", 248 | "axcv.plot(ts, game.sps, ts, bestcvs)" 249 | ] 250 | } 251 | ], 252 | "metadata": { 253 | "kernelspec": { 254 | "display_name": "Python 3", 255 | "language": "python", 256 | "name": "python3" 257 | }, 258 | "language_info": { 259 | "codemirror_mode": { 260 | "name": "ipython", 261 | "version": 3 262 | }, 263 | "file_extension": ".py", 264 | "mimetype": "text/x-python", 265 | "name": "python", 266 | "nbconvert_exporter": "python", 267 | "pygments_lexer": "ipython3", 268 | "version": "3.7.6" 269 | } 270 | }, 271 | "nbformat": 4, 272 | "nbformat_minor": 4 273 | } 274 | -------------------------------------------------------------------------------- /2_Control/1_Conventional_feedback_control/controlgame.py: -------------------------------------------------------------------------------- 1 | import tornado 2 | import time 3 | import numpy 4 | 5 | import matplotlib.pyplot as plt 6 | 7 | from ipywidgets import FloatProgress, FloatSlider, HBox, VBox, Button, Text 8 | 9 | K = 2 10 | tau = 2 11 | 12 | LIMIT = 100 # Limits for CV 13 | SLEEP = 150 # Sleep time - make smaller for more responsive simulation 14 | 15 | MINIMUM = 5 # minimum length the setpoint stays in one place 16 | EXTRA = 5 # maximum additional length that is added randomly 17 | SEED = 1 # control the game - everyone sees the same sequence 18 | 19 | 20 | class SetPointGenerator: 21 | def __init__(self): 22 | self.nextt = 0 23 | self.output = 0 24 | 25 | def generate(self, t): 26 | if t > self.nextt: 27 | self.nextt = t + MINIMUM + numpy.random.random()*EXTRA 28 | self.output = LIMIT*(2*numpy.random.random() - 1) 29 | 30 | return self.output 31 | 32 | 33 | class ControlledSystem: 34 | def __init__(self): 35 | self.state = 0 36 | self.manipulated = 0 37 | self.controlled = 0 38 | 39 | def update(self, t, manipulated): 40 | dt = SLEEP/1000 41 | 42 | dxdt = K/tau*manipulated 43 | self.state += dxdt*dt 44 | self.controlled = self.state 45 | 46 | return self.controlled 47 | 48 | 49 | class ControlGame: 50 | def __init__(self, runtime): 51 | """Initialise the game 52 | 53 | :param runtime: runtime in seconds 54 | """ 55 | self.runtime = runtime 56 | 57 | self.startbutton = Button(description='Run') 58 | self.startbutton.on_click(self.run) 59 | 60 | self.timeprogress = FloatProgress(value=0, min=0, max=self.runtime, description='Time') 61 | self.setpoint = FloatSlider(value=0, min=-LIMIT, max=LIMIT, description='Setpoint') 62 | self.controlled = FloatSlider(value=0, min=-LIMIT, max=LIMIT, description='Controlled') 63 | self.controlled.disabled = True 64 | self.manipulated = FloatSlider(value=0, min=-LIMIT, max=LIMIT, description= 'MV') 65 | self.scoretext = Text(value='0', description='Score') 66 | self.score = 0 67 | 68 | self.timer = tornado.ioloop.PeriodicCallback(self.update, SLEEP) 69 | 70 | self.running = False 71 | self.reset() 72 | 73 | def reset(self): 74 | self.ts = [] 75 | self.sps = [] 76 | self.mvs = [] 77 | self.cvs = [] 78 | 79 | self.t = 0 80 | self.score = 0 81 | self.scoretext.value = '0' 82 | 83 | 84 | def run(self, args): 85 | numpy.random.seed(SEED) 86 | 87 | self.running = True 88 | self.startbutton.disabled = True 89 | self.setpointgenerator = SetPointGenerator() 90 | self.system = ControlledSystem() 91 | 92 | self.reset() 93 | 94 | self.timer.start() 95 | 96 | def update(self): 97 | if not self.running: 98 | return 99 | 100 | t = self.t = self.t + SLEEP/1000 101 | 102 | if t >= self.runtime: 103 | self.running = False 104 | self.timer.stop() 105 | self.startbutton.disabled = False 106 | return 107 | 108 | self.timeprogress.value = t 109 | 110 | sp = self.setpoint.value = self.setpointgenerator.generate(t) 111 | mv = self.manipulated.value 112 | cv = self.controlled.value = self.system.update(t, mv) 113 | 114 | score = 1 - min(abs(sp - cv), LIMIT)/LIMIT 115 | 116 | self.score += score 117 | 118 | self.scoretext.value = f'{self.score:0.1f}' 119 | 120 | self.ts.append(t) 121 | self.sps.append(sp) 122 | self.mvs.append(mv) 123 | self.cvs.append(cv) 124 | 125 | 126 | def ui(self): 127 | return VBox([ 128 | HBox([self.startbutton, self.scoretext]), 129 | self.timeprogress, 130 | self.setpoint, 131 | self.controlled, 132 | self.manipulated, 133 | ]) 134 | 135 | 136 | def plot(self): 137 | fig, [axcv, axmv] = plt.subplots(2, 1) 138 | axcv.plot(self.ts, self.sps, self.ts, self.cvs) 139 | axmv.plot(self.ts, self.mvs) 140 | -------------------------------------------------------------------------------- /2_Control/2_Laplace_domain_analysis_of_control_systems/Root locus diagrams.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Root locus diagrams\n", 8 | "\n", 9 | "Root locus diagrams show where the roots of the characteristic equation lie for different values of controller gain. The control library has a built-in function for plotting these diagrams." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import control" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from matplotlib import pyplot as plt" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 3, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "%matplotlib inline" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 4, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "s = control.tf([1, 0], 1)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 9, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "def rlocus(order, tau_p, K):\n", 55 | " Gp = 1/(tau_p*s + 1)**order\n", 56 | " Gc = 1\n", 57 | "\n", 58 | " L = Gp*Gc\n", 59 | " CL = K*L/(1 + K*L)\n", 60 | " control.root_locus(L);\n", 61 | " control.pzmap(CL)\n", 62 | " plt.title('')" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 10, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "from ipywidgets import interact" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 11, 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "data": { 81 | "application/vnd.jupyter.widget-view+json": { 82 | "model_id": "7ef539c6f72d40299d04b6919c659bc1", 83 | "version_major": 2, 84 | "version_minor": 0 85 | }, 86 | "text/plain": [ 87 | "interactive(children=(IntSlider(value=3, description='order', max=5, min=1), FloatSlider(value=1.05, descripti…" 88 | ] 89 | }, 90 | "metadata": {}, 91 | "output_type": "display_data" 92 | }, 93 | { 94 | "data": { 95 | "text/plain": [ 96 | "" 97 | ] 98 | }, 99 | "execution_count": 11, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "interact(rlocus, order=(1, 5), tau_p=(0.1, 2.), tau_i=(1., 20), K=(0.01, 200))" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.6.5" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 2 137 | } 138 | -------------------------------------------------------------------------------- /2_Control/3_PID_controller_design_tuning_and_troubleshooting/ITAE parameters for FOPDT system.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# ITAE parameters for FOPDT system\n", 8 | "\n", 9 | "This notebook is a convenient interface to the `tbcontrol.fopdtitae` module, which calculates the values of the PI/PID controller settings based on Table 11.3 of Seborg, Edgar, Melichamp and Lewin (itself based on Smith and Corripio, 1997)." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from tbcontrol import fopdtitae" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "We can get the parameters using the function `fopdtitae.parameters`:\n", 26 | "The default is disturbance parameters on a PI controller." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "[0.859, 1.4836795252225519]" 38 | ] 39 | }, 40 | "execution_count": 2, 41 | "metadata": {}, 42 | "output_type": "execute_result" 43 | } 44 | ], 45 | "source": [ 46 | "fopdtitae.parameters(K=1, tau=1, theta=1)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "# Interactive version" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "We'll build an interactive version by printing the parameters with their names and allowing for easy entry." 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 3, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "from ipywidgets import interact, FloatText" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 4, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "names = 'Kc', 'τI', 'τD'" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "This is the function which does the calculations. You can check the values in Example 11.5:" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 5, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def tablefunction(K, tau, theta=1.07, type_of_input='Disturbance', type_of_controller='PI'):\n", 95 | " parameters = fopdtitae.parameters(K, tau, theta, type_of_input, type_of_controller)\n", 96 | " for name, value in zip(names, parameters):\n", 97 | " print(name, \"=\", value)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 6, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Kc = 2.9719324064107253\n", 110 | "τI = 2.745987615154182\n" 111 | ] 112 | } 113 | ], 114 | "source": [ 115 | "tablefunction(1.54, 5.93, 1.07, 'Disturbance', 'PI')" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 7, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "data": { 125 | "application/vnd.jupyter.widget-view+json": { 126 | "model_id": "163384361f3c4f35b767b60a2d39a7bb", 127 | "version_major": 2, 128 | "version_minor": 0 129 | }, 130 | "text/plain": [ 131 | "interactive(children=(FloatText(value=1.54, description='K'), FloatText(value=5.93, description='tau'), FloatT…" 132 | ] 133 | }, 134 | "metadata": {}, 135 | "output_type": "display_data" 136 | } 137 | ], 138 | "source": [ 139 | "interact(tablefunction,\n", 140 | " K=FloatText(value=1.54), tau=FloatText(value=5.93), theta=FloatText(value=1.07),\n", 141 | " type_of_input=['Disturbance', 'Set point'], type_of_controller=['PI', 'PID']);" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [] 150 | } 151 | ], 152 | "metadata": { 153 | "kernelspec": { 154 | "display_name": "Python 3", 155 | "language": "python", 156 | "name": "python3" 157 | }, 158 | "language_info": { 159 | "codemirror_mode": { 160 | "name": "ipython", 161 | "version": 3 162 | }, 163 | "file_extension": ".py", 164 | "mimetype": "text/x-python", 165 | "name": "python", 166 | "nbconvert_exporter": "python", 167 | "pygments_lexer": "ipython3", 168 | "version": "3.7.1" 169 | } 170 | }, 171 | "nbformat": 4, 172 | "nbformat_minor": 2 173 | } 174 | -------------------------------------------------------------------------------- /2_Control/4_Frequency_domain_analysis_of_control_systems/Frequency domain stability.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Stability in the frequency domain\n", 8 | "\n", 9 | "The frequency domain allows us to find the stability of closed loop systems using only open loop transfer functions and simple operations.\n", 10 | "\n", 11 | "This material is also covered in [this video](https://youtu.be/3eYU8qIkp64). The GeoGebra sheet is available [here](https://ggbm.at/cV8QmwXZ)." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy\n", 21 | "from matplotlib import pyplot as plt\n", 22 | "%matplotlib inline" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "Locating poles and zeros of a complex function\n", 30 | "-------------------------------\n", 31 | "\n", 32 | "Let's construct a complex transfer function by specifying the poles, zeros and gain separately." 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 2, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "zeros = [1]\n", 42 | "poles = [-1 + 1j, -1 - 1j]\n", 43 | "gain = 1" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "from numpy.polynomial.polynomial import polyvalfromroots" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "def G(s):\n", 62 | " return gain*polyvalfromroots(s, zeros)/polyvalfromroots(s, poles)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "It will be useful for us to be able to plot a complex curve easily" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 5, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "def plotcomplex(curve, color='blue', marker=None):\n", 79 | " plt.plot(numpy.real(curve), numpy.imag(curve), color=color, marker=marker)" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 6, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "def plotpz():\n", 89 | " for p in poles:\n", 90 | " plotcomplex(p, color='red', marker='x')\n", 91 | " for z in zeros:\n", 92 | " plotcomplex(z, color='red', marker='o')" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "This function will change the axes to be a cross through the origin and have an equal aspect ratio (so that a circle appears as a circle)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 7, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "from tbcontrol.plotting import cross_axis" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "Let's construct a circular contour and see how the image of the contour moves around as the contour moves around. The image is $G(s)$ as $s$ goes through a countour" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 8, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "from ipywidgets import interact" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 9, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "def plotsituation(contour):\n", 134 | " plotcomplex(contour)\n", 135 | " plotcomplex(G(contour), color='red')\n", 136 | " plotpz()\n", 137 | " cross_axis()" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 10, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "theta = numpy.linspace(0, 2*numpy.pi, 1000)" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 11, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "def argumentprinciple(centerreal=(-2., 2.), centerimag=(-2., 2.), radius=(0.5, 3)):\n", 156 | " contour = radius*numpy.exp(1j*theta) + centerreal + 1j*centerimag\n", 157 | " plotsituation(contour) " 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "interact(argumentprinciple)" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "You should be able to verify the **Cauchy argument principle** using the interaction above:\n", 174 | "\n", 175 | "As $s$ describes a simple contour enclosing $N_p$ poles and $N_z$ zeros, the image $G(s)$ encircles the origin $w = N_z - N_p$ times. $w$ is the [winding number](https://en.wikipedia.org/wiki/Winding_number)." 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "metadata": {}, 181 | "source": [ 182 | "Closed loop stability\n", 183 | "---------------------\n", 184 | "Normally we will be looking at transfer functions of the form\n", 185 | "\n", 186 | "$$\\frac{GK}{1 + GK}$$\n", 187 | "\n", 188 | "So we will want to check if the denominator of the above $(1 + GK)$ has roots in the RHP. To do this we can construct a special contour called the Nyquist D contour which encloses the whole of the RHP. It starts at the origin, then goes up to infinity, circles around at infinite distance from the origin in a clockwise direction, and then comes back up the imaginary axis. For most functions, the part at infinity just maps $1 + GK$ to 1 as $GK$ goes to zero as s goes to infinity.\n", 189 | "\n", 190 | " " 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "omega = numpy.logspace(-2, 2, 1000)\n", 200 | "Dcontour = numpy.concatenate([1j*omega, -1j*omega[::-1]]) # We're ignoring the infinite arc" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "Let's assume that $K=1$ and check if our system will be closed loop stable" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "K = 1" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "plotcomplex(K*G(Dcontour) + 1)\n", 226 | "cross_axis(size=2)" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "Counting encirclements of the origin of $1 + GK$ is the same as counting encirclements of $-1$ by $GK$:" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "def nyquistplot(K):\n", 243 | " plotcomplex(K*G(Dcontour))\n", 244 | " plotcomplex(-1, color='red', marker='o')\n", 245 | " cross_axis(size=2)" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": null, 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [ 254 | "nyquistplot(K=1)" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "This enables us to reason easily about the effect of the controller gain on stability:" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "interact(nyquistplot, K=(0.5, 5.))" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "Nyquist stability criterion\n", 278 | "---------------------------\n", 279 | "\n", 280 | "Let $N_P$ be the number of poles of KG(s) encircled by the D contour and $N_Z$ be the number of zeros of $1+KG(s)$ encircled by the D contour. $N_Z$ is the number of poles of the closed loop system in the right half plane. The resultant image shall encircle (clock-wise) the point $(-1+j0)$ $w$ times such that $w = N_Z - N_P$.\n", 281 | "\n", 282 | "For a stable $G$ this boils down to spotting when the Nyquist plot encircles the -1 point." 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "Bode stability criterion\n", 290 | "------------------------\n", 291 | "\n", 292 | "Nyquist plots are hard to draw by hand, though, so we often use the Bode stability criterion instead. This works by noticing that, in order for the Nyquist graph to encircle the -1 point, the phase angle must reach -180 ° and the magnitude must be bigger than 1. We can draw a Bode diagram and a Nyquist diagram next to each other to see the effect of changing gains." 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": null, 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [ 301 | "def bodeplot(K):\n", 302 | " fig = plt.figure(figsize=(10,5))\n", 303 | " \n", 304 | " ax_gain = plt.subplot2grid((2, 2), (0, 0))\n", 305 | " ax_phase = plt.subplot2grid((2, 2), (1, 0))\n", 306 | " ax_complex = plt.subplot2grid((2, 2), (0, 1), rowspan=2)\n", 307 | " \n", 308 | " freqresp = K*G(1j*omega)\n", 309 | " \n", 310 | " ax_gain.loglog(omega, numpy.abs(freqresp))\n", 311 | " ax_gain.axhline(1, color='orange')\n", 312 | " ax_gain.set_ylim([0.1, 10])\n", 313 | " ax_gain.set_ylabel('|G|')\n", 314 | "\n", 315 | " ax_phase.semilogx(omega, numpy.unwrap(numpy.angle(freqresp)) - numpy.angle(freqresp[0])) # We know the angle should start at 0\n", 316 | " ax_phase.axhline(-numpy.pi, color='green')\n", 317 | " ax_phase.set_ylabel('∠G / rad')\n", 318 | " ax_phase.set_xlabel('ω / (rad/s)')\n", 319 | " \n", 320 | " plt.sca(ax_complex)\n", 321 | " nyquistplot(K)\n", 322 | " \n", 323 | " circle = numpy.exp(-1j*numpy.linspace(0, numpy.pi*2))\n", 324 | " ax_complex.plot(circle.real, circle.imag, color='orange')\n", 325 | " ax_complex.plot([-2, 0], [0, 0], color='green', linewidth=4, alpha=1, zorder=-1)" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": null, 331 | "metadata": {}, 332 | "outputs": [], 333 | "source": [ 334 | "interact(bodeplot, K=(0.5, 5.))" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": null, 340 | "metadata": {}, 341 | "outputs": [], 342 | "source": [] 343 | } 344 | ], 345 | "metadata": { 346 | "anaconda-cloud": {}, 347 | "kernelspec": { 348 | "display_name": "Python 3", 349 | "language": "python", 350 | "name": "python3" 351 | }, 352 | "language_info": { 353 | "codemirror_mode": { 354 | "name": "ipython", 355 | "version": 3 356 | }, 357 | "file_extension": ".py", 358 | "mimetype": "text/x-python", 359 | "name": "python", 360 | "nbconvert_exporter": "python", 361 | "pygments_lexer": "ipython3", 362 | "version": "3.7.6" 363 | } 364 | }, 365 | "nbformat": 4, 366 | "nbformat_minor": 1 367 | } 368 | -------------------------------------------------------------------------------- /2_Control/7_Multivariable_control/Decoupling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Decoupling\n", 8 | "Given a general multivariable system with transfer function matrix $G_p$, a decoupler attempts to combine with the system to form a diagonal whole.\n", 9 | "\n", 10 | "" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import sympy\n", 20 | "sympy.init_printing()" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [ 28 | { 29 | "data": { 30 | "text/latex": [ 31 | "$$\\left[\\begin{matrix}G_{p11} & G_{p12}\\\\G_{p21} & G_{p22}\\end{matrix}\\right]$$" 32 | ], 33 | "text/plain": [ 34 | "⎡Gₚ₁₁ Gₚ₁₂⎤\n", 35 | "⎢ ⎥\n", 36 | "⎣Gₚ₂₁ Gₚ₂₂⎦" 37 | ] 38 | }, 39 | "execution_count": 2, 40 | "metadata": {}, 41 | "output_type": "execute_result" 42 | } 43 | ], 44 | "source": [ 45 | "G_p11, G_p12, G_p21, G_p22 = sympy.symbols('G_p11, G_p12, G_p21, G_p22')\n", 46 | "G_p = sympy.Matrix([[G_p11, G_p12],[G_p21, G_p22]])\n", 47 | "G_p" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### 1. Inverse-based\n", 55 | "Wouldn't it be nice if the system didn't have interaction? In other words, we could choose $T$ such that we have this system with the same diagonal elements as the original system but zeros in the off diagonals." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "text/latex": [ 66 | "$$\\left[\\begin{matrix}G_{p11} & 0\\\\0 & G_{p22}\\end{matrix}\\right]$$" 67 | ], 68 | "text/plain": [ 69 | "⎡Gₚ₁₁ 0 ⎤\n", 70 | "⎢ ⎥\n", 71 | "⎣ 0 Gₚ₂₂⎦" 72 | ] 73 | }, 74 | "execution_count": 3, 75 | "metadata": {}, 76 | "output_type": "execute_result" 77 | } 78 | ], 79 | "source": [ 80 | "G_s = sympy.Matrix([[G_p11, 0],[0, G_p22]])\n", 81 | "G_s" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "Recalling that the combination of $T$ and $G_p$ in series is $G_p T$, we can solve for the decoupler directly\n", 89 | "\n", 90 | "$$ G_P T = G_s \\therefore T = G_P^{-1} G_s $$" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 5, 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "data": { 100 | "text/latex": [ 101 | "$$\\left[\\begin{matrix}\\frac{G_{p11} G_{p22}}{G_{p11} G_{p22} - G_{p12} G_{p21}} & - \\frac{G_{p12} G_{p22}}{G_{p11} G_{p22} - G_{p12} G_{p21}}\\\\- \\frac{G_{p11} G_{p21}}{G_{p11} G_{p22} - G_{p12} G_{p21}} & \\frac{G_{p11} G_{p22}}{G_{p11} G_{p22} - G_{p12} G_{p21}}\\end{matrix}\\right]$$" 102 | ], 103 | "text/plain": [ 104 | "⎡ Gₚ₁₁⋅Gₚ₂₂ -Gₚ₁₂⋅Gₚ₂₂ ⎤\n", 105 | "⎢───────────────────── ─────────────────────⎥\n", 106 | "⎢Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁ Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁⎥\n", 107 | "⎢ ⎥\n", 108 | "⎢ -Gₚ₁₁⋅Gₚ₂₁ Gₚ₁₁⋅Gₚ₂₂ ⎥\n", 109 | "⎢───────────────────── ─────────────────────⎥\n", 110 | "⎣Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁ Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁⎦" 111 | ] 112 | }, 113 | "execution_count": 5, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "T = G_p.inv()*G_s\n", 120 | "T" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "Let's see if that worked:" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/latex": [ 138 | "$$\\left[\\begin{matrix}G_{p11} & 0\\\\0 & G_{p22}\\end{matrix}\\right]$$" 139 | ], 140 | "text/plain": [ 141 | "⎡Gₚ₁₁ 0 ⎤\n", 142 | "⎢ ⎥\n", 143 | "⎣ 0 Gₚ₂₂⎦" 144 | ] 145 | }, 146 | "execution_count": 6, 147 | "metadata": {}, 148 | "output_type": "execute_result" 149 | } 150 | ], 151 | "source": [ 152 | "G_pT = G_p*T\n", 153 | "sympy.simplify(G_pT)" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "Pros:\n", 161 | "\n", 162 | "* Controller design can be based on open loop model\n", 163 | "* Apparent dynamics (what the controller sees) are simple\n", 164 | "\n", 165 | "Cons:\n", 166 | "\n", 167 | "* T is often not physically realisable\n", 168 | "* T is complicated" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "### 2. Zero off-diagonals\n", 176 | "A more common strategy is to solve directly for the off-diagonal elements of set equal to zero.\n", 177 | "\n", 178 | "So we just want \n", 179 | "\n", 180 | "$$G_P T = \\begin{bmatrix}d_1&0\\\\0&d_2\\end{bmatrix}$$\n", 181 | "\n", 182 | "Note the difference between the first method and this one - here we are not specifying the diagonal at all, we just want the off-diagonals to be zero." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 7, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "T21, T12 = sympy.symbols('T21, T12')\n", 192 | "T = sympy.Matrix([[1, T12], \n", 193 | " [T21, 1]])\n", 194 | "\n", 195 | "wantdiagonal = G_p*T\n", 196 | "\n", 197 | "sol = sympy.solve([wantdiagonal[0,1], wantdiagonal[1, 0]], [T21, T12])" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 8, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/latex": [ 208 | "$$\\left[\\begin{matrix}1 & - \\frac{G_{p12}}{G_{p11}}\\\\- \\frac{G_{p21}}{G_{p22}} & 1\\end{matrix}\\right]$$" 209 | ], 210 | "text/plain": [ 211 | "⎡ -Gₚ₁₂ ⎤\n", 212 | "⎢ 1 ──────⎥\n", 213 | "⎢ Gₚ₁₁ ⎥\n", 214 | "⎢ ⎥\n", 215 | "⎢-Gₚ₂₁ ⎥\n", 216 | "⎢────── 1 ⎥\n", 217 | "⎣ Gₚ₂₂ ⎦" 218 | ] 219 | }, 220 | "execution_count": 8, 221 | "metadata": {}, 222 | "output_type": "execute_result" 223 | } 224 | ], 225 | "source": [ 226 | "T.subs(sol)" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "So this is the classic/traditional decoupler shown in the diagram (with unit passthrough on the diagonals). This changes the transfer function the controller \"sees\" to" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 9, 239 | "metadata": {}, 240 | "outputs": [ 241 | { 242 | "data": { 243 | "text/latex": [ 244 | "$$\\left[\\begin{matrix}G_{p11} - \\frac{G_{p12} G_{p21}}{G_{p22}} & 0\\\\0 & G_{p22} - \\frac{G_{p12} G_{p21}}{G_{p11}}\\end{matrix}\\right]$$" 245 | ], 246 | "text/plain": [ 247 | "⎡ Gₚ₁₂⋅Gₚ₂₁ ⎤\n", 248 | "⎢Gₚ₁₁ - ───────── 0 ⎥\n", 249 | "⎢ Gₚ₂₂ ⎥\n", 250 | "⎢ ⎥\n", 251 | "⎢ Gₚ₁₂⋅Gₚ₂₁⎥\n", 252 | "⎢ 0 Gₚ₂₂ - ─────────⎥\n", 253 | "⎣ Gₚ₁₁ ⎦" 254 | ] 255 | }, 256 | "execution_count": 9, 257 | "metadata": {}, 258 | "output_type": "execute_result" 259 | } 260 | ], 261 | "source": [ 262 | "G_p*T.subs(sol)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "Pros:\n", 270 | "\n", 271 | "* Relatively simple design process\n", 272 | "* Less complicated decoupler than the inverse-based method\n", 273 | "\n", 274 | "Cons:\n", 275 | "\n", 276 | "* Apparent plant may be higher order than the actual plant\n", 277 | "* Still requires an inverse, may not be physically realisable (but more likely than method 1)" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "### 3. Adjugate method\n", 285 | "The adjugate (previously calld the adjoint) of a matrix will also diagonalise a system" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 10, 291 | "metadata": {}, 292 | "outputs": [ 293 | { 294 | "data": { 295 | "text/latex": [ 296 | "$$\\left[\\begin{matrix}G_{p22} & - G_{p12}\\\\- G_{p21} & G_{p11}\\end{matrix}\\right]$$" 297 | ], 298 | "text/plain": [ 299 | "⎡Gₚ₂₂ -Gₚ₁₂⎤\n", 300 | "⎢ ⎥\n", 301 | "⎣-Gₚ₂₁ Gₚ₁₁ ⎦" 302 | ] 303 | }, 304 | "execution_count": 10, 305 | "metadata": {}, 306 | "output_type": "execute_result" 307 | } 308 | ], 309 | "source": [ 310 | "T = G_p.adjugate()\n", 311 | "T" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": 11, 317 | "metadata": {}, 318 | "outputs": [ 319 | { 320 | "data": { 321 | "text/latex": [ 322 | "$$\\left[\\begin{matrix}G_{p11} G_{p22} - G_{p12} G_{p21} & 0\\\\0 & G_{p11} G_{p22} - G_{p12} G_{p21}\\end{matrix}\\right]$$" 323 | ], 324 | "text/plain": [ 325 | "⎡Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁ 0 ⎤\n", 326 | "⎢ ⎥\n", 327 | "⎣ 0 Gₚ₁₁⋅Gₚ₂₂ - Gₚ₁₂⋅Gₚ₂₁⎦" 328 | ] 329 | }, 330 | "execution_count": 11, 331 | "metadata": {}, 332 | "output_type": "execute_result" 333 | } 334 | ], 335 | "source": [ 336 | "G_p*T" 337 | ] 338 | }, 339 | { 340 | "cell_type": "markdown", 341 | "metadata": {}, 342 | "source": [ 343 | "Pros:\n", 344 | " \n", 345 | "* Decoupler guaranteed to be physically realisable because it only requires \"forward\" models of the system.\n", 346 | "\n", 347 | "Cons:\n", 348 | "\n", 349 | "* Apparent plant now much higher order (look at the products in the $G_pT$ expression)" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "metadata": {}, 356 | "outputs": [], 357 | "source": [] 358 | } 359 | ], 360 | "metadata": { 361 | "kernelspec": { 362 | "display_name": "Python 3", 363 | "language": "python", 364 | "name": "python3" 365 | }, 366 | "language_info": { 367 | "codemirror_mode": { 368 | "name": "ipython", 369 | "version": 3 370 | }, 371 | "file_extension": ".py", 372 | "mimetype": "text/x-python", 373 | "name": "python", 374 | "nbconvert_exporter": "python", 375 | "pygments_lexer": "ipython3", 376 | "version": "3.7.3" 377 | } 378 | }, 379 | "nbformat": 4, 380 | "nbformat_minor": 2 381 | } 382 | -------------------------------------------------------------------------------- /2_Control/7_Multivariable_control/Eigenvalues and Eigenvectors.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy\n", 10 | "import matplotlib.pyplot as plt" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "%matplotlib inline" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "Eigenvalue problem\n", 27 | "==================" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "Matrix transformaion is written as\n", 35 | "\n", 36 | "$$ \\mathbf{y} = A\\mathbf{x} $$\n", 37 | "\n", 38 | "A different vector in the same direction can be written as scalar multiplication:\n", 39 | "\n", 40 | "$$\\mathbf{y} = \\lambda\\mathbf{x}$$ \n", 41 | "\n", 42 | "Equating these $y$s yields:\n", 43 | "\n", 44 | "$$ A\\mathbf{x} = \\lambda \\mathbf{x} \\Rightarrow (A - \\lambda I) \\mathbf{x} = 0$$\n", 45 | "\n", 46 | "$$\\det(A - \\lambda I) = 0 $$\n", 47 | "\n", 48 | "The eigenvalue problem can also be collected with $\\Lambda$ being a diagonal matrix containing all the eigenvalues and $X$ containing the eigenvectors stacked column-wise. This leads to the eigenvalue decomposition:\n", 49 | "\n", 50 | "$$ A X = X \\Lambda \\Rightarrow A = X \\Lambda X^{-1}$$\n", 51 | "\n", 52 | "with\n", 53 | "\n", 54 | "$$ \\Lambda = diag(\\lambda_i)$$\n", 55 | "\n", 56 | "If we try to find a similar decomposition with different constraints, we can write\n", 57 | "\n", 58 | "$$ A = U D V^{H} $$\n", 59 | "\n", 60 | "If $D$ is a diagonal matrix and $U$ and $V$ are [unitary](http://en.wikipedia.org/wiki/Unitary_matrix), this is the singular value decomposition.\n", 61 | "\n", 62 | "In Skogestad \n", 63 | "\n", 64 | "$$ A = U \\Sigma V^{H} $$\n", 65 | "\n", 66 | "$$ \\Sigma = diag(\\sigma_i)$$\n" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "from ipywidgets import interact" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 4, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "def plotvector(x, color='blue'):\n", 85 | " plt.plot([0, x[0,0]], [0, x[1,0]], color=color)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 5, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "import matplotlib.patches as patches" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "Let's investigate the properties of this matrix:" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 77, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "A = numpy.matrix([[4, 3],\n", 111 | " [2, 1]])" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "The eigenvectors and eigenvalues can be calculated as follows. We also calculate the output vectors associated with a unit vector input in the eigenvector directions." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 78, 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "data": { 128 | "text/plain": [ 129 | "matrix([[4, 3],\n", 130 | " [2, 1]])" 131 | ] 132 | }, 133 | "execution_count": 78, 134 | "metadata": {}, 135 | "output_type": "execute_result" 136 | } 137 | ], 138 | "source": [ 139 | "A" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 79, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "v = numpy.asmatrix(numpy.random.random(2)).T" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 80, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "text/plain": [ 159 | "matrix([[0.89474813],\n", 160 | " [0.44657115]])" 161 | ] 162 | }, 163 | "execution_count": 80, 164 | "metadata": {}, 165 | "output_type": "execute_result" 166 | } 167 | ], 168 | "source": [ 169 | "v = A*v\n", 170 | "\n", 171 | "v = v/numpy.linalg.norm(v)\n", 172 | "v" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 81, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "lambdas, eigvectors = numpy.linalg.eig(A)\n", 182 | "ev1 = lambdas[0]*eigvectors[:, 0]\n", 183 | "ev2 = lambdas[1]*eigvectors[:, 1]" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "The singular values determine the main axes of the translation ellipse of the matrix. Note that the `numpy.linalg.svd` function returns the conjugate transpose of the input direction matrix." 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 82, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "U, S, VH = numpy.linalg.svd(A)\n", 200 | "V = VH.H\n", 201 | "ellipseangle = numpy.rad2deg(numpy.angle(complex(*U[:, 0])))" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 83, 207 | "metadata": {}, 208 | "outputs": [ 209 | { 210 | "data": { 211 | "application/vnd.jupyter.widget-view+json": { 212 | "model_id": "412b4de2897149439ff0ce2a158eb4df", 213 | "version_major": 2, 214 | "version_minor": 0 215 | }, 216 | "text/html": [ 217 | "

Failed to display Jupyter Widget of type interactive.

\n", 218 | "

\n", 219 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 220 | " that the widgets JavaScript is still loading. If this message persists, it\n", 221 | " likely means that the widgets JavaScript library is either not installed or\n", 222 | " not enabled. See the Jupyter\n", 223 | " Widgets Documentation for setup instructions.\n", 224 | "

\n", 225 | "

\n", 226 | " If you're reading this message in another frontend (for example, a static\n", 227 | " rendering on GitHub or NBViewer),\n", 228 | " it may mean that your frontend doesn't currently support widgets.\n", 229 | "

\n" 230 | ], 231 | "text/plain": [ 232 | "interactive(children=(FloatSlider(value=5.5, description='scale', max=10.0, min=1.0), FloatSlider(value=3.141592653589793, description='theta', max=6.283185307179586), Output()), _dom_classes=('widget-interact',))" 233 | ] 234 | }, 235 | "metadata": {}, 236 | "output_type": "display_data" 237 | }, 238 | { 239 | "data": { 240 | "text/plain": [ 241 | "" 242 | ] 243 | }, 244 | "execution_count": 83, 245 | "metadata": {}, 246 | "output_type": "execute_result" 247 | } 248 | ], 249 | "source": [ 250 | "def interactive(scale, theta):\n", 251 | " x = numpy.matrix([[numpy.cos(theta)], [numpy.sin(theta)]])\n", 252 | " y = A*x\n", 253 | "\n", 254 | " plotvector(x)\n", 255 | " plotvector(y, color='red')\n", 256 | " plotvector(ev1, 'green')\n", 257 | " plotvector(ev2, 'green')\n", 258 | " plotvector(V[:, 0], 'magenta')\n", 259 | " plotvector(V[:, 1], 'magenta')\n", 260 | " plt.gca().add_artist(patches.Circle([0, 0], 1, \n", 261 | " color='blue', \n", 262 | " alpha=0.1))\n", 263 | " plt.gca().add_artist(patches.Ellipse([0, 0], S[0]*2, S[1]*2, \n", 264 | " ellipseangle,\n", 265 | " color='red',\n", 266 | " alpha=0.1))\n", 267 | " plt.axis([-scale, scale, -scale, scale])\n", 268 | " plt.axes().set_aspect('equal')\n", 269 | " plt.show()\n", 270 | "interact(interactive, scale=(1., 10), theta=(0., numpy.pi*2))" 271 | ] 272 | } 273 | ], 274 | "metadata": { 275 | "kernelspec": { 276 | "display_name": "Python 3", 277 | "language": "python", 278 | "name": "python3" 279 | }, 280 | "language_info": { 281 | "codemirror_mode": { 282 | "name": "ipython", 283 | "version": 3 284 | }, 285 | "file_extension": ".py", 286 | "mimetype": "text/x-python", 287 | "name": "python", 288 | "nbconvert_exporter": "python", 289 | "pygments_lexer": "ipython3", 290 | "version": "3.6.4" 291 | }, 292 | "widgets": { 293 | "state": { 294 | "89b320a62be44ebfa2ca3bd3e8610688": { 295 | "views": [ 296 | { 297 | "cell_index": 13 298 | } 299 | ] 300 | } 301 | }, 302 | "version": "1.2.0" 303 | } 304 | }, 305 | "nbformat": 4, 306 | "nbformat_minor": 2 307 | } 308 | -------------------------------------------------------------------------------- /2_Control/7_Multivariable_control/Multivariable Pairing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Multivariable pairing (RGA)\n", 8 | "\n", 9 | "For a 2$\\times$2 system, we have 2 choices of pairing variables for distributed control:\n", 10 | "\n", 11 | "\n", 12 | "\n", 13 | "\n", 14 | "\n", 15 | "
DiagonalOff-diagonal
$$G_{cd} = \\left[\\begin{array}{cc} G_{c1} & 0 \\\\ 0 & G_{c2} \\end{array}\\right]$$$$G_{co} = \\left[\\begin{array}{cc} 0 & G_{c2} \\\\ G_{c1} & 0 \\end{array}\\right]$$
" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "Bristol developed the Relative Gain Array to determine good pairings based on only the plant transfer function matrix $G_p$. The elements of the RGA are defined as\n", 23 | "\n", 24 | "$$\\lambda_{ij} \\triangleq \\frac{(\\partial y_i/\\partial u_j)_u}{(\\partial y_i/\\partial u_j)_y}= \\frac{\\text{open loop gain}}{\\text{closed loop gain}}$$" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "We could build $\\Lambda$ by direct evaluation of the above derivatives near some point given a time-domain model, but if we already have a transfer function model, we can evaluate the steady-state gain matrix $K$ by using the final value theorem." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 1, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "import sympy\n", 41 | "sympy.init_printing()" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "s = sympy.Symbol('s')" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "def fopdt(k, theta, tau):\n", 60 | " return k*sympy.exp(-theta*s)/(tau*s + 1)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "Using the system from example 16.5" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 4, 73 | "metadata": {}, 74 | "outputs": [ 75 | { 76 | "data": { 77 | "text/latex": [ 78 | "$$\\left[\\begin{matrix}- \\frac{2 e^{- s}}{10 s + 1} & \\frac{1.5 e^{- s}}{s + 1}\\\\\\frac{1.5 e^{- s}}{s + 1} & \\frac{2 e^{- s}}{10 s + 1}\\end{matrix}\\right]$$" 79 | ], 80 | "text/plain": [ 81 | "⎡ -s -s ⎤\n", 82 | "⎢-2⋅ℯ 1.5⋅ℯ ⎥\n", 83 | "⎢──────── ─────── ⎥\n", 84 | "⎢10⋅s + 1 s + 1 ⎥\n", 85 | "⎢ ⎥\n", 86 | "⎢ -s -s ⎥\n", 87 | "⎢1.5⋅ℯ 2⋅ℯ ⎥\n", 88 | "⎢─────── ────────⎥\n", 89 | "⎣ s + 1 10⋅s + 1⎦" 90 | ] 91 | }, 92 | "execution_count": 4, 93 | "metadata": {}, 94 | "output_type": "execute_result" 95 | } 96 | ], 97 | "source": [ 98 | "G_p = sympy.Matrix([[fopdt(-2, 1, 10), fopdt(1.5, 1, 1)],\n", 99 | " [fopdt(1.5, 1, 1), fopdt(2, 1, 10)]])\n", 100 | "G_p" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "Unfortunately sympy cannot calculate limits on matrix expressions" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "#K = sympy.limit(G_p, s, 0)" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "But we can apply a function to the elements:" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 6, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "def gain(G):\n", 133 | " return sympy.limit(G, s, 0)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 7, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "K = G_p.applyfunc(gain)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 8, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "data": { 152 | "text/latex": [ 153 | "$$\\left[\\begin{matrix}-2 & 1.5\\\\1.5 & 2\\end{matrix}\\right]$$" 154 | ], 155 | "text/plain": [ 156 | "⎡-2 1.5⎤\n", 157 | "⎢ ⎥\n", 158 | "⎣1.5 2 ⎦" 159 | ] 160 | }, 161 | "execution_count": 8, 162 | "metadata": {}, 163 | "output_type": "execute_result" 164 | } 165 | ], 166 | "source": [ 167 | "K" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "We can then calculate $\\Lambda = K \\otimes H$ where $H = (K^{-1})^{T}$:" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 9, 180 | "metadata": {}, 181 | "outputs": [ 182 | { 183 | "data": { 184 | "text/latex": [ 185 | "$$\\left[\\begin{matrix}0.64 & 0.36\\\\0.36 & 0.64\\end{matrix}\\right]$$" 186 | ], 187 | "text/plain": [ 188 | "⎡0.64 0.36⎤\n", 189 | "⎢ ⎥\n", 190 | "⎣0.36 0.64⎦" 191 | ] 192 | }, 193 | "execution_count": 9, 194 | "metadata": {}, 195 | "output_type": "execute_result" 196 | } 197 | ], 198 | "source": [ 199 | "Lambda = K.multiply_elementwise(K.inv().transpose())\n", 200 | "Lambda" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "We can do the same calculation (faster) using numpy:" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 10, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "import numpy" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 11, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "def fopdt(k, theta, tau):\n", 226 | " return k*numpy.exp(-theta*s)/(tau*s + 1)" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 12, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "s = 0" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 13, 241 | "metadata": {}, 242 | "outputs": [], 243 | "source": [ 244 | "K = numpy.matrix([[fopdt(-2, 1, 10), fopdt(1.5, 1, 1)],\n", 245 | " [fopdt(1.5, 1, 1), fopdt(2, 1, 10)]])" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "The `.A` attribute in matrices is the matrix as a `numpy.array`, which multiplies elementwise by default." 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": 14, 258 | "metadata": {}, 259 | "outputs": [ 260 | { 261 | "data": { 262 | "text/plain": [ 263 | "array([[0.64, 0.36],\n", 264 | " [0.36, 0.64]])" 265 | ] 266 | }, 267 | "execution_count": 14, 268 | "metadata": {}, 269 | "output_type": "execute_result" 270 | } 271 | ], 272 | "source": [ 273 | "K.A*K.I.T.A" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "The numpy developers recommended that you should use `numpy.array` instead of `numpy.matrix` as much as possible. I find this makes the notation harder to read:" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 15, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "K = numpy.array([[fopdt(-2, 1, 10), fopdt(1.5, 1, 1)],\n", 290 | " [fopdt(1.5, 1, 1), fopdt(2, 1, 10)]])" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 16, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "data": { 300 | "text/plain": [ 301 | "array([[0.64, 0.36],\n", 302 | " [0.36, 0.64]])" 303 | ] 304 | }, 305 | "execution_count": 16, 306 | "metadata": {}, 307 | "output_type": "execute_result" 308 | } 309 | ], 310 | "source": [ 311 | "K*numpy.linalg.inv(K).T" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "metadata": {}, 317 | "source": [ 318 | "### Simulation results\n", 319 | "\n", 320 | "Let's simulate this system to get an idea of how the control works out" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 17, 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "import tbcontrol\n", 330 | "tbcontrol.expectversion('0.1.4')\n", 331 | "from tbcontrol import blocksim" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 18, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "import numpy" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": 19, 346 | "metadata": {}, 347 | "outputs": [], 348 | "source": [ 349 | "N = 2" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 20, 355 | "metadata": {}, 356 | "outputs": [], 357 | "source": [ 358 | "G = {}" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": 21, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [ 367 | "gains = [[-2, 1.5], \n", 368 | " [1.5, 2]]\n", 369 | "taus = [[10, 1], \n", 370 | " [1, 10]]\n", 371 | "delays = [[1, 1], \n", 372 | " [1, 1]]" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 22, 378 | "metadata": {}, 379 | "outputs": [], 380 | "source": [ 381 | "for inp in range(N):\n", 382 | " for outp in range(N):\n", 383 | " G[(outp, inp)] = blocksim.LTI(f\"G_{outp}_{inp}\", f\"u_{inp}\", f\"yp_{inp}_{outp}\", \n", 384 | " gains[outp][inp], [1, taus[outp][inp]], delays[outp][inp])" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": 23, 390 | "metadata": {}, 391 | "outputs": [], 392 | "source": [ 393 | "inputs = {'ysp_0': blocksim.step(),\n", 394 | " 'ysp_1': blocksim.step(starttime=50)}" 395 | ] 396 | }, 397 | { 398 | "cell_type": "code", 399 | "execution_count": 24, 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "sums = {f'y_{outp}': [f\"+yp_{inp}_{outp}\" for inp in range(N)] for outp in range(N)}\n", 404 | "for i in range(N):\n", 405 | " sums[f'e_{i}'] = [f'+ysp_{i}', f'-y_{i}']" 406 | ] 407 | }, 408 | { 409 | "cell_type": "code", 410 | "execution_count": 25, 411 | "metadata": {}, 412 | "outputs": [ 413 | { 414 | "data": { 415 | "text/plain": [ 416 | "{'y_0': ['+yp_0_0', '+yp_1_0'],\n", 417 | " 'y_1': ['+yp_0_1', '+yp_1_1'],\n", 418 | " 'e_0': ['+ysp_0', '-y_0'],\n", 419 | " 'e_1': ['+ysp_1', '-y_1']}" 420 | ] 421 | }, 422 | "execution_count": 25, 423 | "metadata": {}, 424 | "output_type": "execute_result" 425 | } 426 | ], 427 | "source": [ 428 | "sums" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 26, 434 | "metadata": {}, 435 | "outputs": [], 436 | "source": [ 437 | "import matplotlib.pyplot as plt\n", 438 | "%matplotlib inline" 439 | ] 440 | }, 441 | { 442 | "cell_type": "code", 443 | "execution_count": 27, 444 | "metadata": {}, 445 | "outputs": [], 446 | "source": [ 447 | "def simulate(auto1=True, K1=-1, tauI1=10, auto2=True, K2=0.5, tauI2=10):\n", 448 | " controllers = {'Gc_0': blocksim.PI('Gc_0', 'e_0', 'u_0', K1, tauI1),\n", 449 | " 'Gc_1': blocksim.PI('Gc_1', 'e_1', 'u_1', K2, tauI2)}\n", 450 | "\n", 451 | " controllers['Gc_0'].automatic = auto1\n", 452 | " controllers['Gc_1'].automatic = auto2\n", 453 | "\n", 454 | " ts = numpy.arange(0, 100, 0.125)\n", 455 | "\n", 456 | " diagram = blocksim.Diagram(list(G.values()) + list(controllers.values()), sums, inputs)\n", 457 | "\n", 458 | " result = diagram.simulate(ts)\n", 459 | "\n", 460 | "# plt.figure()\n", 461 | "# plt.plot(ts, result['u_0'])\n", 462 | "# plt.plot(ts, result['u_1'])\n", 463 | "\n", 464 | " plt.figure()\n", 465 | " plt.plot(ts, result['y_0'])\n", 466 | " plt.plot(ts, result['ysp_0'])\n", 467 | " plt.plot(ts, result['y_1'])\n", 468 | " plt.plot(ts, result['ysp_1'])" 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": 28, 474 | "metadata": {}, 475 | "outputs": [], 476 | "source": [ 477 | "from ipywidgets import interact" 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": 29, 483 | "metadata": {}, 484 | "outputs": [ 485 | { 486 | "data": { 487 | "application/vnd.jupyter.widget-view+json": { 488 | "model_id": "f544acf5cc3f463091c80c5472d35b15", 489 | "version_major": 2, 490 | "version_minor": 0 491 | }, 492 | "text/plain": [ 493 | "interactive(children=(Dropdown(description='auto1', options=(True, False), value=True), FloatSlider(value=-1.0…" 494 | ] 495 | }, 496 | "metadata": {}, 497 | "output_type": "display_data" 498 | }, 499 | { 500 | "data": { 501 | "text/plain": [ 502 | "" 503 | ] 504 | }, 505 | "execution_count": 29, 506 | "metadata": {}, 507 | "output_type": "execute_result" 508 | } 509 | ], 510 | "source": [ 511 | "interact(simulate, \n", 512 | " auto1=[True, False], K1=(-2., 0), tauI1=(1., 50), \n", 513 | " auto2=[True, False], K2=(0., 2), tauI2=(1., 50))" 514 | ] 515 | } 516 | ], 517 | "metadata": { 518 | "anaconda-cloud": {}, 519 | "kernelspec": { 520 | "display_name": "Python 3", 521 | "language": "python", 522 | "name": "python3" 523 | }, 524 | "language_info": { 525 | "codemirror_mode": { 526 | "name": "ipython", 527 | "version": 3 528 | }, 529 | "file_extension": ".py", 530 | "mimetype": "text/x-python", 531 | "name": "python", 532 | "nbconvert_exporter": "python", 533 | "pygments_lexer": "ipython3", 534 | "version": "3.7.3" 535 | } 536 | }, 537 | "nbformat": 4, 538 | "nbformat_minor": 2 539 | } 540 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: 'tbcontrol: A python library for textbook control problems' 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - given-names: Carl 12 | family-names: Sandrock 13 | email: carl.sandrock@gmail.com 14 | affiliation: University of Pretoria 15 | orcid: 'https://orcid.org/0000-0003-2828-5186' 16 | repository-code: 'https://github.com/alchemyst/Dynamics-and-Control' 17 | abstract: >- 18 | The tbcontrol package collects functions useful to solve 19 | the kinds of problems encountered in undergraduate process 20 | control textbooks. It is the distributable part of a 21 | larger project to develop Jupyter notebooks for dynamics 22 | and control. 23 | 24 | 25 | The repository collects notebooks for the subjects CPN321 26 | (Process Dynamics), and CPB421 (Process Control) at the 27 | Chemical Engineering department of the University of 28 | Pretoria. There is also a Dynamics and Control YouTube 29 | channel with many videos about Dynamics and Control which 30 | also relate to these notebooks. 31 | keywords: 32 | - control 33 | - python 34 | - transfer function 35 | license: GPL-3.0 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = DynamicsControl 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | distclean: 16 | -rm dist/* 17 | 18 | dist: 19 | python3 setup.py sdist bdist_wheel 20 | 21 | upload: 22 | twine upload dist/* 23 | 24 | .PHONY: help Makefile distclean dist upload 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile 29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 30 | -------------------------------------------------------------------------------- /Modelica/Discrete.mo: -------------------------------------------------------------------------------- 1 | model Discrete 2 | Modelica.Blocks.Sources.Ramp ramp1 annotation( 3 | Placement(visible = true, transformation(origin = {-118, 44}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 4 | Modelica.Blocks.Continuous.FirstOrder firstOrder1 annotation( 5 | Placement(visible = true, transformation(origin = {6, 44}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 6 | Modelica.Blocks.Discrete.Sampler sampler1(samplePeriod = 1) annotation( 7 | Placement(visible = true, transformation(origin = {-38, -10}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 8 | Modelica.Blocks.Discrete.ZeroOrderHold zeroOrderHold1(samplePeriod = 1) annotation( 9 | Placement(visible = true, transformation(origin = {56, -10}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 10 | Modelica.Blocks.Discrete.TransferFunction transferFunction1(a = {2, -1}, b = {1, 0}, samplePeriod = 1) annotation( 11 | Placement(visible = true, transformation(origin = {12, -10}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 12 | Modelica.Blocks.Continuous.FirstOrder firstOrder2 annotation( 13 | Placement(visible = true, transformation(origin = {50, -72}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 14 | Modelica.Blocks.Discrete.Sampler sampler2(samplePeriod = 1) annotation( 15 | Placement(visible = true, transformation(origin = {-34, -72}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 16 | Modelica.Blocks.Discrete.ZeroOrderHold zeroOrderHold2(samplePeriod = 1) annotation( 17 | Placement(visible = true, transformation(origin = {6, -72}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 18 | Modelica.Blocks.Discrete.TransferFunction transferFunction2(a = { 1, -exp(-1)}, b = {1 - exp(-1)}, samplePeriod = 1) annotation( 19 | Placement(visible = true, transformation(origin = {42, -126}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 20 | Modelica.Blocks.Discrete.Sampler sampler3(samplePeriod = 1) annotation( 21 | Placement(visible = true, transformation(origin = {90, -72}, extent = {{-10, -10}, {10, 10}}, rotation = 0))); 22 | equation 23 | connect(firstOrder2.y, sampler3.u) annotation( 24 | Line(points = {{61, -72}, {69, -72}, {69, -72}, {79, -72}, {79, -72}, {77, -72}, {77, -72}, {77, -72}}, color = {128, 0, 128}, thickness = 1)); 25 | connect(sampler2.y, transferFunction2.u) annotation( 26 | Line(points = {{-23, -72}, {-17, -72}, {-17, -126}, {30, -126}}, color = {252, 1, 7}, pattern = LinePattern.Dash, thickness = 1)); 27 | connect(sampler2.y, zeroOrderHold2.u) annotation( 28 | Line(points = {{-23, -72}, {-21.875, -72}, {-21.875, -72}, {-20.75, -72}, {-20.75, -72}, {-18.5, -72}, {-18.5, -72}, {-14, -72}, {-14, -72}, {-7, -72}, {-7, -72}, {-6, -72}, {-6, -72}, {-6.5, -72}, {-6.5, -72}, {-6.75, -72}, {-6.75, -72}, {-6.875, -72}, {-6.875, -72}, {-6.9375, -72}, {-6.9375, -72}, {-7, -72}}, color = {252, 1, 7}, pattern = LinePattern.Dash, thickness = 1)); 29 | connect(ramp1.y, firstOrder1.u) annotation( 30 | Line(points = {{-107, 44}, {-6, 44}}, color = {252, 1, 7}, thickness = 1)); 31 | connect(ramp1.y, sampler1.u) annotation( 32 | Line(points = {{-107, 44}, {-85.5, 44}, {-85.5, 44}, {-64, 44}, {-64, -10}, {-57, -10}, {-57, -10}, {-53.5, -10}, {-53.5, -10}, {-50, -10}}, color = {252, 1, 7}, thickness = 1)); 33 | connect(ramp1.y, sampler2.u) annotation( 34 | Line(points = {{-107, 44}, {-79, 44}, {-79, -72}, {-46, -72}}, color = {252, 1, 7}, thickness = 1)); 35 | connect(zeroOrderHold2.y, firstOrder2.u) annotation( 36 | Line(points = {{17, -72}, {20.75, -72}, {20.75, -72}, {22.5, -72}, {22.5, -72}, {28, -72}, {28, -72}, {39, -72}, {39, -72}, {38, -72}, {38, -72}, {37.5, -72}, {37.5, -72}, {37.25, -72}, {37.25, -72}, {37, -72}}, color = {0, 0, 127})); 37 | connect(transferFunction1.y, zeroOrderHold1.u) annotation( 38 | Line(points = {{23, -10}, {34, -10}, {34, -10}, {45, -10}, {45, -10}, {43, -10}, {43, -10}, {43, -10}, {43, -10}, {43, -10}}, color = {0, 0, 127})); 39 | connect(sampler1.y, transferFunction1.u) annotation( 40 | Line(points = {{-27, -10}, {-20.5, -10}, {-20.5, -10}, {-14, -10}, {-14, -10}, {1, -10}, {1, -10}, {3.27826e-07, -10}, {3.27826e-07, -10}, {-1, -10}}, color = {0, 0, 127})); 41 | annotation( 42 | uses(Modelica(version = "3.2.3")), 43 | Diagram(graphics = {Rectangle(origin = {11, -8}, extent = {{-67, 74}, {65, -26}}), Text(origin = {-12, 71}, extent = {{-18, 3}, {56, -5}}, textString = "Discrete approximation (Backward Difference)"), Rectangle(origin = {33, -78}, extent = {{-89, 26}, {79, -68}}), Text(origin = {36, -41}, extent = {{-32, -1}, {38, -11}}, textString = "Sampled equivalent (Table 17.1)"), Line(origin = {106.66, -9.84}, points = {{-41, 0}, {43, 0}}, color = {78, 118, 255}, thickness = 1), Line(origin = {92.24, -126.07}, points = {{-41, 0}, {53, 0}}, color = {128, 0, 128}, pattern = LinePattern.Dash, thickness = 1), Line(origin = {140.195, -71.6426}, points = {{-41, 0}, {3, 0}}, color = {128, 0, 128}, pattern = LinePattern.Dash, thickness = 1), Text(origin = {144, -97}, rotation = 90, extent = {{-32, -1}, {28, -7}}, textString = "Exactly the same"), Text(origin = {152, 17}, rotation = 90, extent = {{-28, -1}, {28, -7}}, textString = "Approximately the same"), Line(origin = {57.2311, 44.2907}, points = {{-41, 0}, {91, 0}}, color = {0, 0, 255}, thickness = 1)}), 44 | experiment(StartTime = 0, StopTime = 10, Tolerance = 1e-6, Interval = 0.02), 45 | __OpenModelica_simulationFlags(lv = "LOG_STATS", outputFormat = "mat", s = "dassl"));end Discrete; -------------------------------------------------------------------------------- /Modelica/FedBatchBioreactorModel.mo: -------------------------------------------------------------------------------- 1 | model FedBatchBioreactorModel 2 | /* This model represents the fed batch bioreactor in section 2.4.9 of 3 | Seborg et al. 4 | 5 | To keep things simple we pretend that the time unit is seconds rather than hours. 6 | */ 7 | parameter Real mu_max = 0.2 "Maximum growth rate"; 8 | parameter Real K_s = 1.0 "Monod constant"; 9 | parameter Real Y_xs = 0.5 "Cell yield coefficient"; 10 | parameter Real Y_px = 0.2 "Product yield coefficient"; 11 | Real X(start=0.05) "Concentration of the cells"; 12 | Real S(start=10) "Concentration of the substrate"; 13 | Real P(start=0) "Concentration of the product"; 14 | Real V(start=1) "Reactor volume"; 15 | parameter Real F=0.05 "Feed rate"; 16 | Real S_f=10 "Concentration of substrate in feed"; 17 | Real mu "Specific growth rate"; 18 | Real rg "Rate of cell growth"; 19 | Real rp "Rate of product formation"; 20 | equation 21 | rg = mu * X; 22 | mu = mu_max * S / (K_s + S); 23 | rp = Y_px * rg; 24 | der(X * V) = V * rg; 25 | der(P * V) = V * rp; 26 | der(S * V) = F * S_f - 1 / Y_xs * V * rg; 27 | der(V) = F; 28 | annotation(experiment(StartTime = 0, StopTime = 30)); 29 | end FedBatchBioreactorModel; 30 | -------------------------------------------------------------------------------- /Modelica/MixingSystem.mo: -------------------------------------------------------------------------------- 1 | model MixingSystem 2 | 3 | // Flow rates, mass / time 4 | Real A, B, C, D, E, F, G, H, I, J, K, L; 5 | // Mass fraction X in each stream 6 | Real xA, xB, xC, xD, xE, xF, xG, xH, xI, xJ, xK, xL; 7 | // For each tank: 8 | Real M1, M2, M3; // Mass 9 | Real x1, x2, x3; // Mass fraction x 10 | Real h1, h2, h3; // Height 11 | Real V1, V2, V3; // Volume 12 | parameter Real A1=3, A2=3, A3=3; // Cross-sectional area 13 | 14 | parameter Real rhox=1000, rhoy=800; // Density of x and Y 15 | 16 | // Setting fixed=false causes these values to be calculated at initialisation 17 | // Discharge coefficients 18 | parameter Real k1(fixed=false), k2(fixed=false); 19 | // Flowrates of the flows with valves 20 | parameter Real Gstart(fixed=false), Hstart(fixed=false), Jstart(fixed=false), Lstart(fixed=false); 21 | 22 | initial equation 23 | 24 | h1 = 1; 25 | h2 = 1; 26 | h3 = 1; 27 | 28 | H = G; 29 | H = 2*J; 30 | J = 2*L; 31 | 32 | der(M1) = 0; 33 | der(M2) = 0; 34 | der(M3) = 0; 35 | 36 | der(x1*M1) = 0; 37 | der(x2*M2) = 0; 38 | der(x3*M3) = 0; 39 | 40 | equation 41 | 42 | // Flowrates in 43 | A = (if time <= 0 then 1 * rhox else 1.5 * rhox); 44 | B = 1 * rhoy; 45 | C = 1 * rhoy; 46 | 47 | // Fractions in 48 | xA = 1; 49 | xB = 0; 50 | xC = 0; 51 | 52 | // Flowrates with valves 53 | // Right now fixed at starting point, but could be changed here 54 | G = Gstart; // For instance try this: G = A + B + C 55 | H = Hstart; 56 | J = Jstart; 57 | L = Lstart; 58 | 59 | // MB over tanks 60 | der(M1) = A + L - D "MB Tank 1"; 61 | der(M2) = B + D + K - E "MB Tank 2"; 62 | der(M3) = C + E + I - F "MB Tank 3"; 63 | 64 | // X balance over tanks 65 | der(x1*M1) = xA*A + xL*L - xD*D "CB Tank 1"; 66 | der(x2*M2) = xB*B + xD*D + xK*K - xE*E "CB Tank 2"; 67 | der(x3*M3) = xC*C + xE*E + xI*I - xF*F "CB Tank 3"; 68 | 69 | //Junctions 70 | F = H + G; 71 | H = I + J; 72 | J = K + L; 73 | 74 | // Fractions are the same for all the junctions 75 | xF = xG; 76 | xF = xH; 77 | xF = xI; 78 | xF = xJ; 79 | xF = xK; 80 | xF = xL; 81 | 82 | // Fractions coming out of tanks 83 | xD = x1; 84 | xE = x2; 85 | xF = x3; 86 | 87 | // Discharge 88 | D = k1 * h1; 89 | E = k2 * h2; 90 | 91 | // Geometry 92 | V1 = A1*h1; 93 | V2 = A2*h2; 94 | V3 = A3*h3; 95 | 96 | // Mass to volume 97 | V1 = M1*(x1/rhox + (1 - x1)/rhoy); 98 | V2 = M2*(x2/rhox + (1 - x2)/rhoy); 99 | V3 = M3*(x3/rhox + (1 - x3)/rhoy); 100 | 101 | annotation( 102 | experiment(StartTime = 0, StopTime = 20, Tolerance = 1e-6, Interval = 0.04)); 103 | 104 | end MixingSystem; 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Python library for solving textbook control problems 2 | 3 | The tbcontrol package collects functions useful to solve the kinds of problems encountered in undergraduate process control textbooks. It is the distributable part of a larger project to develop Jupyter notebooks for dynamics and control. 4 | 5 | The [repository](https://github.com/alchemyst/Dynamics-and-Control) collects notebooks for the subjects CPN321 (Process Dynamics), and CPB421 (Process Control) at the Chemical Engineering department of the University of Pretoria. 6 | There is also a [Dynamics and Control YouTube channel](https://www.youtube.com/channel/UCOf3CPrYPMG4BrgkVPupPqA) with many videos about Dynamics and Control which also relate to these notebooks. 7 | 8 | You can experiment with the notebooks without installing anything using this link: [![Binder](http://mybinder.org/badge.svg)](http://mybinder.org/repo/alchemyst/Dynamics-and-Control) 9 | 10 | The links below will allow you to view the notebooks using the notebook viewer. You can also click on the "Launch Binder" badge above to launch a notebook server which will allow you to run all the code online. 11 | 12 | * [View in Notebook Viewer](https://nbviewer.ipython.org/github/alchemyst/Dynamics-and-Control/blob/master/TOC.ipynb) 13 | * [View on GitHub](https://github.com/alchemyst/Dynamics-and-Control/blob/master/TOC.ipynb) 14 | 15 | We use a GitHub action to check every update made to the repository. 16 | 17 | ![Test status](https://github.com/alchemyst/Dynamics-and-Control/workflows/test.yml/badge.svg?event=push) 18 | 19 | 20 | [![Documentation Status](https://readthedocs.org/projects/dynamics-and-control/badge/?version=latest)](https://dynamics-and-control.readthedocs.io/en/latest/?badge=latest) 21 | πin -------------------------------------------------------------------------------- /Simulation/Classes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Classes" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "You are already familiar with defining your own functions, like this:" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "def f(a):\n", 24 | " return a" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "data": { 34 | "text/plain": [ 35 | "2" 36 | ] 37 | }, 38 | "execution_count": 2, 39 | "metadata": {}, 40 | "output_type": "execute_result" 41 | } 42 | ], 43 | "source": [ 44 | "f(2)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "But what about defining your own types? First, lets remind ourselves of some built-in types:" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "a = 2" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 4, 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "data": { 70 | "text/plain": [ 71 | "int" 72 | ] 73 | }, 74 | "execution_count": 4, 75 | "metadata": {}, 76 | "output_type": "execute_result" 77 | } 78 | ], 79 | "source": [ 80 | "type(a)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "OK, so there is a thing called an `int`." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 5, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "data": { 97 | "text/plain": [ 98 | "int" 99 | ] 100 | }, 101 | "execution_count": 5, 102 | "metadata": {}, 103 | "output_type": "execute_result" 104 | } 105 | ], 106 | "source": [ 107 | "int" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "We can make a new `int` by calling the type name as a function:" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 6, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "text/plain": [ 125 | "0" 126 | ] 127 | }, 128 | "execution_count": 6, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "int()" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "To build our own type, we can use the `class` keyword:" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 7, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "class MyClass:\n", 151 | " pass" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 8, 157 | "metadata": {}, 158 | "outputs": [ 159 | { 160 | "data": { 161 | "text/plain": [ 162 | "__main__.MyClass" 163 | ] 164 | }, 165 | "execution_count": 8, 166 | "metadata": {}, 167 | "output_type": "execute_result" 168 | } 169 | ], 170 | "source": [ 171 | "MyClass" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 9, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "instance = MyClass()" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 10, 186 | "metadata": {}, 187 | "outputs": [ 188 | { 189 | "data": { 190 | "text/plain": [ 191 | "__main__.MyClass" 192 | ] 193 | }, 194 | "execution_count": 10, 195 | "metadata": {}, 196 | "output_type": "execute_result" 197 | } 198 | ], 199 | "source": [ 200 | "type(instance)" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "## What is this good for?" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "metadata": {}, 213 | "source": [ 214 | "The essence of object-oriented programming is allowing for data and algorithms to be combined in one place. Functions allow us to re-use algorithms, but classes allow us to combine them with local data:" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 11, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "class MyClass2:\n", 224 | " def __init__(self, name):\n", 225 | " print(\"I'm in __init__!\")\n", 226 | " self.name = name\n", 227 | " \n", 228 | " def say_my_name(self):\n", 229 | " print(self.name)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 12, 235 | "metadata": {}, 236 | "outputs": [ 237 | { 238 | "name": "stdout", 239 | "output_type": "stream", 240 | "text": [ 241 | "I'm in __init__!\n" 242 | ] 243 | } 244 | ], 245 | "source": [ 246 | "s = MyClass2('Carl')" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 13, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "name": "stdout", 256 | "output_type": "stream", 257 | "text": [ 258 | "Carl\n" 259 | ] 260 | } 261 | ], 262 | "source": [ 263 | "s.say_my_name()" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "## Objects must be \"like\" things" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 14, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "class Person:\n", 280 | " def __init__(self, name):\n", 281 | " self.name = name\n", 282 | "\n", 283 | " def display_name(self):\n", 284 | " print(self.name)" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": 15, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "carl = Person(\"Carl Sandrock\")" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 16, 299 | "metadata": {}, 300 | "outputs": [ 301 | { 302 | "name": "stdout", 303 | "output_type": "stream", 304 | "text": [ 305 | "Carl Sandrock\n" 306 | ] 307 | } 308 | ], 309 | "source": [ 310 | "carl.display_name()" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": null, 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [] 319 | } 320 | ], 321 | "metadata": { 322 | "kernelspec": { 323 | "display_name": "Python 3", 324 | "language": "python", 325 | "name": "python3" 326 | }, 327 | "language_info": { 328 | "codemirror_mode": { 329 | "name": "ipython", 330 | "version": 3 331 | }, 332 | "file_extension": ".py", 333 | "mimetype": "text/x-python", 334 | "name": "python", 335 | "nbconvert_exporter": "python", 336 | "pygments_lexer": "ipython3", 337 | "version": "3.6.4" 338 | } 339 | }, 340 | "nbformat": 4, 341 | "nbformat_minor": 2 342 | } 343 | -------------------------------------------------------------------------------- /Simulation/Special functions in classes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Taking off the engine cover\n", 8 | "---------------------------\n", 9 | "\n", 10 | "How does Python \"know\" how to add two objects? Or what they should look like when printed to the console? Let's dig into the underlying mechanisms that Python provides for this." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "a = 2\n", 20 | "b = 3" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [ 28 | { 29 | "data": { 30 | "text/plain": [ 31 | "5" 32 | ] 33 | }, 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "output_type": "execute_result" 37 | } 38 | ], 39 | "source": [ 40 | "a + b" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "What methods does `a` have?" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 3, 53 | "metadata": {}, 54 | "outputs": [ 55 | { 56 | "data": { 57 | "text/plain": [ 58 | "['__abs__',\n", 59 | " '__add__',\n", 60 | " '__and__',\n", 61 | " '__bool__',\n", 62 | " '__ceil__',\n", 63 | " '__class__',\n", 64 | " '__delattr__',\n", 65 | " '__dir__',\n", 66 | " '__divmod__',\n", 67 | " '__doc__',\n", 68 | " '__eq__',\n", 69 | " '__float__',\n", 70 | " '__floor__',\n", 71 | " '__floordiv__',\n", 72 | " '__format__',\n", 73 | " '__ge__',\n", 74 | " '__getattribute__',\n", 75 | " '__getnewargs__',\n", 76 | " '__gt__',\n", 77 | " '__hash__',\n", 78 | " '__index__',\n", 79 | " '__init__',\n", 80 | " '__init_subclass__',\n", 81 | " '__int__',\n", 82 | " '__invert__',\n", 83 | " '__le__',\n", 84 | " '__lshift__',\n", 85 | " '__lt__',\n", 86 | " '__mod__',\n", 87 | " '__mul__',\n", 88 | " '__ne__',\n", 89 | " '__neg__',\n", 90 | " '__new__',\n", 91 | " '__or__',\n", 92 | " '__pos__',\n", 93 | " '__pow__',\n", 94 | " '__radd__',\n", 95 | " '__rand__',\n", 96 | " '__rdivmod__',\n", 97 | " '__reduce__',\n", 98 | " '__reduce_ex__',\n", 99 | " '__repr__',\n", 100 | " '__rfloordiv__',\n", 101 | " '__rlshift__',\n", 102 | " '__rmod__',\n", 103 | " '__rmul__',\n", 104 | " '__ror__',\n", 105 | " '__round__',\n", 106 | " '__rpow__',\n", 107 | " '__rrshift__',\n", 108 | " '__rshift__',\n", 109 | " '__rsub__',\n", 110 | " '__rtruediv__',\n", 111 | " '__rxor__',\n", 112 | " '__setattr__',\n", 113 | " '__sizeof__',\n", 114 | " '__str__',\n", 115 | " '__sub__',\n", 116 | " '__subclasshook__',\n", 117 | " '__truediv__',\n", 118 | " '__trunc__',\n", 119 | " '__xor__',\n", 120 | " 'bit_length',\n", 121 | " 'conjugate',\n", 122 | " 'denominator',\n", 123 | " 'from_bytes',\n", 124 | " 'imag',\n", 125 | " 'numerator',\n", 126 | " 'real',\n", 127 | " 'to_bytes']" 128 | ] 129 | }, 130 | "execution_count": 3, 131 | "metadata": {}, 132 | "output_type": "execute_result" 133 | } 134 | ], 135 | "source": [ 136 | "dir(a)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "All those methods with `__` on both sides are methods that are not normally shown in the tab completion list for the object. They are often called \"dunder\" methods for brevity, so you would say \"dunder abs\" for `__abs__`. In the documentation for Python these are called [\"special methods\"](https://docs.python.org/3/reference/datamodel.html#special-method-names).\n", 144 | "\n", 145 | "Special methods are how some fundamental properties of objects are implemented in Python. For instance, you can safely imagine that `a + b` is translated to" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 4, 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "data": { 155 | "text/plain": [ 156 | "5" 157 | ] 158 | }, 159 | "execution_count": 4, 160 | "metadata": {}, 161 | "output_type": "execute_result" 162 | } 163 | ], 164 | "source": [ 165 | "a.__add__(b)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "This is the mechanism by which different kinds of objects can do very different kinds of things when `+` is used on them:" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "a = '2'\n", 182 | "b = '3'" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 6, 188 | "metadata": {}, 189 | "outputs": [ 190 | { 191 | "data": { 192 | "text/plain": [ 193 | "'23'" 194 | ] 195 | }, 196 | "execution_count": 6, 197 | "metadata": {}, 198 | "output_type": "execute_result" 199 | } 200 | ], 201 | "source": [ 202 | "a + b" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 7, 208 | "metadata": {}, 209 | "outputs": [ 210 | { 211 | "data": { 212 | "text/plain": [ 213 | "'23'" 214 | ] 215 | }, 216 | "execution_count": 7, 217 | "metadata": {}, 218 | "output_type": "execute_result" 219 | } 220 | ], 221 | "source": [ 222 | "a.__add__(b)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "We've already encountered one special method: `__init__`. Let's build a class which stores a value internally." 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 8, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "class TestClass:\n", 239 | " def __init__(self, value):\n", 240 | " self.value = value" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "We can now create objects of class `TestClass`:" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": 9, 253 | "metadata": {}, 254 | "outputs": [], 255 | "source": [ 256 | "a = TestClass(2)\n", 257 | "b = TestClass(3)" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "But they are a bit hard to use. For one, they don't display anything meaningful when we display them" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 10, 270 | "metadata": {}, 271 | "outputs": [ 272 | { 273 | "data": { 274 | "text/plain": [ 275 | "<__main__.TestClass at 0x10750fcc0>" 276 | ] 277 | }, 278 | "execution_count": 10, 279 | "metadata": {}, 280 | "output_type": "execute_result" 281 | } 282 | ], 283 | "source": [ 284 | "a" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": {}, 290 | "source": [ 291 | "We can extend TestClass with a `__repr__` method. This is short for representation and is used in the console and the notebook to show an object. By convention, the `__repr__` method returns a string that could be copy-pasted to create the object." 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": 11, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "class TestClass:\n", 301 | " def __init__(self, value):\n", 302 | " self.value = value\n", 303 | " def __repr__(self):\n", 304 | " return \"TestClass({})\".format(self.value)" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 12, 310 | "metadata": {}, 311 | "outputs": [], 312 | "source": [ 313 | "a = TestClass(2)\n", 314 | "b = TestClass(3)" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": 13, 320 | "metadata": {}, 321 | "outputs": [ 322 | { 323 | "data": { 324 | "text/plain": [ 325 | "TestClass(2)" 326 | ] 327 | }, 328 | "execution_count": 13, 329 | "metadata": {}, 330 | "output_type": "execute_result" 331 | } 332 | ], 333 | "source": [ 334 | "a" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "Great, at least we can see what we're working with now. But let's say we want to make this class be able to support addition. At the moment, this doesn't work:" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": 14, 347 | "metadata": {}, 348 | "outputs": [], 349 | "source": [ 350 | "# a + b\n", 351 | "# ---------------------------------------------------------------------------\n", 352 | "# TypeError Traceback (most recent call last)\n", 353 | "# in ()\n", 354 | "# ----> 1 a + b\n", 355 | "\n", 356 | "# TypeError: unsupported operand type(s) for +: 'TestClass' and 'TestClass'" 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": 15, 362 | "metadata": {}, 363 | "outputs": [], 364 | "source": [ 365 | "class TestClass:\n", 366 | " def __init__(self, value):\n", 367 | " self.value = value\n", 368 | " \n", 369 | " def __repr__(self):\n", 370 | " return \"TestClass({})\".format(self.value)\n", 371 | " \n", 372 | " def __add__(self, other):\n", 373 | " return TestClass(self.value + other.value)" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": 16, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [ 382 | "a = TestClass(2)\n", 383 | "b = TestClass(3)" 384 | ] 385 | }, 386 | { 387 | "cell_type": "code", 388 | "execution_count": 17, 389 | "metadata": {}, 390 | "outputs": [ 391 | { 392 | "data": { 393 | "text/plain": [ 394 | "TestClass(5)" 395 | ] 396 | }, 397 | "execution_count": 17, 398 | "metadata": {}, 399 | "output_type": "execute_result" 400 | } 401 | ], 402 | "source": [ 403 | "a + b" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "metadata": {}, 409 | "source": [ 410 | "This should give you a glimpse into how libraries like SymPy or Numpy obtain their effects. They are using these mechanisms to make objects which \"do the right thing\" when they are added together, divided and multiplied, as well as giving them \"normal\" non-underscore methods for additional manipulation. All of the objects you have used to do math so far have implemented these operations. Think of `sympy.Symbol` or the `numpy.array`." 411 | ] 412 | }, 413 | { 414 | "cell_type": "code", 415 | "execution_count": null, 416 | "metadata": {}, 417 | "outputs": [], 418 | "source": [] 419 | } 420 | ], 421 | "metadata": { 422 | "anaconda-cloud": {}, 423 | "kernelspec": { 424 | "display_name": "Python 3", 425 | "language": "python", 426 | "name": "python3" 427 | }, 428 | "language_info": { 429 | "codemirror_mode": { 430 | "name": "ipython", 431 | "version": 3 432 | }, 433 | "file_extension": ".py", 434 | "mimetype": "text/x-python", 435 | "name": "python", 436 | "nbconvert_exporter": "python", 437 | "pygments_lexer": "ipython3", 438 | "version": "3.6.5" 439 | } 440 | }, 441 | "nbformat": 4, 442 | "nbformat_minor": 1 443 | } 444 | -------------------------------------------------------------------------------- /TOC.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This is the table of contents, arranged by topic. \n", 8 | "\n", 9 | "If you are looking for the use of a particular function, try the [Function index](Function%20index.ipynb).\n", 10 | "\n", 11 | "There is also a set of [YouTube videos](https://www.youtube.com/channel/UCOf3CPrYPMG4BrgkVPupPqA) linked to the course." 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# 0. Getting Started\n", 19 | "* [Notebook introduction](0_Getting_Started/Notebook%20introduction.ipynb)\n", 20 | "* [Extra Python](0_Getting_Started/Extra%20Python.ipynb)\n", 21 | "* [Cheatsheet](0_Getting_Started/Cheatsheet.ipynb)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "# 1. Dynamics\n", 29 | "## 1.1 Modelling\n", 30 | "* [Draining cup](1_Dynamics/1_Modelling/Draining%20cup.ipynb)" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## 1.2 Time domain simulation\n", 38 | "* [Equation solving tools](1_Dynamics/2_Time_domain_simulation/Equation%20solving%20tools.ipynb)\n", 39 | "* [Numeric representation](1_Dynamics/2_Time_domain_simulation/Numeric%20representation.ipynb)\n", 40 | "* [Read input from file](1_Dynamics/2_Time_domain_simulation/Read%20input%20from%20file.ipynb)\n", 41 | "* [Example: Fed batch bioreactor](1_Dynamics/2_Time_domain_simulation/Fed%20batch%20bioreactor.ipynb)\n", 42 | "* [Example: Nonlinear CSTR](1_Dynamics/2_Time_domain_simulation/Nonlinear%20CSTR.ipynb)\n", 43 | "* [Example: Mixing system](1_Dynamics/2_Time_domain_simulation/Mixing%20system.ipynb)" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "## 1.3 Linear systems\n", 51 | "* [Linearisation](1_Dynamics/3_Linear_systems/Linearisation.ipynb)\n", 52 | "* [Laplace transforms](1_Dynamics/3_Linear_systems/Laplace%20transforms.ipynb)\n", 53 | "* [Convolution](1_Dynamics/3_Linear_systems/Convolution.ipynb)\n", 54 | "* [Visualising complex functions](1_Dynamics/3_Linear_systems/Visualising%20complex%20functions.ipynb)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## 1.4 First and second order system dynamics\n", 62 | "* [Standard process inputs](1_Dynamics/4_First_and_second_order_system_dynamics/Standard%20process%20inputs.ipynb)\n", 63 | "* [First order systems](1_Dynamics/4_First_and_second_order_system_dynamics/First%20order%20systems.ipynb)\n", 64 | "* [Second order systems](1_Dynamics/4_First_and_second_order_system_dynamics/Second%20order%20systems.ipynb)\n", 65 | "* [Sinusoidal response](1_Dynamics/4_First_and_second_order_system_dynamics/Sinusoidal%20response.ipynb)" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "## 1.5 Complex system dynamics\n", 73 | "* [Random response generator](1_Dynamics/5_Complex_system_dynamics/Random%20response%20generator.ipynb)\n", 74 | "* [Simulation of arbitrary transfer functions](1_Dynamics/5_Complex_system_dynamics/Simulation%20of%20arbitrary%20transfer%20functions.ipynb)\n", 75 | "* [Block diagram simplification](1_Dynamics/5_Complex_system_dynamics/Block%20diagram%20simplification.ipynb)\n", 76 | "* [Approximation](1_Dynamics/5_Complex_system_dynamics/Approximation.ipynb)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## 1.6 Multivariable system representation\n", 84 | "* [Transfer function matrices](1_Dynamics/6_Multivariable_system_representation/Transfer%20function%20matrices.ipynb)\n", 85 | "* [State space](1_Dynamics/6_Multivariable_system_representation/State%20space.ipynb)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "## 1.7 System identification\n", 93 | "* [Regression](1_Dynamics/7_System_identification/Regression.ipynb)\n", 94 | "* [Dynamic model parameter estimation](1_Dynamics/7_System_identification/Dynamic%20model%20parameter%20estimation.ipynb)\n", 95 | "* [Neural networks](1_Dynamics/7_System_identification/Neural%20networks.ipynb)\n", 96 | "* [Identifying discrete-time models](1_Dynamics/7_System_identification/Identifying%20discrete-time%20models.ipynb)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "## 1.8 Frequency domain\n", 104 | "* [Fourier series](1_Dynamics/8_Frequency_domain/Fourier%20series.ipynb)\n", 105 | "* [Sound and frequency](1_Dynamics/8_Frequency_domain/Sound%20and%20frequency.ipynb)\n", 106 | "* [Frequency response plots](1_Dynamics/8_Frequency_domain/Frequency%20response%20plots.ipynb)\n", 107 | "* [Asymptotic Bode diagrams](1_Dynamics/8_Frequency_domain/Asymptotic%20Bode%20diagrams.ipynb)" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "## 1.9 Sampled systems\n", 115 | "* [Aliasing](1_Dynamics/9_Sampled_systems/Aliasing.ipynb)\n", 116 | "* [Filtering](1_Dynamics/9_Sampled_systems/Filtering.ipynb)\n", 117 | "* [The z transform](1_Dynamics/9_Sampled_systems/The%20z%20transform.ipynb)\n", 118 | "* [The z domain and continuous systems](1_Dynamics/9_Sampled_systems/The%20z%20domain%20and%20continuous%20systems.ipynb)" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "# 2. Control\n", 126 | "\n", 127 | "## 2.1 Conventional feedback control\n", 128 | "* [Control game](2_Control/1_Conventional_feedback_control/Control%20game.ipynb)\n", 129 | "* [PID controller step responses](2_Control/1_Conventional_feedback_control/PID%20controller%20step%20responses.ipynb)\n", 130 | "* [Effect of Proportional Control](2_Control/1_Conventional_feedback_control/Effect%20of%20Proportional%20Control.ipynb)\n", 131 | "* [PID control on TCLab](tclab/TCLab%20PID.ipynb)\n", 132 | "* [Closed loop controlled responses](2_Control/1_Conventional_feedback_control/Closed%20loop%20controlled%20responses.ipynb)" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": {}, 138 | "source": [ 139 | "## 2.2 Laplace domain analysis of control systems\n", 140 | "* [Stability analysis](2_Control/2_Laplace_domain_analysis_of_control_systems/Stability%20analysis.ipynb)\n", 141 | "* [SymPy Routh Array](2_Control/2_Laplace_domain_analysis_of_control_systems/SymPy%20Routh%20Array.ipynb)\n", 142 | "* [Root locus diagrams](2_Control/2_Laplace_domain_analysis_of_control_systems/Root%20locus%20diagrams.ipynb)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "## 2.3 PID controller design, tuning and troubleshooting\n", 150 | "* [Direct synthesis PID design](2_Control/3_PID_controller_design_tuning_and_troubleshooting/Direct%20synthesis%20PID%20design.ipynb)\n", 151 | "* [Optimal control - minimal integral measures](2_Control/3_PID_controller_design_tuning_and_troubleshooting/Optimal%20control%20-%20minimal%20integral%20measures.ipynb)\n", 152 | "* [ITAE parameters for FOPDT system](2_Control/3_PID_controller_design_tuning_and_troubleshooting/ITAE%20parameters%20for%20FOPDT%20system.ipynb)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## 2.4 Frequency domain analysis of control systems\n", 160 | "* [Frequency domain stability](2_Control/4_Frequency_domain_analysis_of_control_systems/Frequency%20domain%20stability.ipynb)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## 2.5 Advanced control methods\n", 168 | "* [Dead time compensation](2_Control/5_Advanced_control_methods/Dead%20time%20compensation.ipynb)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "## 2.6 Discrete control and analysis\n", 176 | "* [Discrete control](2_Control/6_Discrete_control_and_analysis/Discrete%20control.ipynb)\n", 177 | "* [Discrete PI](2_Control/6_Discrete_control_and_analysis/Discrete%20PI.ipynb)\n", 178 | "* [Dahlin controller](2_Control/6_Discrete_control_and_analysis/Dahlin%20controller.ipynb)\n", 179 | "* [Simple discrete controller simulation](2_Control/6_Discrete_control_and_analysis/Simple%20discrete%20controller%20simulation.ipynb)\n", 180 | "* [Noise models](2_Control/6_Discrete_control_and_analysis/Noise%20models.ipynb)" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": {}, 186 | "source": [ 187 | "## 2.7 Multivariable control\n", 188 | "* [Multivariable closed loop transfer functions](2_Control/7_Multivariable_control/Multivariable%20closed%20loop%20transfer%20functions.ipynb)\n", 189 | "* [Multivariable stability analysis](2_Control/7_Multivariable_control/Multivariable%20stability%20analysis.ipynb)\n", 190 | "* [Multivariable pairing (RGA)](2_Control/7_Multivariable_control/Multivariable%20Pairing.ipynb)\n", 191 | "* [Eigenvalues and Eigenvectors](2_Control/7_Multivariable_control/Eigenvalues%20and%20Eigenvectors.ipynb)\n", 192 | "* [Decoupling](2_Control/7_Multivariable_control/Decoupling.ipynb)\n", 193 | "* [Simple MPC](2_Control/7_Multivariable_control/Simple%20MPC.ipynb)" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "metadata": {}, 199 | "source": [ 200 | "## 2.8 Control Practice\n", 201 | "* [Control valve design](2_Control/8_Control_Practice/Control%20valve%20design.ipynb)" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "# Simulation\n", 209 | "* [Timing study](Simulation/Timing%20study.ipynb)\n", 210 | "* [Hybrid system simulation](Simulation/Hybrid%20system%20simulation.ipynb)\n", 211 | "* [Classes](Simulation/Classes.ipynb)\n", 212 | "* [Special functions in classes](Simulation/Special%20functions%20in%20classes.ipynb)\n", 213 | "* [Object-Oriented simulation](Simulation/Object-Oriented%20simulation.ipynb)\n", 214 | "* [Object-Oriented simulation - Discrete](Simulation/Object-Oriented%20simulation%20-%20Discrete.ipynb)\n", 215 | "* [Blocksim](Simulation/Blocksim.ipynb)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "# TCLab\n", 223 | "* [FOPDT fit](tclab/FOPDT%20fit.ipynb)\n", 224 | "* [Interactive PID simulation](tclab/TCLab%20PID.ipynb)\n", 225 | "* [Frequency domain](tclab/Frequency%20domain.ipynb)" 226 | ] 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "metadata": {}, 231 | "source": [ 232 | "# tbcontrol\n", 233 | "The tbcontrol module contains functions helpful to students learning to solve control problems. It is broken up into submodules." 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "## symbolic\n", 241 | "* `linearise` attempt to linearise a symbolic expression\n", 242 | "* `routh` construct a Routh-Hurwitz array\n", 243 | "* `pade` pade approximation\n", 244 | "* `ss2tf` determine the transfer function version of a state space realisation\n", 245 | "* `sampledvalues` invert the Z transform symbolically\n", 246 | "* `evaluate_at_times` evaluate a sympy expression at numeric times (useful for plotting responses)" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "## numeric\n", 254 | "* `skogestad_half` find an approximation of a high order system by Skogestad's half rule" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "## plotting\n", 262 | "* `cross_axis` create an axis in Matplotlib which has spines going through the origin like a Nyquist diagram would" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "## loops\n", 270 | "* `feedback` calculate the result of having two blocks in a feedback loop" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "## responses\n", 278 | "Step responses of certain systems\n", 279 | "\n", 280 | "* `fopdt` first order plus dead time\n", 281 | "* `sopdt` second order plus dead time" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [] 290 | } 291 | ], 292 | "metadata": { 293 | "kernelspec": { 294 | "display_name": "Python 3", 295 | "language": "python", 296 | "name": "python3" 297 | }, 298 | "language_info": { 299 | "codemirror_mode": { 300 | "name": "ipython", 301 | "version": 3 302 | }, 303 | "file_extension": ".py", 304 | "mimetype": "text/x-python", 305 | "name": "python", 306 | "nbconvert_exporter": "python", 307 | "pygments_lexer": "ipython3", 308 | "version": "3.8.8" 309 | } 310 | }, 311 | "nbformat": 4, 312 | "nbformat_minor": 4 313 | } 314 | -------------------------------------------------------------------------------- /assets/0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/0.1.png -------------------------------------------------------------------------------- /assets/bigblockdiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/bigblockdiagram.png -------------------------------------------------------------------------------- /assets/blending_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/blending_system.png -------------------------------------------------------------------------------- /assets/continuous_controller.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 4 0 0 4 64 736 e 259 | 260 | 261 | 32 736 m 262 | 60 736 l 263 | 264 | 265 | 266 | 192 752 m 267 | 192 720 l 268 | 224 720 l 269 | 224 752 l 270 | h 271 | 272 | H 273 | 274 | 275 | 276 | 192 752 m 277 | 192 720 l 278 | 224 720 l 279 | 224 752 l 280 | h 281 | 282 | H 283 | 284 | TCLab 285 | $G_C$ 286 | 287 | 36 736 m 288 | 64 736 l 289 | 290 | 291 | 64 752 m 292 | 80 736 l 293 | 96 736 l 294 | 295 | 296 | 144 752 m 297 | 144 720 l 298 | 176 720 l 299 | 176 752 l 300 | h 301 | 302 | 303 | 304 752 m 304 | 304 720 l 305 | 352 720 l 306 | 352 752 l 307 | h 308 | 309 | 310 | 280 736 m 311 | 280 736 l 312 | 280 736 l 313 | 280 736 l 314 | h 315 | 316 | 317 | 128 736 m 318 | 144 736 l 319 | 320 | 321 | 176 736 m 322 | 192 736 l 323 | 324 | 325 | 192 752 m 326 | 208 736 l 327 | 224 736 l 328 | 329 | 330 | 256 736 m 331 | 272 736 l 332 | 333 | 334 | 320 736 m 335 | 352 736 l 336 | 337 | 338 | 336 736 m 339 | 336 704 l 340 | 32 704 l 341 | 32 732 l 342 | 343 | 344 | 20 748 m 345 | 20 740 l 346 | 347 | 348 | 16 744 m 349 | 24 744 l 350 | 351 | 352 | 36 724 m 353 | 44 724 l 354 | 355 | $e$ 356 | 357 | 358 | -------------------------------------------------------------------------------- /assets/continuous_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/continuous_controller.png -------------------------------------------------------------------------------- /assets/cstr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/cstr.png -------------------------------------------------------------------------------- /assets/cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/cup.png -------------------------------------------------------------------------------- /assets/data.csv: -------------------------------------------------------------------------------- 1 | k,yk 2 | 1,0.058 3 | 2,0.217 4 | 3,0.360 5 | 4,0.488 6 | 5,0.600 7 | 6,0.692 8 | 7,0.772 9 | 8,0.833 10 | 9,0.888 11 | 10,0.925 12 | -------------------------------------------------------------------------------- /assets/decoupling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/decoupling.png -------------------------------------------------------------------------------- /assets/example_6_1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/example_6_1.xlsx -------------------------------------------------------------------------------- /assets/mimo2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/mimo2x2.png -------------------------------------------------------------------------------- /assets/mimo2x2_off_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/mimo2x2_off_diagonal.png -------------------------------------------------------------------------------- /assets/mixing_tanks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/mixing_tanks.png -------------------------------------------------------------------------------- /assets/mmult.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/mmult.gif -------------------------------------------------------------------------------- /assets/neuron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/neuron.png -------------------------------------------------------------------------------- /assets/nn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/nn.png -------------------------------------------------------------------------------- /assets/rapmusic.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/rapmusic.wav -------------------------------------------------------------------------------- /assets/simple_feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/simple_feedback.png -------------------------------------------------------------------------------- /assets/smith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/smith.png -------------------------------------------------------------------------------- /assets/standard_feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/standard_feedback.png -------------------------------------------------------------------------------- /assets/tankdata.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/tankdata.xlsx -------------------------------------------------------------------------------- /assets/tanksystem.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 128 768 m 259 | 128 640 l 260 | 256 640 l 261 | 256 768 l 262 | 263 | 264 | 128 736 m 265 | 256 736 l 266 | 267 | 268 | 256 640 m 269 | 320 640 l 270 | 271 | 272 | 224 736 m 273 | 224 640 l 274 | 275 | 276 | 64 784 m 277 | 160 784 l 278 | 160 752 l 279 | 280 | $F_{in}$ 281 | $F_{out}$ 282 | $h$ 283 | $V$ 284 | $A$ 285 | 286 | 287 | -------------------------------------------------------------------------------- /assets/tanksystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/tanksystem.png -------------------------------------------------------------------------------- /assets/transfer_function_block_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/transfer_function_block_diagram.png -------------------------------------------------------------------------------- /assets/weddingday.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/weddingday.wav -------------------------------------------------------------------------------- /assets/z_transform_integrated_first_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemyst/Dynamics-and-Control/0db3598d65d399baa8ca21bd3cece790ddfd2031/assets/z_transform_integrated_first_order.png -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Dynamics and Control build configuration file, created by 5 | # sphinx-quickstart on Sat Feb 10 09:41:30 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | from recommonmark.parser import CommonMarkParser 25 | 26 | source_parsers = { 27 | '.md': CommonMarkParser, 28 | } 29 | 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'nbsphinx', 42 | 'sphinx.ext.mathjax' 43 | ] 44 | 45 | nbsphinx_timeout = 1000 46 | nbsphinx_allow_errors = True 47 | nbsphinx_execute = 'never' 48 | 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ['.rst', '.md'] 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = 'Dynamics and Control with Jupyter Notebooks' 64 | copyright = '2018, Carl Sandrock' 65 | author = 'Carl Sandrock' 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | __version__ = "0.0.1" 71 | 72 | # The short X.Y version. 73 | version = '.'.join(__version__.split('.')[0:2]) 74 | 75 | # The full version, including alpha/beta/rc tags. 76 | release = __version__ 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | # 81 | # This is also used if you do content translation via gettext catalogs. 82 | # Usually you set "language" from the command line for these cases. 83 | language = None 84 | 85 | # List of patterns, relative to source directory, that match files and 86 | # directories to ignore when looking for source files. 87 | # This patterns also effect to html_static_path and html_extra_path 88 | exclude_patterns = [ 89 | '_build', 90 | '**.ipynb_checkpoints', 91 | 'Thumbs.db', 92 | '.DS_Store' 93 | ] 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | #pygments_style = 'sphinx' 97 | 98 | # If true, `todo` and `todoList` produce output, else they produce nothing. 99 | todo_include_todos = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | # 107 | 108 | html_theme = 'sphinx_rtd_theme' 109 | html_theme_options = { 110 | 'collapse_navigation': False, 111 | } 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | # 117 | # html_theme_options = {} 118 | 119 | # Add any paths that contain custom static files (such as style sheets) here, 120 | # relative to this directory. They are copied after the builtin static files, 121 | # so a file named "default.css" will overwrite the builtin "default.css". 122 | html_static_path = ['_static'] 123 | 124 | # Custom sidebar templates, must be a dictionary that maps document names 125 | # to template names. 126 | # 127 | # This is required for the alabaster theme 128 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 129 | html_sidebars = { 130 | '**': [ 131 | 'relations.html', # needs 'show_related': True theme option to display 132 | 'searchbox.html', 133 | ] 134 | } 135 | 136 | 137 | # -- Options for HTMLHelp output ------------------------------------------ 138 | 139 | # Output file base name for HTML help builder. 140 | htmlhelp_basename = 'DynamicsControl' 141 | 142 | 143 | # -- Options for LaTeX output --------------------------------------------- 144 | 145 | latex_elements = { 146 | # The paper size ('letterpaper' or 'a4paper'). 147 | # 148 | # 'papersize': 'letterpaper', 149 | 150 | # The font size ('10pt', '11pt' or '12pt'). 151 | # 152 | # 'pointsize': '10pt', 153 | 154 | # Additional stuff for the LaTeX preamble. 155 | # 156 | # 'preamble': '', 157 | 158 | # Latex figure (float) alignment 159 | # 160 | # 'figure_align': 'htbp', 161 | } 162 | 163 | # Grouping the document tree into LaTeX files. List of tuples 164 | # (source start file, target name, title, 165 | # author, documentclass [howto, manual, or own class]). 166 | latex_documents = [ 167 | (master_doc, 'DynamicsControl.tex', 'Dynamics and Control', 168 | 'Carl Sandrock', 'manual'), 169 | ] 170 | 171 | latex_engine = "xelatex" 172 | 173 | # -- Options for manual page output --------------------------------------- 174 | 175 | # One entry per manual page. List of tuples 176 | # (source start file, name, description, authors, manual section). 177 | man_pages = [ 178 | (master_doc, 'DynamicsControl', 'Dynamics and Control', 179 | [author], 1) 180 | ] 181 | 182 | 183 | # -- Options for Texinfo output ------------------------------------------- 184 | 185 | # Grouping the document tree into Texinfo files. List of tuples 186 | # (source start file, target name, title, author, 187 | # dir menu entry, description, category) 188 | texinfo_documents = [ 189 | (master_doc, 'DynamicsControl', 'Dynamics and Control', 190 | author, 'DynamicsControl', 'One line description of project.', 191 | 'Miscellaneous'), 192 | ] 193 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: dynamicscontrol 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | #- python=3.7.3 7 | - matplotlib 8 | - numpy 9 | - scipy 10 | - sympy 11 | - control 12 | - slycot 13 | - pandas 14 | - keras 15 | - tensorflow-base 16 | - scikit-learn 17 | - pytest 18 | - jupyter 19 | - ipython 20 | - ipywidgets 21 | - pytest-xdist 22 | - xlrd 23 | - openpyxl 24 | - patsy 25 | - pip # we need this for pip to work right 26 | - pip: 27 | - tbcontrol 28 | -------------------------------------------------------------------------------- /find_unlinked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import nbformat 5 | import urllib 6 | import pathlib 7 | 8 | linkre = re.compile(r'\* \[.*\]\((.*)\)') 9 | 10 | toc = nbformat.read('TOC.ipynb', as_version=4) 11 | 12 | linkedfiles = set() 13 | 14 | for cell in toc.cells: 15 | if cell.cell_type != 'markdown': 16 | continue 17 | links = linkre.findall(cell.source) 18 | for link in links: 19 | link = urllib.parse.unquote(link) 20 | if not pathlib.Path(link).exists(): 21 | print(f"Linked file '{link}' doesn't exist") 22 | linkedfiles.add(link) 23 | 24 | for f in pathlib.Path('.').glob('**/*.ipynb'): 25 | if 'ipynb_checkpoints' in str(f): 26 | continue 27 | if str(f) not in linkedfiles: 28 | print(f"File '{str(f)}' not linked in TOC") 29 | 30 | -------------------------------------------------------------------------------- /function_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ast 3 | import sys 4 | import nbformat 5 | import collections 6 | from link import link 7 | 8 | def validline(line): 9 | return not (line.startswith('%') 10 | or line.endswith('?') 11 | or line.endswith('.') 12 | or line.startswith('!')) 13 | 14 | 15 | def namer(obj): 16 | if isinstance(obj, ast.Name): 17 | return obj.id 18 | elif isinstance(obj, ast.Attribute): 19 | return namer(obj.value) + '.' + obj.attr 20 | else: 21 | raise(ValueError) 22 | 23 | 24 | class FunctionFinder(ast.NodeVisitor): 25 | def __init__(self): 26 | super().__init__() 27 | self.functions = set() 28 | def visit_Call(self, node): 29 | try: 30 | self.functions.add(namer(node.func)) 31 | except ValueError: 32 | pass 33 | 34 | function_index = collections.defaultdict(set) 35 | all_functions = set() 36 | 37 | for f in sys.argv[1:]: 38 | finder = FunctionFinder() 39 | nb = nbformat.read(f, as_version=4) 40 | for cell in nb.cells: 41 | if cell.cell_type == 'code': 42 | block = '\n'.join([line for line in cell.source.split('\n') 43 | if validline(line)]) 44 | # print(block) 45 | try: 46 | parsed = ast.parse(block) 47 | except: 48 | # print(f"Parse failed in {f} on this block:") 49 | # print(block) 50 | continue 51 | 52 | finder.visit(parsed) 53 | for func in finder.functions: 54 | function_index[func].add(f) 55 | all_functions.update(finder.functions) 56 | 57 | known_prefixes = ['numpy', 'scipy', 58 | 'sympy', 'mpmath', 'pandas', 59 | 'plt', 60 | 'control', 61 | 'tclab', 62 | 'tbcontrol', 63 | 'blocksim' 64 | ] 65 | 66 | sortedfunctions = list(all_functions) 67 | sortedfunctions.sort() 68 | 69 | def announce(func): 70 | links = ", ".join(link(f) for f in sorted(function_index[func])) 71 | print(f'* `{func}`: {links}') 72 | 73 | for prefix in known_prefixes: 74 | print(f"# {prefix}") 75 | print() 76 | for func in sortedfunctions: 77 | if func.startswith(prefix + '.'): 78 | announce(func) 79 | all_functions.remove(func) 80 | print() 81 | 82 | print('# Other') 83 | for func in sorted(all_functions): 84 | announce(func) 85 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | Dynamics and control 2 | ==================== 3 | 4 | ############### 5 | Getting Started 6 | ############### 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :numbered: 11 | 12 | 0_Getting_Started/Notebook introduction.ipynb 13 | 0_Getting_Started/Extra Python.ipynb 14 | 0_Getting_Started/Cheatsheet.ipynb 15 | 16 | ############### 17 | Dynamics 18 | ############### 19 | 20 | ********* 21 | Modelling 22 | ********* 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :numbered: 27 | 28 | 1_Dynamics/1_Modelling/Draining cup.ipynb 29 | 30 | ********************** 31 | Time domain simulation 32 | ********************** 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | :numbered: 37 | 38 | 39 | 1_Dynamics/2_Time_domain_simulation/Equation solving tools.ipynb 40 | 1_Dynamics/2_Time_domain_simulation/Numeric representation.ipynb 41 | 1_Dynamics/2_Time_domain_simulation/Read input from file.ipynb 42 | 1_Dynamics/2_Time_domain_simulation/Fed batch bioreactor.ipynb 43 | 1_Dynamics/2_Time_domain_simulation/Nonlinear CSTR.ipynb 44 | 1_Dynamics/2_Time_domain_simulation/Mixing system.ipynb 45 | 46 | ********* 47 | Linear systems 48 | ********* 49 | 50 | .. toctree:: 51 | :maxdepth: 2 52 | :numbered: 53 | 54 | 1_Dynamics/3_Linear_systems/Linearisation.ipynb 55 | 1_Dynamics/3_Linear_systems/Laplace transforms.ipynb 56 | 1_Dynamics/3_Linear_systems/Convolution.ipynb 57 | 1_Dynamics/3_Linear_systems/Visualising complex functions.ipynb 58 | 59 | ********* 60 | First and second order system Dynamics 61 | ********* 62 | 63 | .. toctree:: 64 | :maxdepth: 2 65 | :numbered: 66 | 67 | 1_Dynamics/4_First_and_second_order_system_dynamics/Standard process inputs.ipynb 68 | 1_Dynamics/4_First_and_second_order_system_dynamics/First order systems.ipynb 69 | 1_Dynamics/4_First_and_second_order_system_dynamics/Second order systems.ipynb 70 | 1_Dynamics/4_First_and_second_order_system_dynamics/Sinusoidal response.ipynb 71 | 72 | *********************** 73 | Complex system dynamics 74 | *********************** 75 | 76 | .. toctree:: 77 | :maxdepth: 2 78 | :numbered: 79 | 80 | 1_Dynamics/5_Complex_system_dynamics/Random response generator.ipynb 81 | 1_Dynamics/5_Complex_system_dynamics/Simulation of arbitrary transfer functions.ipynb 82 | 1_Dynamics/5_Complex_system_dynamics/Block diagram simplification.ipynb 83 | 1_Dynamics/5_Complex_system_dynamics/Approximation.ipynb 84 | 85 | ********* 86 | Multivariable system representations 87 | ********* 88 | 89 | .. toctree:: 90 | :maxdepth: 2 91 | :numbered: 92 | 93 | 1_Dynamics/6_Multivariable_system_representation/Transfer function matrices.ipynb 94 | 1_Dynamics/6_Multivariable_system_representation/State space.ipynb 95 | 96 | ********************* 97 | System identification 98 | ********************* 99 | 100 | .. toctree:: 101 | :maxdepth: 2 102 | :numbered: 103 | 104 | 1_Dynamics/7_System_identification/Regression.ipynb 105 | 1_Dynamics/7_System_identification/Dynamic model parameter estimation.ipynb 106 | 1_Dynamics/7_System_identification/Neural networks.ipynb 107 | 1_Dynamics/7_System_identification/Identifying discrete-time models.ipynb 108 | 109 | **************** 110 | Frequency domain 111 | **************** 112 | 113 | .. toctree:: 114 | :maxdepth: 2 115 | :numbered: 116 | 117 | 1_Dynamics/8_Frequency_domain/Fourier series.ipynb 118 | 1_Dynamics/8_Frequency_domain/Sound and frequency.ipynb 119 | 1_Dynamics/8_Frequency_domain/Frequency response plots.ipynb 120 | 1_Dynamics/8_Frequency_domain/Asymptotic Bode diagrams.ipynb 121 | 122 | *************** 123 | Sampled systems 124 | *************** 125 | 126 | .. toctree:: 127 | :maxdepth: 2 128 | :numbered: 129 | 130 | 1_Dynamics/9_Sampled_systems/Aliasing.ipynb 131 | 1_Dynamics/9_Sampled_systems/Filtering.ipynb 132 | 1_Dynamics/9_Sampled_systems/The z transform.ipynb 133 | 1_Dynamics/9_Sampled_systems/The z domain and continuous systems.ipynb 134 | 135 | ####### 136 | Control 137 | ####### 138 | 139 | ***************************** 140 | Conventional feedback control 141 | ***************************** 142 | 143 | .. toctree:: 144 | :maxdepth: 2 145 | :numbered: 146 | 147 | 2_Control/1_Conventional_feedback_control/Control game.ipynb 148 | 2_Control/1_Conventional_feedback_control/PID controller step responses.ipynb 149 | 2_Control/1_Conventional_feedback_control/Effect of Proportional Control.ipynb 150 | tclab/TCLab PID.ipynb 151 | 2_Control/1_Conventional_feedback_control/Closed loop controlled responses.ipynb 152 | 153 | ****************************************** 154 | Laplace domain analysis of control systems 155 | ****************************************** 156 | 157 | .. toctree:: 158 | :maxdepth: 2 159 | :numbered: 160 | 161 | 2_Control/2_Laplace_domain_analysis_of_control_systems/Stability analysis.ipynb 162 | 2_Control/2_Laplace_domain_analysis_of_control_systems/SymPy Routh Array.ipynb 163 | 2_Control/2_Laplace_domain_analysis_of_control_systems/Root locus diagrams.ipynb 164 | 165 | ********* 166 | PID controller design, tuning and troubleshooting 167 | ********* 168 | 169 | .. toctree:: 170 | :maxdepth: 2 171 | :numbered: 172 | 173 | 2_Control/3_PID_controller_design_tuning_and_troubleshooting/Direct synthesis PID design.ipynb 174 | 2_Control/3_PID_controller_design_tuning_and_troubleshooting/Optimal control - minimal integral measures.ipynb 175 | 2_Control/3_PID_controller_design_tuning_and_troubleshooting/ITAE parameters for FOPDT system.ipynb 176 | 177 | ******************************************** 178 | Frequency domain analysis of control systems 179 | ******************************************** 180 | 181 | .. toctree:: 182 | :maxdepth: 2 183 | :numbered: 184 | 185 | 186 | 2_Control/4_Frequency_domain_analysis_of_control_systems/Frequency domain stability.ipynb 187 | 188 | ************************ 189 | Advanced control methods 190 | ************************ 191 | 192 | .. toctree:: 193 | :maxdepth: 2 194 | :numbered: 195 | 196 | 2_Control/5_Advanced_control_methods/Dead time compensation.ipynb 197 | 198 | ***************************** 199 | Discrete control and analysis 200 | ***************************** 201 | 202 | .. toctree:: 203 | :maxdepth: 2 204 | :numbered: 205 | 206 | 2_Control/6_Discrete_control_and_analysis/Discrete control.ipynb 207 | 2_Control/6_Discrete_control_and_analysis/Discrete PI.ipynb 208 | 2_Control/6_Discrete_control_and_analysis/Dahlin controller.ipynb 209 | 2_Control/6_Discrete_control_and_analysis/Simple discrete controller simulation.ipynb 210 | 2_Control/6_Discrete_control_and_analysis/Noise models.ipynb 211 | 212 | ********************* 213 | Multivariable control 214 | ********************* 215 | 216 | .. toctree:: 217 | :maxdepth: 2 218 | :numbered: 219 | 220 | 221 | 2_Control/7_Multivariable_control/Multivariable closed loop transfer functions.ipynb 222 | 2_Control/7_Multivariable_control/Multivariable stability analysis.ipynb 223 | 2_Control/7_Multivariable_control/Multivariable Pairing.ipynb 224 | 2_Control/7_Multivariable_control/Eigenvalues and Eigenvectors.ipynb 225 | 2_Control/7_Multivariable_control/Decoupling.ipynb 226 | 2_Control/7_Multivariable_control/Simple MPC.ipynb 227 | 228 | **************** 229 | Control Practice 230 | **************** 231 | 232 | .. toctree:: 233 | :maxdepth: 2 234 | :numbered: 235 | 236 | 237 | 2_Control/8_Control_Practice/Control valve design.ipynb 238 | 239 | ########## 240 | Simulation 241 | ########## 242 | 243 | .. toctree:: 244 | :maxdepth: 2 245 | :numbered: 246 | 247 | Simulation/Timing study.ipynb 248 | Simulation/Hybrid system simulation.ipynb 249 | Simulation/Classes.ipynb 250 | Simulation/Special functions in classes.ipynb 251 | Simulation/Object-Oriented simulation.ipynb 252 | Simulation/Object-Oriented simulation - Discrete.ipynb 253 | Simulation/Blocksim.ipynb 254 | 255 | ############################### 256 | Temperature Control Lab (TCLab) 257 | ############################### 258 | 259 | .. toctree:: 260 | :maxdepth: 2 261 | :numbered: 262 | 263 | 264 | tclab/FOPDT fit.ipynb 265 | tclab/TCLab PID.ipynb 266 | tclab/Frequency domain.ipynb 267 | 268 | Search Page 269 | ================== 270 | 271 | * :ref:`search` 272 | -------------------------------------------------------------------------------- /link.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pathlib 3 | import urllib 4 | 5 | def link(f): 6 | p = pathlib.Path(f) 7 | return f"[{p.stem.replace('_', ' ')}]({urllib.parse.quote(f)})" 8 | 9 | if __name__ == "__main__": 10 | import sys 11 | for f in sys.argv[1:]: 12 | print(link(f)) 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | # See https://packaging.python.org/guides/single-sourcing-package-version/ 7 | exec(open("tbcontrol/version.py").read()) 8 | 9 | setuptools.setup( 10 | name="tbcontrol", 11 | version=__version__, 12 | author="Carl Sandrock", 13 | author_email="carl.sandrock@gmail.com", 14 | description="Textbook Control Problem package", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/alchemyst/Dynamics-and-Control", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 22 | "Operating System :: OS Independent", 23 | "Intended Audience :: Education", 24 | ], 25 | python_requires=">=3.5", 26 | install_requires=["numpy", "scipy", "tqdm", "packaging"], 27 | ) 28 | -------------------------------------------------------------------------------- /sphinx_requirements.txt: -------------------------------------------------------------------------------- 1 | nbsphinx 2 | recommonmark 3 | ipykernel 4 | sphinx-rtd-theme 5 | -------------------------------------------------------------------------------- /tbcontrol/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | class VersionError(Exception): 4 | pass 5 | 6 | def expectversion(version): 7 | import packaging.version 8 | 9 | currentversion = packaging.version.parse(__version__) 10 | expectedversion = packaging.version.parse(version) 11 | 12 | if currentversion < expectedversion: 13 | raise VersionError(f"Please upgrade tbcontrol to at least version " 14 | f"{version} - you have {__version__}.") -------------------------------------------------------------------------------- /tbcontrol/blocksim.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import scipy 3 | import scipy.signal 4 | import numpy 5 | 6 | class Block: 7 | def __init__(self, name, inputname, outputname): 8 | self.name = name 9 | self.inputname = inputname 10 | self.outputname = outputname 11 | 12 | def __repr__(self): 13 | return f"{self.__class__.__name__}: {self.inputname} →[ {self.name} ]→ {self.outputname}" 14 | 15 | 16 | class LTI(Block): 17 | """Represents a general Linear Time Invariant system with optional delay""" 18 | def __init__(self, name, inputname, outputname, numerator, denominator=1, delay=0): 19 | """:param name: str, The name of the block 20 | :param inputname: str, the name of the input signal 21 | :param outputname: str, the name of the output signal 22 | :param numerator: number or iterable of numbers for numerator coefficients in descending order 23 | :param denominator: number or iterable of numbers for denominator coefficients in descending order 24 | :param delay: number, delay 25 | 26 | """ 27 | super().__init__(name, inputname, outputname) 28 | 29 | self.G = scipy.signal.lti(numerator, denominator) 30 | self.Gss = self.G.to_ss() 31 | if delay > 0: 32 | self.delay = Deadtime(None, None, None, delay) 33 | else: 34 | self.delay = None 35 | self.reset() 36 | 37 | def reset(self): 38 | self.change_state(numpy.zeros((self.Gss.A.shape[0], 1))) 39 | self.y = self.output = 0 40 | if self.delay: 41 | self.delay.reset() 42 | 43 | def change_input(self, t, u): 44 | self.y = (self.Gss.C.dot(self.x) + self.Gss.D.dot(u))[0, 0] 45 | if self.delay: 46 | self.y = self.delay.change_input(t, self.y) 47 | self.output = self.y 48 | return self.output 49 | 50 | def change_state(self, x): 51 | self.x = self.state = x 52 | 53 | def derivative(self, e): 54 | return self.Gss.A.dot(self.x) + self.Gss.B.dot(e) 55 | 56 | 57 | class Controller(LTI): 58 | def __init__(self, name, inputname, outputname, numerator, denominator=1, delay=0, automatic=True): 59 | self.automatic = True 60 | super().__init__(name, inputname, outputname, numerator, denominator, delay) 61 | 62 | def change_input(self, t, u): 63 | if self.automatic: 64 | return super().change_input(t, u) 65 | else: 66 | return self.output 67 | 68 | class PI(Controller): 69 | def __init__(self, name, inputname, outputname, Kc, tau_i): 70 | """Textbook PI controller""" 71 | super().__init__(name, inputname, outputname, [Kc*tau_i, Kc], [tau_i, 0]) 72 | 73 | 74 | class PID(Controller): 75 | def __init__(self, name, inputname, outputname, Kc, tau_i, tau_d=0, alpha_f=0.1): 76 | """Standard realisable parallel form ISA PID controller with first order filter. 77 | 78 | If tau_d=0, a PI controller is returned""" 79 | 80 | if tau_d == 0: 81 | return PI(name, inputname, outputname, Kc, tau_i) 82 | 83 | super().__init__(name, inputname, outputname, 84 | numerator=[Kc*alpha_f*tau_d*tau_i + Kc*tau_d*tau_i, 85 | Kc*alpha_f*tau_d + Kc*tau_i, 86 | Kc], 87 | denominator=[alpha_f*tau_d*tau_i, 88 | tau_i, 89 | 0.0]) 90 | 91 | 92 | class Zero(Block): 93 | def __init__(self, name, inputname, outputname): 94 | super().__init__(name, inputname, outputname) 95 | 96 | def reset(self): 97 | self.change_state(0) 98 | pass 99 | 100 | def change_input(self, t, u): 101 | return 0 102 | 103 | def change_state(self, x): 104 | self.x = self.state = 0 105 | 106 | def derivative(self, e): 107 | return 0 108 | 109 | 110 | class AlgebraicEquation(Block): 111 | def __init__(self, name, inputname, outputname, f): 112 | """Relationship between input and output specified by an external function 113 | 114 | :param f: function(t, u) 115 | """ 116 | super().__init__(name, inputname, outputname) 117 | self.f = f 118 | self.reset() 119 | 120 | def reset(self): 121 | self.change_state(0) 122 | self.y = self.output = 0 123 | 124 | def change_input(self, t, u): 125 | self.y = self.output = self.f(t, u) 126 | return self.output 127 | 128 | def change_state(self, x): 129 | self.x = self.state = x 130 | 131 | def derivative(self, e): 132 | return 0 133 | 134 | 135 | class Deadtime(Block): 136 | def __init__(self, name, inputname, outputname, delay): 137 | super().__init__(name, inputname, outputname) 138 | 139 | self.delay = delay 140 | self.reset() 141 | 142 | def reset(self): 143 | self.change_state(0) 144 | self.y = self.output = 0 145 | self.ts = [0] 146 | self.us = [0] 147 | 148 | def change_input(self, t, u): 149 | self.ts.append(t) 150 | self.us.append(u) 151 | if self.delay > 0: 152 | u = numpy.interp(t - self.delay, self.ts, self.us) 153 | 154 | self.y = u 155 | self.output = self.y 156 | return self.output 157 | 158 | def change_state(self, x): 159 | self.x = self.state = x 160 | 161 | def derivative(self, e): 162 | return 0 163 | 164 | 165 | class DiscreteTF(Block): 166 | 167 | def __init__(self, name, input_name, output_name, dt, numerator, denominator): 168 | """ 169 | Represents a discrete transfer function. 170 | 171 | The TF must be of the form: 172 | 173 | -1 -2 -n 174 | b + b z + b z + ... + b z 175 | 0 1 2 n 176 | ----------------------------------- 177 | -1 -2 -m 178 | a + a z + a z + ... + a z 179 | 0 1 2 m 180 | 181 | 182 | Parameters 183 | ---------- 184 | dt : float 185 | The sampling time of the transfer function. 186 | numerator : array_like 187 | The numerator coefficient vector in a 1-D sequence. 188 | [b_0, ..., b_n] 189 | denominator : array_like 190 | The denominator coefficient vector in a 1-D sequence. 191 | [a_0, ..., a_m]; a_0 != 0 192 | 193 | """ 194 | super().__init__(name, input_name, output_name) 195 | 196 | if denominator[0] == 0: 197 | raise ValueError('The leading coefficient of the denominator cannot be zero') 198 | 199 | self.dt = dt 200 | self.y_cos = denominator[::-1] 201 | self.u_cos = numerator[::-1] 202 | 203 | self.ys = numpy.zeros(len(self.y_cos)) 204 | self.us = numpy.zeros(len(self.u_cos)) 205 | self.next_sample = 0 206 | 207 | self.state = 0.0 208 | self.output = 0.0 209 | 210 | def reset(self): 211 | self.ys = numpy.zeros(len(self.y_cos)) 212 | self.us = numpy.zeros(len(self.u_cos)) 213 | self.next_sample = 0 214 | self.state = 0.0 215 | self.output = 0.0 216 | 217 | def change_input(self, t, u): 218 | if t > self.next_sample: 219 | self.next_sample += self.dt 220 | 221 | self.us[:-1] = self.us[1:] 222 | self.us[-1] = u 223 | 224 | self.ys[:-1] = self.ys[1:] 225 | self.ys[-1] = None # done to ensure that if anything should go wrong, it does 226 | 227 | u_sum = numpy.inner(self.u_cos, self.us) 228 | y_sum = numpy.inner(self.y_cos[:-1], self.ys[:-1]) 229 | y = (u_sum - y_sum)/self.y_cos[-1] 230 | 231 | self.output = self.ys[-1] = y 232 | return self.output 233 | 234 | def change_state(self, x): 235 | return 0 236 | 237 | def derivative(self, e): 238 | return 0 239 | 240 | 241 | 242 | class Diagram: 243 | def __init__(self, blocks, sums, inputs): 244 | """Create a diagram 245 | 246 | :param blocks: list of blocks 247 | :param sums: sums specified as dictionaries with keys equal to output signal and values as tuples of strings of the form "" 248 | :param inputs: inputs specified as dictionaries with keys equal to signal names and values functions of time 249 | 250 | 251 | Example 252 | 253 | >>> blocks = [Gc, G] 254 | >>> sums = {'e': ('+ysp', '-y')} 255 | >>> inputs = {'ysp': step()} 256 | >>> diagram = Diagram(blocks, sums, inputs) 257 | 258 | """ 259 | if not all(isinstance(block, Block) for block in blocks): 260 | raise TypeError("blocks must be a list of blocks") 261 | for output, cinputs in sums.items(): 262 | for s in cinputs: 263 | if s[0] not in '+-': 264 | raise ValueError(f"In the sum '{output}': {cinputs}, there is no sign for '{s}'") 265 | 266 | self.blocks = blocks 267 | self.sums = sums 268 | self.inputs = inputs 269 | self.reset() 270 | 271 | def reset(self): 272 | self.signals = {b.inputname: 0 for b in self.blocks} 273 | self.signals.update({b.outputname: 0 for b in self.blocks}) 274 | self.signals.update({output: 0 for output in self.sums}) 275 | for block in self.blocks: 276 | block.reset() 277 | 278 | def step(self, t, dt): 279 | signals = self.signals 280 | # Evaluate all inputs 281 | for signal, function in self.inputs.items(): 282 | signals[signal] = function(t) 283 | # Evaluate sums 284 | for output, inputs in self.sums.items(): 285 | signals[output] = sum(int(s[0]+'1')*signals[s[1:]] for s in inputs) 286 | # Evaluate blocks and integrate 287 | for block in self.blocks: 288 | u = signals[block.inputname] 289 | signals[block.outputname] = block.change_input(t, u) 290 | block.change_state(block.state + block.derivative(u)*dt) 291 | return signals 292 | 293 | def simulate(self, ts, progress=False): 294 | """Simulate diagram 295 | 296 | :param ts: iterable, timesteps to simulate. Note this should be equally spaced 297 | :param progress: display progress bar 298 | 299 | Returns dictionary with keys for each signal in the diagram and values an iterable of values 300 | """ 301 | 302 | if progress: 303 | from tqdm.auto import tqdm as tqdm 304 | pbar = tqdm(total=len(ts)) 305 | dt = ts[1] 306 | outputs = defaultdict(list) 307 | self.reset() 308 | for t in ts: 309 | newoutputs = self.step(t, dt) 310 | for signal, value in newoutputs.items(): 311 | outputs[signal].append(value) 312 | if progress: 313 | pbar.update() 314 | return outputs 315 | 316 | def __repr__(self): 317 | return '\n'.join(str(b) for b in self.blocks) 318 | 319 | 320 | # Input functions 321 | def step(initial=0, starttime=0, size=1): 322 | """Return a function which can be used to simulate a step""" 323 | def stepfunction(t): 324 | if t < starttime: 325 | return initial 326 | else: 327 | return initial + size 328 | return stepfunction 329 | 330 | 331 | def zero(t): 332 | """This function returns zero for all time""" 333 | return 0 334 | 335 | 336 | def simple_control_diagram(Gc, G, Gd=None, Gm=None, ysp=step(), d=zero): 337 | """Construct a simple control diagram for quick controller simulations 338 | 339 | | d 340 | ┌─────┐ 341 | │ Gd │ 342 | └──┬──┘ 343 | │ yd 344 | ysp + e ┌──────┐ u ┌─────┐ yu v y 345 | ──>o─>│ Gc ├────>│ G ├───>o─┬──> 346 | ─↑ └──────┘ └─────┘ │ 347 | │ │ 348 | │ ym ┌─────┐ │ 349 | └─────────┤ Gm │<───────────┘ 350 | └─────┘ 351 | 352 | 353 | Required arguments: 354 | 355 | Gc: Controller, a blocksim.Block with name='Gc', input='e', output='u' 356 | G: System, a blocksim.Block with name='G', input='u', output='yu' 357 | 358 | Optional arguments: 359 | Gd: disturbance response, a blocksim.Block with name='Gd', input='d', output='yd' 360 | ysp: a function of time to represent the input 361 | d: a function of time to represent the disturbance 362 | 363 | Returns 364 | 365 | blocksim.Diagram object representing this diagram 366 | 367 | """ 368 | 369 | if Gd is None: 370 | Gd = Zero('Gd', 'd', 'yd') 371 | 372 | if Gm is None: 373 | Gm = LTI('Gm', 'y', 'ym', 1) 374 | 375 | blocks = [Gc, G, Gd, Gm] 376 | 377 | sums = {'e': ('+ysp', '-ym'), 378 | 'y': ('+yu', '+yd')} 379 | 380 | inputs = {'ysp': ysp, 381 | 'd': d} 382 | 383 | return Diagram(blocks, sums, inputs) 384 | -------------------------------------------------------------------------------- /tbcontrol/conversion.py: -------------------------------------------------------------------------------- 1 | def discrete_coeffs_neg_to_pos(numer, denom): 2 | """Convert the coefficients of a transfer function from negative to positive powers of z 3 | 4 | numer and denom must be lists of coefficients of negative powers of z, in descending powers of z 5 | 6 | convert from 7 | 8 | -1 -2 -n 9 | b + b z + b z + ... + b z numer = [b0, b1, b2, ...] 10 | 0 1 2 n 11 | ------------------------------ 12 | -1 -2 -m 13 | a + a z + a z + ... + a z denom = [a0, b1, b2, ...] 14 | 0 1 2 m 15 | 16 | to 17 | 18 | k k-1 k-2 19 | b z + b z + b z + ... 20 | 0 1 2 21 | ------------------------------- 22 | k k-1 k-2 23 | a z + a z + a z + ... 24 | 0 1 2 25 | 26 | """ 27 | 28 | n = len(numer) 29 | m = len(denom) 30 | 31 | k = max(m, n) 32 | 33 | return list(numer) + [0]*(k - n), list(denom) + [0]*(k - m) 34 | 35 | 36 | def discrete_coeffs_pos_to_neg(numer, denom): 37 | """Convert the coefficients of a transfer function from positive to negative powers of z 38 | 39 | numer and denom must be lists of coefficients of negative powers of z, in descending powers of z 40 | 41 | convert from 42 | 43 | m m-1 m-2 44 | b z + b z + b z + ... numer = [b0, b1, b2, ...] 45 | 0 1 2 46 | ------------------------- 47 | n n-1 n-2 48 | a z + a z + a z + ... denom = [a0, b1, b2, ...] 49 | 0 1 2 50 | 51 | 52 | to 53 | 54 | -1 -2 -n 55 | b + b z + b z + ... + b z 56 | 0 1 2 n 57 | ------------------------------ 58 | -1 -2 -m 59 | a + a z + a z + ... + a z 60 | 0 1 2 m 61 | 62 | """ 63 | 64 | n = len(numer) 65 | m = len(denom) 66 | 67 | k = max(m, n) 68 | 69 | return [0]*(k - n) + list(numer), [0]*(k - m) + list(denom) 70 | 71 | -------------------------------------------------------------------------------- /tbcontrol/fopdtitae.py: -------------------------------------------------------------------------------- 1 | """ 2 | ITAE parameters for FOPDT system 3 | 4 | This module calculates the values of the PI/PID controller settings 5 | based on Table 11.3 of Seborg, Edgar, Melichamp and Lewin (itself 6 | based on Smith and Corripio, 1997). 7 | """ 8 | 9 | # There are four different design relationships 10 | def f1(K, tau, theta, A, B): 11 | Y = A*(theta/tau)**B 12 | Kc = Y/K 13 | return Kc 14 | 15 | 16 | def f2(K, tau, theta, A, B): 17 | Y = A*(theta/tau)**B 18 | tau_I = tau/Y 19 | return tau_I 20 | 21 | 22 | def f3(K, tau, theta, A, B): 23 | Y = A*(theta/tau)**B 24 | tau_D = Y*tau 25 | return tau_D 26 | 27 | 28 | def f4(K, tau, theta, A, B): 29 | Y = A + B*(theta/tau) 30 | tau_I = tau/Y 31 | return tau_I 32 | 33 | 34 | # dictionary to allow lookup of the coefficients and the relation to use 35 | table = {'Disturbance': {'PI': {'P': (0.859, -0.977, f1), 36 | 'I': (0.674, -0.68, f2)}, 37 | 'PID': {'P': (1.357, -0.947, f1), 38 | 'I': (0.842, -0.738, f2), 39 | 'D': (0.381, 0.995, f3)}}, 40 | 'Set point': {'PI': {'P': (0.586, -0.916, f1), 41 | 'I': (1.03, -0.165, f4)}, 42 | 'PID': {'P': (0.965, -0.85, f1), 43 | 'I': (0.796, -0.1465, f4), 44 | 'D': (0.308, 0.929, f3)}}} 45 | 46 | 47 | def parameters(K, tau, theta, type_of_input='Disturbance', type_of_controller='PI'): 48 | retval = [] 49 | for mode in type_of_controller: 50 | A, B, f = table[type_of_input][type_of_controller][mode] 51 | value = f(K, tau, theta, A, B) 52 | retval.append(value) 53 | return retval 54 | -------------------------------------------------------------------------------- /tbcontrol/loops.py: -------------------------------------------------------------------------------- 1 | """Utility functions for handling feedback loop calculations""" 2 | 3 | 4 | def feedback(forward, backward=1): 5 | """Calculate closed loop transfer function 6 | 7 | + ┌────────┐ 8 | ────>o──>│forward ├────┬──> 9 | -↑ └────────┘ │ 10 | │ ┌────────┐ │ 11 | └───┤backward│<───┘ 12 | └────────┘ 13 | """ 14 | loop = forward*backward 15 | return forward/(1 + loop) 16 | -------------------------------------------------------------------------------- /tbcontrol/numeric.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | def skogestad_half(num_timeconstants, den_timeconstants, delay, order): 4 | """ 5 | Approximate higher order transfer function using Skogestad's Half rule 6 | 7 | This method is specifically aimed at finding a first or second order 8 | plus dead time model of a transfer function. 9 | 10 | Note that the sign should be included in the time constant, so a term 11 | (s - 1) will have a time constant of 1, while a term (s + 1) will have a 12 | time constant of -1. 13 | 14 | :param num_timeconstants: numerator time constants 15 | :param den_timeconstants: denominator time constants 16 | :param delay: delay in original transfer function 17 | :param order: order of target transfer function 18 | :return approx_delay: delay time of approximation 19 | :return approx_timeconstants: time constants of approximation 20 | 21 | Reference: http://folk.ntnu.no/skoge/presentation/promatch-cybernetica-06/4tuning_short.pdf 22 | """ 23 | 24 | if order > 2: 25 | raise NotImplementedError("Skogestad's rule is only meant for first or second order.") 26 | 27 | def coerce(inp): 28 | return numpy.atleast_1d(numpy.asarray(inp, dtype=float)) 29 | 30 | num_timeconstants = coerce(num_timeconstants) 31 | den_timeconstants = coerce(den_timeconstants) 32 | 33 | # Ensure time constants are in descending order 34 | den_timeconstants = numpy.sort(den_timeconstants)[::-1] 35 | 36 | if (den_timeconstants <= 0).any(): 37 | raise ValueError("Skogestad's rule is meant for stable systems only.") 38 | 39 | approx_timeconstants = den_timeconstants[:order] 40 | neglected_constants = den_timeconstants[order:] 41 | 42 | approx_delay = delay + neglected_constants[0]/2 + sum(neglected_constants[1:]) - sum(num_timeconstants) 43 | approx_timeconstants[-1] += neglected_constants[0]/2 44 | 45 | return approx_delay, approx_timeconstants 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tbcontrol/plotting.py: -------------------------------------------------------------------------------- 1 | """Commonly used plotting functions""" 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | def cross_axis(size=5, ax=None): 6 | """Change to cross-style axes through the origin and fix size 7 | 8 | :param size: either a single number or a collection with 4 elements 9 | suitable for a call to plt.axis. If a single number is given 10 | it is the size of the axis on all sides of the origin 11 | 12 | :param ax: the axis to set. If None, the current axis is used. 13 | """ 14 | 15 | if ax is None: 16 | ax = plt.gca() 17 | if not isinstance(size, (list, tuple)): 18 | size = [-size, size, -size, size] 19 | ax.axis(size) 20 | # from http://stackoverflow.com/questions/25689238/show-origin-axis-x-y-in-matplotlib-plot 21 | ax.spines['left'].set_position('zero') 22 | 23 | # turn off the right spine/ticks 24 | ax.spines['right'].set_color('none') 25 | ax.yaxis.tick_left() 26 | 27 | # set the y-spine 28 | ax.spines['bottom'].set_position('zero') 29 | 30 | # turn off the top spine/ticks 31 | ax.spines['top'].set_color('none') 32 | ax.xaxis.tick_bottom() 33 | 34 | # Set axis to equal aspect ratio 35 | ax.set_aspect('equal') 36 | -------------------------------------------------------------------------------- /tbcontrol/responses.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | def fopdt(t, K, tau, theta=0, y0=0): 4 | """ First Order Plus Dead Time response with bias 5 | 6 | Step response of 7 | K 8 | G(s) = --------- 9 | tau*s + 1 10 | 11 | :param t: time 12 | :param K: gain 13 | :param tau: time constant 14 | :param theta: dead time 15 | :param y0: bias 16 | """ 17 | 18 | return y0 + numpy.sign(K)*numpy.maximum(0, abs(K)*(1 - numpy.exp(-(t - theta)/tau))) 19 | 20 | def sopdt(t, K, tau, zeta, theta=0, y0=0): 21 | """ Second Order Plus Dead Time response with bias 22 | 23 | Step response of 24 | K 25 | G(s) = -------------------------- 26 | tau**2s + 2*tau*zeta*s + 1 27 | 28 | :param t: time 29 | :param K: gain 30 | :param tau: time constant 31 | :param zeta: damping coefficient 32 | :param theta: dead time 33 | :param y0: bias 34 | """ 35 | 36 | tu = numpy.maximum(0, t - theta) # undelayed time 37 | 38 | ttau = tu/tau 39 | exp = numpy.exp 40 | 41 | if zeta == 1: 42 | return y0 + K*(1 - (1 + ttau)*exp(-ttau)) 43 | 44 | if zeta > 1: 45 | coslike = numpy.cosh 46 | sinlike = numpy.sinh 47 | root = numpy.sqrt(zeta**2 - 1) 48 | elif zeta < 1: 49 | coslike = numpy.cos 50 | sinlike = numpy.sin 51 | root = numpy.sqrt(1 - zeta**2) 52 | 53 | return y0 + K*(1 - exp(-zeta*ttau)*(coslike(root*ttau) 54 | + zeta/root*sinlike(root*ttau))) 55 | -------------------------------------------------------------------------------- /tbcontrol/symbolic.py: -------------------------------------------------------------------------------- 1 | """Control functions which operate symbolically using sympy""" 2 | 3 | import sympy 4 | 5 | 6 | def linearise(expr, variables, bars=None): 7 | """Linearise a nonlinear expression 8 | 9 | Parameters 10 | ---------- 11 | expr: A sympy expression - this could be a single term or a sum of terms 12 | variables: The variables in the expression - all the other symbols in the expression will be treated as parameters 13 | bars: the "barred" versions of the variables. If bars is None, new barred versions will be built and returned 14 | 15 | Note: This function will *not work correctly with derivatives in expr* 16 | """ 17 | if bars is None: 18 | returnbars = True 19 | bars = [sympy.Symbol(f"{variable.name}bar") for variable in variables] 20 | else: 21 | returnbars = False 22 | 23 | vars_and_bars = list(zip(variables, bars)) 24 | 25 | # This is the constant term 26 | result = expr.subs(vars_and_bars) 27 | 28 | # now, we take the derivative with respect to each variable, evaluated at the steady state: 29 | for variable, bar in vars_and_bars: 30 | result += (variable - bar)*expr.diff(variable).subs(vars_and_bars) 31 | 32 | if returnbars: 33 | return bars, result 34 | else: 35 | return result 36 | 37 | 38 | def routh(p): 39 | """ Construct the Routh-Hurwitz array given a polynomial in s 40 | 41 | Input: p - a sympy.Poly object 42 | Output: The Routh-Hurwitz array as a sympy.Matrix object 43 | """ 44 | coefficients = p.all_coeffs() 45 | N = len(coefficients) 46 | M = sympy.zeros(N, (N+1)//2 + 1) 47 | 48 | r1 = coefficients[0::2] 49 | r2 = coefficients[1::2] 50 | M[0, :len(r1)] = [r1] 51 | M[1, :len(r2)] = [r2] 52 | for i in range(2, N): 53 | for j in range(N//2): 54 | S = M[[i-2, i-1], [0, j+1]] 55 | M[i, j] = sympy.simplify(-S.det()/M[i-1, 0]) 56 | # If a row of the routh table becomes zero,Take the derivative of the previous row and substitute it instead 57 | # Ref: Norman S. Nise, Control Systems Engineering, 8th Edition, Chapter 6, Section 3 58 | if M[i, :] == sympy.Matrix([[0]*(M.shape[1])]): 59 | # Find the coefficients on taking the derivative of the Auxiliary polynomial 60 | diff_arr = list(range(N-i, -1, -2)) 61 | diff_arr.extend([0]*(M.shape[1] - len(diff_arr))) 62 | diff_arr = sympy.Matrix([diff_arr]) 63 | # Multiply the coefficients with the value in previous row 64 | M[i, :] = sympy.matrix_multiply_elementwise(diff_arr, M[i-1, :]) 65 | return M[:, :-1] 66 | 67 | 68 | def pade(G, s, M, N, p=0): 69 | """Return a Padé approximation of the function G in terms of variable s, of order M/N around point p""" 70 | M += 1 71 | N += 1 72 | b = sympy.symbols('b:{}'.format(M)) 73 | a = sympy.symbols('a:{}'.format(N)) 74 | approximation = sum(b[i]*s**i for i in range(M)) / \ 75 | sum(a[i]*s**i for i in range(N)) 76 | nder = M + N 77 | derivatives = [(G - approximation).diff(s, 78 | i).subs({s: p}) for i in range(nder-1)] 79 | denominator_constant = [a[0] - 1] # set denom constant term = 1 80 | equations = derivatives + denominator_constant 81 | pars = sympy.solve(equations, a + b, dict=True) 82 | return approximation.subs(pars[0]) 83 | 84 | 85 | def ss2tf(A, B, C, D, s): 86 | """Convert state space matrices to a transfer function matrix 87 | 88 | Arguments: 89 | A, B, C, D : sympy.Matrix objects containing the state space matrices 90 | s : symbol to use for s 91 | 92 | Returns: 93 | G : Transfer function matrix 94 | """ 95 | I = sympy.eye(A.shape[0]) 96 | 97 | G = C*(s*I - A).inv()*B + D 98 | return G 99 | 100 | 101 | def sampledvalues(fz, z, N): 102 | """Return the first N values of a z transform's inverse via Taylor series expansion 103 | 104 | Arguments: 105 | 106 | fz: sympy symbolic expression in terms of z 107 | z: the symbol used in fz for the z transform 108 | N: The number of time steps to return 109 | """ 110 | q = sympy.Symbol('q') 111 | return sympy.poly(fz.subs(z, q**-1).series(q, 0, N).removeO(), q).all_coeffs()[::-1] 112 | 113 | 114 | def evaluate_at_times(expression, t, ts): 115 | """Evaluate a sympy expression over time 116 | 117 | Arguments: 118 | 119 | expression: a sympy expression containing references to a time variable `t` 120 | t : a `sympy.Symbol` which is in `expression` and will be subsitituted from `ts` 121 | ts: an iterable containing times for evaluation. Should be only ints or floats 122 | 123 | versionadded: 0.1.3 124 | """ 125 | 126 | return [expression.subs(t, ti).subs({sympy.Heaviside(float(0)): 1, 127 | sympy.Heaviside(0): 1}) for ti in ts] 128 | -------------------------------------------------------------------------------- /tbcontrol/version.py: -------------------------------------------------------------------------------- 1 | # This file is designed to be `exec`ed, don't do too much here 2 | 3 | __version__ = "0.2.1" 4 | -------------------------------------------------------------------------------- /tclab/pidgui.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import Button, Label, FloatSlider, HBox, VBox, Checkbox, IntText 2 | import tornado 3 | from tclab import Plotter, Historian, TCLab, TCLabModel 4 | from tclab.gui import actionbutton, labelledvalue, slider, NotebookInteraction 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | class PID: 9 | def __init__(self): 10 | self.K = 1 11 | self.taui = 100 12 | self.taud = 0 13 | 14 | self.e = 0 15 | self.dedt = 0 16 | self.eint = 0 17 | self.output = 0 18 | 19 | def update(self, setpoint, mv): 20 | e = setpoint - mv 21 | self.dedt = self.e - e 22 | self.eint += e 23 | self.e = e 24 | 25 | self.output = self.K * (self.e + 1/self.taui*self.eint + self.taud*self.dedt) 26 | return self.output 27 | 28 | 29 | class PIDGUI(NotebookInteraction): 30 | def __init__(self): 31 | self.lab = None 32 | self.layout = (('T1', 'setpoint'), 33 | ('error',), 34 | ('eint',), 35 | ('dedt',), 36 | ('Q1', 'output')) 37 | 38 | self.pid = PID() 39 | 40 | self.mode = 'manual' 41 | 42 | self.manual = actionbutton('Manual', self.action_manual) 43 | self.auto = actionbutton('Auto', self.action_auto) 44 | 45 | buttons = HBox([self.auto, self.manual]) 46 | 47 | # Sliders for heaters 48 | self.gain = slider('Gain', self.action_gain, maxvalue=100) 49 | self.gain.value = 1 50 | self.tau_i = slider(r'$\tau_I$', self.action_tau_i, minvalue=0) 51 | self.tau_i.value = 100 52 | self.tau_d = slider(r'$\tau_D$', self.action_tau_d, maxvalue=10) 53 | 54 | parameters = HBox([self.gain, self.tau_i, self.tau_d]) 55 | 56 | # Setpoint 57 | self.setpoint = slider('Setpoint', minvalue=20, maxvalue=70) 58 | self.setpoint.value = 30 59 | self.Q1 = slider('Q1') 60 | 61 | self.ui = VBox([buttons, 62 | parameters, 63 | HBox([self.setpoint, self.Q1]), 64 | ]) 65 | 66 | def update(self, t): 67 | if self.mode == 'auto': 68 | Q1 = self.pid.update(self.setpoint.value, self.lab.T1) 69 | else: 70 | Q1 = self.Q1.value 71 | 72 | self.lab.Q1(Q1) 73 | 74 | def connect(self, lab): 75 | super().connect(lab) 76 | self.sources = [('T1', lambda: self.lab.T1), 77 | ('Q1', self.lab.Q1), 78 | ('setpoint', lambda: self.setpoint.value), 79 | ('error', lambda: self.pid.e), 80 | ('eint', lambda: self.pid.eint), 81 | ('dedt', lambda: self.pid.dedt), 82 | ('output', lambda: self.pid.output), 83 | ('gain', lambda: self.gain.value), 84 | ('taui', lambda: self.tau_i.value), 85 | ('taud', lambda: self.tau_d.value), 86 | ] 87 | 88 | def start(self): 89 | self.action_manual() 90 | 91 | def stop(self): 92 | self.auto.disabled = True 93 | self.manual.disabled = True 94 | 95 | def action_auto(self, w=None): 96 | self.mode = 'auto' 97 | self.manual.disabled = False 98 | self.auto.disabled = True 99 | 100 | self.Q1.disabled = True 101 | self.setpoint.disabled = False 102 | self.gain.disabled = False 103 | self.tau_i.disabled = False 104 | self.tau_d.disabled = False 105 | 106 | def action_manual(self, w=None): 107 | self.mode = 'manual' 108 | self.manual.disabled = True 109 | self.auto.disabled = False 110 | 111 | self.Q1.disabled = False 112 | self.setpoint.disabled = True 113 | self.gain.disabled = True 114 | self.tau_i.disabled = True 115 | self.tau_d.disabled = True 116 | 117 | def action_gain(self, w): 118 | self.pid.K = self.gain.value 119 | 120 | def action_tau_i(self, w): 121 | self.pid.taui = self.tau_i.value 122 | 123 | def action_tau_d(self, w): 124 | self.pid.taud = self.tau_d.value -------------------------------------------------------------------------------- /test_all_notebooks.py: -------------------------------------------------------------------------------- 1 | """This file is intended to be the target of a pytest run. 2 | 3 | It will find all .ipynb files in the current directory, ignoring directories 4 | which start with . and any files which match patterins found in the file 5 | .testignore 6 | 7 | Example .testignore pattern to ignore files in a directory: 8 | 9 | under_construction/* 10 | 11 | Sample invocations of pytest which make the output nicely readable: 12 | 13 | pytest --verbose --durations=5 test.py 14 | 15 | If you install pytest-xdist you can run tests in parallel with 16 | 17 | pytest --verbose --durations=5 -n 4 test.py 18 | 19 | """ 20 | 21 | import pathlib 22 | 23 | import pytest 24 | 25 | import nbformat 26 | from nbconvert.preprocessors import ExecutePreprocessor, CellExecutionError 27 | 28 | # Default search path is the current directory 29 | searchpath = pathlib.Path('.') 30 | 31 | # Read patterns from .testignore file 32 | ignores = [line.strip() for line in open('.testignore') if line.strip()] 33 | 34 | # Ignore hidden folders (startswith('.')) and files matching ignore patterns 35 | notebooks = [notebook for notebook in searchpath.glob('**/*.ipynb') 36 | if not (any(parent.startswith('.') 37 | for parent in notebook.parent.parts) 38 | or any(notebook.match(pattern) 39 | for pattern in ignores))] 40 | 41 | notebooks.sort() 42 | ids = [str(n) for n in notebooks] 43 | 44 | @pytest.mark.parametrize("notebook", notebooks, ids=ids) 45 | def test_run_notebook(notebook): 46 | """Read and execute notebook 47 | 48 | The method here is directly from the nbconvert docs 49 | 50 | Note that there is no error handling in this file as any errors will be 51 | caught by pytest 52 | 53 | """ 54 | with open(notebook) as f: 55 | nb = nbformat.read(f, as_version=4) 56 | ep = ExecutePreprocessor(timeout=600) 57 | ep.preprocess(nb, {'metadata': {'path': notebook.parent}}) 58 | -------------------------------------------------------------------------------- /tests/test_numeric.py: -------------------------------------------------------------------------------- 1 | from tbcontrol.numeric import skogestad_half 2 | import pytest 3 | 4 | def test_skogestad_half_skogestad(): 5 | # This is the example from 6 | # http://folk.ntnu.no/skoge/presentation/promatch-cybernetica-06/4tuning_short.pdf 7 | num_timeconstants = [-0.3, 0.08] 8 | den_timeconstants = [2, 1, 0.4, 0.2, 0.05, 0.05, 0.05] 9 | 10 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=0, order=1) 11 | 12 | assert delay == pytest.approx(1.47, 0.01) 13 | assert timeconstants[0] == pytest.approx(2.5) 14 | 15 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=0, order=2) 16 | 17 | assert delay == pytest.approx(0.77, 0.01) 18 | assert timeconstants[0] == pytest.approx(2) 19 | assert timeconstants[1] == pytest.approx(1.2) 20 | 21 | def test_skogestad_half_seborg_ex5_4(): 22 | # This is the example from seborg, Example 5.4 23 | num_timeconstants = [-0.1] 24 | den_timeconstants = [5, 3, 0.5] 25 | 26 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=0, order=1) 27 | 28 | assert delay == pytest.approx(2.1, 0.01) 29 | assert timeconstants[0] == pytest.approx(6.5) 30 | 31 | def test_skogestad_half_seborg_ex5_5(): 32 | # This is the example from seborg, Example 5.4 33 | num_timeconstants = [-1] 34 | den_timeconstants = [12, 3, 0.2, 0.05] 35 | 36 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=1, order=1) 37 | 38 | assert delay == pytest.approx(3.75, 0.01) 39 | assert timeconstants[0] == pytest.approx(13.5) 40 | 41 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=1, order=2) 42 | 43 | assert delay == pytest.approx(2.15, 0.01) 44 | assert timeconstants[0] == pytest.approx(12) 45 | assert timeconstants[1] == pytest.approx(3.1) 46 | 47 | def test_skogestad_half_integercoefficients(): 48 | # this used to break with earlier versions of the code 49 | num_timeconstants = [10] 50 | den_timeconstants = [20, 5, 5] 51 | 52 | delay, timeconstants = skogestad_half(num_timeconstants, den_timeconstants, delay=10, order=1) 53 | 54 | assert delay == pytest.approx(7.5, 0.01) 55 | assert timeconstants[0] == pytest.approx(22.5) 56 | --------------------------------------------------------------------------------