Dependencies

The SimInterface package depends on the Scipy stack. So nothing will work if you don't have that installed.

In order to see generate the block diagrams, you'll also need the graphviz package.

The DC Motor Example

In this notebook, we'll demonstrate how to use the SimInterface package to build a model of a DC motor connected with an RL circuit.

The dynamic equations are given by: \begin{align*} L\dot I_t + RI_t &= V_t-K\dot\theta \\ J\ddot \theta_t + b\theta_t &= KI_t. \end{align*}

Here, $V_t$ is an input voltage, $I$ is the current through the circuit, and $\theta$ is the angle of the motor.

Before we get started we need to initialize, let's initialize the packages that we'll need.


In [1]:
import SimInterface as SI
import matplotlib.pyplot as plt
import numpy as np
import scipy.integrate as integ
%matplotlib inline

Defining Parameters

This system depends on the parameters, $L$, $R$, $K$, $J$, and $b$. To run a simulation with a fixed parameter set, their values could be hard-coded into the description of the differential equations. However, in many studies it is useful to be able to see how the system response to variations in the parameters.

To facilitate this, we have a dedicated class for static variables called Parameter. To see how it works, we'll just initialize all of the parameters to some nominal values.


In [2]:
L = SI.Parameter(label='L',data=1.0)
R = SI.Parameter(label='R',data=2.0)
K = SI.Parameter(label='K',data=1.0)
J = SI.Parameter(label='J',data=5.0)
b = SI.Parameter(label='b',data=.5)

Every parameter instance has a Pandas dataframe attribute .data:


In [3]:
L.data


Out[3]:
L
Value 1

Defining Signals

The voltage, $V$ is an exogenous input signal. We will take it to be a sinusoid of period $2$ and amplitude $3$, lasting for $10s$.

To define a Signal, we enter both the data, as well as time-stamps for the data. This is so that we know what time $V(t)$ takes a particular value.


In [4]:
Time = np.linspace(0,5,201)
Vsig = 3 * np.sin(np.pi * Time)
V = SI.Signal(label='V',data=Vsig,TimeStamp=Time)

Just like Parameter objects, the data for a Signal object is stored in a Pandas dataframe called .data. Let's look at the first 10 entries and also plot the data.


In [5]:
V.data.iloc[:10]


Out[5]:
V
Time 0.000 0.000000
0.025 0.235377
0.050 0.469303
0.075 0.700336
0.100 0.927051
0.125 1.148050
0.150 1.361971
0.175 1.567496
0.200 1.763356
0.225 1.948344

In [6]:
plt.plot(Time,V.data)
plt.xlabel('Time')
plt.ylabel(V.label)


Out[6]:
<matplotlib.text.Text at 0x106e598d0>

Defining the RL Circuit

The equations are sufficiently simple that we could code up one differential equation for all of them. However, it is instructive to code up the individual component systems separately, and then connect then connect them up later.

For just the RL circuit, the dynamics of the current are given in first-order form as \begin{equation*} \dot I = -\frac{R}{L} I + \frac{1}{L}V_{diff}, \end{equation*} where $V_{diff}$ is an auxilliary variable used in place of $V-K\theta$.

In code this is given by:


In [7]:
def RLVectorField(I,Vdiff,R,L):
    Idot = -(R/L) * I + (1./L) * Vdiff
    return Idot

We'll initialize $V_{diff}$ as $V$, as we'll assume for now that $\theta=0$. Later, we'll replace it with the true value of $V-K\theta$.


In [8]:
Vdiff = SI.Signal(label='Vdiff',data=Vsig,TimeStamp=Time)

Caution: Argument names should match variable labels.

It is important that in the function the arguments, I, V, R, and L, should match the labels given to associated signals and parameters. If they do not, the program will not know which data to associate with it.

Initializing the current

We will assume that the initial current is given by $I(0) = 1$. We will store the current as a signal variable. At the moment, we only set the initial condition.


In [9]:
I = SI.Signal(label='I',data=1,TimeStamp=0.)
# Just display the signal to see that it worked.
I.data


Out[9]:
I
Time 0 1

Creating the differential equation system

We can create a differential equation system as follows. Note that we must group the variables as state variables and input variables. Otherwise the system will not know what variables are exogenous and what are state variables.


In [10]:
RLEquation = SI.DifferentialEquation(func=RLVectorField,StateVars=I,InputVars=(Vdiff,R,L),label='RL Circuit')
# If graphviz is installed a graph is automatically generated. It can be displayed as follows:
RLEquation.graph


[array(['dI/dt'], 
      dtype='|S5'), array([0])]
Out[10]:
RL Circuit RL Circuit RL Circuit RL Circuit->RL Circuit I L L L->RL Circuit R R R->RL Circuit Vdiff Vdiff Vdiff->RL Circuit

Simulating the RL Circuit

Defining the differential equation creates a method called .VectorField. This method is a function of the form $f(t,x)$, corresponding to the differential equation: \begin{equation*} \dot x = f(t,x) \end{equation*} Thus, the method can be used in conjunction with standard ODE integrators.

In our particular example, $f$ corresponds to the vector field for the current, $I$.

Likewise, it also creates an initial state, vector, .InitialState.

Let's see how to simulate this.


In [11]:
# This is a standard way to use the ODE class from scipy
Integrator = integ.ode(RLEquation.VectorField)
Integrator.set_integrator('dopri5').set_initial_value(RLEquation.InitialState,0.0)

X = np.zeros((len(Time),2))
for k in range(len(Time)):
    t = Time[k]
    x = Integrator.integrate(t)
    X[k] = x
    
# There appears to be a weird memory effect with ODE, so get rid of the integrator after we're done
del Integrator


/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/scipy/integrate/_ode.py:1018: UserWarning: dopri5: step size becomes too small
  self.messages.get(idid, 'Unexpected idid=%s' % idid))

Putting the values into the Signal variables

Simulating the differential equation defined by RLEquation.VectorField is a "read-only" operation in the sense that none of the signal variables, I, Vdiff, etc are updated in the process. Indeed, while $I$, the current was the desired state, the variable I data remains unchanged by the simulation process:


In [12]:
I.data


Out[12]:
I
Time 0 1

We want the differential equations generated by SimInterface to behave just like those that would be coded "by hand". Thus, it is desirable that simulating the differential equation has no "side effects" that the user does not expect. One such side effect would be updating the data tables for the variables, such as I.

In order to update the values we use the .UpdateSignals method. Once this is applied, the new data will be stored in I.


In [13]:
RLEquation.UpdateSignals(Time,X)
VH = plt.plot(Time,Vdiff.data,label=Vdiff.label)[0]
IH = plt.plot(Time,I.data,label=I.label)[0]
plt.xlabel('Time')
plt.legend(handles=(VH,IH))


Out[13]:
<matplotlib.legend.Legend at 0x106fe08d0>

Defining the Motor

Recall the equation for the motor was given by:

\begin{equation*} J \ddot \theta + b\dot\theta = KI. \end{equation*}

This equation is of second-order. We can put it into first-order form by setting $\omega = \dot\theta$: \begin{align*} \dot\theta &= \omega \\ \dot\omega &= -\frac{b}{J}\omega + \frac{K}{J} I. \end{align*}

In code, we can define this as:


In [14]:
def MotorVectorField(theta,omega,I,J,b,K):
    theta_dot = omega
    omega_dot = -(b/J) * omega + (K/J) * I
    return theta_dot, omega_dot

In [15]:
theta = SI.Signal(label='theta',data=0.,TimeStamp=0)
omega = SI.Signal(label='omega',data=0.,TimeStamp=0)
MotorEquation = SI.DifferentialEquation(func=MotorVectorField,StateVars=(theta,omega),InputVars=(I,J,b,K),
                                        label='Motor')
MotorEquation.graph


[array(['dtheta/dt'], 
      dtype='|S9'), array([0])]
[array(['domega/dt'], 
      dtype='|S9'), array([0])]
Out[15]:
Motor Motor Motor Motor->Motor theta Motor->Motor omega I I I->Motor J J J->Motor b b b->Motor K K K->Motor

In [16]:
def VdiffFunc(V,omega,K):
    Vdiff = V - K * omega
    return Vdiff

In [17]:
VdiffSys = SI.StaticFunction(func=VdiffFunc,InputVars=(V,omega,K),OutputVars=Vdiff,label='V-K*omega')
VdiffSys.graph


Out[17]:
V-K*omega V-K*omega V-K*omega Vdiff Vdiff V-K*omega->Vdiff K K K->V-K*omega V V V->V-K*omega omega omega omega->V-K*omega

In [18]:
Sys = SI.Connect((MotorEquation,RLEquation,VdiffSys))
Sys.graph


Out[18]:
Sys Motor Motor Motor->Motor theta Motor->Motor omega V-K*omega V-K*omega Motor->V-K*omega omega RL Circuit RL Circuit V-K*omega->RL Circuit Vdiff RL Circuit->Motor I RL Circuit->RL Circuit I L L L->RL Circuit R R R->RL Circuit V V V->V-K*omega J J J->Motor b b b->Motor K K K->Motor K->V-K*omega

In [20]:
Sys.UpdateParameters()
IntegratorTotal = integ.ode(Sys.VectorField)
IntegratorTotal.set_integrator('dopri5').set_initial_value(Sys.InitialState,0.0)

X = np.zeros((len(Time),len(Sys.InitialState)))
for k in range(len(Time)):
    t = Time[k]
    x = IntegratorTotal.integrate(t)
    X[k] = x
    
del IntegratorTotal
Sys.UpdateSignals(Time,X)

plt.figure()
hV = plt.plot(Time,V.data,label='V')[0]
hI = plt.plot(Time,I.data,label='I')[0]
plt.legend(handles=(hV,hI))
plt.xlabel('Time')

plt.figure()
hTheta = plt.plot(Time,theta.data,label=R'$\theta$')[0]
hOmega = plt.plot(Time,omega.data,label='$\omega$')[0]
plt.legend(handles=(hTheta,hOmega))
plt.xlabel('Time')


Out[20]:
<matplotlib.text.Text at 0x107787750>

In [ ]: