.. 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: .. math:: \frac{dx}{dt} &= A x + B u \\ y &= C x + D u where :math:`u` is the input, :math:`y` is the output, and :math:`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 .. math:: 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 :math:`n` is greater than or equal to :math:`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: 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) .. doctest:: >>> sys = ct.rss(2, 1, 1, dt=True) >>> sys.poles() array([-0.53807661+0.j, 0.86313342+0.j]) .. include:: statesp.rst .. include:: xferfcn.rst 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: .. doctest:: >>> sys_ct = ct.rss(4, 2, 2, name='sys') >>> sys_dt = ct.sample_system(sys_ct, 0.1, method='bilinear') >>> print(sys_dt) : 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 :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: .. doctest:: >>> sys_ss = ct.rss(4, 1, 1, name='sys_ss') >>> sys_frd = ct.frd(sys_ss, np.logspace(-1, 1, 5)) >>> print(sys_frd) : 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 :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: Displaying LTI System Information ================================= Information about an LTI system can be obtained using the Python `~python.print` function: .. doctest:: >>> sys = ct.rss(4, 2, 2, name='sys_2x2') >>> print(sys) : 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: .. doctest:: >>> 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: .. doctest:: >>> sys_tf = ct.tf([1, 0], [1, 2, 1], 0.1, name='sys') >>> print(sys_tf) : sys Inputs (1): ['u[0]'] Outputs (1): ['y[0]'] dt = 0.1 z ------------- z^2 + 2 z + 1