Modeling Closed Loops¶
Here, we illustrate how to use controlSBML
to model a closed loop system
for the SBML model.
Most of the work is done by the
CalTech control library.
For the open loop system, we use the Antimony model below.
1SIMPLE_OLS_MODEL = """
2 J1: S1 -> S2; S1
3 J2: S2 -> S1; S2
4 J3: S2 -> ; k*S2
5 S1 = 10; S2 = 0;
6 k = 0.8
7"""
Open loop system¶
Suppose we want the response to a unit step in S1
to
be that S2
is 3.
That is, REF = 3
.
To analyze well does the open loop system achieve this goal,
we use the following script, which produces the plot below.
1# Desired value for S2
2REF = 3
3# Open loop system
4ctlsb = ctl.ControlSBML(SIMPLE_OLS_MODEL,
5 input_names=["S1"], output_names=["S2"])
6simple_ols = ctlsb.makeNonlinearIOSystem("simple_ols")
7# Plot the impulse response response relative to reference
8times = [0.1*n for n in range(201)]
9time_response = control.input_output_response(simple_ols,
10 times, X0=ctl.makeStateVector(simple_ols), U=0)
11plotTimeResponse(time_response, ["S2"], is_legend=False)
12plt.plot([times[0], times[-1]], [REF, REF], linestyle="--")
13plt.legend(["S2", "REF"])
Controller¶
To achieve our control objective, we will construct a closed loop system, as depicted in the block diagram below. This system includes the original open loop system, and it adds a controller.
We will use a proportional-integral (PI) controller. This controller is the two controllers. A proportional controller (P) and an integral controller (I). The equation for the proportional controller is \(u(t) = k_P e(t)\). The equation for the integral controller is \(u(t) = k_I\int_0^t e(u) du\). Thus, the equation for the PI controller is:
Modeling the PI controller as a control.NonlinearIOSystem
object requires that we construct functions that correspond to
the two state equations above.
The updfcn
function
describes the derivative of state;
the outfcn
function produces the outputs from the
current values of state.
Since our model does not include a separate element
for calculating \(e(t)\), this is done inside
the controller.
Thus, these functions input \(y(t)\).
The full collection of arguments to the functions are:
\(t\), \(x(t)\):, and \(y(t)\).
1# Calculate derivative of state
2def updfcn(t, x, y, _):
3 # Accumulate the control error
4 # t: float (time)
5 # x: array-float (state)
6 # y: array-float (output from OLS)
7 # returns: array-float (derivative of state)
8 dx = REF - y[0]
9 return dx
1# Calculate output value
2kP = 0.5
3kI = 0.1
4def outfcn(t, x, y, _):
5 # Calculate the output from the input
6 # t: float (time)
7 # x: array-float (state)
8 # e: array-float (inputs)
9 # returns: array (output)
10 new_err = REF - y[0]
11 output = kI*x[0] + kP*new_err
12 return output
Notice that we introduced a new variable \(x(t)\). This is a state variable. It is used to calculate the integral of the control errors.
Now we can construct the control.NonlinearIOSystem
object.
1controller = control.NonlinearIOSystem(
2 updfcn,
3 outfcn,
4 states=1,
5 inputs=['in'],
6 outputs=['out'],
7 name='controller')
In addition to specifying the update and output functions,
this constructor also indicates the number of internal states
(one, \(x(t)\)), the name of the input to the
controller (in
) and the name of the output from the
controller (out
).
These names are used later when we connect the controller
to the open loop system.
Closed loop system¶
The next step is to connect the controller with the OLS.
Specifying these connections requires that we have
names for the inputs to elements and the outputs
from elements.
For example, the controller generates an output
signal named output
; its name is
controller.output
(because we named this element
controller
in the constructor).
ctl.ControlSBML.makeNonlinearIOSystem
creates A
NonlinearIOSystem
object with predefined names for the
inputs and outputs.
For example, simple_ols
has the input
simple_ols.S1
and the output simple_ols.S2
.
Connects are specified by
a collection of input and output
relationships specified by a python list
.
The first element of the list is the name of the signal source;
the second element is a signal destination (an input
to a control element).
So, to connect the output from simple_ols
to the
input to the controller, we use the list
[S2, controller.in]
.
The control.interconnect
objects are connections
of NonlinearIOSystem
elements.
Below is a constructor for the model we are
building.
Note that the first argument of the constructor
is the list of NonlinearIOSystem
objects
being interconncted.
inplist
and outlist
are lists of the inputs to
and outputs from the assembly of
NonlinearIOSystem
objects.
1# Create the closed loop system
2simple_closed = control.interconnect(
3[simple_ols, controller], # systems
4connections=[
5 ['simple_ols.S1', 'controller.out'],
6 ['controller.in', 'simple_ols.S2'],
7],
8inplist=["controller.in"],
9outlist=["simple_ols.S2", "controller.out"],
10)
Evaluation¶
The final step is to evaluate the performance of the above system.
This is done in the same way as what we did for simple_ols
using control.input_output_response
.
One consideration here is that this function
requires specifying the initial state for
all elements in simple_closed
.
Even with just two elements, this can be complicated.
The function ctrl.makeStateVector
handles this problem
although there are some subtlties.
1initial_state = ctl.makeStateVector(simple_closed)
2times = [0.1*n for n in range(301)]
3time_response = control.input_output_response(simple_closed,
4 times, X0=initial_state)
5plt.plot([times[0], times[-1]], [REF, REF], linestyle="--")
6stmts=["plt.ylim([-5, 5])"]
7stmts.append('plt.legend(["REF", "S2", "e(t)"])')
8plotTimeResponse(time_response,
9 ["S2", "controller.out"],
10 stmts=stmts, is_legend=False)