None Notebook

This notebook contains material from CBE30338; content is available on Github.

< A.1 Python Library for CBE 30338 | Contents | Tag Index | A.3 Animation in Jupyter Notebooks >

Open in Colab

Download

A.2 Modular Simulation using Python Generators

This notebook show how to use Python generators for creating system simulation. This technique implements simulation blocks as Python generators, then pieces blocks together to create more complex systems. This is an advanced technique that may be useful in control projects and a convenient alternative to block diagram simulators.

A.2.1 Simulation using scipy.integrate.odeint()

A.2.1.1 Typical Usage

The SciPy library provides a convenient and familiar means of simulating systems modeled by systems of ordinary differential equations. As demonstrated in other notebooks, the straightforward approach consists of several common steps

  1. Initialize graphics and import libraries
  2. Fix parameter values
  3. Write a function to evaluate RHS of the differential equations
  4. Choose initial conditions and time grid
  5. Perform the simulation by numerical solution of the differential equations
  6. Prepare visualizations and post-processing

Here we demonstrate this approach for a two gravity-drained tanks connected in series with constant inflow.

A.2.2 What's Wrong with That?

If direct simulation as outlined above meets the needs of your project, then be satisfied and move on. This is how these tools are intended to be used.

However, as written above, simulation with scipy.integrate.odeint requires you to write a function that calculates the right hand side of a system of differential equations. This can be challenging for complex system. For example, you may have multiple PID controllers, each implementing logic for anti-reset windup. Or you may have components in the process that exhibit hysterisis, time-delay, or other difficult-to-model dynamics. These cases call for a more modular approach to modeling and simulation.

In these cases we'd like to combine the continous time dynamics modeled by differential equations with more complex logic executed at discrete points in the time.

A.2.3 Python Generators

A.2.3.1 Yield Statement

One of the more advanced and often overlooked features of Python is the use of generators and iterators for performing operations on sequences of information. In particular, a generator is a function that returns information to via the yield statement rather then the more commonly encountered return statement. When called again, the generator picks right up at the point of the yield statement.

Let's demonsrate this by writing a generator of Fibonacci numbers. This generator returns all Fibonacci numbers less or equal to a given number $n$.

Here's a typical usage. What are the Fibonacci numbers less than or equal to 100?

The generator can also be used inside list comprehensions.

A.2.3.2 Iterators

When called, a generator function creates an intermediate function called an iterator. Here we construct the iterator and use it within a loop to find the first 10 Fibonacci numbers.

Using next on an iterator returns the next value.

A.2.3.3 Two-way communcation with Generators using Send

So far we have demonstrated the use of yield as a way to communicate information from the generator to the calling program. Which is fine if all you need is one-way communication. But for the modular simulation of processes, we need to be able to send information both ways. A feedback control module, for example, will need to obtain current values of the process variable in order to update its internal state to provide an update of the manipulated variable to calling programm.

Here's the definition of a generator for negative feedback proportional control where the control gain $K_p$ and setpoint $SP$ are specified constants.

The yield statement is now doing double duty. When first called it sends the value of MV back to the calling program, then stops and waits. It is waiting for the calling program to send a value of PV using the .send() method. Execution resumes until the yield statement is encountered again and the new value of MV returned to the calling program.

With this behavior in mind, gettting the generator ready for use is a two step process. The first step is to create an instance (i.e., an iterator). The second step is to initialize the instance by issuing .send(None) command. This is will halt execution at the first yield statement. At that point the generator instance will be ready to go for subsequent simulation.

Here's the initialization of a new instance of proportional control with $K_p = 2.5$ and $SP = 2$.

This shows it in use.

You can verify that these results satisfy the proportional control relationship.

A.2.4 Example Application: Modeling Gravity-Drained Tanks with Python Generators

The first step in using a Python generator for simulation is to write the generator. It will be used to create instances of the dynamical process being modeled by the generator. Parameters should include a sample time dt and any other model parameters you choose to specify a particular instance of the process. The yield statement should provide time plus any other relevant process data. The yield statement will produce new values of process inputs valid for the next time step.

A.2.4.1 Generator for a Gravity-Drained Tank

A.2.4.2 Simulation of a Single Tank with Constant Inflow

Next we show how to use the generator to create a simulation consisting of a single gravity drained tank with constant inflow.

  1. Choose a sample time for the simulation.
  2. Create instances of the processes to be used in your simulation.
  3. The first call to an instance is f.send(None). This will return the initial condition.
  4. Subsequent calls to the instance should be f.send(u) where u is variable, tuple, or other data time being passed to the process. The return value will be a tuple contaning the next value of time plus other process data.

A.2.4.3 Simulation of Two Tanks in Series

A.2.4.4 Simulation of Two Tanks in Series with PI Level Control on the Second Tank

A.2.4.5 Adding a PI Control Generator

A.2.4.6 Implementing Cascade Control for Two Tanks in Series with Unmeasured Disturbance

A.2.5 Enhancing Modularity with Class Definitions for Process Units

One of the key goals of a modular approach to simulation is to implement process specific behavior within the definitions of the process, and separate from the organization of information flow among units that takes place in the main simulation loop.

Below we define two examples of class definitions demonstrating how this can be done. The class definitions add features for defining names and parameters for instances of each class, and functions to log and plot data gathered in the course of simulations.

A.2.5.1 Gravity-Drained Tank Class

A.2.5.2 PI Controller Class

A.2.5.3 Modular Simulation of Cascade Control for Two Tanks in Series

The following simulation shows how to use the class definitions in a simulation. Each process instance used in the simulation requires three actions:

  1. Create an instance of the process. This is the step at which you can provide an instance name, parameters specific to the process and instance. Methods associated with the instance will be used to examine simulation logs and plot simulation results.

  2. Create a generator. A call to the generator function for each process instance creates an associated iterator. A sample time must be specified.

  3. An initial call to the iterator with an argument of None is needed to advance execution to the first yield statement.

< A.1 Python Library for CBE 30338 | Contents | Tag Index | A.3 Animation in Jupyter Notebooks >

Open in Colab

Download