{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "

Basic Block Diagram Operations

\n", "

MCHE474: Control Systems

\n", "

Dr. Joshua Vaughan
\n", "joshua.vaughan@louisiana.edu
\n", "http://www.ucs.louisiana.edu/~jev9637/

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this notebook, we'll take a quick look at some of the ways to manipulate transfer functions using the [Control System Library](http://python-control.readthedocs.io/en/latest/index.html), mimicing the block diagram algebra that we can do by hand. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Notebook Imports\n", "\n", "As usual, we'll start by importing the libraries we'll need to work. These include [NumPy](http://www.numpy.org), [matplotlib](https://matplotlib.org) for plotting, and the Control Systems Library itself. The code cells here will appear unchanged in most of the notebooks we'll use in *MCHE474*.\n", "\n", "We first import `numpy` and give it a \"nickname\" `np`. In doing so, we can preface calls to all NumPy functions with `np.` rather than having to type `numpy.`. This is a common convention in the use of NumPy. We say that we have imported NumPy into the namespace `np`. I'm oversimplifying a bit, but having different namespaces allows libraries to have functions of the same name, since all calls to the library or module will include the namespace information." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Grab all of the NumPy functions with namespace np\n", "import numpy as np " ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Next, we'll import matplotlib. This is another cell that will show up unchanges in nearly every Notebook that we do in *MCHE474*. The `%matplotlib inline` command tells the Notebook to include the plots inline with it, rather than plotting them in a seperate window. We import the `matplotlib.pyplot` module into the namepace `plt`. As you'll see, this means that nearly all of our plotting commands are prefixed by `plt.`." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "%matplotlib inline\n", "\n", "# Import the plotting functions \n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally we'll import the Control Systems Library. We don't specify a shorthand namespace, so we'll need to preface any commands from it with `control.`." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import control # This will import the control library. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we've imported the necessary libraries, let's walk through the basic use of them, roughly mirroring Section 2.9 of [*Modern Control Systems (13th Edition)* by Richard Dorf and Robert Bishop](http://amzn.com/0134407628), the current (as of Fall 2017) textbook for *MCHE474*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Defining the Plant Transfer Function\n", "We'll start by looking at the system in Figure 1. It's a simple mass-spring-damper system, with mass $m$ connected to ground through a spring $k$ and damper $c$. There is also a input force, $f$, acting on the mass.\n", "\n", "

\n", "\t\"Example
\n", " Figure 1: Example Mass-spring-damper System\n", "

\n", "\n", "The equation of motion of this system is:\n", "\n", "$ \\quad m \\ddot{x} + c \\dot{x} + kx = f $\n", "\n", "If we define $\\omega_n$ to be the system's *natural frequency* and $\\zeta$ as the system's *damping ratio*, we can rewrite this equation as:\n", "\n", "$ \\quad \\ddot{x} + \\frac{c}{m} \\dot{x} + \\frac{k}{m}x = \\frac{f}{m} $\n", "\n", "Then,\n", "\n", "$ \\quad \\ddot{x} + 2\\zeta\\omega_n \\dot{x} + \\omega_n^2 x = \\frac{f}{m} $\n", "\n", "where\n", "\n", "$ \\quad \\omega_n = \\sqrt{\\frac{k}{m}} $\n", "\n", "and \n", "\n", "$ \\quad 2\\zeta\\omega_n = \\frac{c}{m} $" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The transfer function of this system is:\n", "\n", "$ \\quad G(s) = \\frac{X(s)}{F(s)} = \\frac{1}{ms^2 + cs + k} $\n", "\n", "Like the time-domain solution, we can also write this transfer function in terms of the natural frquency, $\\omega_n$, and damping ratio, $\\zeta$, of the system:\n", "\n", "$ \\quad G(s) = \\frac{X(s)}{F(s)} = \\frac{1/m}{s^2 + 2\\zeta\\omega_n s + \\omega_n^2} $\n", "\n", "Let's use this second form. \n", "\n", "We'll first define an array, which we'll call `num` that contains the numerator value. Since it only has one value, the command will be of the form:\n", "\n", " num = [1 / m]\n", "\n", "To define the denominator, we need to create an array where the element represents the power of `s` for the constant there. In this case:\n", "\n", " den = [1, 2 * zeta * w_n, w_n**2]\n", "\n", "To check this, work from the \"right\" side of the definition. `w_n**2` is $\\omega_n^2$, which is mulitplied by $s^0$. The next element is `2 * zeta * w_n`, which is the $2\\zeta\\omega_n$ term and given its position in the first indexed element of the array is multiplied by $s^1$. This continues for all similar definitions.\n", "\n", "***Note:*** This is one difference between Python and MATLAB. In Python, the commas are always needed between elements. In MATLAB, they are (sometimes) optional." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll first define the system parameters, picking the natural frequency and damping ratio to match the one used in the book:\n", "\n", "$\\quad \\omega_n = \\sqrt{2} $ rad/s\n", "\n", "and\n", "\n", "$ \\quad \\zeta = \\frac{1}{2\\sqrt{2}} $." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Define the natural frequency. We use the numpy sqrt function, so we need to preface its call with np.\n", "w_n = np.sqrt(2) \n", "\n", "# Define the damping ratio. \n", "zeta = 1 / (2 * np.sqrt(2))\n", "\n", "# We'll also define the mass of the system\n", "m = 1.0 # mass (kg)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Define the numerator and denominator of the transfer function\n", "num = [1 / m]\n", "den = [1, 2 * zeta * w_n, w_n**2]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, that these are defined, we can pass them to the `tf` function of the library. Remember that since `tf` is a function in the `control` module, we need to use `control.tf()` to call it. We'll assign the variable `sys` to hold transfer function returned. " ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Define the transfer function form of the system defined by num and den\n", "sys = control.tf(num, den)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can check that the transfer function is defined correctly by printing `sys`." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " 1\n", "-----------\n", "s^2 + s + 2\n", "\n" ] } ], "source": [ "print(sys)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Block Diagram Algebra" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Series Connections\n", "If two transfer functions are connected in series, with the output of one serving as the input to the other, as in the block diagram in Figure 2, we can use the [`series` function](http://python-control.readthedocs.io/en/latest/generated/control.series.html#control.series) in the Control System Toolbox to calculate the equivalent transfer function.\n", "\n", "

\n", "\t\"Block
\n", " Figure 2: Block Diagram of Series Connected Transfer Functions\n", "

\n", "\n", "Let's consider the mass-spring-damper system as $G_2$ from this image and define $G_1$ to be:\n", "\n", "$ \\quad G_1(s) = \\frac{k_d s + k_p}{1} $" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# First define kp and kd\n", "kp = 1.0\n", "kd = 0.1\n", "\n", "# Define the G1 transfer function\n", "g1_num = [kd, kp]\n", "g1_den = [1]\n", "\n", "g1_sys = control.tf(g1_num, g1_den)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can use the Control System Toolbox to calculate the transfer function of the simplified version of this block diagram, as shown in Figure 3.\n", "

\n", "\t\"Reduced
\n", " Figure 3: Reduced Block Diagram of a Series Connection\n", "

" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " 0.1 s + 1\n", "-----------\n", "s^2 + s + 2\n", "\n" ] } ], "source": [ "# Calculate the series connection\n", "series_sys = control.series(g1_sys, sys)\n", "\n", "# print the resulting transfer function\n", "print(series_sys)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Short aside on string formatting:**\n", "We can nicely format our printing in Python. [This article](https://pyformat.info) contains a nice summary of the `.format()` method of doing so that our notebooks will typically use. There is also a new (in Python 3.6) [`f-string` formatting](https://docs.python.org/3.6/reference/lexical_analysis.html#f-strings) method that may be more convenient. So, we can print the transfer function above a bit more nicely by either:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The resulting transfer function is: \n", " 0.1 s + 1\n", "-----------\n", "s^2 + s + 2\n", "\n" ] } ], "source": [ "print('The resulting transfer function is: {}'.format(series_sys))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The resulting transfer function is: \n", " 0.1 s + 1\n", "-----------\n", "s^2 + s + 2\n", "\n" ] } ], "source": [ "print(f'The resulting transfer function is: {series_sys}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Unity Feedback Connections\n", "If we close the loop around the $G_1$ and $G_2$, as shown in Figure 4, we can use the [`feedback` function](http://python-control.readthedocs.io/en/latest/generated/control.feedback.html#control.feedback) of the library to calculate the resulting closed-loop transfer function.\n", "

\n", "\t\"Block
\n", " Figure 4: Block Diagram with Feedback\n", "

\n", "\n", "The feedback function will also work if there is a block in the feedback path. By default, the function assumes that the system has unity feedback, as this one does. The feedback is also is negative (as is most often the case), which is the default for the function. We should specify positive, if we needed." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The closed-loop transfer function is \n", " 0.1 s + 1\n", "---------------\n", "s^2 + 1.1 s + 3\n", "\n" ] } ], "source": [ "# Calculate the closed-loop transfer function\n", "sys_closedLoop = control.feedback(series_sys)\n", "\n", "# Then print out the result\n", "print('The closed-loop transfer function is {}'.format(sys_closedLoop))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This matches the reduced form of that block diagram:\n", "\n", "$\\quad G_{CL} = \\frac{G_1 G_2}{1 + G_1 G_2} $" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Non-unity Feedback Connections\n", "If there is a block in the feedback path, like that shown in Figure 5, then the system no longer has unity feedback. In this case, we need to include that transfer function in the call to `feedback`. Below, we'll first define that transfer function, $H(s)$, then include it in the function call.\n", "\n", "

\n", "\t\"Block
\n", " Figure 5: Block Diagram with Non-unity Feedback\n", "

" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# First define the numerator and denominator of H(s)\n", "h_num = [1]\n", "h_den = [0.05, 1]\n", "\n", "# Now, define the transfer function for H(s)\n", "h_sys = control.tf(h_num, h_den)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we've define the transfer function of the block in the feedback loop, we can include it in the call to the `feedback` function of the library:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": true }, "outputs": [], "source": [ "sys_closedLoop_withH = control.feedback(series_sys, h_sys)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and print out the result:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The closed-loop transfer function for the non-unity feedback system is \n", " 0.005 s^2 + 0.15 s + 1\n", "-------------------------------\n", "0.05 s^3 + 1.05 s^2 + 1.2 s + 3\n", "\n" ] } ], "source": [ "print('The closed-loop transfer function for the non-unity feedback system is {}'.format(sys_closedLoop_withH))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Parallel Connections\n", "Parallel connections, like the one shown in Figure 5, are handled by, you guessed it, the [`parallel` function](http://python-control.readthedocs.io/en/latest/generated/control.parallel.html#control.parallel) of the library.\n", "\n", "

\n", "\t\"Block
\n", " Figure 5: Block Diagram with a Parallel Connection\n", "

\n", "We'll use the previously defined transfer functions for this example." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The transfer function for the parallel connection is \n", "0.1 s^3 + 1.1 s^2 + 1.2 s + 3\n", "-----------------------------\n", " s^2 + s + 2\n", "\n" ] } ], "source": [ "# Calculate the transfer function\n", "sys_parallel = control.parallel(sys, g1_sys)\n", "\n", "# Then, print it out\n", "print('The transfer function for the parallel connection is {}'.format(sys_parallel))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For these parallel connections, we know that the reduced block diagram results in a transfer function of:\n", "\n", "$ \\quad G_{parallel}(s) = G_1 + G_2 $\n", "\n", "Because of this, we just add the two transfer functions and should get the same result as the `parallel` function:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\n", "0.1 s^3 + 1.1 s^2 + 1.2 s + 3\n", "-----------------------------\n", " s^2 + s + 2" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sys + g1_sys" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can, of course, apply these functions multiple times (similar to how we did for the feedback loop examples above) to achieve the level of block diagram reduction we desire. We'll do so for some examples in other notebooks." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "#### Licenses\n", "Code is licensed under a 3-clause BSD style license. See the licenses/LICENSE.md file.\n", "\n", "Other content is provided under a [Creative Commons Attribution-NonCommercial 4.0 International License](http://creativecommons.org/licenses/by-nc/4.0/), CC-BY-NC 4.0." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# This cell will just improve the styling of the notebook\n", "from IPython.core.display import HTML\n", "import urllib.request\n", "response = urllib.request.urlopen(\"https://cl.ly/1B1y452Z1d35\")\n", "HTML(response.read().decode(\"utf-8\"))" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.2" } }, "nbformat": 4, "nbformat_minor": 1 }