3. Linear System Modeling, Analysis, and Design
Linear time invariant (LTI) systems are represented in python-control
in
state space, transfer function, or frequency response data (FRD) form. Most
functions in the toolbox will operate on any of these data types, and
functions for converting between compatible types are provided.
3.1. Creating LTI Systems
LTI systems are created using “factory functions” that accept the parameters required to define the system. Three factory functions are available for LTI systems:
|
Create a state space system. |
|
Create a transfer function system. |
|
Construct a frequency response data (FRD) model. |
Each of these functions returns an object of an appropriate class to represent the system.
State space systems
The StateSpace
class is used to represent state-space realizations
of linear time-invariant (LTI) systems:
where is the input,
is the output, and
is the state.
To create a state space system, use the ss()
function:
sys = ct.ss(A, B, C, D)
State space systems can be manipulated using standard arithmetic
operations as well as the feedback()
, parallel()
, and
series()
function. A full list of “block diagram algebra”
functions can be found in the System Interconnections section of
the Function Reference.
Systems, inputs, outputs, and states can be given labels to allow more customized access to system information:
sys = ct.ss(
A, B, C, D, name='sys',
states=['x1', 'x2'], inputs=['u1', 'u2'], outputs=['y'])
The rss()
function can be used to create a random state space
system with a desired number or inputs, outputs, and states:
sys = ct.rss(states=4, outputs=1, inputs=1, strictly_proper=True)
The states
, inputs
, and output
parameters can also be
given as lists of strings to create named signals. All systems
generated by rss()
are stable.
Transfer functions
The TransferFunction
class is used to represent input/output
transfer functions
where is greater than or equal to
for a proper
transfer function. Improper transfer functions are also allowed.
To create a transfer function, use the tf()
function:
num = [a0, a1, ..., am]
den = [b0, b1, ..., bn]
sys = ct.tf(num, den)
The system name as well as input and output labels can be specified in the same way as state space systems:
sys = ct.tf(num, den, name='sys', inputs=['u'], outputs=['y'])
Transfer functions can be manipulated using standard arithmetic
operations as well as the feedback()
, parallel()
, and
series()
functions. A full list of “block diagram algebra”
functions can be found in the System Interconnections section of the
Function Reference.
To aid in the construction of transfer functions, the tf()
factory function can used to create transfer function corresponding
to the derivative or difference operator:
s = ct.tf('s')
Standard algebraic operations can be used to construct more complicated transfer functions:
sys = 5 * (s + 10)/(s**2 + 2*s + 1)
Transfer functions can be evaluated at a point in the complex plane by calling the transfer function object:
val = sys(1 + 0.5j)
Discrete time transfer functions (described in more detail below) can
be created using z = ct.tf('z')
.
Frequency response data (FRD) systems
The FrequencyResponseData
(FRD) class is used to represent
systems in frequency response data form. The main data attributes are
omega
and frdata
, where omega
is a 1D array of frequencies (in
rad/sec) and frdata
is the (complex-value) value of the transfer
function at each frequency point.
FRD systems can be created with the frd()
factory function:
sys = ct.frd(frdata, omega)
FRD systems can also be created by evaluating an LTI system at a given set of frequencies:
frd_sys = ct.frd(sys_lti, omega)
Frequency response data systems have a somewhat more limited set of functions that are available, although all of the standard algebraic manipulations can be performed.
The FRD class is also used as the return type for the
frequency_response()
function. This object can be assigned to a
tuple using:
response = ct.frequency_response(sys_lti)
mag, phase, omega = response
where mag
is the magnitude (absolute value, not dB or log10) of the
system frequency response, phase
is the wrapped phase in radians of
the system frequency response, and omega
is the (sorted) frequencies
at which the response was evaluated.
Frequency response properties are also available as named attributes of
the response
object: response.magnitude
, response.phase
,
and response.response
(for the complex response).
Multi-input, multi-output (MIMO) systems
Multi-input, multi-output (MIMO) systems are created by providing
parameters of the appropriate dimensions to the relevant factory
function. For state space systems, the input matrix B
, output
matrix C
, and direct term D
should be 2D matrices of the
appropriate shape. For transfer functions, this is done by providing
a 2D list of numerator and denominator polynomials to the tf()
function, e.g.:
sys = ct.tf(
[[num11, num12], [num21, num22]],
[[den11, den12], [den21, den22]])
Similarly, MIMO frequency response data (FRD) systems are created by
providing the frd()
function with a 3D array of response
values,with the first dimension corresponding to the output index of
the system, the second dimension corresponding to the input index, and
the 3rd dimension corresponding to the frequency points in omega
.
Signal names for MIMO systems are specified using lists of labels:
sys = ct.ss(A, B, C, D, inputs=['u1', 'u2'], outputs=['y1', 'y2', 'y3'])
Signals that are not given explicit labels are given labels of the form ‘s[i]’ where the default value of ‘s’ is ‘x’ for states, ‘u’ for inputs, and ‘y’ for outputs, and ‘i’ ranges over the dimension of the signal (starting at 0).
Subsets of input/output pairs for LTI systems can be obtained by indexing the system using either numerical indices (including slices) or signal names:
subsys = sys[[0, 2], 0:2]
subsys = sys[['y1', 'y3'], ['u1', 'u2']]
Signal names for an indexed subsystem are preserved from the original
system and the subsystem name is set according to the values of
config.defaults['iosys.indexed_system_name_prefix']
and
config.defaults['iosys.indexed_system_name_suffix']
(see
Package Configuration Parameters for more information). The
default subsystem name is the original system name with ‘$indexed’
appended.
For FRD objects, the frequency response properties for MIMO systems can be accessed using the names of the inputs and outputs:
response.magnitude['y[0]', 'u[1]']
where the signal names are based on the system that generated the frequency response.
Note
If a system is single-input, single-output (SISO),
magnitude
and phase
default to 1D arrays, indexed by
frequency. If the system is not SISO or squeeze
is set to
False generating the response, the array is 3D, indexed by
the output, input, and frequency. If squeeze
is True for
a MIMO system then single-dimensional axes are removed. The
processing of the squeeze
keyword can be changed by
calling the response function with a new argument:
mag, phase, omega = response(squeeze=False)
Note
The frdata
data member is stored as a NumPy array and
cannot be accessed with signal names. Use
response.complex
to access the complex frequency response
using signal names.
3.2. Discrete Time Systems
A discrete-time system is created by specifying a nonzero “timebase”
dt
when the system is constructed:
sys_ss = ct.ss(A, B, C, D, dt)
sys_tf = ct.tf(num, den, dt)
The timebase argument is interpreted as follows:
dt
= 0: continuous-time system (default)dt
> 0: discrete-time system with sampling perioddt
dt
= True: discrete time with unspecified sampling perioddt
= None: no timebase specified (see below)
Systems must have compatible timebases in order to be combined. A
discrete-time system with unspecified sampling time (dt
= True) can
be combined with a system having a specified sampling time; the result
will be a discrete-time system with the sample time of the other
system. Similarly, a system with timebase None can be combined with a
system having a specified timebase; the result will have the timebase
of the other system. For continuous-time systems, the
sample_system()
function or the StateSpace.sample()
and
TransferFunction.sample()
methods can be used to create a
discrete-time system from a continuous-time system. The default value
of dt
can be changed by changing the value of
config.defaults['control.default_dt']
.
Functions operating on LTI systems will take into account whether a
system is continuous time or discrete time when carrying out operations
that depend on this difference. For example, the rss()
function
will place all system eigenvalues within the unit circle when called
using dt
corresponding to a discrete-time system:
>>> sys = ct.rss(2, 1, 1, dt=True)
>>> sys.poles()
array([-0.53807661+0.j, 0.86313342+0.j])
3.3. State Space Analysis and Design
This section describes the functions the are available to analyze state space systems and design state feedback controllers. The functionality described here is mainly specific to state space system representations; additional functions for analysis of linear input/output systems, including transfer functions and frequency response data systems, are defined in the next section and can also be applied to LTI systems in state space form.
State space properties
The following basic attributes and methods are available for
StateSpace
objects:
Dynamics matrix. |
|
Input matrix. |
|
Output matrix. |
|
Direct term. |
|
System timebase. |
|
2-tuple of I/O system dimension, (noutputs, ninputs). |
|
Number of system states. |
|
|
Compute the poles of a state space system. |
|
Compute the zeros of a state space system. |
|
Return the zero-frequency ("DC") gain. |
|
Convert a continuous-time system to discrete time. |
|
Return a list of a list of |
|
Evaluate system transfer function at point in complex plane. |
A complete list of attributes, methods, and properties is available in
the StateSpace
class documentation.
Similarity transformations and canonical forms
State space systems can be transformed into different internal
representations representing a variety of standard canonical forms
that have the same input/output properties. The
similarity_transform()
function allows a change of internal
state variable via similarity transformation and the
canonical_form()
function converts systems into different
canonical forms. Additional information is available on the
documentation pages for the individual functions:
|
Convert a system into canonical form. |
|
Convert a system into observable canonical form. |
|
Convert a system into modal canonical form. |
|
Convert a system into reachable canonical form. |
|
Similarity transformation, with optional time rescaling. |
Time domain properties
The following functions are available to analyze the time domain properties of a linear system:
|
Compute system's natural frequencies, damping ratios, and poles. |
|
Compute the output of a linear system given the input. |
|
Compute the impulse response for a linear system. |
|
Compute the initial condition response for a linear system. |
|
Return state space data objects for a system. |
|
Step response characteristics (rise time, settling time, etc). |
|
Compute the step response for a linear system. |
The time response functions (impulse_response()
,
initial_response()
, forced_response()
, and
step_response()
) are described in more detail in the
Input/Output Response and Plotting chapter.
State feedback design
State feedback controllers for a linear system are controllers of the form
where is a matrix of feedback
gains. Assuming the systems is controllable, the resulting closed
loop system will have dynamics matrix
with stable
eigenvalues.
Feedback controllers can be designed using one of several methods:
|
Linear quadratic regulator design. |
|
Place closed loop eigenvalues. |
|
Pole placement using Ackermann method. |
|
Place closed loop eigenvalues using Varga method. |
The place()
, place_acker()
, and place_varga()
functions
place the eigenvalues of the closed loop system to a desired set of
values. Each takes the A
and B
matrices of the state space system
and the desired location of the eigenvalues and returns a gain matrix
K
:
K = ct.place(sys.A, sys.B, E)
where E
is a 1D array of desired eigenvalues.
The lqr()
function computes the optimal state feedback controller
that minimizes the quadratic cost
by solving the appropriate Riccati equation. It returns the gain
matrix K
, the solution to the Riccati equation S
, and the location
of the closed loop eigenvalues E
. It can be called in one of
several forms:
K, S, E = ct.lqr(sys, Q, R)
K, S, E = ct.lqr(sys, Q, R, N)
K, S, E = ct.lqr(A, B, Q, R)
K, S, E = ct.lqr(A, B, Q, R, N)
If sys
is a discrete-time system, the first two forms will compute
the discrete-time optimal controller. For the second two forms, the
dlqr()
function can be used to compute the discrete-time optimal
controller. Additional arguments and details are given on the
lqr()
and dlqr()
documentation pages.
State estimation
State estimators (or observers) are dynamical systems that estimate the state of a system given a model of the dynamics and the input and output signals as a function of time. Linear state estimators have the form
where is an estimate of the state and
represents the estimator gain. The gain
is chosen such that the eigenvalues of the matrix
are stable, resulting in an estimate that converges to the value
of the system state.
The gain matrix can be chosen using eigenvalue placement by
calling the
place()
function:
L = ct.place(sys.A.T, sys.C.T, E).T
where E
is the desired location of the eigenvalues and .T
computes
the transpose of a matrix.
More sophisticated estimators can be constructed by modeling noise and disturbances as stochastic signals generated by a random process. Estimators constructed using these models are described in more detail in the Linear Quadratic Estimation (Kalman Filter) section of the Stochastic Systems chapter.
3.4. Frequency Domain Analysis and Design
Transfer function properties
The following basic attributes and methods are available for
TransferFunction
objects:
Numerator polynomial coefficients as a 2D array of 1D coefficients. |
|
Denominator polynomial coefficients as a 2D array of 1D coefficients. |
|
2-tuple of I/O system dimension, (noutputs, ninputs). |
|
|
Compute the poles of a transfer function. |
|
Compute the zeros of a transfer function. |
|
Return the zero-frequency ("DC") gain. |
|
Convert a continuous-time system to discrete time. |
|
Return a 2D array of |
|
Evaluate system transfer function at point in complex plane. |
A complete list of attributes, methods, and properties is available in
the TransferFunction
class documentation.
Frequency domain properties
The following functions are available to analyze the frequency domain properties of a linear systems:
|
Find first frequency where gain drops by 3 dB. |
|
Return the zero-frequency (or DC) gain of the given system. |
|
Frequency response of an LTI system. |
Compute Nyquist plot real-axis crossover frequencies and gains. |
|
|
Singular value response for a system. |
|
Stability margins and associated crossover frequencies. |
|
Return transfer function data objects for a system. |
These functions work on both state space and transfer function models.
The frequency_response()
and singular_values_response()
functions are described in more detail in the Input/Output Response and Plotting
chapter.
Input/output norms
Continuous and discrete-time signals can be represented as a normed
linear space with the appropriate choice of signal norm. For
continuous time signals, the three most common norms are the 1-norm,
2-norm, and the -norm:
Name |
Continuous time |
Discrete time |
---|---|---|
1-norm |
||
2-norm |
||
|
Given a norm for input signals and a norm for output signals, we can
define the induced norm for an input/output system. The
following table summarizes the induced norms for a transfer function
with impulse response
:
The system 2-norm and -norm can be computed using
system_norm()
:
sysnorm = ct.system_norm(sys, p=<val>)
where val
is either 2 or ‘inf’ (the 1-norm is not yet implemented).
Stability margins
The stability margin of a system indicates the robustness of a feedback system to perturbations that might cause the system to become unstable. Standard measures of robustness include gain margin, phase margin, and stability margin (distance to the -1 point on the Nyquist curve). These margins are computed based on the loop transfer function for a feedback system, assuming the loop will be closed using negative feedback with gain 1.
The stability_margins()
function computes all three of these
margins as well as the frequencies at which they occur:
>>> sys = ct.tf(10, [1, 2, 3, 4])
>>> gm, pm, sm, wpc, wgc, wms = ct.stability_margins(sys)
>>> print(f"Gain margin: {gm:2.2} at omega = {wpc:2.2} rad/sec")
Gain margin: 0.2 at omega = 1.7 rad/sec
Frequency domain synthesis
Synthesis of feedback controllers in the frequency domain can be done using the following functions:
|
H2 control synthesis for plant P. |
|
H-infinity control synthesis for plant P. |
|
Mixed-sensitivity H-infinity synthesis. |
The mixsyn()
function computes a feedback controller
that minimizes the mixed sensitivity gain
where
are the sensitivity function and complementary sensitivity function,
and represents the process dynamics.
The h2syn()
and hinfsyn()
functions compute a feedback
controller that minimizes the 2-norm and the
-norm of the sensitivity function for the closed loop
system, respectively.
Systems with time delays
Time delays are not directly representable in python-control
, but
the pade()
function generates a linear system that approximates
a time delay to a given order:
>>> num, den = ct.pade(0.1, 3)
>>> delay = ct.tf(num, den, name='delay')
>>> print(delay)
<TransferFunction>: delay
Inputs (1): ['u[0]']
Outputs (1): ['y[0]']
-s^3 + 120 s^2 - 6000 s + 1.2e+05
---------------------------------
s^3 + 120 s^2 + 6000 s + 1.2e+05
The plot below shows how the Pade approximation compares to a pure time delay.

3.5. Model Conversion and Reduction
A variety of functions are available to manipulate LTI systems, including functions for converting between state space and frequency domain, sampling systems in time and frequency domain, and creating reduced order models.
Conversion between representations
LTI systems can be converted between representations either by calling
the factory function for the desired data type using the original
system as the sole argument or using the explicit conversion functions
ss2tf()
and tf2ss()
. In most cases these types of
explicit conversions are not necessary, since functions designed to
operate on LTI systems will work on any subclass.
To explicitly convert a state space system into a transfer function
representation, the state space system can be passed as an argument to
the tf()
factory functions:
sys_ss = ct.rss(4, 2, 2, name='sys_ss')
sys_tf = ct.tf(sys_ss, name='sys_tf')
The ss2tf()
function can also be used, passing either the state
space system or the matrices that represent the state space systems:
sys_tf = ct.ss2tf(A, B, C, D)
In either form, system and signal names can be changed by passing the appropriate keyword arguments.
Conversion of transfer functions to state space form is also possible:
sys_ss = ct.ss(sys_tf)
sys_ss = ct.tf2ss(sys_tf)
sys_ss = ct.tf2ss(num, den)
Note
State space realizations of transfer functions are not unique and the state space representation obtained via these functions may not match realizations obtained by other algorithms.
Time sampling
Continuous time systems can be converted to discrete-time systems using
the sample_system()
function and specifying a sampling time:
>>> sys_ct = ct.rss(4, 2, 2, name='sys')
>>> sys_dt = ct.sample_system(sys_ct, 0.1, method='bilinear')
>>> print(sys_dt)
<StateSpace>: sys$sampled
Inputs (2): ['u[0]', 'u[1]']
Outputs (2): ['y[0]', 'y[1]']
States (4): ['x[0]', 'x[1]', 'x[2]', 'x[3]']
dt = 0.1
A = [[-0.79324497 -0.51484336 -1.09297036 -0.05363047]
[-3.5428559 -0.9340972 -1.85691838 -0.74843144]
[ 3.90565206 1.9409475 3.21968314 0.48558594]
[ 3.47315264 1.55258121 2.09562768 1.25466845]]
B = [[-0.01098544 0.00485652]
[-0.41579876 0.02204956]
[ 0.45553908 -0.02459682]
[ 0.50510046 -0.05448362]]
C = [[-2.74490135 -0.3064149 -2.27909612 -0.64793559]
[ 2.56376145 1.09663807 2.4332544 0.30768752]]
D = [[-0.34680884 0.02138098]
[ 0.29124186 -0.01476461]]
Note that the system name for the discrete-time system is the name of the original system with the string ‘$sampled’ appended.
Discrete time systems can also be created using the
StateSpace.sample()
or TransferFunction.sample()
methods
applied directly to the system:
sys_dt = sys_ct.sample(0.1)
Frequency sampling
Transfer functions can be sampled at a selected set of frequencies to
obtain a frequency response data representation of a system by calling
the frd()
factory function with an LTI system and an
array of frequencies:
>>> sys_ss = ct.rss(4, 1, 1, name='sys_ss')
>>> sys_frd = ct.frd(sys_ss, np.logspace(-1, 1, 5))
>>> print(sys_frd)
<FrequencyResponseData>: sys_ss$sampled
Inputs (1): ['u[0]']
Outputs (1): ['y[0]']
Freq [rad/s] Response
------------ ---------------------
0.100 -0.2648+0.0006429j
0.316 -0.2653 +0.003783j
1.000 -0.2561 +0.008021j
3.162 -0.2528 -0.001438j
10.000 -0.2578 -0.002443j
The frequency_response()
function can also be used for this
purpose, although in that case the output is usually used for plotting
the frequency response, as described in more detail in the
Frequency Response Data section.
Model reduction
Reduced order models for LTI systems can be obtained by approximating the system by a system of lower order that has similar input/output properties. A variety of functions are available in the python-control package that perform various types of model simplification:
|
Balanced reduced order model of system of a given order. |
|
Eliminate uncontrollable or unobservable states. |
|
Model reduction by input, output, or state elimination. |
The balanced_reduction()
function eliminate states based on the
Hankel singular values of a system. Intuitively, a system (or
subsystem) with small Hankel singular values corresponds to a situation
in which it is difficult to observe a state and/or difficult to
control that state. Eliminating states corresponding to small Hankel
singular values thus represents a good approximation in terms of the
input/output properties of a system. For systems with unstable modes,
balanced_reduction()
first removes the states corresponding to
the unstable subspace from the system, then carries out a balanced
realization on the stable part, and then reinserts the unstable modes.
The minimal_realization()
function eliminates uncontrollable or
unobservable states in state space models or cancels pole-zero pairs
in transfer functions. The resulting output system has minimal order
and the same input/output response characteristics as the original
model system. Unlike the balanced_reduction()
function, the
minimal_realization()
eliminates all uncontrollable and/or
unobservable modes, so should be used with caution if applied to an
unstable system.
The model_reduction()
function produces a reduced-order model of
a system by eliminating specified inputs, outputs, and/or states from
the original system. The specific states, inputs, or outputs that are
eliminated can be specified by either listing the states, inputs, or
outputs to be eliminated or those to be kept. Two methods of state
reduction are possible: ‘truncate’ removes the states marked for
elimination, while ‘matchdc’ replaces the eliminated states with their
equilibrium values (thereby keeping the input/output gain unchanged at
zero frequency [“DC”]).
3.6. Displaying LTI System Information
Information about an LTI system can be obtained using the Python
print
function:
>>> sys = ct.rss(4, 2, 2, name='sys_2x2')
>>> print(sys)
<StateSpace>: sys_2x2
Inputs (2): ['u[0]', 'u[1]']
Outputs (2): ['y[0]', 'y[1]']
States (4): ['x[0]', 'x[1]', 'x[2]', 'x[3]']
A = [[-2.06417506 0.28005277 0.49875395 -0.40364606]
[-0.18000232 -0.91682581 0.03179904 -0.16708786]
[-0.7963147 0.19042684 -0.72505525 -0.52196969]
[ 0.69457346 -0.20403756 -0.59611373 -0.94713748]]
B = [[-2.3400013 -1.02252469]
[-0.76682007 -0. ]
[ 0.13399373 0.94404387]
[ 0.71412443 -0.45903835]]
C = [[ 0.62432205 -0.55879494 -0.08717116 1.05092654]
[-0.94352373 0.19332285 1.05341936 0.60141772]]
D = [[ 0. 0.]
[-0. 0.]]
A loadable description of a system can be obtained just by displaying the system object:
>>> sys = ct.rss(2, 1, 1, name='sys_siso')
>>> sys
StateSpace(
array([[ 0.91008302, -0.87770371],
[ 6.83039608, -5.19117213]]),
array([[0.9810374],
[0.516694 ]]),
array([[1.38255365, 0.96999883]]),
array([[-0.]]),
name='sys_siso', states=2, outputs=1, inputs=1)
Alternative representations of the system are available using the
iosys_repr()
function and can be configured using
config.defaults['iosys.repr_format']
.
Transfer functions are displayed as ratios of polynomials, using either ‘s’ or ‘z’ depending on whether the systems is continuous or discrete time:
>>> sys_tf = ct.tf([1, 0], [1, 2, 1], 0.1, name='sys')
>>> print(sys_tf)
<TransferFunction>: sys
Inputs (1): ['u[0]']
Outputs (1): ['y[0]']
dt = 0.1
z
-------------
z^2 + 2 z + 1