5.1. Response of a First Order System to Step and Square Wave Inputs#

Keywords: Simulator, ipopt usage

This notebook demonstrates simulation of a linear first-order system in Pyomo using the Simulator class from Pyomo. The notebook also demonstrates the construction and use of analytical approximations to step functions and square wave inputs.

5.1.1. Imports#

import matplotlib.pyplot as plt

from math import pi

import shutil
import sys
import os.path

if not shutil.which("pyomo"):
    !pip install -q pyomo
    assert(shutil.which("pyomo"))

if not (shutil.which("ipopt") or os.path.isfile("ipopt")):
    if "google.colab" in sys.modules:
        !wget -N -q "https://ampl.com/dl/open/ipopt/ipopt-linux64.zip"
        !unzip -o -q ipopt-linux64
    else:
        try:
            !conda install -c conda-forge ipopt 
        except:
            pass

assert(shutil.which("ipopt") or os.path.isfile("ipopt"))
    
from pyomo.environ import *
from pyomo.dae import *

5.1.2. First-order differential equation with constant input#

The following cell simulates the response of a first-order linear model in the form

\[\begin{split} \begin{align} \tau\frac{dy}{dt} + y & = K u(t) \\ \end{align} \end{split}\]

where \(\tau\) and \(K\) are model parameters, and \(u(t)=1\) is an external process input.

tfinal = 10
tau = 1
K = 5

# define u(t)
def u(t):
    return 1.0

# create a model object
model = ConcreteModel()

# define the independent variable
model.t = ContinuousSet(bounds=(0, tfinal))

# define the dependent variables
model.y = Var(model.t)
model.dydt = DerivativeVar(model.y)

# fix the initial value of y
model.y[0].fix(4)

# define the differential equation as a constraint
@model.Constraint(model.t)
def ode(m, t):
    return tau*model.dydt[t] + model.y[t] == K*u(t)
model.y.display()
y : Size=2, Index=t
    Key : Lower : Value : Upper : Fixed : Stale : Domain
      0 :  None :     4 :  None :  True : False :  Reals
     10 :  None :  None :  None : False :  True :  Reals
# simulation using scipy integrators
tsim, profiles = Simulator(model, package='scipy').simulate(numpoints=101)
tsim
profiles
array([[4.        ],
       [4.09516518],
       [4.18126635],
       [4.25917761],
       [4.32967597],
       [4.39346666],
       [4.45118776],
       [4.50341441],
       [4.55067095],
       [4.5934305 ],
       [4.63212104],
       [4.66713048],
       [4.69880924],
       [4.72747142],
       [4.75340218],
       [4.77686354],
       [4.79809743],
       [4.81731337],
       [4.83469911],
       [4.85042886],
       [4.86466192],
       [4.87754068],
       [4.88919421],
       [4.89973902],
       [4.90928022],
       [4.91791317],
       [4.92572462],
       [4.93279283],
       [4.93918842],
       [4.9449753 ],
       [4.95021132],
       [4.95494926],
       [4.95923631],
       [4.96311531],
       [4.9666254 ],
       [4.96980139],
       [4.97267511],
       [4.97527546],
       [4.97762825],
       [4.97975715],
       [4.98168354],
       [4.98342653],
       [4.9850037 ],
       [4.9864308 ],
       [4.98772205],
       [4.98889045],
       [4.98994758],
       [4.99090399],
       [4.99176946],
       [4.99255276],
       [4.99326147],
       [4.99390272],
       [4.99448304],
       [4.99500813],
       [4.99548319],
       [4.99591304],
       [4.99630201],
       [4.99665391],
       [4.99697229],
       [4.99726045],
       [4.99752118],
       [4.99775709],
       [4.99797055],
       [4.99816375],
       [4.99833848],
       [4.99849659],
       [4.99863967],
       [4.9987691 ],
       [4.99888618],
       [4.99899214],
       [4.99908806],
       [4.99917492],
       [4.99925359],
       [4.99932482],
       [4.99938923],
       [4.99944739],
       [4.99950001],
       [4.99954764],
       [4.99959071],
       [4.99962961],
       [4.99966475],
       [4.99969657],
       [4.99972537],
       [4.99975145],
       [4.99977533],
       [4.99979649],
       [4.99981579],
       [4.99983331],
       [4.99984916],
       [4.99986348],
       [4.99987643],
       [4.99988812],
       [4.99989869],
       [4.99990825],
       [4.99991692],
       [4.99992473],
       [4.99993178],
       [4.99993818],
       [4.999944  ],
       [4.99994935],
       [4.99995422]])
fig, ax = plt.subplots(1, 1)
ax.plot(tsim, profiles, label='y')
ax.plot(tsim, [u(t) for t in tsim], label='u')
ax.set_xlabel('time / sec')
ax.set_ylabel('response')
ax.set_title('Response of a linear first-order ODE')
ax.legend()
ax.grid(True)
../_images/b8133e0a1dc3a12ef9026a8e33dd6b28ec9853b4c434bf2c0582893121175384.png

5.1.3. Encapsulating into a function#

In following cells we would like to explore the response of a first order system to changes in parameters and input functions. To facilitate this study, the next cell encapsulates the simulation into a function that can be called with different parameter values and input functions.

def first_order(K=1, tau=1, tfinal=1, u=lambda t: 1):
    model = ConcreteModel()
    model.t = ContinuousSet(bounds=(0, tfinal))
    model.y = Var(model.t)
    model.dydt = DerivativeVar(model.y)
    model.y[0].fix(0)
    @model.Constraint(model.t)
    def ode(m, t):
        return tau*model.dydt[t] + model.y[t] == K*u(t)
    
    tsim, profiles = Simulator(model, package='scipy').simulate(numpoints=1000)

    fig, ax = plt.subplots(1, 1)
    ax.plot(tsim, profiles, label='y')
    ax.plot(tsim, [u(t) for t in tsim], label='u')
    ax.set_xlabel('time / sec')
    ax.set_ylabel('response')
    ax.set_title('Response of a linear first-order ODE')
    ax.legend()
    ax.grid(True)
    
first_order(5, 1, 30, sin)
../_images/0b8f435cb98988877575028ae39f5314e39d7bfd9d911005ff63eaf06e7b8c39.png

5.1.4. Analytical approximation to a step input#

The math functions supported by Pyomo are limited to that are imported to the standard arithmetic operations of Python (*, /, *, *=, /=, *=) and a particular set of nonlinear functions from the Python math library. Simulating the response of a system to a discontinuous step input, for example, requires an approximate.

An infinitely differentiable approximation to a step change is given by the Butterworth function \(b_n(t)\)

\[ b_n(t) = \frac{1}{1 + (\frac{t}{c})^n} \]

where \(n\) is the order of a approximation, and \(c\) is value of \(t\) where the step change occurs.

u = lambda t: 1/(1 + (t/10)**100)

first_order(5, 1, 30, u)
../_images/d7c574ee6252ca5ea39997a84b8e16bee7e011186c9037d9d26165d8acd1f495.png
u = lambda t: 1 - 1/(1 + (t/10)**100)

first_order(5, 1, 30, u)
../_images/7bf264d844b21ba94ae010f6429bfc5f7d73c16290acd0d9e36bb40fc4a4218c.png

5.1.5. Analytical approximation to a square wave input#

An analytical approximation to a square wave with frequency \(f\) is given by

\[\frac{4}{\pi} \sum_{k=1, 3, 5,\ldots}^N \frac{sin(k\pi/N)}{k\pi/N}\frac{sin(2\pi ft)}{k}\]

where the first term is the Lanczos sigma factor designed to suppress the Gibb’s phenomenon associated with Fourier series approximations.

def square(t, f=1, N=31):
    return (4/pi)*sum((N*sin(k*pi/N)/k/pi)*sin(2*k*f*pi*t)/k for k in range(1, N+1,2))
  
u = lambda t: square(t, 0.1)

first_order(5, 1, 30, u)
../_images/71ff42ce96bb52a0b3c9708ed4be81fe8e1b46c80a622507591c3d9f91a9d75f.png