Skip to content

Latest commit

 

History

History
602 lines (434 loc) · 18.8 KB

File metadata and controls

602 lines (434 loc) · 18.8 KB
.. currentmodule:: control

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.

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:

.. autosummary::

   ss
   tf
   frd

Each of these functions returns an object of an appropriate class to represent the system.

State space systems

The :class:`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. All vectors and matrices must be real-valued.

To create a state space system, use the :func:`ss` function:

.. testsetup:: statesp

  A = np.diag([-1, -2])
  B = np.eye(2)
  C = np.eye(1, 2)
  D = np.zeros((1, 2))

.. testcode:: statesp

  sys = ct.ss(A, B, C, D)

State space systems can be manipulated using standard arithmetic operations as well as the :func:`feedback`, :func:`parallel`, and :func:`series` function. A full list of "block diagram algebra" functions can be found in the :ref:`interconnections-ref` section of the :ref:`function-ref`.

Systems, inputs, outputs, and states can be given labels to allow more customized access to system information:

.. testcode:: statesp

  sys = ct.ss(
      A, B, C, D, name='sys',
      states=['x1', 'x2'], inputs=['u1', 'u2'], outputs=['y'])

The :func:`rss` function can be used to create a random state space system with a desired number or inputs, outputs, and states:

.. testcode:: statesp

  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 :func:`rss` are stable.

Transfer functions

The :class:`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. All coefficients must be real-valued.

To create a transfer function, use the :func:`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:

.. testsetup:: xferfcn

  num = [1, 2]
  den = [3, 4]

.. testcode:: xferfcn

  sys = ct.tf(num, den, name='sys', inputs=['u'], outputs=['y'])

Transfer functions can be manipulated using standard arithmetic operations as well as the :func:`feedback`, :func:`parallel`, and :func:`series` functions. A full list of "block diagram algebra" functions can be found in the :ref:`interconnections-ref` section of the :ref:`function-ref`.

To aid in the construction of transfer functions, the :func:`tf` factory function can used to create transfer function corresponding to the derivative or difference operator:

.. testcode:: xferfcn

  s = ct.tf('s')

Standard algebraic operations can be used to construct more complicated transfer functions:

.. testcode:: xferfcn

  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:

.. testcode:: xferfcn

  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 :class:`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 :func:`frd` factory function:

.. testsetup:: frdata

  sys_lti = ct.rss(2, 2, 2)
  lti_resp = ct.frequency_response(sys_lti)
  frdata = lti_resp.complex
  omega = lti_resp.frequency

.. testcode:: frdata

  sys = ct.frd(frdata, omega)

FRD systems can also be created by evaluating an LTI system at a given set of frequencies:

.. testcode:: frdata

  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 :func:`frequency_response` function. This object can be assigned to a tuple using:

.. testcode:: frdata

  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 :func:`tf` function, e.g.:

.. testsetup:: mimo

  sys = ct.tf(ct.rss(4, 2, 2))
  [[num11, num12], [num21, num22]] = sys.num_list
  [[den11, den12], [den21, den22]] = sys.den_list

  A, B, C, D = ct.ssdata(ct.rss(4, 3, 2))  # 3 output, 2 input

.. testcode:: mimo

  sys = ct.tf(
      [[num11, num12], [num21, num22]],
      [[den11, den12], [den21, den22]])

Similarly, MIMO frequency response data (FRD) systems are created by providing the :func:`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:

.. testcode:: mimo

  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:

.. testcode:: mimo

  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 :ref:`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:

.. testcode:: frdata

  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.

Discrete Time Systems

A discrete-time system is created by specifying a nonzero "timebase" dt when the system is constructed:

.. testsetup:: dtime

  A, B, C, D = ct.ssdata(ct.rss(2, 1, 1))
  num, den = ct.tfdata(ct.rss(2, 1, 1))
  dt = 0.1

.. testcode:: dtime

  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 :func:`sample_system` function or the :meth:`StateSpace.sample` and :meth:`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 :func:`rss` function will place all system eigenvalues within the unit circle when called using dt corresponding to a discrete-time system:

.. testsetup::

   import random
   random.seed(117)
   np.random.seed(117)

>>> sys = ct.rss(2, 1, 1, dt=True)
>>> sys.poles()
array([-0.53807661+0.j,  0.86313342+0.j])

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 :func:`ss2tf` and :func:`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 :func:`tf` factory functions:

.. testcode:: convert

  sys_ss = ct.rss(4, 2, 2, name='sys_ss')
  sys_tf = ct.tf(sys_ss, name='sys_tf')

The :func:`ss2tf` function can also be used, passing either the state space system or the matrices that represent the state space systems:

.. testcode:: convert
  :hide:

  A, B, C, D = ct.ssdata(sys_ss)

.. testcode:: convert

  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:

.. testcode:: convert
  :hide:

  num, den = ct.tfdata(sys_tf)

.. testcode:: convert

  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 :func:`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
<BLANKLINE>
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]]
<BLANKLINE>
B = [[-0.01098544  0.00485652]
     [-0.41579876  0.02204956]
     [ 0.45553908 -0.02459682]
     [ 0.50510046 -0.05448362]]
<BLANKLINE>
C = [[-2.74490135 -0.3064149  -2.27909612 -0.64793559]
     [ 2.56376145  1.09663807  2.4332544   0.30768752]]
<BLANKLINE>
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 :func:`StateSpace.sample` or :func:`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 :func:`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]']
<BLANKLINE>
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 :func:`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 :ref:`frequency_response` 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:

.. autosummary::

   balanced_reduction
   minimal_realization
   model_reduction

The :func:`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, :func:`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 :func:`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 :func:`balanced_reduction` function, the :func:`minimal_realization` eliminates all uncontrollable and/or unobservable modes, so should be used with caution if applied to an unstable system.

The :func:`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"]).

Displaying LTI System Information

Information about an LTI system can be obtained using the Python ~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]']
<BLANKLINE>
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]]
<BLANKLINE>
B = [[-2.3400013  -1.02252469]
     [-0.76682007 -0.        ]
     [ 0.13399373  0.94404387]
     [ 0.71412443 -0.45903835]]
<BLANKLINE>
C = [[ 0.62432205 -0.55879494 -0.08717116  1.05092654]
     [-0.94352373  0.19332285  1.05341936  0.60141772]]
<BLANKLINE>
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 :func:`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
<BLANKLINE>
        z
  -------------
  z^2 + 2 z + 1