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:

ss(A, B, C, D[, dt])

Create a state space system.

tf(num, den[, dt])

Create a transfer function system.

frd(frdata, omega[, dt])

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:

\frac{dx}{dt} &= A x + B u \\
y &= C x + D u

where u is the input, y is the output, and x 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

G(s) = \frac{\text{num}(s)}{\text{den}(s)}
     = \frac{a_0 s^m + a_1 s^{m-1} + \cdots + a_m}
            {b_0 s^n + b_1 s^{n-1} + \cdots + b_n},

where n is greater than or equal to m 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 period dt

  • dt = True: discrete time with unspecified sampling period

  • dt = 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:

A

Dynamics matrix.

B

Input matrix.

C

Output matrix.

D

Direct term.

dt

System timebase.

shape

2-tuple of I/O system dimension, (noutputs, ninputs).

nstates

Number of system states.

poles()

Compute the poles of a state space system.

zeros()

Compute the zeros of a state space system.

dcgain([warn_infinite])

Return the zero-frequency ("DC") gain.

sample(Ts[, method, alpha, ...])

Convert a continuous-time system to discrete time.

returnScipySignalLTI([strict])

Return a list of a list of scipy.signal.lti objects.

__call__(x[, squeeze, warn_infinite])

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:

canonical_form(xsys[, form])

Convert a system into canonical form.

observable_form(xsys)

Convert a system into observable canonical form.

modal_form(xsys[, condmax, sort])

Convert a system into modal canonical form.

reachable_form(xsys)

Convert a system into reachable canonical form.

similarity_transform(xsys, T[, timescale, ...])

Similarity transformation, with optional time rescaling.

Time domain properties

The following functions are available to analyze the time domain properties of a linear system:

damp(sys[, doprint])

Compute system's natural frequencies, damping ratios, and poles.

forced_response(sysdata[, timepts, inputs, ...])

Compute the output of a linear system given the input.

impulse_response(sysdata[, timepts, ...])

Compute the impulse response for a linear system.

initial_response(sysdata[, timepts, ...])

Compute the initial condition response for a linear system.

ssdata(sys)

Return state space data objects for a system.

step_info(sysdata[, timepts, timepts_num, ...])

Step response characteristics (rise time, settling time, etc).

step_response(sysdata[, timepts, ...])

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

u = -K x

where K \in {\mathbb R}^{m \times n} is a matrix of feedback gains. Assuming the systems is controllable, the resulting closed loop system will have dynamics matrix A - B K with stable eigenvalues.

Feedback controllers can be designed using one of several methods:

lqr(A, B, Q, R[, N])

Linear quadratic regulator design.

place(A, B, p)

Place closed loop eigenvalues.

place_acker(A, B, poles)

Pole placement using Ackermann method.

place_varga(A, B, p[, dtime, alpha])

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

J = \int_0^\infty (x' Q x + u' R u + 2 x' N u) dt

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

\frac{d\hat x}{dt} = A \hat x + B u + L(y - C\hat x - D u),

where \hat x is an estimate of the state and L \in
{\mathbb R}^{n \times p} represents the estimator gain. The gain L is chosen such that the eigenvalues of the matrix A -
L C are stable, resulting in an estimate that converges to the value of the system state.

The gain matrix L 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:

num_array

Numerator polynomial coefficients as a 2D array of 1D coefficients.

den_array

Denominator polynomial coefficients as a 2D array of 1D coefficients.

shape

2-tuple of I/O system dimension, (noutputs, ninputs).

poles()

Compute the poles of a transfer function.

zeros()

Compute the zeros of a transfer function.

dcgain([warn_infinite])

Return the zero-frequency ("DC") gain.

sample(Ts[, method, alpha, ...])

Convert a continuous-time system to discrete time.

returnScipySignalLTI([strict])

Return a 2D array of scipy.signal.lti objects.

__call__(x[, squeeze, warn_infinite])

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:

bandwidth(sys[, dbdrop])

Find first frequency where gain drops by 3 dB.

dcgain(sys)

Return the zero-frequency (or DC) gain of the given system.

frequency_response(sysdata[, omega, ...])

Frequency response of an LTI system.

phase_crossover_frequencies(sys)

Compute Nyquist plot real-axis crossover frequencies and gains.

singular_values_response(sysdata[, omega, ...])

Singular value response for a system.

stability_margins(sysdata[, returnall, ...])

Stability margins and associated crossover frequencies.

tfdata(sys)

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 \infty-norm:

Name

Continuous time

Discrete time

1-norm

\int_{-\infty}^\infty |u(\tau)|, d\tau

\sum_k \|x[k]\|

2-norm

\left(\int_{-\infty}^\infty |u(\tau)|^2, d\tau \right)^{1/2}

\left(\sum_k \|x[k]\|^2 \right)^{1/2}

\infty-norm

\sup_t |u(t)|

\max_k \|x[k]\|

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 G(s) with impulse response g(t):

\|u\|_2

\| u \|_\infty

\| y \|_2

\| G \|_\infty

\infty

\| y \|_\infty

\| G \|_2

\| g \|_1

The system 2-norm and \infty-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:

h2syn(P, nmeas, ncon)

H2 control synthesis for plant P.

hinfsyn(P, nmeas, ncon)

H-infinity control synthesis for plant P.

mixsyn(g[, w1, w2, w3])

Mixed-sensitivity H-infinity synthesis.

The mixsyn() function computes a feedback controller C(s) that minimizes the mixed sensitivity gain

\| W_1 S \|_\infty + \| W_2 C \|_\infty + \| W_3 T \|_\infty,

where

S = \frac{1}{1 + P C}, \qquad T = \frac{P C}{1 + P C}

are the sensitivity function and complementary sensitivity function, and P(s) represents the process dynamics.

The h2syn() and hinfsyn() functions compute a feedback controller C(s) that minimizes the 2-norm and the \infty-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.

_images/xferfcn-delay-compare.png

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_reduction(sys, orders[, method, alpha])

Balanced reduced order model of system of a given order.

minimal_realization(sys[, tol, verbose])

Eliminate uncontrollable or unobservable states.

model_reduction(sys[, elim_states, method, ...])

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