diff --git a/.travis.yml b/.travis.yml index a719fa131..ddde6f906 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ sudo: false language: python +dist: xenial + +services: + - xvfb cache: apt: true @@ -9,8 +13,8 @@ cache: - $HOME/.local python: + - "3.7" - "3.6" - - "3.5" - "2.7" # Test against multiple version of SciPy, with and without slycot @@ -20,21 +24,57 @@ python: # # We also want to test with and without slycot env: - - SCIPY=scipy SLYCOT=slycot # default, with slycot - - SCIPY=scipy SLYCOT= # default, w/out slycot - - SCIPY="scipy==0.19.1" SLYCOT= # legacy support, w/out slycot + - SCIPY=scipy SLYCOT=conda # default, with slycot via conda + - SCIPY=scipy SLYCOT= # default, w/out slycot + - SCIPY="scipy==0.19.1" SLYCOT= # legacy support, w/out slycot + +# Add optional builds that test against latest version of slycot +jobs: + include: + - name: "linux, Python 2.7, slycot=source" + os: linux + dist: xenial + services: xvfb + python: "2.7" + env: SCIPY=scipy SLYCOT=source + - name: "linux, Python 3.7, slycot=source" + os: linux + dist: xenial + services: xvfb + python: "3.7" + env: SCIPY=scipy SLYCOT=source + +matrix: + # Exclude combinations that are very unlikely (and don't work) + exclude: + - python: "3.7" # python3.7 should use latest scipy + env: SCIPY="scipy==0.19.1" SLYCOT= + + allow_failures: + - name: "linux, Python 2.7, slycot=source" + os: linux + dist: xenial + services: xvfb + python: "2.7" + env: SCIPY=scipy SLYCOT=source + - name: "linux, Python 3.7, slycot=source" + os: linux + dist: xenial + services: xvfb + python: "3.7" + env: SCIPY=scipy SLYCOT=source # install required system libraries before_install: # Install gfortran for testing slycot; use apt-get instead of conda in # order to include the proper CXXABI dependency (updated in GCC 4.9) - - if [[ "$SLYCOT" != "" ]]; then + # Note: these commands should match the slycot .travis.yml configuration + - if [[ "$SLYCOT" = "source" ]]; then sudo apt-get update -qq; + sudo apt-get install liblapack-dev libblas-dev; sudo apt-get install gfortran; + sudo apt-get install cmake; fi - # Install display manager to allow testing of plotting functions - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start # use miniconda to install numpy/scipy, to avoid lengthy build from source - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; @@ -50,9 +90,8 @@ before_install: - conda info -a - conda create -q -n test-environment python="$TRAVIS_PYTHON_VERSION" pip coverage - source activate test-environment - # Install openblas if slycot is being used - # also install scikit-build for the build process - - if [[ "$SLYCOT" != "" ]]; then + # Install scikit-build for the build process if slycot is being used + - if [[ "$SLYCOT" = "source" ]]; then conda install openblas; conda install -c conda-forge scikit-build; fi @@ -65,13 +104,15 @@ before_install: install: # Install packages needed by python-control - conda install $SCIPY matplotlib - # Build slycot from source - # For python 3, need to provide pointer to python library - # Use "Unix Makefiles" as generator, because Ninja cannot handle Fortran - #! git clone https://github.com/repagh/Slycot.git slycot; - - if [[ "$SLYCOT" != "" ]]; then + + # Figure out how to build slycot + # source: use "Unix Makefiles" as generator; Ninja cannot handle Fortran + # conda: use pre-compiled version of slycot on conda-forge + - if [[ "$SLYCOT" = "source" ]]; then git clone https://github.com/python-control/Slycot.git slycot; cd slycot; python setup.py install -G "Unix Makefiles"; cd ..; + elif [[ "$SLYCOT" = "conda" ]]; then + conda install -c conda-forge slycot; fi # command to run tests @@ -83,14 +124,11 @@ script: # set PYTHONPATH for examples # pmw needed for examples/tfvis.py # future is needed for Python 2, also for examples/tfvis.py - - if [[ "$SLYCOT" != "" ]]; then export PYTHONPATH=$PWD; conda install -c conda-forge pmw future; - cd examples; bash run_examples.sh; cd ..; + (cd examples; bash run_examples.sh); fi -# arbitrary change to try to trigger travis build - after_success: - coveralls diff --git a/README.rst b/README.rst index 7a30f2cb8..97c1cc96c 100644 --- a/README.rst +++ b/README.rst @@ -46,23 +46,32 @@ https://github.com/python-control/Slycot Installation ============ -The package may be installed using pip, conda, or distutils. +Conda and conda-forge +--------------------- + +The easiest way to get started with the Control Systems library is +using `Conda `_. + +The Control Systems library has been packages for the `conda-forge +`_ Conda channel, and as of Slycot version +0.3.4, binaries for that package are available for 64-bit Windows, +OSX, and Linux. + +To install both the Control Systems library and Slycot in an existing +conda environment, run:: + + conda install -c conda-forge control slycot Pip --- To install using pip:: - pip install slycot # optional + pip install slycot # optional; see below pip install control -conda-forge ------------ - -Binaries are available from conda-forge for selected platforms (Linux and -MacOS). Install using - - conda install -c conda-forge control +If you install Slycot using pip you'll need a development environment +(e.g., Python development files, C and Fortran compilers). Distutils --------- diff --git a/control/__init__.py b/control/__init__.py index 4746d28a3..3dec2c12f 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -68,6 +68,7 @@ from .robust import * from .config import * from .sisotool import * +from .iosys import * # Exceptions from .exception import * @@ -83,3 +84,6 @@ from numpy.testing import Tester test = Tester().test bench = Tester().bench + +# Initialize default parameter values +reset_defaults() diff --git a/control/bdalg.py b/control/bdalg.py index 0f4a14c1a..3f13fb1b3 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -53,7 +53,6 @@ """ -import scipy as sp import numpy as np from . import xferfcn as tf from . import statesp as ss @@ -61,17 +60,18 @@ __all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect'] + def series(sys1, *sysn): - """Return the series connection (... \* sys3 \*) sys2 \* sys1 + """Return the series connection (sysn \\* ... \\*) sys2 \\* sys1 Parameters ---------- - sys1: scalar, StateSpace, TransferFunction, or FRD - sysn: other scalars, StateSpaces, TransferFunctions, or FRDs + sys1 : scalar, StateSpace, TransferFunction, or FRD + *sysn : other scalars, StateSpaces, TransferFunctions, or FRDs Returns ------- - out: scalar, StateSpace, or TransferFunction + out : scalar, StateSpace, or TransferFunction Raises ------ @@ -105,18 +105,19 @@ def series(sys1, *sysn): from functools import reduce return reduce(lambda x, y:y*x, sysn, sys1) + def parallel(sys1, *sysn): """ - Return the parallel connection sys1 + sys2 (+ sys3 + ...) + Return the parallel connection sys1 + sys2 (+ ... + sysn) Parameters ---------- - sys1: scalar, StateSpace, TransferFunction, or FRD - *sysn: other scalars, StateSpaces, TransferFunctions, or FRDs + sys1 : scalar, StateSpace, TransferFunction, or FRD + *sysn : other scalars, StateSpaces, TransferFunctions, or FRDs Returns ------- - out: scalar, StateSpace, or TransferFunction + out : scalar, StateSpace, or TransferFunction Raises ------ @@ -150,34 +151,29 @@ def parallel(sys1, *sysn): from functools import reduce return reduce(lambda x, y:x+y, sysn, sys1) + def negate(sys): """ Return the negative of a system. Parameters ---------- - sys: StateSpace, TransferFunction or FRD + sys : StateSpace, TransferFunction or FRD Returns ------- - out: StateSpace or TransferFunction + out : StateSpace or TransferFunction Notes ----- This function is a wrapper for the __neg__ function in the StateSpace and TransferFunction classes. The output type is the same as the input type. - If both systems have a defined timebase (dt = 0 for continuous time, - dt > 0 for discrete time), then the timebase for both systems must - match. If only one of the system has a timebase, the return - timebase will be set to match it. - Examples -------- >>> sys2 = negate(sys1) # Same as sys2 = -sys1. """ - return -sys; #! TODO: expand to allow sys2 default to work in MIMO case? @@ -187,10 +183,10 @@ def feedback(sys1, sys2=1, sign=-1): Parameters ---------- - sys1: scalar, StateSpace, TransferFunction, FRD - The primary plant. - sys2: scalar, StateSpace, TransferFunction, FRD - The feedback plant (often a feedback controller). + sys1 : scalar, StateSpace, TransferFunction, FRD + The primary process. + sys2 : scalar, StateSpace, TransferFunction, FRD + The feedback process (often a feedback controller). sign: scalar The sign of feedback. `sign` = -1 indicates negative feedback, and `sign` = 1 indicates positive feedback. `sign` is an optional @@ -198,7 +194,7 @@ def feedback(sys1, sys2=1, sign=-1): Returns ------- - out: StateSpace or TransferFunction + out : StateSpace or TransferFunction Raises ------ @@ -224,6 +220,11 @@ def feedback(sys1, sys2=1, sign=-1): scalars, then TransferFunction.feedback is used. """ + # Allow anything with a feedback function to call that function + try: + return sys1.feedback(sys2, sign) + except AttributeError: + pass # Check for correct input types. if not isinstance(sys1, (int, float, complex, np.number, @@ -243,7 +244,7 @@ def feedback(sys1, sys2=1, sign=-1): elif isinstance(sys2, ss.StateSpace): sys1 = ss._convertToStateSpace(sys1) elif isinstance(sys2, frd.FRD): - sys1 = ss._convertToFRD(sys1) + sys1 = frd._convertToFRD(sys1, sys2.omega) else: # sys2 is a scalar. sys1 = tf._convert_to_transfer_function(sys1) sys2 = tf._convert_to_transfer_function(sys2) @@ -251,7 +252,7 @@ def feedback(sys1, sys2=1, sign=-1): return sys1.feedback(sys2, sign) def append(*sys): - '''append(sys1, sys2, ..., sysn) + """append(sys1, sys2, ..., sysn) Group models by appending their inputs and outputs @@ -262,7 +263,7 @@ def append(*sys): Parameters ---------- - sys1, sys2, ... sysn: StateSpace or Transferfunction + sys1, sys2, ..., sysn: StateSpace or Transferfunction LTI systems to combine @@ -274,42 +275,40 @@ def append(*sys): Examples -------- - >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") - >>> sys2 = ss("-1.", "1.", "1.", "0.") + >>> sys1 = ss([[1., -2], [3., -4]], [[5.], [7]]", [[6., 8]], [[9.]]) + >>> sys2 = ss([[-1.]], [[1.]], [[1.]], [[0.]]) >>> sys = append(sys1, sys2) - .. todo:: - also implement for transfer function, zpk, etc. - ''' + """ s1 = sys[0] for s in sys[1:]: s1 = s1.append(s) return s1 def connect(sys, Q, inputv, outputv): - ''' - Index-base interconnection of system + """Index-based interconnection of an LTI system. - The system sys is a system typically constructed with append, with - multiple inputs and outputs. The inputs and outputs are connected - according to the interconnection matrix Q, and then the final - inputs and outputs are trimmed according to the inputs and outputs - listed in inputv and outputv. + The system `sys` is a system typically constructed with `append`, with + multiple inputs and outputs. The inputs and outputs are connected + according to the interconnection matrix `Q`, and then the final inputs and + outputs are trimmed according to the inputs and outputs listed in `inputv` + and `outputv`. - Note: to have this work, inputs and outputs start counting at 1!!!! + NOTE: Inputs and outputs are indexed starting at 1 and negative values + correspond to a negative feedback interconnection. Parameters ---------- - sys: StateSpace Transferfunction + sys : StateSpace Transferfunction System to be connected - Q: 2d array + Q : 2D array Interconnection matrix. First column gives the input to be connected - second column gives the output to be fed into this input. Negative + second column gives the output to be fed into this input. Negative values for the second column mean the feedback is negative, 0 means - no connection is made - inputv: 1d array + no connection is made. Inputs and outputs are indexed starting at 1. + inputv : 1D array list of final external inputs - outputv: 1d array + outputv : 1D array list of final external outputs Returns @@ -319,28 +318,30 @@ def connect(sys, Q, inputv, outputv): Examples -------- - >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6, 8", "9.") - >>> sys2 = ss("-1.", "1.", "1.", "0.") + >>> sys1 = ss([[1., -2], [3., -4]], [[5.], [7]], [[6, 8]], [[9.]]) + >>> sys2 = ss([[-1.]], [[1.]], [[1.]], [[0.]]) >>> sys = append(sys1, sys2) - >>> Q = sp.mat([ [ 1, 2], [2, -1] ]) # basically feedback, output 2 in 1 + >>> Q = [[1, 2], [2, -1]] # negative feedback interconnection >>> sysc = connect(sys, Q, [2], [1, 2]) - ''' + + """ # first connect - K = sp.zeros( (sys.inputs, sys.outputs) ) - for r in sp.array(Q).astype(int): + K = np.zeros((sys.inputs, sys.outputs)) + for r in np.array(Q).astype(int): inp = r[0]-1 for outp in r[1:]: if outp > 0 and outp <= sys.outputs: K[inp,outp-1] = 1. elif outp < 0 and -outp >= -sys.outputs: K[inp,-outp-1] = -1. - sys = sys.feedback(sp.matrix(K), sign=1) + sys = sys.feedback(np.array(K), sign=1) # now trim - Ytrim = sp.zeros( (len(outputv), sys.outputs) ) - Utrim = sp.zeros( (sys.inputs, len(inputv)) ) + Ytrim = np.zeros((len(outputv), sys.outputs)) + Utrim = np.zeros((sys.inputs, len(inputv))) for i,u in enumerate(inputv): Utrim[u-1,i] = 1. for i,y in enumerate(outputv): Ytrim[i,y-1] = 1. - return sp.matrix(Ytrim)*sys*sp.matrix(Utrim) + + return Ytrim * sys * Utrim diff --git a/control/canonical.py b/control/canonical.py index c0244d75f..b578418bd 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -6,10 +6,11 @@ from .statesp import StateSpace from .statefbk import ctrb, obsv -from numpy import zeros, shape, poly, iscomplex, hstack +from numpy import zeros, shape, poly, iscomplex, hstack, dot, transpose from numpy.linalg import solve, matrix_rank, eig -__all__ = ['canonical_form', 'reachable_form', 'observable_form'] +__all__ = ['canonical_form', 'reachable_form', 'observable_form', 'modal_form', + 'similarity_transform'] def canonical_form(xsys, form='reachable'): """Convert a system into canonical form @@ -88,7 +89,9 @@ def reachable_form(xsys): # Transformation from one form to another Tzx = solve(Wrx.T, Wrz.T).T # matrix right division, Tzx = Wrz * inv(Wrx) - if matrix_rank(Tzx) != xsys.states: + # Check to make sure inversion was OK. Note that since we are inverting + # Wrx and we already checked its rank, this exception should never occur + if matrix_rank(Tzx) != xsys.states: # pragma: no cover raise ValueError("Transformation matrix singular to working precision.") # Finally, compute the output matrix @@ -210,3 +213,37 @@ def modal_form(xsys): zsys.C = xsys.C.dot(Tzx) return zsys, Tzx + + +def similarity_transform(xsys, T, timescale=1): + """Perform a similarity transformation, with option time rescaling. + + Transform a linear state space system to a new state space representation + z = T x, where T is an invertible matrix. + + Parameters + ---------- + T : 2D invertible array + The matrix `T` defines the new set of coordinates z = T x. + timescale : float + If present, also rescale the time unit to tau = timescale * t + + Returns + ------- + zsys : StateSpace object + System in transformed coordinates, with state 'z' + + """ + # Create a new system, starting with a copy of the old one + zsys = StateSpace(xsys) + + # Define a function to compute the right inverse (solve x M = y) + def rsolve(M, y): + return transpose(solve(transpose(M), transpose(y))) + + # Update the system matrices + zsys.A = rsolve(T, dot(T, zsys.A)) / timescale + zsys.B = dot(T, zsys.B) / timescale + zsys.C = rsolve(T, zsys.C) + + return zsys diff --git a/control/config.py b/control/config.py index 10ab8a1ed..f61469394 100644 --- a/control/config.py +++ b/control/config.py @@ -5,38 +5,154 @@ # variables that control the behavior of the control package. # Eventually it will be possible to read and write configuration # files. For now, you can just choose between MATLAB and FBS default -# values. +# values + tweak a few other things. + +import warnings + +__all__ = ['defaults', 'set_defaults', 'reset_defaults', + 'use_matlab_defaults', 'use_fbs_defaults', + 'use_numpy_matrix'] + +# Package level default values +_control_defaults = { + # No package level defaults (yet) +} +defaults = dict(_control_defaults) + + +def set_defaults(module, **keywords): + """Set default values of parameters for a module. + + The set_defaults() function can be used to modify multiple parameter + values for a module at the same time, using keyword arguments: + + control.set_defaults('module', param1=val, param2=val) + + """ + if not isinstance(module, str): + raise ValueError("module must be a string") + for key, val in keywords.items(): + defaults[module + '.' + key] = val + + +def reset_defaults(): + """Reset configuration values to their default (initial) values.""" + # System level defaults + defaults.update(_control_defaults) + + from .freqplot import _bode_defaults, _freqplot_defaults + defaults.update(_bode_defaults) + defaults.update(_freqplot_defaults) + + from .nichols import _nichols_defaults + defaults.update(_nichols_defaults) + + from .pzmap import _pzmap_defaults + defaults.update(_pzmap_defaults) + + from .rlocus import _rlocus_defaults + defaults.update(_rlocus_defaults) + + from .statesp import _statesp_defaults + defaults.update(_statesp_defaults) + + +def _get_param(module, param, argval=None, defval=None, pop=False): + """Return the default value for a configuration option. + + The _get_param() function is a utility function used to get the value of a + parameter for a module based on the default parameter settings and any + arguments passed to the function. The precedence order for parameters is + the value passed to the function (as a keyword), the value from the + config.defaults dictionary, and the default value `defval`. + + Parameters + ---------- + module : str + Name of the module whose parameters are being requested. + param : str + Name of the parameter value to be determeind. + argval : object or dict + Value of the parameter as passed to the function. This can either be + an object or a dictionary (i.e. the keyword list from the function + call). Defaults to None. + defval : object + Default value of the parameter to use, if it is not located in the + `config.defaults` dictionary. If a dictionary is provided, then + `module.param` is used to determine the default value. Defaults to + None. + pop : bool + If True and if argval is a dict, then pop the remove the parameter + entry from the argval dict after retreiving it. This allows the use + of a keyword argument list to be passed through to other functions + internal to the function being called. + + """ + + # Make sure that we were passed sensible arguments + if not isinstance(module, str) or not isinstance(param, str): + raise ValueError("module and param must be strings") + + # Construction the name of the key, for later use + key = module + '.' + param + + # If we were passed a dict for the argval, get the param value from there + if isinstance(argval, dict): + argval = argval.pop(param, None) if pop else argval.get(param, None) + + # If we were passed a dict for the defval, get the param value from there + if isinstance(defval, dict): + defval = defval.get(key, None) + + # Return the parameter value to use (argval > defaults > defval) + return argval if argval is not None else defaults.get(key, defval) -# Bode plot defaults -bode_dB = False # Bode plot magnitude units -bode_deg = True # Bode Plot phase units -bode_Hz = False # Bode plot frequency units -bode_number_of_samples = None # Bode plot number of samples -bode_feature_periphery_decade = 1.0 # Bode plot feature periphery in decades # Set defaults to match MATLAB def use_matlab_defaults(): - """ - Use MATLAB compatible configuration settings + """Use MATLAB compatible configuration settings. The following conventions are used: - * Bode plots plot gain in dB, phase in degrees, frequency in Hertz + * Bode plots plot gain in dB, phase in degrees, frequency in + Hertz, with grids + * State space class and functions use Numpy matrix objects + """ - # Bode plot defaults - global bode_dB; bode_dB = True - global bode_deg; bode_deg = True - global bode_Hz; bode_Hz = True + set_defaults('bode', dB=True, deg=True, Hz=True, grid=True) + set_defaults('statesp', use_numpy_matrix=True) + # Set defaults to match FBS (Astrom and Murray) def use_fbs_defaults(): - """ - Use `Feedback Systems `_ (FBS) compatible settings + """Use `Feedback Systems `_ (FBS) compatible settings. The following conventions are used: * Bode plots plot gain in powers of ten, phase in degrees, - frequency in Hertz + frequency in Hertz, no grid + + """ + set_defaults('bode', dB=False, deg=True, Hz=False, grid=False) + + +# Decide whether to use numpy.matrix for state space operations +def use_numpy_matrix(flag=True, warn=True): + """Turn on/off use of Numpy `matrix` class for state space operations. + + Parameters + ---------- + flag : bool + If flag is `True` (default), use the Numpy (soon to be deprecated) + `matrix` class to represent matrices in the `~control.StateSpace` + class and functions. If flat is `False`, then matrices are + represented by a 2D `ndarray` object. + + warn : bool + If flag is `True` (default), issue a warning when turning on the use + of the Numpy `matrix` class. Set `warn` to false to omit display of + the warning message. + """ - # Bode plot defaults - global bode_dB; bode_dB = False - global bode_deg; bode_deg = True - global bode_Hz; bode_Hz = True + if flag and warn: + warnings.warn("Return type numpy.matrix is soon to be deprecated.", + stacklevel=2) + set_defaults('statesp', use_numpy_matrix=flag) diff --git a/control/dtime.py b/control/dtime.py index 36053da33..211aa86a1 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -112,6 +112,9 @@ def c2d(sysc, Ts, method='zoh'): ''' # Call the sample_system() function to do the work sysd = sample_system(sysc, Ts, method) + + # TODO: is this check needed? If sysc is StateSpace, sysd is too? if isinstance(sysc, StateSpace) and not isinstance(sysd, StateSpace): - return _convertToStateSpace(sysd) + return _convertToStateSpace(sysd) # pragma: no cover + return sysd diff --git a/control/exception.py b/control/exception.py index 2c4f29704..9dde243af 100644 --- a/control/exception.py +++ b/control/exception.py @@ -39,24 +39,24 @@ # # $Id$ -class ControlSlycot(Exception): +class ControlSlycot(ImportError): """Exception for Slycot import. Used when we can't import a function from the slycot package""" pass -class ControlDimension(Exception): +class ControlDimension(ValueError): """Raised when dimensions of system objects are not correct""" pass -class ControlArgument(Exception): +class ControlArgument(TypeError): """Raised when arguments to a function are not correct""" pass -class ControlMIMONotImplemented(Exception): +class ControlMIMONotImplemented(NotImplementedError): """Function is not currently implemented for MIMO systems""" pass -class ControlNotImplemented(Exception): +class ControlNotImplemented(NotImplementedError): """Functionality is not yet implemented""" pass diff --git a/control/flatsys/__init__.py b/control/flatsys/__init__.py new file mode 100644 index 000000000..9ff1e2337 --- /dev/null +++ b/control/flatsys/__init__.py @@ -0,0 +1,63 @@ +# flatsys/__init__.py: flat systems package initialization file +# +# Copyright (c) 2019 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Author: Richard M. Murray +# Date: 1 Jul 2019 + +r"""The :mod:`control.flatsys` package contains a set of classes and functions +that can be used to compute trajectories for differentially flat systems. + +A differentially flat system is defined by creating an object using the +:class:`~control.flatsys.FlatSystem` class, which has member functions for +mapping the system state and input into and out of flat coordinates. The +:func:`~control.flatsys.point_to_point` function can be used to create a +trajectory between two endpoints, written in terms of a set of basis functions +defined using the :class:`~control.flatsys.BasisFamily` class. The resulting +trajectory is return as a :class:`~control.flatsys.SystemTrajectory` object +and can be evaluated using the :func:`~control.flatsys.SystemTrajectory.eval` +member function. + +""" + +# Basis function families +from .basis import BasisFamily +from .poly import PolyFamily + +# Classes +from .systraj import SystemTrajectory +from .flatsys import FlatSystem +from .linflat import LinearFlatSystem + +# Package functions +from .flatsys import point_to_point diff --git a/control/flatsys/basis.py b/control/flatsys/basis.py new file mode 100644 index 000000000..83ea89cbd --- /dev/null +++ b/control/flatsys/basis.py @@ -0,0 +1,53 @@ +# basis.py - BasisFamily class +# RMM, 10 Nov 2012 +# +# The BasisFamily class is used to specify a set of basis functions for +# implementing differential flatness computations. +# +# Copyright (c) 2012 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + + +# Basis family class (for use as a base class) +class BasisFamily: + """Base class for implementing basis functions for flat systems. + + A BasisFamily object is used to construct trajectories for a flat system. + The class must implement a single function that computes the jth + derivative of the ith basis function at a time t: + + :math:`z_i^{(q)}(t)` = basis.eval_deriv(self, i, j, t) + + """ + def __init__(self, N): + """Create a basis family of order N.""" + self.N = N # save number of basis functions diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py new file mode 100644 index 000000000..a5dec2950 --- /dev/null +++ b/control/flatsys/flatsys.py @@ -0,0 +1,358 @@ +# flatsys.py - trajectory generation for differentially flat systems +# RMM, 10 Nov 2012 +# +# This file contains routines for computing trajectories for differentially +# flat nonlinear systems. It is (very) loosely based on the NTG software +# package developed by Mark Milam and Kudah Mushambi, but rewritten from +# scratch in python. +# +# Copyright (c) 2012 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import numpy as np +from .poly import PolyFamily +from .systraj import SystemTrajectory +from ..iosys import NonlinearIOSystem + + +# Flat system class (for use as a base class) +class FlatSystem(NonlinearIOSystem): + """Base class for representing a differentially flat system. + + The FlatSystem class is used as a base class to describe differentially + flat systems for trajectory generation. The class must implement two + functions: + + zflag = flatsys.foward(x, u) + This function computes the flag (derivatives) of the flat output. + The inputs to this function are the state 'x' and inputs 'u' (both + 1D arrays). The output should be a 2D array with the first + dimension equal to the number of system inputs and the second + dimension of the length required to represent the full system + dynamics (typically the number of states) + + x, u = flatsys.reverse(zflag) + This function system state and inputs give the the flag (derivatives) + of the flat output. The input to this function is an 2D array whose + first dimension is equal to the number of system inputs and whose + second dimension is of length required to represent the full system + dynamics (typically the number of states). The output is the state + `x` and inputs `u` (both 1D arrays). + + A flat system is also an input/output system supporting simulation, + composition, and linearization. If the update and output methods are + given, they are used in place of the flat coordinates. + + """ + def __init__(self, + forward, reverse, # flat system + updfcn=None, outfcn=None, # I/O system + inputs=None, outputs=None, + states=None, params={}, dt=None, name=None): + """Create a differentially flat input/output system. + + The FlatIOSystem constructor is used to create an input/output system + object that also represents a differentially flat system. The output + of the system does not need to be the differentially flat output. + + Parameters + ---------- + forward : callable + A function to compute the flat flag given the states and input. + reverse : callable + A function to compute the states and input given the flat flag. + updfcn : callable, optional + Function returning the state update function + + `updfcn(t, x, u[, param]) -> array` + + where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array + with shape (ninputs,), `t` is a float representing the currrent + time, and `param` is an optional dict containing the values of + parameters used by the function. If not specified, the state + space update will be computed using the flat system coordinates. + outfcn : callable + Function returning the output at the given state + + `outfcn(t, x, u[, param]) -> array` + + where the arguments are the same as for `upfcn`. If not + specified, the output will be the flat outputs. + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + this parameter is not given or given as `None`, the relevant + quantity will be determined when possible based on other + information provided to functions using the system. + outputs : int, list of str, or None + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + Returns + ------- + InputOutputSystem + Input/output system object + + """ + # TODO: specify default update and output functions + if updfcn is None: updfcn = self._flat_updfcn + if outfcn is None: outfcn = self._flat_outfcn + + # Initialize as an input/output system + NonlinearIOSystem.__init__( + self, updfcn, outfcn, inputs=inputs, outputs=outputs, + states=states, params=params, dt=dt, name=name) + + # Save the functions to compute forward and reverse conversions + if forward is not None: self.forward = forward + if reverse is not None: self.reverse = reverse + + def forward(self, x, u, params={}): + """Compute the flat flag given the states and input. + + Given the states and inputs for a system, compute the flat + outputs and their derivatives (the flat "flag") for the + system. + + Parameters + ---------- + x : list or array + The state of the system. + u : list or array + The input to the system. + params : dict, optional + Parameter values for the system. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + + Returns + ------- + zflag : list of 1D arrays + For each flat output :math:`z_i`, zflag[i] should be an + ndarray of length :math:`q_i` that contains the flat + output and its first :math:`q_i` derivatives. + + """ + pass + + def reverse(self, zflag, params={}): + """Compute the states and input given the flat flag. + + Parameters + ---------- + zflag : list of arrays + For each flat output :math:`z_i`, zflag[i] should be an + ndarray of length :math:`q_i` that contains the flat + output and its first :math:`q_i` derivatives. + params : dict, optional + Parameter values for the system. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + + Returns + ------- + x : 1D array + The state of the system corresponding to the flat flag. + u : 1D array + The input to the system corresponding to the flat flag. + + """ + pass + + def _flat_updfcn(self, t, x, u, params={}): + # TODO: implement state space update using flat coordinates + raise NotImplementedError("update function for flat system not given") + + def _flat_outfcn(self, t, x, u, params={}): + # Return the flat output + zflag = self.forward(x, u, params) + return np.array(zflag[:][0]) + + +# Solve a point to point trajectory generation problem for a linear system +def point_to_point(sys, x0, u0, xf, uf, Tf, T0=0, basis=None, cost=None): + """Compute trajectory between an initial and final conditions. + + Compute a feasible trajectory for a differentially flat system between an + initial condition and a final condition. + + Parameters + ---------- + flatsys : FlatSystem object + Description of the differentially flat system. This object must + define a function flatsys.forward() that takes the system state and + produceds the flag of flat outputs and a system flatsys.reverse() + that takes the flag of the flat output and prodes the state and + input. + + x0, u0, xf, uf : 1D arrays + Define the desired initial and final conditions for the system. If + any of the values are given as None, they are replaced by a vector of + zeros of the appropriate dimension. + + Tf : float + The final time for the trajectory (corresponding to xf) + + T0 : float (optional) + The initial time for the trajectory (corresponding to x0). If not + specified, its value is taken to be zero. + + basis : BasisFamily object (optional) + The basis functions to use for generating the trajectory. If not + specified, the PolyFamily basis family will be used, with the minimal + number of elements required to find a feasible trajectory (twice + the number of system states) + + Returns + ------- + traj : SystemTrajectory object + The system trajectory is returned as an object that implements the + eval() function, we can be used to compute the value of the state + and input and a given time t. + + """ + # + # Make sure the problem is one that we can handle + # + # TODO: put in tests for flat system input + # TODO: process initial and final conditions to allow x0 or (x0, u0) + if x0 is None: x0 = np.zeros(sys.nstates) + if u0 is None: u0 = np.zeros(sys.ninputs) + if xf is None: xf = np.zeros(sys.nstates) + if uf is None: uf = np.zeros(sys.ninputs) + + # + # Determine the basis function set to use and make sure it is big enough + # + + # If no basis set was specified, use a polynomial basis (poor choice...) + if (basis is None): basis = PolyFamily(2*sys.nstates, Tf) + + # Make sure we have enough basis functions to solve the problem + if (basis.N * sys.ninputs < 2 * (sys.nstates + sys.ninputs)): + raise ValueError("basis set is too small") + + # + # Map the initial and final conditions to flat output conditions + # + # We need to compute the output "flag": [z(t), z'(t), z''(t), ...] + # and then evaluate this at the initial and final condition. + # + # TODO: should be able to represent flag variables as 1D arrays + # TODO: need inputs to fully define the flag + zflag_T0 = sys.forward(x0, u0) + zflag_Tf = sys.forward(xf, uf) + + # + # Compute the matrix constraints for initial and final conditions + # + # This computation depends on the basis function we are using. It + # essentially amounts to evaluating the basis functions and their + # derivatives at the initial and final conditions. + + # Figure out the size of the problem we are solving + flag_tot = np.sum([len(zflag_T0[i]) for i in range(sys.ninputs)]) + + # Start by creating an empty matrix that we can fill up + # TODO: allow a different number of basis elements for each flat output + M = np.zeros((2 * flag_tot, basis.N * sys.ninputs)) + + # Now fill in the rows for the initial and final states + flag_off = 0 + coeff_off = 0 + for i in range(sys.ninputs): + flag_len = len(zflag_T0[i]) + for j in range(basis.N): + for k in range(flag_len): + M[flag_off + k, coeff_off + j] = basis.eval_deriv(j, k, T0) + M[flag_tot + flag_off + k, coeff_off + j] = \ + basis.eval_deriv(j, k, Tf) + flag_off += flag_len + coeff_off += basis.N + + # Create an empty matrix that we can fill up + Z = np.zeros(2 * flag_tot) + + # Compute the flag vector to use for the right hand side by + # stacking up the flags for each input + # TODO: make this more pythonic + flag_off = 0 + for i in range(sys.ninputs): + flag_len = len(zflag_T0[i]) + for j in range(flag_len): + Z[flag_off + j] = zflag_T0[i][j] + Z[flag_tot + flag_off + j] = zflag_Tf[i][j] + flag_off += flag_len + + # + # Solve for the coefficients of the flat outputs + # + # At this point, we need to solve the equation M alpha = zflag, where M + # is the matrix constrains for initial and final conditions and zflag = + # [zflag_T0; zflag_tf]. Since everything is linear, just compute the + # least squares solution for now. + # + # TODO: need to allow cost and constraints... + alpha, residuals, rank, s = np.linalg.lstsq(M, Z, rcond=None) + + # + # Transform the trajectory from flat outputs to states and inputs + # + systraj = SystemTrajectory(sys, basis) + + # Store the flag lengths and coefficients + # TODO: make this more pythonic + coeff_off = 0 + for i in range(sys.ninputs): + # Grab the coefficients corresponding to this flat output + systraj.coeffs.append(alpha[coeff_off:coeff_off + basis.N]) + coeff_off += basis.N + + # Keep track of the length of the flat flag for this output + systraj.flaglen.append(len(zflag_T0[i])) + + # Return a function that computes inputs and states as a function of time + return systraj diff --git a/control/flatsys/linflat.py b/control/flatsys/linflat.py new file mode 100644 index 000000000..41a68537a --- /dev/null +++ b/control/flatsys/linflat.py @@ -0,0 +1,139 @@ +# linflat.py - FlatSystem subclass for linear systems +# RMM, 10 November 2012 +# +# This file defines a FlatSystem class for a linear system. +# +# Copyright (c) 2012 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import numpy as np +import control +from .flatsys import FlatSystem +from ..iosys import LinearIOSystem + + +class LinearFlatSystem(FlatSystem, LinearIOSystem): + def __init__(self, linsys, inputs=None, outputs=None, states=None, + name=None): + """Define a flat system from a SISO LTI system. + + Given a reachable, single-input/single-output, linear time-invariant + system, create a differentially flat system representation. + + Parameters + ---------- + linsys : StateSpace + LTI StateSpace system to be converted + inputs : int, list of str or None, optional + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + this parameter is not given or given as `None`, the relevant + quantity will be determined when possible based on other + information provided to functions using the system. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + Returns + ------- + iosys : LinearFlatSystem + Linear system represented as an flat input/output system + + """ + # Make sure we can handle the system + if (not control.isctime(linsys)): + raise control.ControlNotImplemented( + "requires continuous time, linear control system") + elif (not control.issiso(linsys)): + raise control.ControlNotImplemented( + "only single input, single output systems are supported") + + # Initialize the object as a LinearIO system + LinearIOSystem.__init__( + self, linsys, inputs=inputs, outputs=outputs, states=states, + name=name) + + # Find the transformation to chain of integrators form + zsys, Tr = control.reachable_form(linsys) + Tr = Tr[::-1, ::] # flip rows + + # Extract the information that we need + self.F = zsys.A[0, ::-1] # input function coeffs + self.T = Tr # state space transformation + self.Tinv = np.linalg.inv(Tr) # compute inverse once + + # Compute the flat output variable z = C x + Cfz = np.zeros(np.shape(linsys.C)); Cfz[0, 0] = 1 + self.Cf = np.dot(Cfz, Tr) + + # Compute the flat flag from the state (and input) + def forward(self, x, u): + """Compute the flat flag given the states and input. + + See :func:`control.flatsys.FlatSystem.forward` for more info. + + """ + x = np.reshape(x, (-1, 1)) + u = np.reshape(u, (1, -1)) + zflag = [np.zeros(self.nstates + 1)] + zflag[0][0] = np.dot(self.Cf, x) + H = self.Cf # initial state transformation + for i in range(1, self.nstates + 1): + zflag[0][i] = np.dot(H, np.dot(self.A, x) + np.dot(self.B, u)) + H = np.dot(H, self.A) # derivative for next iteration + return zflag + + # Compute state and input from flat flag + def reverse(self, zflag): + """Compute the states and input given the flat flag. + + See :func:`control.flatsys.FlatSystem.reverse` for more info. + + """ + z = zflag[0][0:-1] + x = np.dot(self.Tinv, z) + u = zflag[0][-1] - np.dot(self.F, z) + return np.reshape(x, self.nstates), np.reshape(u, self.ninputs) diff --git a/control/flatsys/poly.py b/control/flatsys/poly.py new file mode 100644 index 000000000..2d9f62455 --- /dev/null +++ b/control/flatsys/poly.py @@ -0,0 +1,61 @@ +# poly.m - simple set of polynomial basis functions +# TODO: rename this as taylor.m +# RMM, 10 Nov 2012 +# +# This class implements a set of simple basis functions consisting of powers +# of t: 1, t, t^2, ... +# +# Copyright (c) 2012 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import numpy as np +from scipy.special import factorial +from .basis import BasisFamily + +class PolyFamily(BasisFamily): + r"""Polynomial basis functions. + + This class represents the family of polynomials of the form + + .. math:: + \phi_i(t) = t^i + + """ + def __init__(self, N): + """Create a polynomial basis of order N.""" + self.N = N # save number of basis functions + + # Compute the kth derivative of the ith basis function at time t + def eval_deriv(self, i, k, t): + """Evaluate the kth derivative of the ith basis function at time t.""" + if (i < k): return 0; # higher derivative than power + return factorial(i)/factorial(i-k) * np.power(t, i-k) diff --git a/control/flatsys/systraj.py b/control/flatsys/systraj.py new file mode 100644 index 000000000..4505d3563 --- /dev/null +++ b/control/flatsys/systraj.py @@ -0,0 +1,118 @@ +# systraj.py - SystemTrajectory class +# RMM, 10 November 2012 +# +# The SystemTrajetory class is used to store a feasible trajectory for +# the state and input of a (nonlinear) control system. +# +# Copyright (c) 2012 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import numpy as np + +class SystemTrajectory: + """Class representing a system trajectory. + + The `SystemTrajectory` class is used to represent the trajectory of + a (differentially flat) system. Used by the + :func:`~control.trajsys.point_to_point` function to return a + trajectory. + + """ + def __init__(self, sys, basis, coeffs=[], flaglen=[]): + """Initilize a system trajectory object. + + Parameters + ---------- + sys : FlatSystem + Flat system object associated with this trajectory. + basis : BasisFamily + Family of basis vectors to use to represent the trajectory. + coeffs : list of 1D arrays, optional + For each flat output, define the coefficients of the basis + functions used to represent the trajectory. Defaults to an empty + list. + flaglen : list of ints, optional + For each flat output, the number of derivatives of the flat output + used to define the trajectory. Defaults to an empty list. + + """ + self.nstates = sys.nstates + self.ninputs = sys.ninputs + self.system = sys + self.basis = basis + self.coeffs = list(coeffs) + self.flaglen = list(flaglen) + + # Evaluate the trajectory over a list of time points + def eval(self, tlist): + """Return the state and input for a trajectory at a list of times. + + Evaluate the trajectory at a list of time points, returning the state + and input vectors for the trajectory: + + x, u = traj.eval(tlist) + + Parameters + ---------- + tlist : 1D array + List of times to evaluate the trajectory. + + Returns + ------- + x : 2D array + For each state, the values of the state at the given times. + u : 2D array + For each input, the values of the input at the given times. + + """ + # Allocate space for the outputs + xd = np.zeros((self.nstates, len(tlist))) + ud = np.zeros((self.ninputs, len(tlist))) + + # Go through each time point and compute xd and ud via flat variables + # TODO: make this more pythonic + for tind, t in enumerate(tlist): + zflag = [] + for i in range(self.ninputs): + flag_len = self.flaglen[i] + zflag.append(np.zeros(flag_len)) + for j in range(self.basis.N): + for k in range(flag_len): + #! TODO: rewrite eval_deriv to take in time vector + zflag[i][k] += self.coeffs[i][j] * \ + self.basis.eval_deriv(j, k, t) + + # Now copy the states and inputs + # TODO: revisit order of list arguments + xd[:,tind], ud[:,tind] = self.system.reverse(zflag) + + return xd, ud diff --git a/control/frdata.py b/control/frdata.py index d34200455..14705947e 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -1,4 +1,42 @@ +# Copyright (c) 2010 by California Institute of Technology +# Copyright (c) 2012 by Delft University of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the names of the California Institute of Technology nor +# the Delft University of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Author: M.M. (Rene) van Paassen (using xferfcn.py as basis) +# Date: 02 Oct 12 + from __future__ import division + """ Frequency response data representation and functions. @@ -6,70 +44,30 @@ FRD data. """ -"""Copyright (c) 2010 by California Institute of Technology - Copyright (c) 2012 by Delft University of Technology -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the names of the California Institute of Technology nor - the Delft University of Technology nor - the names of its contributors may be used to endorse or promote - products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH -OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. - -Author: M.M. (Rene) van Paassen (using xferfcn.py as basis) -Date: 02 Oct 12 -Revised: - -$Id: frd.py 185 2012-08-30 05:44:32Z murrayrm $ - -""" - # External function declarations +from warnings import warn import numpy as np from numpy import angle, array, empty, ones, \ - real, imag, matrix, absolute, eye, linalg, where, dot + real, imag, absolute, eye, linalg, where, dot from scipy.interpolate import splprep, splev from .lti import LTI -__all__ = ['FRD', 'frd'] +__all__ = ['FrequencyResponseData', 'FRD', 'frd'] -class FRD(LTI): - """FRD(d, w) + +class FrequencyResponseData(LTI): + """FrequencyResponseData(d, w) A class for models defined by frequency response data (FRD) - The FRD class is used to represent systems in frequency response data form. + The FrequencyResponseData (FRD) class is used to represent systems in + frequency response data form. - The main data members are 'omega' and 'fresp', where `omega` is a 1D - array with the frequency points of the response, and `fresp` is a 3D array, - with the first dimension corresponding to the output index of the FRD, - the second dimension corresponding to the input index, and the 3rd dimension - corresponding to the frequency points in omega. - For example, + The main data members are 'omega' and 'fresp', where `omega` is a 1D array + with the frequency points of the response, and `fresp` is a 3D array, with + the first dimension corresponding to the output index of the FRD, the + second dimension corresponding to the input index, and the 3rd dimension + corresponding to the frequency points in omega. For example, >>> frdata[2,5,:] = numpy.array([1., 0.8-0.2j, 0.2-0.8j]) @@ -80,12 +78,14 @@ class FRD(LTI): """ + # Allow NDarray * StateSpace to give StateSpace._rmul_() priority + # https://docs.scipy.org/doc/numpy/reference/arrays.classes.html + __array_priority__ = 11 # override ndarray and matrix types + epsw = 1e-8 def __init__(self, *args, **kwargs): - """FRD(d, w) - - Construct an FRD object + """Construct an FRD object. The default constructor is FRD(d, w), where w is an iterable of frequency points, and d is the matching frequency data. @@ -149,10 +149,10 @@ def __init__(self, *args, **kwargs): dtype=tuple) for i in range(self.fresp.shape[0]): for j in range(self.fresp.shape[1]): - self.ifunc[i,j],u = splprep( + self.ifunc[i, j], u = splprep( u=self.omega, x=[real(self.fresp[i, j, :]), imag(self.fresp[i, j, :])], - w=1.0/(absolute(self.fresp[i, j, :])+0.001), s=0.0) + w=1.0/(absolute(self.fresp[i, j, :]) + 0.001), s=0.0) else: self.ifunc = None LTI.__init__(self, self.fresp.shape[1], self.fresp.shape[0]) @@ -161,7 +161,7 @@ def __str__(self): """String representation of the transfer function.""" mimo = self.inputs > 1 or self.outputs > 1 - outstr = [ 'frequency response data ' ] + outstr = ['frequency response data '] mt, pt, wt = self.freqresp(self.omega) for i in range(self.inputs): @@ -171,9 +171,9 @@ def __str__(self): outstr.append('Freq [rad/s] Response ') outstr.append('------------ ---------------------') outstr.extend( - [ '%12.3f %10.4g%+10.4gj' % (w, m, p) - for m, p, w in zip(real(self.fresp[j,i,:]), imag(self.fresp[j,i,:]), wt) ]) - + ['%12.3f %10.4g%+10.4gj' % (w, m, p) + for m, p, w in zip(real(self.fresp[j, i, :]), + imag(self.fresp[j, i, :]), wt)]) return '\n'.join(outstr) @@ -187,9 +187,10 @@ def __add__(self, other): if isinstance(other, FRD): # verify that the frequencies match - if (other.omega != self.omega).any(): - print("Warning: frequency points do not match; expect" - " truncation and interpolation") + if len(other.omega) != len(self.omega) or \ + (other.omega != self.omega).any(): + warn("Frequency points do not match; expect " + "truncation and interpolation.") # Convert the second argument to a frequency response function. # or re-base the frd to the current omega (if needed) @@ -208,7 +209,7 @@ def __add__(self, other): def __radd__(self, other): """Right add two LTI objects (parallel connection).""" - return self + other; + return self + other def __sub__(self, other): """Subtract two LTI objects.""" @@ -232,8 +233,9 @@ def __mul__(self, other): # Check that the input-output sizes are consistent. if self.inputs != other.outputs: - raise ValueError("H = G1*G2: input-output size mismatch" - " G1 has %i input(s), G2 has %i output(s)." % + raise ValueError( + "H = G1*G2: input-output size mismatch: " + "G1 has %i input(s), G2 has %i output(s)." % (self.inputs, other.outputs)) inputs = other.inputs @@ -241,7 +243,7 @@ def __mul__(self, other): fresp = empty((outputs, inputs, len(self.omega)), dtype=self.fresp.dtype) for i in range(len(self.omega)): - fresp[:,:,i] = dot(self.fresp[:,:,i], other.fresp[:,:,i]) + fresp[:, :, i] = dot(self.fresp[:, :, i], other.fresp[:, :, i]) return FRD(fresp, self.omega, smooth=(self.ifunc is not None) and (other.ifunc is not None)) @@ -258,8 +260,9 @@ def __rmul__(self, other): # Check that the input-output sizes are consistent. if self.outputs != other.inputs: - raise ValueError("H = G1*G2: input-output size mismatch" - " G1 has %i input(s), G2 has %i output(s)." % + raise ValueError( + "H = G1*G2: input-output size mismatch: " + "G1 has %i input(s), G2 has %i output(s)." % (other.inputs, self.outputs)) inputs = self.inputs @@ -268,7 +271,7 @@ def __rmul__(self, other): fresp = empty((outputs, inputs, len(self.omega)), dtype=self.fresp.dtype) for i in range(len(self.omega)): - fresp[:,:,i] = dot(other.fresp[:,:,i], self.fresp[:,:,i]) + fresp[:, :, i] = dot(other.fresp[:, :, i], self.fresp[:, :, i]) return FRD(fresp, self.omega, smooth=(self.ifunc is not None) and (other.ifunc is not None)) @@ -283,11 +286,11 @@ def __truediv__(self, other): else: other = _convertToFRD(other, omega=self.omega) - if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): raise NotImplementedError( - "FRD.__truediv__ is currently implemented only for SISO systems.") + "FRD.__truediv__ is currently only implemented for SISO " + "systems.") return FRD(self.fresp/other.fresp, self.omega, smooth=(self.ifunc is not None) and @@ -309,7 +312,8 @@ def __rtruediv__(self, other): if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): raise NotImplementedError( - "FRD.__rtruediv__ is currently implemented only for SISO systems.") + "FRD.__rtruediv__ is currently only implemented for " + "SISO systems.") return other / self @@ -317,12 +321,12 @@ def __rtruediv__(self, other): def __rdiv__(self, other): return self.__rtruediv__(other) - def __pow__(self,other): + def __pow__(self, other): if not type(other) == int: raise ValueError("Exponent must be an integer") if other == 0: - return FRD(ones(self.fresp.shape),self.omega, - smooth=(self.ifunc is not None)) #unity + return FRD(ones(self.fresp.shape), self.omega, + smooth=(self.ifunc is not None)) # unity if other > 0: return self * (self**(other-1)) if other < 0: @@ -340,8 +344,9 @@ def evalfr(self, omega): intermediate values. """ - warn("FRD.evalfr(omega) will be deprecated in a future release of python-control; use sys.eval(omega) instead", - PendingDeprecationWarning) + warn("FRD.evalfr(omega) will be deprecated in a future release " + "of python-control; use sys.eval(omega) instead", + PendingDeprecationWarning) # pragma: no coverage return self._evalfr(omega) # Define the `eval` function to evaluate an FRD at a given (real) @@ -352,7 +357,7 @@ def evalfr(self, omega): def eval(self, omega): """Evaluate a transfer function at a single angular frequency. - self._evalfr(omega) returns the value of the frequency response + self.evalfr(omega) returns the value of the frequency response at frequency omega. Note that a "normal" FRD only returns values for which there is an @@ -374,7 +379,7 @@ def _evalfr(self, omega): if self.ifunc is None: try: out = self.fresp[:, :, where(self.omega == omega)[0][0]] - except: + except Exception: raise ValueError( "Frequency %f not in frequency list, try an interpolating" " FRD if you want additional points" % omega) @@ -382,14 +387,14 @@ def _evalfr(self, omega): if getattr(omega, '__iter__', False): for i in range(self.outputs): for j in range(self.inputs): - for k,w in enumerate(omega): - frraw = splev(w, self.ifunc[i,j], der=0) - out[i,j,k] = frraw[0] + 1.0j*frraw[1] + for k, w in enumerate(omega): + frraw = splev(w, self.ifunc[i, j], der=0) + out[i, j, k] = frraw[0] + 1.0j * frraw[1] else: for i in range(self.outputs): for j in range(self.inputs): - frraw = splev(omega, self.ifunc[i,j], der=0) - out[i,j] = frraw[0] + 1.0j*frraw[1] + frraw = splev(omega, self.ifunc[i, j], der=0) + out[i, j] = frraw[0] + 1.0j * frraw[1] return out @@ -399,9 +404,10 @@ def freqresp(self, omega): mag, phase, omega = self.freqresp(omega) - reports the value of the magnitude, phase, and angular frequency of the - transfer function matrix evaluated at s = i * omega, where omega is a - list of angular frequencies, and is a sorted version of the input omega. + reports the value of the magnitude, phase, and angular frequency of + the transfer function matrix evaluated at s = i * omega, where omega + is a list of angular frequencies, and is a sorted version of the input + omega. """ @@ -424,24 +430,40 @@ def feedback(self, other=1, sign=-1): other = _convertToFRD(other, omega=self.omega) - if (self.outputs != other.inputs or - self.inputs != other.outputs): + if (self.outputs != other.inputs or self.inputs != other.outputs): raise ValueError( "FRD.feedback, inputs/outputs mismatch") fresp = empty((self.outputs, self.inputs, len(other.omega)), dtype=complex) # TODO: vectorize this # TODO: handle omega re-mapping + # TODO: is there a reason to use linalg.solve instead of linalg.inv? + # https://github.com/python-control/python-control/pull/314#discussion_r294075154 for k, w in enumerate(other.omega): - fresp[:, :, k] = self.fresp[:, :, k].view(type=matrix)* \ + fresp[:, :, k] = np.dot( + self.fresp[:, :, k], linalg.solve( - eye(self.inputs) + - other.fresp[:, :, k].view(type=matrix) * - self.fresp[:, :, k].view(type=matrix), - eye(self.inputs)) + eye(self.inputs) + + np.dot(other.fresp[:, :, k], self.fresp[:, :, k]), + eye(self.inputs)) + ) return FRD(fresp, other.omega, smooth=(self.ifunc is not None)) +# +# Allow FRD as an alias for the FrequencyResponseData class +# +# Note: This class was initially given the name "FRD", but this caused +# problems with documentation on MacOS platforms, since files were generated +# for control.frd and control.FRD, which are not differentiated on most MacOS +# filesystems, which are case insensitive. Renaming the FRD class to be +# FrequenceResponseData and then assigning FRD to point to the same object +# fixes this problem. +# + +FRD = FrequencyResponseData + + def _convertToFRD(sys, omega, inputs=1, outputs=1): """Convert a system to frequency response data form (if needed). @@ -462,7 +484,8 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1): if isinstance(sys, FRD): omega.sort() - if (abs(omega - sys.omega) < FRD.epsw).all(): + if len(omega) == len(sys.omega) and \ + (abs(omega - sys.omega) < FRD.epsw).all(): # frequencies match, and system was already frd; simply use return sys @@ -484,18 +507,19 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1): # try converting constant matrices try: sys = array(sys) - outputs,inputs = sys.shape + outputs, inputs = sys.shape fresp = empty((outputs, inputs, len(omega)), dtype=float) for i in range(outputs): for j in range(inputs): - fresp[i,j,:] = sys[i,j] + fresp[i, j, :] = sys[i, j] return FRD(fresp, omega, smooth=True) - except: + except Exception: pass raise TypeError('''Can't convert given type "%s" to FRD system.''' % sys.__class__) + def frd(*args): """frd(d, w) diff --git a/control/freqplot.py b/control/freqplot.py index 6600a5b4a..1bb1fc7a5 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -40,7 +40,7 @@ # SUCH DAMAGE. # # $Id$ -import matplotlib +import matplotlib as mpl import matplotlib.pyplot as plt import scipy as sp import numpy as np @@ -48,10 +48,17 @@ from .ctrlutil import unwrap from .bdalg import feedback from .margins import stability_margins +from . import config __all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot', 'bode', 'nyquist', 'gangof4'] +# Default values for module parameter variables +_freqplot_defaults = { + 'freqplot.feature_periphery_decades': 1, + 'freqplot.number_of_samples': None, +} + # # Main plotting functions # @@ -59,13 +66,23 @@ # frequency domain plots # +# # Bode plot +# +# Default values for Bode plot configuration variables +_bode_defaults = { + 'bode.dB': False, # Plot gain in dB + 'bode.deg': True, # Plot phase in degrees + 'bode.Hz': False, # Plot frequency in Hertz + 'bode.grid': True, # Turn on grid for gain and phase +} -def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, - Plot=True, omega_limits=None, omega_num=None, margins=None, *args, **kwargs): - """ - Bode plot for a system + +def bode_plot(syslist, omega=None, + Plot=True, omega_limits=None, omega_num=None, + margins=None, *args, **kwargs): + """Bode plot for a system Plots a Bode plot for the system over a (optional) frequency range. @@ -75,23 +92,28 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, List of linear input/output systems (single system is OK) omega : list List of frequencies in rad/sec to be used for frequency response - dB : boolean - If True, plot result in dB - Hz : boolean - If True, plot frequency in Hz (omega must be provided in rad/sec) - deg : boolean - If True, plot phase in degrees (else radians) - Plot : boolean - If True, plot magnitude and phase + dB : bool + If True, plot result in dB. Default is false. + Hz : bool + If True, plot frequency in Hz (omega must be provided in rad/sec). + Default value (False) set by config.defaults['bode.Hz'] + deg : bool + If True, plot phase in degrees (else radians). Default value (True) + config.defaults['bode.deg'] + Plot : bool + If True (default), plot magnitude and phase omega_limits: tuple, list, ... of two values Limits of the to generate frequency vector. If Hz=True the limits are in Hz otherwise in rad/s. omega_num: int - number of samples - margins : boolean - If True, plot gain and phase margin - \*args, \**kwargs: - Additional options to matplotlib (color, linestyle, etc) + Number of samples to plot. Defaults to + config.defaults['freqplot.number_of_samples']. + margins : bool + If True, plot gain and phase margin. + *args + Additional arguments for :func:`matplotlib.plot` (color, linestyle, etc) + **kwargs: + Additional keywords (passed to `matplotlib`) Returns ------- @@ -102,6 +124,15 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, omega : array (list if len(syslist) > 1) frequency in rad/sec + Other Parameters + ---------------- + grid : bool + If True, plot grid lines on gain and phase plots. Default is set by + config.defaults['bode.grid']. + + The default values for Bode plot configuration parameters can be reset + using the `config.defaults` dictionary, with module name 'bode'. + Notes ----- 1. Alternatively, you may use the lower-level method (mag, phase, freq) @@ -110,22 +141,25 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, 2. If a discrete time model is given, the frequency response is plotted along the upper branch of the unit circle, using the mapping z = exp(j - \omega dt) where omega ranges from 0 to pi/dt and dt is the discrete + \\omega dt) where omega ranges from 0 to pi/dt and dt is the discrete timebase. If not timebase is specified (dt = True), dt is set to 1. Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> mag, phase, omega = bode(sys) + """ - # Set default values for options - from . import config - if dB is None: - dB = config.bode_dB - if deg is None: - deg = config.bode_deg - if Hz is None: - Hz = config.bode_Hz + # Make a copy of the kwargs dictonary since we will modify it + kwargs = dict(kwargs) + + # Get values for params (and pop from list to allow keyword use in plot) + dB = config._get_param('bode', 'dB', kwargs, _bode_defaults, pop=True) + deg = config._get_param('bode', 'deg', kwargs, _bode_defaults, pop=True) + Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True) + grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True) + Plot = config._get_param('bode', 'grid', Plot, True) + margins = config._get_param('bode', 'margins', margins, False) # If argument was a singleton, turn it into a list if not getattr(syslist, '__iter__', False): @@ -134,26 +168,28 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, if omega is None: if omega_limits is None: # Select a default range if none is provided - omega = default_frequency_range(syslist, Hz=Hz, number_of_samples=omega_num) + omega = default_frequency_range(syslist, Hz=Hz, + number_of_samples=omega_num) else: omega_limits = np.array(omega_limits) if Hz: omega_limits *= 2. * math.pi if omega_num: - omega = sp.logspace(np.log10(omega_limits[0]), - np.log10(omega_limits[1]), - num=omega_num, + omega = sp.logspace(np.log10(omega_limits[0]), + np.log10(omega_limits[1]), + num=omega_num, endpoint=True) else: - omega = sp.logspace(np.log10(omega_limits[0]), - np.log10(omega_limits[1]), + omega = sp.logspace(np.log10(omega_limits[0]), + np.log10(omega_limits[1]), endpoint=True) mags, phases, omegas, nyquistfrqs = [], [], [], [] for sys in syslist: if sys.inputs > 1 or sys.outputs > 1: # TODO: Add MIMO bode plots. - raise NotImplementedError("Bode is currently only implemented for SISO systems.") + raise NotImplementedError( + "Bode is currently only implemented for SISO systems.") else: omega_sys = np.array(omega) if sys.isdtime(True): @@ -235,7 +271,7 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, color=pltline[0].get_color()) # Add a grid to the plot + labeling - ax_mag.grid(False if margins else True, which='both') + ax_mag.grid(grid and not margins, which='both') ax_mag.set_ylabel("Magnitude (dB)" if dB else "Magnitude") # Phase plot @@ -248,7 +284,8 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, # Show the phase and gain margins in the plot if margins: margin = stability_margins(sys) - gm, pm, Wcg, Wcp = margin[0], margin[1], margin[3], margin[4] + gm, pm, Wcg, Wcp = \ + margin[0], margin[1], margin[3], margin[4] # TODO: add some documentation describing why this is here phase_at_cp = phases[0][(np.abs(omegas[0] - Wcp)).argmin()] if phase_at_cp >= 0.: @@ -257,102 +294,106 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, phase_limit = -180. if Hz: - Wcg, Wcp = Wcg/(2*math.pi),Wcp/(2*math.pi) + Wcg, Wcp = Wcg/(2*math.pi), Wcp/(2*math.pi) ax_mag.axhline(y=0 if dB else 1, color='k', linestyle=':', zorder=-20) - ax_phase.axhline(y=phase_limit if deg else math.radians(phase_limit), + ax_phase.axhline(y=phase_limit if deg else + math.radians(phase_limit), color='k', linestyle=':', zorder=-20) mag_ylim = ax_mag.get_ylim() phase_ylim = ax_phase.get_ylim() if pm != float('inf') and Wcp != float('nan'): if dB: - ax_mag.semilogx([Wcp, Wcp], [0.,-1e5], - color='k', linestyle=':', - zorder=-20) + ax_mag.semilogx( + [Wcp, Wcp], [0., -1e5], + color='k', linestyle=':', zorder=-20) else: - ax_mag.loglog([Wcp,Wcp], [1.,1e-8],color='k', - linestyle=':', zorder=-20) + ax_mag.loglog( + [Wcp, Wcp], [1., 1e-8], + color='k', linestyle=':', zorder=-20) if deg: - ax_phase.semilogx([Wcp, Wcp], - [1e5, phase_limit+pm], - color='k', linestyle=':', - zorder=-20) - ax_phase.semilogx([Wcp, Wcp], - [phase_limit + pm, phase_limit], - color='k', zorder=-20) + ax_phase.semilogx( + [Wcp, Wcp], [1e5, phase_limit+pm], + color='k', linestyle=':', zorder=-20) + ax_phase.semilogx( + [Wcp, Wcp], [phase_limit + pm, phase_limit], + color='k', zorder=-20) else: - ax_phase.semilogx([Wcp, Wcp], - [1e5, math.radians(phase_limit) + - math.radians(pm)], - color='k', linestyle=':', - zorder=-20) - ax_phase.semilogx([Wcp, Wcp], - [math.radians(phase_limit) + - math.radians(pm), - math.radians(phase_limit)], - color='k', zorder=-20) + ax_phase.semilogx( + [Wcp, Wcp], [1e5, math.radians(phase_limit) + + math.radians(pm)], + color='k', linestyle=':', zorder=-20) + ax_phase.semilogx( + [Wcp, Wcp], [math.radians(phase_limit) + + math.radians(pm), + math.radians(phase_limit)], + color='k', zorder=-20) if gm != float('inf') and Wcg != float('nan'): if dB: - ax_mag.semilogx([Wcg, Wcg], - [-20.*np.log10(gm), -1e5], - color='k', linestyle=':', - zorder=-20) - ax_mag.semilogx([Wcg, Wcg], [0,-20*np.log10(gm)], - color='k', zorder=-20) + ax_mag.semilogx( + [Wcg, Wcg], [-20.*np.log10(gm), -1e5], + color='k', linestyle=':', zorder=-20) + ax_mag.semilogx( + [Wcg, Wcg], [0, -20*np.log10(gm)], + color='k', zorder=-20) else: - ax_mag.loglog([Wcg, Wcg], - [1./gm,1e-8],color='k', - linestyle=':', zorder=-20) - ax_mag.loglog([Wcg, Wcg], - [1.,1./gm],color='k', zorder=-20) + ax_mag.loglog( + [Wcg, Wcg], [1./gm, 1e-8], color='k', + linestyle=':', zorder=-20) + ax_mag.loglog( + [Wcg, Wcg], [1., 1./gm], color='k', zorder=-20) if deg: - ax_phase.semilogx([Wcg, Wcg], [1e-8, phase_limit], - color='k', linestyle=':', - zorder=-20) + ax_phase.semilogx( + [Wcg, Wcg], [1e-8, phase_limit], + color='k', linestyle=':', zorder=-20) else: - ax_phase.semilogx([Wcg, Wcg], - [1e-8, math.radians(phase_limit)], - color='k', linestyle=':', - zorder=-20) + ax_phase.semilogx( + [Wcg, Wcg], [1e-8, math.radians(phase_limit)], + color='k', linestyle=':', zorder=-20) ax_mag.set_ylim(mag_ylim) ax_phase.set_ylim(phase_ylim) if sisotool: - ax_mag.text(0.04, 0.06, - 'G.M.: %.2f %s\nFreq: %.2f %s' % - (20*np.log10(gm) if dB else gm, - 'dB ' if dB else '', - Wcg, 'Hz' if Hz else 'rad/s'), - horizontalalignment='left', - verticalalignment='bottom', - transform=ax_mag.transAxes, - fontsize=8 if int(matplotlib.__version__[0]) == 1 else 6) - ax_phase.text(0.04, 0.06, - 'P.M.: %.2f %s\nFreq: %.2f %s' % - (pm if deg else math.radians(pm), - 'deg' if deg else 'rad', - Wcp, 'Hz' if Hz else 'rad/s'), - horizontalalignment='left', - verticalalignment='bottom', - transform=ax_phase.transAxes, - fontsize=8 if int(matplotlib.__version__[0]) == 1 else 6) + ax_mag.text( + 0.04, 0.06, + 'G.M.: %.2f %s\nFreq: %.2f %s' % + (20*np.log10(gm) if dB else gm, + 'dB ' if dB else '', + Wcg, 'Hz' if Hz else 'rad/s'), + horizontalalignment='left', + verticalalignment='bottom', + transform=ax_mag.transAxes, + fontsize=8 if int(mpl.__version__[0]) == 1 else 6) + ax_phase.text( + 0.04, 0.06, + 'P.M.: %.2f %s\nFreq: %.2f %s' % + (pm if deg else math.radians(pm), + 'deg' if deg else 'rad', + Wcp, 'Hz' if Hz else 'rad/s'), + horizontalalignment='left', + verticalalignment='bottom', + transform=ax_phase.transAxes, + fontsize=8 if int(mpl.__version__[0]) == 1 else 6) else: - plt.suptitle('Gm = %.2f %s(at %.2f %s), Pm = %.2f %s (at %.2f %s)' % - (20*np.log10(gm) if dB else gm, - 'dB ' if dB else '\b', - Wcg, 'Hz' if Hz else 'rad/s', - pm if deg else math.radians(pm), - 'deg' if deg else 'rad', - Wcp, 'Hz' if Hz else 'rad/s')) + plt.suptitle( + "Gm = %.2f %s(at %.2f %s), " + "Pm = %.2f %s (at %.2f %s)" % + (20*np.log10(gm) if dB else gm, + 'dB ' if dB else '\b', + Wcg, 'Hz' if Hz else 'rad/s', + pm if deg else math.radians(pm), + 'deg' if deg else 'rad', + Wcp, 'Hz' if Hz else 'rad/s')) if nyquistfrq_plot: - ax_phase.axvline(nyquistfrq_plot, color=pltline[0].get_color()) + ax_phase.axvline( + nyquistfrq_plot, color=pltline[0].get_color()) # Add a grid to the plot + labeling ax_phase.set_ylabel("Phase (deg)" if deg else "Phase (rad)") @@ -363,21 +404,17 @@ def gen_zero_centered_series(val_min, val_max, period): return np.arange(v1, v2 + 1) * period if deg: ylim = ax_phase.get_ylim() - ax_phase.set_yticks(gen_zero_centered_series(ylim[0], - ylim[1], 45.)) - ax_phase.set_yticks(gen_zero_centered_series(ylim[0], - ylim[1], 15.), - minor=True) + ax_phase.set_yticks(gen_zero_centered_series( + ylim[0], ylim[1], 45.)) + ax_phase.set_yticks(gen_zero_centered_series( + ylim[0], ylim[1], 15.), minor=True) else: ylim = ax_phase.get_ylim() - ax_phase.set_yticks(gen_zero_centered_series(ylim[0], - ylim[1], - math.pi / 4.)) - ax_phase.set_yticks(gen_zero_centered_series(ylim[0], - ylim[1], - math.pi / 12.), - minor=True) - ax_phase.grid(False if margins else True, which='both') + ax_phase.set_yticks(gen_zero_centered_series( + ylim[0], ylim[1], math.pi / 4.)) + ax_phase.set_yticks(gen_zero_centered_series( + ylim[0], ylim[1], math.pi / 12.), minor=True) + ax_phase.grid(grid and not margins, which='both') # ax_mag.grid(which='minor', alpha=0.3) # ax_mag.grid(which='major', alpha=0.9) # ax_phase.grid(which='minor', alpha=0.3) @@ -392,6 +429,9 @@ def gen_zero_centered_series(val_min, val_max, period): else: return mags, phases, omegas +# +# Nyquist plot +# def nyquist_plot(syslist, omega=None, Plot=True, color=None, labelFreq=0, *args, **kwargs): @@ -412,8 +452,10 @@ def nyquist_plot(syslist, omega=None, Plot=True, color=None, Used to specify the color of the plot labelFreq : int Label every nth frequency on the plot - \*args, \**kwargs: - Additional options to matplotlib (color, linestyle, etc) + *args + Additional arguments for :func:`matplotlib.plot` (color, linestyle, etc) + **kwargs: + Additional keywords (passed to `matplotlib`) Returns ------- @@ -442,15 +484,16 @@ def nyquist_plot(syslist, omega=None, Plot=True, color=None, elif isinstance(omega, list) or isinstance(omega, tuple): # Only accept tuple or list of length 2 if len(omega) != 2: - raise ValueError("Supported frequency arguments are (wmin,wmax) tuple or list, " - "or frequency vector. ") + raise ValueError("Supported frequency arguments are (wmin,wmax)" + "tuple or list, or frequency vector. ") omega = np.logspace(np.log10(omega[0]), np.log10(omega[1]), num=50, endpoint=True, base=10.0) for sys in syslist: if sys.inputs > 1 or sys.outputs > 1: # TODO: Add MIMO nyquist plots. - raise NotImplementedError("Nyquist is currently only implemented for SISO systems.") + raise NotImplementedError( + "Nyquist is currently only implemented for SISO systems.") else: # Get the magnitude and phase of the system mag_tmp, phase_tmp, omega = sys.freqresp(omega) @@ -471,8 +514,9 @@ def nyquist_plot(syslist, omega=None, Plot=True, color=None, head_width=0.2, head_length=0.2) plt.plot(x, -y, '-', color=c, *args, **kwargs) - ax.arrow(x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2, fc=c, ec=c, - head_width=0.2, head_length=0.2) + ax.arrow( + x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2, + fc=c, ec=c, head_width=0.2, head_length=0.2) # Mark the -1 point plt.plot([-1], [0], 'r+') @@ -497,7 +541,8 @@ def nyquist_plot(syslist, omega=None, Plot=True, color=None, # np.round() is used because 0.99... appears # instead of 1.0, and this would otherwise be # truncated to 0. - plt.text(xpt, ypt, ' ' + str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' + + plt.text(xpt, ypt, ' ' + + str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' + prefix + 'Hz') if Plot: @@ -508,9 +553,12 @@ def nyquist_plot(syslist, omega=None, Plot=True, color=None, return x, y, omega +# +# Gang of Four plot +# # TODO: think about how (and whether) to handle lists of systems -def gangof4_plot(P, C, omega=None): +def gangof4_plot(P, C, omega=None, **kwargs): """Plot the "Gang of 4" transfer functions for a system Generates a 2x2 plot showing the "Gang of 4" sensitivity functions @@ -529,58 +577,81 @@ def gangof4_plot(P, C, omega=None): """ if P.inputs > 1 or P.outputs > 1 or C.inputs > 1 or C.outputs > 1: # TODO: Add MIMO go4 plots. - raise NotImplementedError("Gang of four is currently only implemented for SISO systems.") - else: + raise NotImplementedError( + "Gang of four is currently only implemented for SISO systems.") - # Select a default range if none is provided - # TODO: This needs to be made more intelligent - if omega is None: - omega = default_frequency_range((P, C)) - - # Compute the senstivity functions - L = P * C - S = feedback(1, L) - T = L * S - - # Set up the axes with labels so that multiple calls to - # gangof4_plot will superimpose the data. See details in bode_plot. - plot_axes = {'t': None, 's': None, 'ps': None, 'cs': None} - for ax in plt.gcf().axes: - label = ax.get_label() - if label.startswith('control-gangof4-'): - key = label[len('control-gangof4-'):] - if key not in plot_axes: - raise RuntimeError("unknown gangof4 axis type '{}'".format(label)) - plot_axes[key] = ax - - # if any of the axes are missing, start from scratch - if any((ax is None for ax in plot_axes.values())): - plt.clf() - plot_axes = {'t': plt.subplot(221, label='control-gangof4-t'), - 'ps': plt.subplot(222, label='control-gangof4-ps'), - 'cs': plt.subplot(223, label='control-gangof4-cs'), - 's': plt.subplot(224, label='control-gangof4-s')} - - # - # Plot the four sensitivity functions - # - - # TODO: Need to add in the mag = 1 lines - mag_tmp, phase_tmp, omega = T.freqresp(omega) - mag = np.squeeze(mag_tmp) - plot_axes['t'].loglog(omega, mag) - - mag_tmp, phase_tmp, omega = (P * S).freqresp(omega) - mag = np.squeeze(mag_tmp) - plot_axes['ps'].loglog(omega, mag) - - mag_tmp, phase_tmp, omega = (C * S).freqresp(omega) - mag = np.squeeze(mag_tmp) - plot_axes['cs'].loglog(omega, mag) - - mag_tmp, phase_tmp, omega = S.freqresp(omega) - mag = np.squeeze(mag_tmp) - plot_axes['s'].loglog(omega, mag) + # Get the default parameter values + dB = config._get_param('bode', 'dB', kwargs, _bode_defaults, pop=True) + Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True) + grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True) + + # Select a default range if none is provided + # TODO: This needs to be made more intelligent + if omega is None: + omega = default_frequency_range((P, C)) + + # Compute the senstivity functions + L = P * C + S = feedback(1, L) + T = L * S + + # Set up the axes with labels so that multiple calls to + # gangof4_plot will superimpose the data. See details in bode_plot. + plot_axes = {'t': None, 's': None, 'ps': None, 'cs': None} + for ax in plt.gcf().axes: + label = ax.get_label() + if label.startswith('control-gangof4-'): + key = label[len('control-gangof4-'):] + if key not in plot_axes: + raise RuntimeError( + "unknown gangof4 axis type '{}'".format(label)) + plot_axes[key] = ax + + # if any of the axes are missing, start from scratch + if any((ax is None for ax in plot_axes.values())): + plt.clf() + plot_axes = {'s': plt.subplot(221, label='control-gangof4-s'), + 'ps': plt.subplot(222, label='control-gangof4-ps'), + 'cs': plt.subplot(223, label='control-gangof4-cs'), + 't': plt.subplot(224, label='control-gangof4-t')} + + # + # Plot the four sensitivity functions + # + omega_plot = omega / (2. * math.pi) if Hz else omega + + # TODO: Need to add in the mag = 1 lines + mag_tmp, phase_tmp, omega = S.freqresp(omega) + mag = np.squeeze(mag_tmp) + plot_axes['s'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + plot_axes['s'].set_ylabel("$|S|$") + plot_axes['s'].tick_params(labelbottom=False) + plot_axes['s'].grid(grid, which='both') + + mag_tmp, phase_tmp, omega = (P * S).freqresp(omega) + mag = np.squeeze(mag_tmp) + plot_axes['ps'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + plot_axes['ps'].tick_params(labelbottom=False) + plot_axes['ps'].set_ylabel("$|PS|$") + plot_axes['ps'].grid(grid, which='both') + + mag_tmp, phase_tmp, omega = (C * S).freqresp(omega) + mag = np.squeeze(mag_tmp) + plot_axes['cs'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + plot_axes['cs'].set_xlabel( + "Frequency (Hz)" if Hz else "Frequency (rad/sec)") + plot_axes['cs'].set_ylabel("$|CS|$") + plot_axes['cs'].grid(grid, which='both') + + mag_tmp, phase_tmp, omega = T.freqresp(omega) + mag = np.squeeze(mag_tmp) + plot_axes['t'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + plot_axes['t'].set_xlabel( + "Frequency (Hz)" if Hz else "Frequency (rad/sec)") + plot_axes['t'].set_ylabel("$|T|$") + plot_axes['t'].grid(grid, which='both') + + plt.tight_layout() # # Utility functions @@ -589,10 +660,9 @@ def gangof4_plot(P, C, omega=None): # generating frequency domain plots # - # Compute reasonable defaults for axes -def default_frequency_range(syslist, Hz=None, number_of_samples=None, - feature_periphery_decade=None): +def default_frequency_range(syslist, Hz=None, number_of_samples=None, + feature_periphery_decades=None): """Compute a reasonable default frequency range for frequency domain plots. @@ -603,18 +673,18 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, ---------- syslist : list of LTI List of linear input/output systems (single system is OK) - Hz: boolean + Hz : bool If True, the limits (first and last value) of the frequencies are set to full decades in Hz so it fits plotting with logarithmic scale in Hz otherwise in rad/s. Omega is always returned in rad/sec. - number_of_samples: int - Number of samples to generate - feature_periphery_decade: float + number_of_samples : int, optional + Number of samples to generate. The default value is read from + ``config.defaults['freqplot.number_of_samples']. If None, then the + default from `numpy.logspace` is used. + feature_periphery_decades : float, optional Defines how many decades shall be included in the frequency range on - both sides of features (poles, zeros). - Example: If there is a feature, e.g. a pole, at 1Hz and feature_periphery_decade=1. - then the range of frequencies shall span 0.1 .. 10 Hz. - The default value is read from config.bode_feature_periphery_decade. + both sides of features (poles, zeros). The default value is read from + ``config.defaults['freqplot.feature_periphery_decades']``. Returns ------- @@ -626,6 +696,7 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, >>> from matlab import ss >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> omega = default_frequency_range(sys) + """ # This code looks at the poles and zeros of all of the systems that # we are plotting and sets the frequency range to be one decade above @@ -634,11 +705,10 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, # are found, it turns logspace(-1, 1) # Set default values for options - from . import config - if number_of_samples is None: - number_of_samples = config.bode_number_of_samples - if feature_periphery_decade is None: - feature_periphery_decade = config.bode_feature_periphery_decade + number_of_samples = config._get_param( + 'freqplot', 'number_of_samples', number_of_samples) + feature_periphery_decades = config._get_param( + 'freqplot', 'feature_periphery_decades', feature_periphery_decades, 1) # Find the list of all poles and zeros in the systems features = np.array(()) @@ -667,15 +737,18 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, # Get rid of poles and zeros # * at the origin and real <= 0 & imag==0: log! # * at 1.: would result in omega=0. (logaritmic plot!) - features_ = features_[(features_.imag != 0.0) | (features_.real > 0.)] - features_ = features_[np.bitwise_not((features_.imag == 0.0) & - (np.abs(features_.real - 1.0) < 1.e-10))] + features_ = features_[ + (features_.imag != 0.0) | (features_.real > 0.)] + features_ = features_[ + np.bitwise_not((features_.imag == 0.0) & + (np.abs(features_.real - 1.0) < 1.e-10))] # TODO: improve features__ = np.abs(np.log(features_) / (1.j * sys.dt)) features = np.concatenate((features, features__)) else: # TODO - raise NotImplementedError('type of system in not implemented now') + raise NotImplementedError( + "type of system in not implemented now") except: pass @@ -686,14 +759,14 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, if Hz: features /= 2. * math.pi features = np.log10(features) - lsp_min = np.floor(np.min(features) - feature_periphery_decade) - lsp_max = np.ceil(np.max(features) + feature_periphery_decade) + lsp_min = np.floor(np.min(features) - feature_periphery_decades) + lsp_max = np.ceil(np.max(features) + feature_periphery_decades) lsp_min += np.log10(2. * math.pi) lsp_max += np.log10(2. * math.pi) else: features = np.log10(features) - lsp_min = np.floor(np.min(features) - feature_periphery_decade) - lsp_max = np.ceil(np.max(features) + feature_periphery_decade) + lsp_min = np.floor(np.min(features) - feature_periphery_decades) + lsp_max = np.ceil(np.max(features) + feature_periphery_decades) if freq_interesting: lsp_min = min(lsp_min, np.log10(min(freq_interesting))) lsp_max = max(lsp_max, np.log10(max(freq_interesting))) @@ -703,17 +776,18 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, # Set the range to be an order of magnitude beyond any features if number_of_samples: - omega = sp.logspace(lsp_min, lsp_max, num=number_of_samples, endpoint=True) + omega = sp.logspace( + lsp_min, lsp_max, num=number_of_samples, endpoint=True) else: omega = sp.logspace(lsp_min, lsp_max, endpoint=True) return omega - # # KLD 5/23/11: Two functions to create nice looking labels # + def get_pow1000(num): - """Determine the exponent for which the significand of a number is within the + """Determine exponent for which significand of a number is within the range [1, 1000). """ # Based on algorithm from http://www.mail-archive.com/matplotlib-users@lists.sourceforge.net/msg14433.html, accessed 2010/11/7 @@ -734,7 +808,8 @@ def gen_prefix(pow1000): # Prefixes according to Table 5 of [BIPM 2006] (excluding hecto, # deca, deci, and centi). if pow1000 < -8 or pow1000 > 8: - raise ValueError("Value is out of the range covered by the SI prefixes.") + raise ValueError( + "Value is out of the range covered by the SI prefixes.") return ['Y', # yotta (10^24) 'Z', # zetta (10^21) 'E', # exa (10^18) @@ -753,11 +828,12 @@ def gen_prefix(pow1000): 'z', # zepto (10^-21) 'y'][8 - pow1000] # yocto (10^-24) + def find_nearest_omega(omega_list, omega): omega_list = np.asarray(omega_list) - idx = (np.abs(omega_list - omega)).argmin() return omega_list[(np.abs(omega_list - omega)).argmin()] + # Function aliases bode = bode_plot nyquist = nyquist_plot diff --git a/control/grid.py b/control/grid.py index 33fc9e975..ed46ff0f7 100644 --- a/control/grid.py +++ b/control/grid.py @@ -175,7 +175,7 @@ def zgrid(zetas=None, wns=None): an_x = xret[an_i] an_y = yret[an_i] num = '{:1.1f}'.format(a) - ax.annotate("$\\frac{"+num+"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=9) + ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=9) _final_setup(ax) return ax, fig diff --git a/control/iosys.py b/control/iosys.py new file mode 100644 index 000000000..908f407b3 --- /dev/null +++ b/control/iosys.py @@ -0,0 +1,1771 @@ +# iosys.py - input/output system module +# +# RMM, 28 April 2019 +# +# Additional features to add +# * Improve support for signal names, specially in operator overloads +# - Figure out how to handle "nested" names (icsys.sys[1].x[1]) +# - Use this to implement signal names for operators? +# * Allow constant inputs for MIMO input_output_response (w/out ones) +# * Add support for constants/matrices as part of operators (1 + P) +# * Add unit tests (and example?) for time-varying systems +# * Allow time vector for discrete time simulations to be multiples of dt +# * Check the way initial outputs for discrete time systems are handled +# * Rename 'connections' as 'conlist' to match 'inplist' and 'outlist'? +# * Allow signal summation in InterconnectedSystem diagrams (via new output?) +# + +"""The :mod:`~control.iosys` module contains the +:class:`~control.InputOutputSystem` class that represents (possibly nonlinear) +input/output systems. The :class:`~control.InputOutputSystem` class is a +general class that defines any continuous or discrete time dynamical system. +Input/output systems can be simulated and also used to compute equilibrium +points and linearizations. + +""" + +__author__ = "Richard Murray" +__copyright__ = "Copyright 2019, California Institute of Technology" +__credits__ = ["Richard Murray"] +__license__ = "BSD" +__maintainer__ = "Richard Murray" +__email__ = "murray@cds.caltech.edu" + +import numpy as np +import scipy as sp +import copy +from warnings import warn + +from .statesp import StateSpace, tf2ss +from .timeresp import _check_convert_array +from .lti import isctime, isdtime, _find_timebase + +__all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', + 'InterconnectedSystem', 'input_output_response', 'find_eqpt', + 'linearize', 'ss2io', 'tf2io'] + + +class InputOutputSystem(object): + """A class for representing input/output systems. + + The InputOutputSystem class allows (possibly nonlinear) input/output + systems to be represented in Python. It is intended as a parent + class for a set of subclasses that are used to implement specific + structures and operations for different types of input/output + dynamical systems. + + Parameters + ---------- + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. If an + integer count is specified, the names of the signal will be of the + form `s[i]` (where `s` is one of `u`, `y`, or `x`). If this parameter + is not given or given as `None`, the relevant quantity will be + determined when possible based on other information provided to + functions using the system. + outputs : int, list of str, or None + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous time, True + indicates discrete time with undefined sampling time, positive number + is discrete time with specified sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. + name : string, optional + System name (used for specifying signals) + + Attributes + ---------- + ninputs, noutputs, nstates : int + Number of input, output and state variables + input_index, output_index, state_index : dict + Dictionary of signal names for the inputs, outputs and states and the + index of the corresponding array + dt : None, True or float + System timebase. None (default) indicates continuous time, True + indicates discrete time with undefined sampling time, positive number + is discrete time with specified sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. + name : string, optional + System name (used for specifying signals) + + Notes + ----- + The `InputOuputSystem` class (and its subclasses) makes use of two special + methods for implementing much of the work of the class: + + * _rhs(t, x, u): compute the right hand side of the differential or + difference equation for the system. This must be specified by the + subclass for the system. + + * _out(t, x, u): compute the output for the current state of the system. + The default is to return the entire system state. + + """ + def __init__(self, inputs=None, outputs=None, states=None, params={}, + dt=None, name=None): + """Create an input/output system. + + The InputOutputSystem contructor is used to create an input/output + object with the core information required for all input/output + systems. Instances of this class are normally created by one of the + input/output subclasses: :class:`~control.LinearIOSystem`, + :class:`~control.NonlinearIOSystem`, + :class:`~control.InterconnectedSystem`. + + Parameters + ---------- + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + this parameter is not given or given as `None`, the relevant + quantity will be determined when possible based on other + information provided to functions using the system. + outputs : int, list of str, or None + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + Returns + ------- + InputOutputSystem + Input/output system object + + """ + # Store the input arguments + self.params = params.copy() # default parameters + self.dt = dt # timebase + self.name = name # system name + + # Parse and store the number of inputs, outputs, and states + self.set_inputs(inputs) + self.set_outputs(outputs) + self.set_states(states) + + def __repr__(self): + return self.name if self.name is not None else str(type(self)) + + def __str__(self): + """String representation of an input/output system""" + str = "System: " + (self.name if self.name else "(None)") + "\n" + str += "Inputs (%s): " % self.ninputs + for key in self.input_index: str += key + ", " + str += "\nOutputs (%s): " % self.noutputs + for key in self.output_index: str += key + ", " + str += "\nStates (%s): " % self.nstates + for key in self.state_index: str += key + ", " + return str + + def __mul__(sys2, sys1): + """Multiply two input/output systems (series interconnection)""" + + if isinstance(sys1, (int, float, np.number)): + # TODO: Scale the output + raise NotImplemented("Scalar multiplication not yet implemented") + elif isinstance(sys1, np.ndarray): + # TODO: Post-multiply by a matrix + raise NotImplemented("Matrix multiplication not yet implemented") + elif isinstance(sys1, StateSpace) and isinstance(sys2, StateSpace): + # Special case: maintain linear systems structure + new_ss_sys = StateSpace.__mul__(sys2, sys1) + # TODO: set input and output names + new_io_sys = LinearIOSystem(new_ss_sys) + + return new_io_sys + elif not isinstance(sys1, InputOutputSystem): + raise ValueError("Unknown I/O system object ", sys1) + + # Make sure systems can be interconnected + if sys1.noutputs != sys2.ninputs: + raise ValueError("Can't multiply systems with incompatible " + "inputs and outputs") + + # Make sure timebase are compatible + dt = _find_timebase(sys1, sys2) + if dt is False: + raise ValueError("System timebases are not compabile") + + # Return the series interconnection between the systems + newsys = InterconnectedSystem((sys1, sys2)) + + # Set up the connecton map + newsys.set_connect_map(np.block( + [[np.zeros((sys1.ninputs, sys1.noutputs)), + np.zeros((sys1.ninputs, sys2.noutputs))], + [np.eye(sys2.ninputs, sys1.noutputs), + np.zeros((sys2.ninputs, sys2.noutputs))]] + )) + + # Set up the input map + newsys.set_input_map(np.concatenate( + (np.eye(sys1.ninputs), np.zeros((sys2.ninputs, sys1.ninputs))), + axis=0)) + # TODO: set up input names + + # Set up the output map + newsys.set_output_map(np.concatenate( + (np.zeros((sys2.noutputs, sys1.noutputs)), np.eye(sys2.noutputs)), + axis=1)) + # TODO: set up output names + + # Return the newly created system + return newsys + + def __rmul__(sys1, sys2): + """Pre-multiply an input/output systems by a scalar/matrix""" + if isinstance(sys2, (int, float, np.number)): + # TODO: Scale the output + raise NotImplemented("Scalar multiplication not yet implemented") + elif isinstance(sys2, np.ndarray): + # TODO: Post-multiply by a matrix + raise NotImplemented("Matrix multiplication not yet implemented") + elif isinstance(sys1, StateSpace) and isinstance(sys2, StateSpace): + # Special case: maintain linear systems structure + new_ss_sys = StateSpace.__rmul__(sys1, sys2) + # TODO: set input and output names + new_io_sys = LinearIOSystem(new_ss_sys) + + return new_io_sys + elif not isinstance(sys2, InputOutputSystem): + raise ValueError("Unknown I/O system object ", sys1) + else: + # Both systetms are InputOutputSystems => use __mul__ + return InputOutputSystem.__mul__(sys2, sys1) + + def __add__(sys1, sys2): + """Add two input/output systems (parallel interconnection)""" + # TODO: Allow addition of scalars and matrices + if not isinstance(sys2, InputOutputSystem): + raise ValueError("Unknown I/O system object ", sys2) + elif isinstance(sys1, StateSpace) and isinstance(sys2, StateSpace): + # Special case: maintain linear systems structure + new_ss_sys = StateSpace.__add__(sys1, sys2) + # TODO: set input and output names + new_io_sys = LinearIOSystem(new_ss_sys) + + return new_io_sys + + # Make sure number of input and outputs match + if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs: + raise ValueError("Can't add systems with different numbers of " + "inputs or outputs.") + ninputs = sys1.ninputs + noutputs = sys1.noutputs + + # Create a new system to handle the composition + newsys = InterconnectedSystem((sys1, sys2)) + + # Set up the input map + newsys.set_input_map(np.concatenate( + (np.eye(ninputs), np.eye(ninputs)), axis=0)) + # TODO: set up input names + + # Set up the output map + newsys.set_output_map(np.concatenate( + (np.eye(noutputs), np.eye(noutputs)), axis=1)) + # TODO: set up output names + + # Return the newly created system + return newsys + + # TODO: add __radd__ to allow postaddition by scalars and matrices + + def __neg__(sys): + """Negate an input/output systems (rescale)""" + if isinstance(sys, StateSpace): + # Special case: maintain linear systems structure + new_ss_sys = StateSpace.__neg__(sys) + # TODO: set input and output names + new_io_sys = LinearIOSystem(new_ss_sys) + + return new_io_sys + if sys.ninputs is None or sys.noutputs is None: + raise ValueError("Can't determine number of inputs or outputs") + + # Create a new system to hold the negation + newsys = InterconnectedSystem((sys,), dt=sys.dt) + + # Set up the input map (identity) + newsys.set_input_map(np.eye(sys.ninputs)) + # TODO: set up input names + + # Set up the output map (negate the output) + newsys.set_output_map(-np.eye(sys.noutputs)) + # TODO: set up output names + + # Return the newly created system + return newsys + + # Utility function to parse a list of signals + def _process_signal_list(self, signals, prefix='s'): + if signals is None: + # No information provided; try and make it up later + return None, {} + + elif isinstance(signals, int): + # Number of signals given; make up the names + return signals, {'%s[%d]' % (prefix, i): i for i in range(signals)} + + elif isinstance(signals, str): + # Single string given => single signal with given name + return 1, {signals: 0} + + elif all(isinstance(s, str) for s in signals): + # Use the list of strings as the signal names + return len(signals), {signals[i]: i for i in range(len(signals))} + + else: + raise TypeError("Can't parse signal list %s" % str(signals)) + + # Find a signal by name + def _find_signal(self, name, sigdict): return sigdict.get(name, None) + + # Update parameters used for _rhs, _out (used by subclasses) + def _update_params(self, params, warning=False): + if (warning): + warn("Parameters passed to InputOutputSystem ignored.") + + def _rhs(self, t, x, u): + """Evaluate right hand side of a differential or difference equation. + + Private function used to compute the right hand side of an + input/output system model. + + """ + NotImplemented("Evaluation not implemented for system of type ", + type(self)) + + def _out(self, t, x, u, params={}): + """Evaluate the output of a system at a given state, input, and time + + Private function used to compute the output of of an input/output + system model given the state, input, parameters, and time. + + """ + # If no output function was defined in subclass, return state + return x + + def set_inputs(self, inputs, prefix='u'): + """Set the number/names of the system inputs. + + Parameters + ---------- + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `inputs` is an integer, create the names of the states using + the given prefix (default = 'u'). The names of the input will be + of the form `prefix[i]`. + + """ + self.ninputs, self.input_index = \ + self._process_signal_list(inputs, prefix=prefix) + + def set_outputs(self, outputs, prefix='y'): + """Set the number/names of the system outputs. + + Parameters + ---------- + outputs : int, list of str, or None + Description of the system outputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `outputs` is an integer, create the names of the states using + the given prefix (default = 'y'). The names of the input will be + of the form `prefix[i]`. + + """ + self.noutputs, self.output_index = \ + self._process_signal_list(outputs, prefix=prefix) + + def set_states(self, states, prefix='x'): + """Set the number/names of the system states. + + Parameters + ---------- + states : int, list of str, or None + Description of the system states. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `states` is an integer, create the names of the states using + the given prefix (default = 'x'). The names of the input will be + of the form `prefix[i]`. + + """ + self.nstates, self.state_index = \ + self._process_signal_list(states, prefix=prefix) + + def find_input(self, name): + """Find the index for an input given its name (`None` if not found)""" + return self.input_index.get(name, None) + + def find_output(self, name): + """Find the index for an output given its name (`None` if not found)""" + return self.output_index.get(name, None) + + def find_state(self, name): + """Find the index for a state given its name (`None` if not found)""" + return self.state_index.get(name, None) + + def feedback(self, other=1, sign=-1, params={}): + """Feedback interconnection between two input/output systems + + Parameters + ---------- + sys1: InputOutputSystem + The primary process. + sys2: InputOutputSystem + The feedback process (often a feedback controller). + sign: scalar, optional + The sign of feedback. `sign` = -1 indicates negative feedback, + and `sign` = 1 indicates positive feedback. `sign` is an optional + argument; it assumes a value of -1 if not specified. + + Returns + ------- + out: InputOutputSystem + + Raises + ------ + ValueError + if the inputs, outputs, or timebases of the systems are + incompatible. + + """ + # TODO: add conversion to I/O system when needed + if not isinstance(other, InputOutputSystem): + raise TypeError("Feedback around I/O system must be I/O system.") + elif isinstance(self, StateSpace) and isinstance(other, StateSpace): + # Special case: maintain linear systems structure + new_ss_sys = StateSpace.feedback(self, other, sign=sign) + # TODO: set input and output names + new_io_sys = LinearIOSystem(new_ss_sys) + + return new_io_sys + + # Make sure systems can be interconnected + if self.noutputs != other.ninputs or other.noutputs != self.ninputs: + raise ValueError("Can't connect systems with incompatible " + "inputs and outputs") + + # Make sure timebases are compatible + dt = _find_timebase(self, other) + if dt is False: + raise ValueError("System timebases are not compabile") + + # Return the series interconnection between the systems + newsys = InterconnectedSystem((self, other), params=params, dt=dt) + + # Set up the connecton map + newsys.set_connect_map(np.block( + [[np.zeros((self.ninputs, self.noutputs)), + sign * np.eye(self.ninputs, other.noutputs)], + [np.eye(other.ninputs, self.noutputs), + np.zeros((other.ninputs, other.noutputs))]] + )) + + # Set up the input map + newsys.set_input_map(np.concatenate( + (np.eye(self.ninputs), np.zeros((other.ninputs, self.ninputs))), + axis=0)) + # TODO: set up input names + + # Set up the output map + newsys.set_output_map(np.concatenate( + (np.eye(self.noutputs), np.zeros((self.noutputs, other.noutputs))), + axis=1)) + # TODO: set up output names + + # Return the newly created system + return newsys + + def linearize(self, x0, u0, t=0, params={}, eps=1e-6): + """Linearize an input/output system at a given state and input. + + Return the linearization of an input/output system at a given state + and input value as a StateSpace system. See + :func:`~control.linearize` for complete documentation. + + """ + # + # If the linearization is not defined by the subclass, perform a + # numerical linearization use the `_rhs()` and `_out()` member + # functions. + # + + # Figure out dimensions if they were not specified. + nstates = _find_size(self.nstates, x0) + ninputs = _find_size(self.ninputs, u0) + + # Convert x0, u0 to arrays, if needed + if np.isscalar(x0): x0 = np.ones((nstates,)) * x0 + if np.isscalar(u0): u0 = np.ones((ninputs,)) * u0 + + # Compute number of outputs by evaluating the output function + noutputs = _find_size(self.noutputs, self._out(t, x0, u0)) + + # Update the current parameters + self._update_params(params) + + # Compute the nominal value of the update law and output + F0 = self._rhs(t, x0, u0) + H0 = self._out(t, x0, u0) + + # Create empty matrices that we can fill up with linearizations + A = np.zeros((nstates, nstates)) # Dynamics matrix + B = np.zeros((nstates, ninputs)) # Input matrix + C = np.zeros((noutputs, nstates)) # Output matrix + D = np.zeros((noutputs, ninputs)) # Direct term + + # Perturb each of the state variables and compute linearization + for i in range(nstates): + dx = np.zeros((nstates,)) + dx[i] = eps + A[:, i] = (self._rhs(t, x0 + dx, u0) - F0) / eps + C[:, i] = (self._out(t, x0 + dx, u0) - H0) / eps + + # Perturb each of the input variables and compute linearization + for i in range(ninputs): + du = np.zeros((ninputs,)) + du[i] = eps + B[:, i] = (self._rhs(t, x0, u0 + du) - F0) / eps + D[:, i] = (self._out(t, x0, u0 + du) - H0) / eps + + # Create the state space system + linsys = StateSpace(A, B, C, D, self.dt, remove_useless=False) + return LinearIOSystem(linsys) + + def copy(self): + """Make a copy of an input/output system.""" + return copy.copy(self) + + +class LinearIOSystem(InputOutputSystem, StateSpace): + """Input/output representation of a linear (state space) system. + + This class is used to implementat a system that is a linear state + space system (defined by the StateSpace system object). + + """ + def __init__(self, linsys, inputs=None, outputs=None, states=None, + name=None): + """Create an I/O system from a state space linear system. + + Converts a :class:`~control.StateSpace` system into an + :class:`~control.InputOutputSystem` with the same inputs, outputs, and + states. The new system can be a continuous or discrete time system + + Parameters + ---------- + linsys : StateSpace + LTI StateSpace system to be converted + inputs : int, list of str or None, optional + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + this parameter is not given or given as `None`, the relevant + quantity will be determined when possible based on other + information provided to functions using the system. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + Returns + ------- + iosys : LinearIOSystem + Linear system represented as an input/output system + + """ + if not isinstance(linsys, StateSpace): + raise TypeError("Linear I/O system must be a state space object") + + # Create the I/O system object + super(LinearIOSystem, self).__init__( + inputs=linsys.inputs, outputs=linsys.outputs, + states=linsys.states, params={}, dt=linsys.dt, name=name) + + # Initalize additional state space variables + StateSpace.__init__(self, linsys, remove_useless=False) + + # Process input, output, state lists, if given + # Make sure they match the size of the linear system + ninputs, self.input_index = self._process_signal_list( + inputs if inputs is not None else linsys.inputs, prefix='u') + if ninputs is not None and linsys.inputs != ninputs: + raise ValueError("Wrong number/type of inputs given.") + noutputs, self.output_index = self._process_signal_list( + outputs if outputs is not None else linsys.outputs, prefix='y') + if noutputs is not None and linsys.outputs != noutputs: + raise ValueError("Wrong number/type of outputs given.") + nstates, self.state_index = self._process_signal_list( + states if states is not None else linsys.states, prefix='x') + if nstates is not None and linsys.states != nstates: + raise ValueError("Wrong number/type of states given.") + + def _update_params(self, params={}, warning=True): + # Parameters not supported; issue a warning + if params and warning: + warn("Parameters passed to LinearIOSystems are ignored.") + + def _rhs(self, t, x, u): + # Convert input to column vector and then change output to 1D array + xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \ + + np.dot(self.B, np.reshape(u, (-1, 1))) + return np.array(xdot).reshape((-1,)) + + def _out(self, t, x, u): + y = self.C * np.reshape(x, (-1, 1)) + self.D * np.reshape(u, (-1, 1)) + return np.array(y).reshape((self.noutputs,)) + + +class NonlinearIOSystem(InputOutputSystem): + """Nonlinear I/O system. + + This class is used to implement a system that is a nonlinear state + space system (defined by and update function and an output function). + + """ + def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, + states=None, params={}, dt=None, name=None): + """Create a nonlinear I/O system given update and output functions. + + Creates an `InputOutputSystem` for a nonlinear system by specifying a + state update function and an output function. The new system can be a + continuous or discrete time system (Note: discrete-time systems not + yet supported by most function.) + + Parameters + ---------- + updfcn : callable + Function returning the state update function + + `updfcn(t, x, u[, param]) -> array` + + where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array + with shape (ninputs,), `t` is a float representing the currrent + time, and `param` is an optional dict containing the values of + parameters used by the function. + + outfcn : callable + Function returning the output at the given state + + `outfcn(t, x, u[, param]) -> array` + + where the arguments are the same as for `upfcn`. + + inputs : int, list of str or None, optional + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + this parameter is not given or given as `None`, the relevant + quantity will be determined when possible based on other + information provided to functions using the system. + + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + + dt : timebase, optional + The timebase for the system, used to specify whether the system is + operating in continuous or discrete time. It can have the + following values: + + * dt = None No timebase specified + * dt = 0 Continuous time system + * dt > 0 Discrete time system with sampling time dt + * dt = True Discrete time with unspecified sampling time + + name : string, optional + System name (used for specifying signals). + + Returns + ------- + iosys : NonlinearIOSystem + Nonlinear system represented as an input/output system. + + """ + # Store the update and output functions + self.updfcn = updfcn + self.outfcn = outfcn + + # Initialize the rest of the structure + super(NonlinearIOSystem, self).__init__( + inputs=inputs, outputs=outputs, states=states, + params=params, dt=dt, name=name + ) + + # Check to make sure arguments are consistent + if updfcn is None: + if self.nstates is None: + self.nstates = 0 + else: + raise ValueError("States specified but no update function " + "given.") + if outfcn is None: + # No output function specified => outputs = states + if self.noutputs is None and self.nstates is not None: + self.noutputs = self.nstates + elif self.noutputs is not None and self.noutputs == self.nstates: + # Number of outputs = number of states => all is OK + pass + elif self.noutputs is not None and self.noutputs != 0: + raise ValueError("Outputs specified but no output function " + "(and nstates not known).") + + # Initialize current parameters to default parameters + self._current_params = params.copy() + + def _update_params(self, params, warning=False): + # Update the current parameter values + self._current_params = self.params.copy() + self._current_params.update(params) + + def _rhs(self, t, x, u): + xdot = self.updfcn(t, x, u, self._current_params) \ + if self.updfcn is not None else [] + return np.array(xdot).reshape((-1,)) + + def _out(self, t, x, u): + y = self.outfcn(t, x, u, self._current_params) \ + if self.outfcn is not None else x + return np.array(y).reshape((-1,)) + + +class InterconnectedSystem(InputOutputSystem): + """Interconnection of a set of input/output systems. + + This class is used to implement a system that is an interconnection of + input/output systems. The sys consists of a collection of subsystems + whose inputs and outputs are connected via a connection map. The overall + system inputs and outputs are subsets of the subsystem inputs and outputs. + + """ + def __init__(self, syslist, connections=[], inplist=[], outlist=[], + inputs=None, outputs=None, states=None, + params={}, dt=None, name=None): + """Create an I/O system from a list of systems + connection info. + + The InterconnectedSystem class is used to represent an input/output + system that consists of an interconnection between a set of subystems. + The outputs of each subsystem can be summed together to to provide + inputs to other subsystems. The overall system inputs and outputs can + be any subset of subsystem inputs and outputs. + + Parameters + ---------- + syslist : array_like of InputOutputSystems + The list of input/output systems to be connected + + connections : tuple of connection specifications, optional + Description of the internal connections between the subsystems. + Each element of the tuple describes an input to one of the + subsystems. The entries are are of the form: + + (input-spec, output-spec1, output-spec2, ...) + + The input-spec should be a tuple of the form `(subsys_i, inp_j)` + where `subsys_i` is the index into `syslist` and `inp_j` is the + index into the input vector for the subsystem. If `subsys_i` has + a single input, then the subsystem index `subsys_i` can be listed + as the input-spec. If systems and signals are given names, then + the form 'sys.sig' or ('sys', 'sig') are also recognized. + + Each output-spec should be a tuple of the form `(subsys_i, out_j, + gain)`. The input will be constructed by summing the listed + outputs after multiplying by the gain term. If the gain term is + omitted, it is assumed to be 1. If the system has a single + output, then the subsystem index `subsys_i` can be listed as the + input-spec. If systems and signals are given names, then the form + 'sys.sig', ('sys', 'sig') or ('sys', 'sig', gain) are also + recognized, and the special form '-sys.sig' can be used to specify + a signal with gain -1. + + If omitted, the connection map (matrix) can be specified using the + :func:`~control.InterconnectedSystem.set_connect_map` method. + + inplist : tuple of input specifications, optional + List of specifications for how the inputs for the overall system + are mapped to the subsystem inputs. The input specification is + the same as the form defined in the connection specification. + Each system input is added to the input for the listed subsystem. + + If omitted, the input map can be specified using the + `set_input_map` method. + + outlist : tuple of output specifications, optional + List of specifications for how the outputs for the subsystems are + mapped to overall system outputs. The output specification is the + same as the form defined in the connection specification + (including the optional gain term). Numbered outputs must be + chosen from the list of subsystem outputs, but named outputs can + also be contained in the list of subsystem inputs. + + If omitted, the output map can be specified using the + `set_output_map` method. + + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + + dt : timebase, optional + The timebase for the system, used to specify whether the system is + operating in continuous or discrete time. It can have the + following values: + + * dt = None No timebase specified + * dt = 0 Continuous time system + * dt > 0 Discrete time system with sampling time dt + * dt = True Discrete time with unspecified sampling time + + name : string, optional + System name (used for specifying signals). + + """ + # Convert input and output names to lists if they aren't already + if not isinstance(inplist, (list, tuple)): inplist = [inplist] + if not isinstance(outlist, (list, tuple)): outlist = [outlist] + + # Check to make sure all systems are consistent + self.syslist = syslist + self.syslist_index = {} + dt = None + nstates = 0; self.state_offset = [] + ninputs = 0; self.input_offset = [] + noutputs = 0; self.output_offset = [] + system_count = 0 + for sys in syslist: + # Make sure time bases are consistent + # TODO: Use lti._find_timebase() instead? + if dt is None and sys.dt is not None: + # Timebase was not specified; set to match this system + dt = sys.dt + elif dt != sys.dt: + raise TypeError("System timebases are not compatible") + + # Make sure number of inputs, outputs, states is given + if sys.ninputs is None or sys.noutputs is None or \ + sys.nstates is None: + raise TypeError("System '%s' must define number of inputs, " + "outputs, states in order to be connected" % + sys.name) + + # Keep track of the offsets into the states, inputs, outputs + self.input_offset.append(ninputs) + self.output_offset.append(noutputs) + self.state_offset.append(nstates) + + # Keep track of the total number of states, inputs, outputs + nstates += sys.nstates + ninputs += sys.ninputs + noutputs += sys.noutputs + + # Store the index to the system for later retrieval + # TODO: look for duplicated system names + self.syslist_index[sys.name] = system_count + system_count += 1 + + # Check for duplicate systems or duplicate names + sysobj_list = [] + sysname_list = [] + for sys in syslist: + if sys in sysobj_list: + warn("Duplicate object found in system list: %s" % str(sys)) + elif sys.name is not None and sys.name in sysname_list: + warn("Duplicate name found in system list: %s" % sys.name) + sysobj_list.append(sys) + sysname_list.append(sys.name) + + # Create the I/O system + super(InterconnectedSystem, self).__init__( + inputs=len(inplist), outputs=len(outlist), + states=nstates, params=params, dt=dt) + + # If input or output list was specified, update it + nsignals, self.input_index = \ + self._process_signal_list(inputs, prefix='u') + if nsignals is not None and len(inplist) != nsignals: + raise ValueError("Wrong number/type of inputs given.") + nsignals, self.output_index = \ + self._process_signal_list(outputs, prefix='y') + if nsignals is not None and len(outlist) != nsignals: + raise ValueError("Wrong number/type of outputs given.") + + # Convert the list of interconnections to a connection map (matrix) + self.connect_map = np.zeros((ninputs, noutputs)) + for connection in connections: + input_index = self._parse_input_spec(connection[0]) + for output_spec in connection[1:]: + output_index, gain = self._parse_output_spec(output_spec) + self.connect_map[input_index, output_index] = gain + + # Convert the input list to a matrix: maps system to subsystems + self.input_map = np.zeros((ninputs, self.ninputs)) + for index, inpspec in enumerate(inplist): + if isinstance(inpspec, (int, str, tuple)): inpspec = [inpspec] + for spec in inpspec: + self.input_map[self._parse_input_spec(spec), index] = 1 + + # Convert the output list to a matrix: maps subsystems to system + self.output_map = np.zeros((self.noutputs, noutputs + ninputs)) + for index in range(len(outlist)): + ylist_index, gain = self._parse_output_spec(outlist[index]) + self.output_map[index, ylist_index] = gain + + # Save the parameters for the system + self.params = params.copy() + + def __add__(self, sys): + # TODO: implement special processing to maintain flat structure + return super(InterconnectedSystem, self).__add__(sys) + + def __radd__(self, sys): + # TODO: implement special processing to maintain flat structure + return super(InterconnectedSystem, self).__radd__(sys) + + def __mul__(self, sys): + # TODO: implement special processing to maintain flat structure + return super(InterconnectedSystem, self).__mul__(sys) + + def __rmul__(self, sys): + # TODO: implement special processing to maintain flat structure + return super(InterconnectedSystem, self).__rmul__(sys) + + def __neg__(self): + # TODO: implement special processing to maintain flat structure + return super(InterconnectedSystem, self).__neg__() + + def _update_params(self, params, warning=False): + for sys in self.syslist: + local = sys.params.copy() # start with system parameters + local.update(self.params) # update with global params + local.update(params) # update with locally passed parameters + sys._update_params(local, warning=warning) + + def _rhs(self, t, x, u): + # Make sure state and input are vectors + x = np.array(x, ndmin=1) + u = np.array(u, ndmin=1) + + # Compute the input and output vectors + ulist, ylist = self._compute_static_io(t, x, u) + + # Go through each system and update the right hand side for that system + xdot = np.zeros((self.nstates,)) # Array to hold results + state_index = 0; input_index = 0 # Start at the beginning + for sys in self.syslist: + # Update the right hand side for this subsystem + if sys.nstates != 0: + xdot[state_index:state_index + sys.nstates] = sys._rhs( + t, x[state_index:state_index + sys.nstates], + ulist[input_index:input_index + sys.ninputs]) + + # Update the state and input index counters + state_index += sys.nstates + input_index += sys.ninputs + + return xdot + + def _out(self, t, x, u): + # Make sure state and input are vectors + x = np.array(x, ndmin=1) + u = np.array(u, ndmin=1) + + # Compute the input and output vectors + ulist, ylist = self._compute_static_io(t, x, u) + + # Make the full set of subsystem outputs to system output + return np.dot(self.output_map, ylist) + + def _compute_static_io(self, t, x, u): + # Figure out the total number of inputs and outputs + (ninputs, noutputs) = self.connect_map.shape + + # + # Get the outputs and inputs at the current system state + # + + # Initialize the lists used to keep track of internal signals + ulist = np.dot(self.input_map, u) + ylist = np.zeros((noutputs + ninputs,)) + + # To allow for feedthrough terms, iterate multiple times to allow + # feedthrough elements to propagate. For n systems, we could need to + # cycle through n+1 times before reaching steady state + # TODO (later): see if there is a more efficient way to compute + cycle_count = len(self.syslist) + 1 + while cycle_count > 0: + state_index = 0; input_index = 0; output_index = 0 + for sys in self.syslist: + # Compute outputs for each system from current state + ysys = sys._out( + t, x[state_index:state_index + sys.nstates], + ulist[input_index:input_index + sys.ninputs]) + + # Store the outputs at the start of ylist + ylist[output_index:output_index + sys.noutputs] = \ + ysys.reshape((-1,)) + + # Store the input in the second part of ylist + ylist[noutputs + input_index: + noutputs + input_index + sys.ninputs] = \ + ulist[input_index:input_index + sys.ninputs] + + # Increment the index pointers + state_index += sys.nstates + input_index += sys.ninputs + output_index += sys.noutputs + + # Compute inputs based on connection map + new_ulist = np.dot(self.connect_map, ylist[:noutputs]) \ + + np.dot(self.input_map, u) + + # Check to see if any of the inputs changed + if (ulist == new_ulist).all(): + break + else: + ulist = new_ulist + + # Decrease the cycle counter + cycle_count -= 1 + + # Make sure that we stopped before detecting an algebraic loop + if cycle_count == 0: + raise RuntimeError("Algebraic loop detected.") + + return ulist, ylist + + def _parse_input_spec(self, spec): + """Parse an input specification and returns the index + + This function parses a specification of an input of an interconnected + system component and returns the index of that input in the internal + input vector. Input specifications are of one of the following forms: + + i first input for the ith system + (i,) first input for the ith system + (i, j) jth input for the ith system + 'sys.sig' signal 'sig' in subsys 'sys' + ('sys', 'sig') signal 'sig' in subsys 'sys' + + The function returns an index into the input vector array and + the gain to use for that input. + + """ + # Parse the signal that we received + subsys_index, input_index = self._parse_signal(spec, 'input') + + # Return the index into the input vector list (ylist) + return self.input_offset[subsys_index] + input_index + + def _parse_output_spec(self, spec): + """Parse an output specification and returns the index and gain + + This function parses a specification of an output of an + interconnected system component and returns the index of that + output in the internal output vector (ylist). Output specifications + are of one of the following forms: + + i first output for the ith system + (i,) first output for the ith system + (i, j) jth output for the ith system + (i, j, gain) jth output for the ith system with gain + 'sys.sig' signal 'sig' in subsys 'sys' + '-sys.sig' signal 'sig' in subsys 'sys' with gain -1 + ('sys', 'sig', gain) signal 'sig' in subsys 'sys' with gain + + If the gain is not specified, it is taken to be 1. Numbered outputs + must be chosen from the list of subsystem outputs, but named outputs + can also be contained in the list of subsystem inputs. + + The function returns an index into the output vector array and + the gain to use for that output. + + """ + gain = 1 # Default gain + + # Check for special forms of the input + if isinstance(spec, tuple) and len(spec) == 3: + gain = spec[2] + spec = spec[:2] + elif isinstance(spec, str) and spec[0] == '-': + gain = -1 + spec = spec[1:] + + # Parse the rest of the spec with standard signal parsing routine + try: + # Start by looking in the set of subsystem outputs + subsys_index, output_index = self._parse_signal(spec, 'output') + + # Return the index into the input vector list (ylist) + return self.output_offset[subsys_index] + output_index, gain + + except ValueError: + # Try looking in the set of subsystem *inputs* + subsys_index, input_index = self._parse_signal( + spec, 'input or output', dictname='input_index') + + # Return the index into the input vector list (ylist) + noutputs = sum(sys.noutputs for sys in self.syslist) + return noutputs + \ + self.input_offset[subsys_index] + input_index, gain + + def _parse_signal(self, spec, signame='input', dictname=None): + """Parse a signal specification, returning system and signal index. + + Signal specifications are of one of the following forms: + + i system_index = i, signal_index = 0 + (i,) system_index = i, signal_index = 0 + (i, j) system_index = i, signal_index = j + 'sys.sig' signal 'sig' in subsys 'sys' + ('sys', 'sig') signal 'sig' in subsys 'sys' + ('sys', j) signal_index j in subsys 'sys' + + The function returns an index into the input vector array and + the gain to use for that input. + """ + import re + + # Process cases where we are given indices as integers + if isinstance(spec, int): + return spec, 0 + + elif isinstance(spec, tuple) and len(spec) == 1 \ + and isinstance(spec[0], int): + return spec[0], 0 + + elif isinstance(spec, tuple) and len(spec) == 2 \ + and all([isinstance(index, int) for index in spec]): + return spec + + # Figure out the name of the dictionary to use + if dictname is None: dictname = signame + '_index' + + if isinstance(spec, str): + # If we got a dotted string, break up into pieces + namelist = re.split(r'\.', spec) + + # For now, only allow signal level of system name + # TODO: expand to allow nested signal names + if len(namelist) != 2: + raise ValueError("Couldn't parse %s signal reference '%s'." + % (signame, spec)) + + system_index = self._find_system(namelist[0]) + if system_index is None: + raise ValueError("Couldn't find system '%s'." % namelist[0]) + + signal_index = self.syslist[system_index]._find_signal( + namelist[1], getattr(self.syslist[system_index], dictname)) + if signal_index is None: + raise ValueError("Couldn't find %s signal '%s.%s'." % + (signame, namelist[0], namelist[1])) + + return system_index, signal_index + + # Handle the ('sys', 'sig'), (i, j), and mixed cases + elif isinstance(spec, tuple) and len(spec) == 2 and \ + isinstance(spec[0], (str, int)) and \ + isinstance(spec[1], (str, int)): + if isinstance(spec[0], int): + system_index = spec[0] + if system_index < 0 or system_index > len(self.syslist): + system_index = None + else: + system_index = self._find_system(spec[0]) + if system_index is None: + raise ValueError("Couldn't find system %s." % spec[0]) + + if isinstance(spec[1], int): + signal_index = spec[1] + # TODO (later): check against max length of appropriate list? + if signal_index < 0: + system_index = None + else: + signal_index = self.syslist[system_index]._find_signal( + spec[1], getattr(self.syslist[system_index], dictname)) + if signal_index is None: + raise ValueError("Couldn't find signal %s.%s." % tuple(spec)) + + return system_index, signal_index + + else: + raise ValueError("Couldn't parse signal reference %s." % str(spec)) + + def _find_system(self, name): + return self.syslist_index.get(name, None) + + def set_connect_map(self, connect_map): + """Set the connection map for an interconnected I/O system. + + Parameters + ---------- + connect_map : 2D array + Specify the matrix that will be used to multiply the vector of + subsystem outputs to obtain the vector of subsystem inputs. + + """ + # Make sure the connection map is the right size + if connect_map.shape != self.connect_map.shape: + ValueError("Connection map is not the right shape") + self.connect_map = connect_map + + def set_input_map(self, input_map): + """Set the input map for an interconnected I/O system. + + Parameters + ---------- + input_map : 2D array + Specify the matrix that will be used to multiply the vector of + system inputs to obtain the vector of subsystem inputs. These + values are added to the inputs specified in the connection map. + + """ + # Figure out the number of internal inputs + ninputs = sum(sys.ninputs for sys in self.syslist) + + # Make sure the input map is the right size + if input_map.shape[0] != ninputs: + ValueError("Input map is not the right shape") + self.input_map = input_map + self.ninputs = input_map.shape[1] + + def set_output_map(self, output_map): + """Set the output map for an interconnected I/O system. + + Parameters + ---------- + output_map : 2D array + Specify the matrix that will be used to multiply the vector of + subsystem outputs to obtain the vector of system outputs. + """ + # Figure out the number of internal inputs and outputs + ninputs = sum(sys.ninputs for sys in self.syslist) + noutputs = sum(sys.noutputs for sys in self.syslist) + + # Make sure the output map is the right size + if output_map.shape[1] == noutputs: + # For backward compatibility, add zeros to the end of the array + output_map = np.concatenate( + (output_map, + np.zeros((output_map.shape[0], ninputs))), + axis=1) + + if output_map.shape[1] != noutputs + ninputs: + ValueError("Output map is not the right shape") + self.output_map = output_map + self.noutputs = output_map.shape[0] + + +def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45', + return_x=False, squeeze=True): + + """Compute the output response of a system to a given input. + + Simulate a dynamical system with a given input and return its output + and state values. + + Parameters + ---------- + sys: InputOutputSystem + Input/output system to simulate. + T: array-like + Time steps at which the input is defined; values must be evenly spaced. + U: array-like or number, optional + Input array giving input at each time `T` (default = 0). + X0: array-like or number, optional + Initial condition (default = 0). + return_x : bool, optional + If True, return the values of the state at each time (default = False). + squeeze : bool, optional + If True (default), squeeze unused dimensions out of the output + response. In particular, for a single output system, return a + vector of shape (nsteps) instead of (nsteps, 1). + + Returns + ------- + T : array + Time values of the output. + yout : array + Response of the system. + xout : array + Time evolution of the state vector (if return_x=True) + + Raises + ------ + TypeError + If the system is not an input/output system. + ValueError + If time step does not match sampling time (for discrete time systems) + + """ + # Sanity checking on the input + if not isinstance(sys, InputOutputSystem): + raise TypeError("System of type ", type(sys), " not valid") + + # Compute the time interval and number of steps + T0, Tf = T[0], T[-1] + n_steps = len(T) + + # Check and convert the input, if needed + # TODO: improve MIMO ninputs check (choose from U) + if sys.ninputs is None or sys.ninputs == 1: + legal_shapes = [(n_steps,), (1, n_steps)] + else: + legal_shapes = [(sys.ninputs, n_steps)] + U = _check_convert_array(U, legal_shapes, + 'Parameter ``U``: ', squeeze=False) + + # Check to make sure this is not a static function + nstates = _find_size(sys.nstates, X0) + if nstates == 0: + # No states => map input to output + u = U[0] if len(U.shape) == 1 else U[:, 0] + y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T))) + for i in range(len(T)): + u = U[i] if len(U.shape) == 1 else U[:, i] + y[:, i] = sys._out(T[i], [], u) + if (squeeze): y = np.squeeze(y) + if return_x: + return T, y, [] + else: + return T, y + + # create X0 if not given, test if X0 has correct shape + X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)], + 'Parameter ``X0``: ', squeeze=True) + + # Update the parameter values + sys._update_params(params) + + # Create a lambda function for the right hand side + u = sp.interpolate.interp1d(T, U, fill_value="extrapolate") + def ivp_rhs(t, x): return sys._rhs(t, x, u(t)) + + # Perform the simulation + if isctime(sys): + if not hasattr(sp.integrate, 'solve_ivp'): + raise NameError("scipy.integrate.solve_ivp not found; " + "use SciPy 1.0 or greater") + soln = sp.integrate.solve_ivp(ivp_rhs, (T0, Tf), X0, t_eval=T, + method=method, vectorized=False) + + # Compute the output associated with the state (and use sys.out to + # figure out the number of outputs just in case it wasn't specified) + u = U[0] if len(U.shape) == 1 else U[:, 0] + y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T))) + for i in range(len(T)): + u = U[i] if len(U.shape) == 1 else U[:, i] + y[:, i] = sys._out(T[i], soln.y[:, i], u) + + elif isdtime(sys): + # Make sure the time vector is uniformly spaced + dt = T[1] - T[0] + if not np.allclose(T[1:] - T[:-1], dt): + raise ValueError("Parameter ``T``: time values must be " + "equally spaced.") + + # Make sure the sample time matches the given time + if (sys.dt is not True): + # Make sure that the time increment is a multiple of sampling time + + # TODO: add back functionality for undersampling + # TODO: this test is brittle if dt = sys.dt + # First make sure that time increment is bigger than sampling time + # if dt < sys.dt: + # raise ValueError("Time steps ``T`` must match sampling time") + + # Check to make sure sampling time matches time increments + if not np.isclose(dt, sys.dt): + raise ValueError("Time steps ``T`` must be equal to " + "sampling time") + + # Compute the solution + soln = sp.optimize.OptimizeResult() + soln.t = T # Store the time vector directly + x = [float(x0) for x0 in X0] # State vector (store as floats) + soln.y = [] # Solution, following scipy convention + y = [] # System output + for i in range(len(T)): + # Store the current state and output + soln.y.append(x) + y.append(sys._out(T[i], x, u(T[i]))) + + # Update the state for the next iteration + x = sys._rhs(T[i], x, u(T[i])) + + # Convert output to numpy arrays + soln.y = np.transpose(np.array(soln.y)) + y = np.transpose(np.array(y)) + + # Mark solution as successful + soln.success = True # No way to fail + + else: # Neither ctime or dtime?? + raise TypeError("Can't determine system type") + + # Get rid of extra dimensions in the output, of desired + if (squeeze): y = np.squeeze(y) + + if return_x: + return soln.t, y, soln.y + else: + return soln.t, y + + +def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={}, + iu=None, iy=None, ix=None, idx=None, dx0=None, + return_y=False, return_result=False, **kw): + """Find the equilibrium point for an input/output system. + + Returns the value of an equlibrium point given the initial state and + either input value or desired output value for the equilibrium point. + + Parameters + ---------- + x0 : list of initial state values + Initial guess for the value of the state near the equilibrium point. + u0 : list of input values, optional + If `y0` is not specified, sets the equilibrium value of the input. If + `y0` is given, provides an initial guess for the value of the input. + Can be omitted if the system does not have any inputs. + y0 : list of output values, optional + If specified, sets the desired values of the outputs at the + equilibrium point. + t : float, optional + Evaluation time, for time-varying systems + params : dict, optional + Parameter values for the system. Passed to the evaluation functions + for the system as default values, overriding internal defaults. + iu : list of input indices, optional + If specified, only the inputs with the given indices will be fixed at + the specified values in solving for an equilibrium point. All other + inputs will be varied. Input indices can be listed in any order. + iy : list of output indices, optional + If specified, only the outputs with the given indices will be fixed at + the specified values in solving for an equilibrium point. All other + outputs will be varied. Output indices can be listed in any order. + ix : list of state indices, optional + If specified, states with the given indices will be fixed at the + specified values in solving for an equilibrium point. All other + states will be varied. State indices can be listed in any order. + dx0 : list of update values, optional + If specified, the value of update map must match the listed value + instead of the default value of 0. + idx : list of state indices, optional + If specified, state updates with the given indices will have their + update maps fixed at the values given in `dx0`. All other update + values will be ignored in solving for an equilibrium point. State + indices can be listed in any order. By default, all updates will be + fixed at `dx0` in searching for an equilibrium point. + return_y : bool, optional + If True, return the value of output at the equilibrium point. + return_result : bool, optional + If True, return the `result` option from the scipy root function used + to compute the equilibrium point. + + Returns + ------- + xeq : array of states + Value of the states at the equilibrium point, or `None` if no + equilibrium point was found and `return_result` was False. + ueq : array of input values + Value of the inputs at the equilibrium point, or `None` if no + equilibrium point was found and `return_result` was False. + yeq : array of output values, optional + If `return_y` is True, returns the value of the outputs at the + equilibrium point, or `None` if no equilibrium point was found and + `return_result` was False. + result : scipy root() result object, optional + If `return_result` is True, returns the `result` from the scipy root + function. + + """ + from scipy.optimize import root + + # Figure out the number of states, inputs, and outputs + nstates = _find_size(sys.nstates, x0) + ninputs = _find_size(sys.ninputs, u0) + noutputs = _find_size(sys.noutputs, y0) + + # Convert x0, u0, y0 to arrays, if needed + if np.isscalar(x0): x0 = np.ones((nstates,)) * x0 + if np.isscalar(u0): u0 = np.ones((ninputs,)) * u0 + if np.isscalar(y0): y0 = np.ones((ninputs,)) * y0 + + # Discrete-time not yet supported + if isdtime(sys, strict=True): + raise NotImplementedError( + "Discrete time systems are not yet supported.") + + # Make sure the input arguments match the sizes of the system + if len(x0) != nstates or \ + (u0 is not None and len(u0) != ninputs) or \ + (y0 is not None and len(y0) != noutputs) or \ + (dx0 is not None and len(dx0) != nstates): + raise ValueError("Length of input arguments does not match system.") + + # Update the parameter values + sys._update_params(params) + + # Decide what variables to minimize + if all([x is None for x in (iu, iy, ix, idx)]): + # Special cases: either inputs or outputs are constrained + if y0 is None: + # Take u0 as fixed and minimize over x + # TODO: update to allow discrete time systems + def ode_rhs(z): return sys._rhs(t, z, u0) + result = root(ode_rhs, x0, **kw) + z = (result.x, u0, sys._out(t, result.x, u0)) + else: + # Take y0 as fixed and minimize over x and u + def rootfun(z): + # Split z into x and u + x, u = np.split(z, [nstates]) + # TODO: update to allow discrete time systems + return np.concatenate( + (sys._rhs(t, x, u), sys._out(t, x, u) - y0), axis=0) + z0 = np.concatenate((x0, u0), axis=0) # Put variables together + result = root(rootfun, z0, **kw) # Find the eq point + x, u = np.split(result.x, [nstates]) # Split result back in two + z = (x, u, sys._out(t, x, u)) + + else: + # General case: figure out what variables to constrain + # Verify the indices we are using are all in range + if iu is not None: + iu = np.unique(iu) + if any([not isinstance(x, int) for x in iu]) or \ + (len(iu) > 0 and (min(iu) < 0 or max(iu) >= ninputs)): + assert ValueError("One or more input indices is invalid") + else: + iu = [] + + if iy is not None: + iy = np.unique(iy) + if any([not isinstance(x, int) for x in iy]) or \ + min(iy) < 0 or max(iy) >= noutputs: + assert ValueError("One or more output indices is invalid") + else: + iy = list(range(noutputs)) + + if ix is not None: + ix = np.unique(ix) + if any([not isinstance(x, int) for x in ix]) or \ + min(ix) < 0 or max(ix) >= nstates: + assert ValueError("One or more state indices is invalid") + else: + ix = [] + + if idx is not None: + idx = np.unique(idx) + if any([not isinstance(x, int) for x in idx]) or \ + min(idx) < 0 or max(idx) >= nstates: + assert ValueError("One or more deriv indices is invalid") + else: + idx = list(range(nstates)) + + # Construct the index lists for mapping variables and constraints + # + # The mechanism by which we implement the root finding function is to + # map the subset of variables we are searching over into the inputs + # and states, and then return a function that represents the equations + # we are trying to solve. + # + # To do this, we need to carry out the following operations: + # + # 1. Given the current values of the free variables (z), map them into + # the portions of the state and input vectors that are not fixed. + # + # 2. Compute the update and output maps for the input/output system + # and extract the subset of equations that should be equal to zero. + # + # We perform these functions by computing four sets of index lists: + # + # * state_vars: indices of states that are allowed to vary + # * input_vars: indices of inputs that are allowed to vary + # * deriv_vars: indices of derivatives that must be constrained + # * output_vars: indices of outputs that must be constrained + # + # This index lists can all be precomputed based on the `iu`, `iy`, + # `ix`, and `idx` lists that were passed as arguments to `find_eqpt` + # and were processed above. + + # Get the states and inputs that were not listed as fixed + state_vars = np.delete(np.array(range(nstates)), ix) + input_vars = np.delete(np.array(range(ninputs)), iu) + + # Set the outputs and derivs that will serve as constraints + output_vars = np.array(iy) + deriv_vars = np.array(idx) + + # Verify that the number of degrees of freedom all add up correctly + num_freedoms = len(state_vars) + len(input_vars) + num_constraints = len(output_vars) + len(deriv_vars) + if num_constraints != num_freedoms: + warn("Number of constraints (%d) does not match number of degrees " + "of freedom (%d). Results may be meaningless." % + (num_constraints, num_freedoms)) + + # Make copies of the state and input variables to avoid overwriting + # and convert to floats (in case ints were used for initial conditions) + x = np.array(x0, dtype=float) + u = np.array(u0, dtype=float) + dx0 = np.array(dx0, dtype=float) if dx0 is not None \ + else np.zeros(x.shape) + + # Keep track of the number of states in the set of free variables + nstate_vars = len(state_vars) + dtime = isdtime(sys, strict=True) + + def rootfun(z): + # Map the vector of values into the states and inputs + x[state_vars] = z[:nstate_vars] + u[input_vars] = z[nstate_vars:] + + # Compute the update and output maps + dx = sys._rhs(t, x, u) - dx0 + if dtime: dx -= x # TODO: check + dy = sys._out(t, x, u) - y0 + + # Map the results into the constrained variables + return np.concatenate((dx[deriv_vars], dy[output_vars]), axis=0) + + # Set the initial condition for the root finding algorithm + z0 = np.concatenate((x[state_vars], u[input_vars]), axis=0) + + # Finally, call the root finding function + result = root(rootfun, z0, **kw) + + # Extract out the results and insert into x and u + x[state_vars] = result.x[:nstate_vars] + u[input_vars] = result.x[nstate_vars:] + z = (x, u, sys._out(t, x, u)) + + # Return the result based on what the user wants and what we found + if not return_y: z = z[0:2] # Strip y from result if not desired + if return_result: + # Return whatever we got, along with the result dictionary + return z + (result,) + elif result.success: + # Return the result of the optimization + return z + else: + # Something went wrong, don't return anything + return (None, None, None) if return_y else (None, None) + + +# Linearize an input/output system +def linearize(sys, xeq, ueq=[], t=0, params={}, **kw): + """Linearize an input/output system at a given state and input. + + This function computes the linearization of an input/output system at a + given state and input value and returns a :class:`control.StateSpace` + object. The eavaluation point need not be an equilibrium point. + + Parameters + ---------- + sys : InputOutputSystem + The system to be linearized + xeq : array + The state at which the linearization will be evaluated (does not need + to be an equlibrium state). + ueq : array + The input at which the linearization will be evaluated (does not need + to correspond to an equlibrium state). + t : float, optional + The time at which the linearization will be computed (for time-varying + systems). + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. + + Returns + ------- + ss_sys : LinearIOSystem + The linearization of the system, as a :class:`~control.LinearIOSystem` + object (which is also a :class:`~control.StateSpace` object. + + """ + if not isinstance(sys, InputOutputSystem): + raise TypeError("Can only linearize InputOutputSystem types") + return sys.linearize(xeq, ueq, t=t, params=params, **kw) + + +# Utility function to find the size of a system parameter +def _find_size(sysval, vecval): + if sysval is not None: + return sysval + elif hasattr(vecval, '__len__'): + return len(vecval) + elif vecval is None: + return 0 + else: + raise ValueError("Can't determine size of system component.") + + +# Convert a state space system into an input/output system (wrapper) +def ss2io(*args, **kw): return LinearIOSystem(*args, **kw) +ss2io.__doc__ = LinearIOSystem.__init__.__doc__ + + +# Convert a transfer function into an input/output system (wrapper) +def tf2io(*args, **kw): + """Convert a transfer function into an I/O system""" + # TODO: add remaining documentation + # Convert the system to a state space system + linsys = tf2ss(*args) + + # Now convert the state space system to an I/O system + return LinearIOSystem(linsys, **kw) diff --git a/control/lti.py b/control/lti.py index 5950d9d58..c9a58f9c0 100644 --- a/control/lti.py +++ b/control/lti.py @@ -54,8 +54,9 @@ def isdtime(self, strict=False): Parameters ---------- - strict: bool (default = False) - If strict is True, make sure that timebase is not None + strict: bool, optional + If strict is True, make sure that timebase is not None. Default + is False. """ # If no timebase is given, answer depends on strict flag @@ -73,8 +74,9 @@ def isctime(self, strict=False): ---------- sys : LTI system System to be checked - strict: bool (default = False) - If strict is True, make sure that timebase is not None + strict: bool, optional + If strict is True, make sure that timebase is not None. Default + is False. """ # If no timebase is given, answer depends on strict flag if self.dt is None: @@ -176,6 +178,28 @@ def timebaseEqual(sys1, sys2): else: return sys1.dt == sys2.dt +# Find a common timebase between two or more systems +def _find_timebase(sys1, *sysn): + """Find the common timebase between systems, otherwise return False""" + + # Create a list of systems to check + syslist = [sys1] + syslist.append(*sysn) + + # Look for a common timebase + dt = None + + for sys in syslist: + # Make sure time bases are consistent + if (dt is None and sys.dt is not None) or \ + (dt is True and isdiscrete(sys)): + # Timebase was not specified; set to match this system + dt = sys.dt + elif dt != sys.dt: + return False + return dt + + # Check to see if a system is a discrete time system def isdtime(sys, strict=False): """ @@ -198,6 +222,15 @@ def isdtime(sys, strict=False): if isinstance(sys, LTI): return sys.isdtime(strict) + # Check to see if object has a dt object + if hasattr(sys, 'dt'): + # If no timebase is given, answer depends on strict flag + if sys.dt == None: + return True if not strict else False + + # Look for dt > 0 (also works if dt = True) + return sys.dt > 0 + # Got passed something we don't recognize return False @@ -223,6 +256,13 @@ def isctime(sys, strict=False): if isinstance(sys, LTI): return sys.isctime(strict) + # Check to see if object has a dt object + if hasattr(sys, 'dt'): + # If no timebase is given, answer depends on strict flag + if sys.dt is None: + return True if not strict else False + return sys.dt == 0 + # Got passed something we don't recognize return False diff --git a/control/mateqn.py b/control/mateqn.py index 7e842234a..87dd00dab 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -41,16 +41,18 @@ Author: Bjorn Olofsson """ -from scipy import shape, size, asarray, asmatrix, copy, zeros, eye, dot +from numpy import shape, size, asarray, copy, zeros, eye, dot, \ + finfo, inexact, atleast_2d from scipy.linalg import eigvals, solve_discrete_are, solve from .exception import ControlSlycot, ControlArgument +from .statesp import _ssmatrix __all__ = ['lyap', 'dlyap', 'dare', 'care'] #### Lyapunov equation solvers lyap and dlyap -def lyap(A,Q,C=None,E=None): - """ X = lyap(A,Q) solves the continuous-time Lyapunov equation +def lyap(A, Q, C=None, E=None): + """X = lyap(A, Q) solves the continuous-time Lyapunov equation :math:`A X + X A^T + Q = 0` @@ -69,7 +71,9 @@ def lyap(A,Q,C=None,E=None): :math:`A X E^T + E X A^T + Q = 0` where Q is a symmetric matrix and A, Q and E are square matrices - of the same dimension. """ + of the same dimension. + + """ # Make sure we have access to the right slycot routines try: @@ -84,27 +88,27 @@ def lyap(A,Q,C=None,E=None): # Reshape 1-d arrays if len(shape(A)) == 1: - A = A.reshape(1,A.size) + A = A.reshape(1, A.size) if len(shape(Q)) == 1: - Q = Q.reshape(1,Q.size) + Q = Q.reshape(1, Q.size) if C is not None and len(shape(C)) == 1: - C = C.reshape(1,C.size) + C = C.reshape(1, C.size) if E is not None and len(shape(E)) == 1: - E = E.reshape(1,E.size) + E = E.reshape(1, E.size) # Determine main dimensions if size(A) == 1: n = 1 else: - n = size(A,0) + n = size(A, 0) if size(Q) == 1: m = 1 else: - m = size(Q,0) + m = size(Q, 0) # Solve standard Lyapunov equation if C is None and E is None: @@ -119,7 +123,7 @@ def lyap(A,Q,C=None,E=None): if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: raise ControlArgument("Q must be a quadratic matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") # Solve the Lyapunov equation by calling Slycot function sb03md @@ -185,7 +189,7 @@ def lyap(A,Q,C=None,E=None): raise ControlArgument("E must be a square matrix with the same \ dimension as A.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") # Make sure we have access to the write slicot routine @@ -228,7 +232,7 @@ def lyap(A,Q,C=None,E=None): else: raise ControlArgument("Invalid set of input parameters") - return X + return _ssmatrix(X) def dlyap(A,Q,C=None,E=None): @@ -306,7 +310,7 @@ def dlyap(A,Q,C=None,E=None): if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: raise ControlArgument("Q must be a quadratic matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") # Solve the Lyapunov equation by calling the Slycot function sb03md @@ -368,7 +372,7 @@ def dlyap(A,Q,C=None,E=None): raise ControlArgument("E must be a square matrix with the same \ dimension as A.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") # Solve the generalized Lyapunov equation by calling Slycot @@ -405,12 +409,10 @@ def dlyap(A,Q,C=None,E=None): else: raise ControlArgument("Invalid set of input parameters") - return X - + return _ssmatrix(X) #### Riccati equation solvers care and dare - def care(A, B, Q, R=None, S=None, E=None, stabilizing=True): """ (X,L,G) = care(A,B,Q,R=None) solves the continuous-time algebraic Riccati equation @@ -499,10 +501,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True): size(B) == 1 and n > 1: raise ControlArgument("Incompatible dimensions of B matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") - if not (asarray(R) == asarray(R).T).all(): + if not _is_symmetric(R): raise ControlArgument("R must be a symmetric matrix.") # Create back-up of arrays needed for later computations @@ -566,7 +568,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True): # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return (X , w[:n] , G ) + return (_ssmatrix(X) , w[:n] , _ssmatrix(G)) # Solve the generalized algebraic Riccati equation elif S is not None and E is not None: @@ -602,10 +604,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True): size(S) == 1 and m > 1: raise ControlArgument("Incompatible dimensions of S matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") - if not (asarray(R) == asarray(R).T).all(): + if not _is_symmetric(R): raise ControlArgument("R must be a symmetric matrix.") # Create back-up of arrays needed for later computations @@ -673,7 +675,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True): # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return (X , L , G) + return (_ssmatrix(X), L, _ssmatrix(G)) # Invalid set of input parameters else: @@ -703,12 +705,12 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True): if S is not None or E is not None or not stabilizing: return dare_old(A, B, Q, R, S, E, stabilizing) else: - Rmat = asmatrix(R) - Qmat = asmatrix(Q) + Rmat = _ssmatrix(R) + Qmat = _ssmatrix(Q) X = solve_discrete_are(A, B, Qmat, Rmat) G = solve(B.T.dot(X).dot(B) + Rmat, B.T.dot(X).dot(A)) L = eigvals(A - B.dot(G)) - return X, L, G + return _ssmatrix(X), L, _ssmatrix(G) def dare_old(A, B, Q, R, S=None, E=None, stabilizing=True): # Make sure we can import required slycot routine @@ -774,10 +776,10 @@ def dare_old(A, B, Q, R, S=None, E=None, stabilizing=True): size(B) == 1 and n > 1: raise ControlArgument("Incompatible dimensions of B matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") - if not (asarray(R) == asarray(R).T).all(): + if not _is_symmetric(R): raise ControlArgument("R must be a symmetric matrix.") # Create back-up of arrays needed for later computations @@ -845,7 +847,7 @@ def dare_old(A, B, Q, R, S=None, E=None, stabilizing=True): # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return (X , w[:n] , G) + return (_ssmatrix(X) , w[:n], _ssmatrix(G)) # Solve the generalized algebraic Riccati equation elif S is not None and E is not None: @@ -881,10 +883,10 @@ def dare_old(A, B, Q, R, S=None, E=None, stabilizing=True): size(S) == 1 and m > 1: raise ControlArgument("Incompatible dimensions of S matrix.") - if not (asarray(Q) == asarray(Q).T).all(): + if not _is_symmetric(Q): raise ControlArgument("Q must be a symmetric matrix.") - if not (asarray(R) == asarray(R).T).all(): + if not _is_symmetric(R): raise ControlArgument("R must be a symmetric matrix.") # Create back-up of arrays needed for later computations @@ -954,8 +956,17 @@ def dare_old(A, B, Q, R, S=None, E=None, stabilizing=True): # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return (X , L , G) + return (_ssmatrix(X), L, _ssmatrix(G)) # Invalid set of input parameters else: raise ControlArgument("Invalid set of input parameters.") + + +def _is_symmetric(M): + M = atleast_2d(M) + if isinstance(M[0, 0], inexact): + eps = finfo(M.dtype).eps + return ((M - M.T) < eps).all() + else: + return (M == M.T).all() diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 3c9111b3b..413dc6d86 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -81,11 +81,16 @@ from ..margins import margin from ..rlocus import rlocus from ..dtime import c2d +from ..sisotool import sisotool # Import functions specific to Matlab compatibility package from .timeresp import * from .wrappers import * +# Set up defaults corresponding to MATLAB conventions +from ..config import * +use_matlab_defaults() + r""" The following tables give an overview of the module ``control.matlab``. They also show the implementation progress and the planned features of the @@ -241,6 +246,7 @@ == ========================== ============================================ \* :func:`rlocus` evans root locus +\* :func:`sisotool` SISO controller design \* :func:`~control.place` pole placement \ estim form estimator given estimator gain \ reg form regulator given state-feedback and diff --git a/control/matlab/wrappers.py b/control/matlab/wrappers.py index d83890a33..b0fda30a3 100644 --- a/control/matlab/wrappers.py +++ b/control/matlab/wrappers.py @@ -130,7 +130,7 @@ def dcgain(*args): ------- gain: ndarray The gain of each output versus each input: - :math:`y = gain \cdot u` + :math:`y = gain \\cdot u` Notes ----- @@ -140,7 +140,7 @@ def dcgain(*args): All systems are first converted to state space form. The function then computes: - .. math:: gain = - C \cdot A^{-1} \cdot B + D + .. math:: gain = - C \\cdot A^{-1} \\cdot B + D ''' #Convert the parameters to state space form if len(args) == 4: diff --git a/control/modelsimp.py b/control/modelsimp.py index 46f468685..9fd36923e 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -66,7 +66,7 @@ def hsvd(sys): Returns ------- - H : Matrix + H : array A list of Hankel singular values See Also @@ -96,11 +96,10 @@ def hsvd(sys): w, v = np.linalg.eig(WoWc) hsv = np.sqrt(w) - hsv = np.matrix(hsv) + hsv = np.array(hsv) hsv = np.sort(hsv) - hsv = np.fliplr(hsv) - # Return the Hankel singular values - return hsv + # Return the Hankel singular values, high to low + return hsv[::-1] def modred(sys, ELIM, method='matchdc'): """ @@ -125,9 +124,12 @@ def modred(sys, ELIM, method='matchdc'): Raises ------ ValueError - - if `method` is not either ``'matchdc'`` or ``'truncate'`` - - if eigenvalues of `sys.A` are not all in left half plane - (`sys` must be stable) + Raised under the following conditions: + + * if `method` is not either ``'matchdc'`` or ``'truncate'`` + + * if eigenvalues of `sys.A` are not all in left half plane + (`sys` must be stable) Examples -------- @@ -156,15 +158,15 @@ def modred(sys, ELIM, method='matchdc'): # Create list of elements not to eliminate (NELIM) NELIM = [i for i in range(len(sys.A)) if i not in ELIM] # A1 is a matrix of all columns of sys.A not to eliminate - A1 = sys.A[:,NELIM[0]] + A1 = sys.A[:, NELIM[0]].reshape(-1, 1) for i in NELIM[1:]: - A1 = np.hstack((A1, sys.A[:,i])) + A1 = np.hstack((A1, sys.A[:,i].reshape(-1, 1))) A11 = A1[NELIM,:] A21 = A1[ELIM,:] # A2 is a matrix of all columns of sys.A to eliminate - A2 = sys.A[:,ELIM[0]] + A2 = sys.A[:, ELIM[0]].reshape(-1, 1) for i in ELIM[1:]: - A2 = np.hstack((A2, sys.A[:,i])) + A2 = np.hstack((A2, sys.A[:,i].reshape(-1, 1))) A12 = A2[NELIM,:] A22 = A2[ELIM,:] @@ -189,10 +191,10 @@ def modred(sys, ELIM, method='matchdc'): A22I_A21 = A22I_A21_B2[:, :A21.shape[1]] A22I_B2 = A22I_A21_B2[:, A21.shape[1]:] - Ar = A11 - A12*A22I_A21 - Br = B1 - A12*A22I_B2 - Cr = C1 - C2*A22I_A21 - Dr = sys.D - C2*A22I_B2 + Ar = A11 - np.dot(A12, A22I_A21) + Br = B1 - np.dot(A12, A22I_B2) + Cr = C1 - np.dot(C2, A22I_A21) + Dr = sys.D - np.dot(C2, A22I_B2) elif method=='truncate': # if truncate, simply discard state x2 Ar = A11 @@ -377,7 +379,7 @@ def era(YY, m, n, nin, nout, r): """ raise NotImplementedError('This function is not implemented yet.') -def markov(Y, U, M): +def markov(Y, U, m): """ Calculate the first `M` Markov parameters [D CB CAB ...] from input `U`, output `Y`. @@ -388,13 +390,13 @@ def markov(Y, U, M): Output data U: array_like Input data - M: integer + m: int Number of Markov parameters to output Returns ------- - H: matrix - First M Markov parameters + H: ndarray + First m Markov parameters Notes ----- @@ -402,21 +404,22 @@ def markov(Y, U, M): Examples -------- - >>> H = markov(Y, U, M) + >>> H = markov(Y, U, m) """ # Convert input parameters to matrices (if they aren't already) - Ymat = np.mat(Y) - Umat = np.mat(U) + Ymat = np.array(Y) + Umat = np.array(U) n = np.size(U) # Construct a matrix of control inputs to invert UU = Umat - for i in range(1, M-1): - newCol = np.vstack((0, UU[0:n-1,i-2])) + for i in range(1, m-1): + # TODO: second index on UU doesn't seem right; could be neg or pos?? + newCol = np.vstack((0, np.reshape(UU[0:n-1, i-2], (-1, 1)))) UU = np.hstack((UU, newCol)) - Ulast = np.vstack((0, UU[0:n-1,M-2])) - for i in range(n-1,0,-1): + Ulast = np.vstack((0, np.reshape(UU[0:n-1, m-2], (-1, 1)))) + for i in range(n-1, 0, -1): Ulast[i] = np.sum(Ulast[0:i-1]) UU = np.hstack((UU, Ulast)) diff --git a/control/nichols.py b/control/nichols.py index a4c5795a4..48abffa0a 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -54,11 +54,17 @@ import matplotlib.pyplot as plt from .ctrlutil import unwrap from .freqplot import default_frequency_range +from . import config __all__ = ['nichols_plot', 'nichols', 'nichols_grid'] +# Default parameters values for the nichols module +_nichols_defaults = { + 'nichols.grid':True, +} -def nichols_plot(sys_list, omega=None, grid=True): + +def nichols_plot(sys_list, omega=None, grid=None): """Nichols plot for a system Plots a Nichols plot for the system over a (optional) frequency range. @@ -76,6 +82,9 @@ def nichols_plot(sys_list, omega=None, grid=True): ------- None """ + # Get parameter values + grid = config._get_param('nichols', 'grid', grid, True) + # If argument was a singleton, turn it into a list if not getattr(sys_list, '__iter__', False): diff --git a/control/phaseplot.py b/control/phaseplot.py index 10fbec640..6cac09e6c 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -56,8 +56,7 @@ def _find(condition): def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, lingrid=None, lintime=None, logtime=None, timepts=None, parms=(), verbose=True): - """ - Phase plot for 2D dynamical systems + """Phase plot for 2D dynamical systems Produces a vector field or stream line plot for a planar system. @@ -98,30 +97,30 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, len(X0) that gives the simulation time for each initial condition. Default value = 50. - lingrid = N or (N, M): integer or 2-tuple of integers, optional - If X0 is given and X, Y are missing, a grid of arrows is - produced using the limits of the initial conditions, with N - grid points in each dimension or N grid points in x and M grid - points in y. - - lintime = N: integer, optional - Draw N arrows using equally space time points + lingrid : integer or 2-tuple of integers, optional + Argument is either N or (N, M). If X0 is given and X, Y are missing, + a grid of arrows is produced using the limits of the initial + conditions, with N grid points in each dimension or N grid points in x + and M grid points in y. - logtime = (N, lambda): (integer, float), optional - Draw N arrows using exponential time constant lambda + lintime : integer or tuple (integer, float), optional + If a single integer N is given, draw N arrows using equally space time + points. If a tuple (N, lambda) is given, draw N arrows using + exponential time constant lambda - timepts = [t1, t2, ...]: array-like, optional - Draw arrows at the given list times + timepts : array-like, optional + Draw arrows at the given list times [t1, t2, ...] parms: tuple, optional List of parameters to pass to vector field: `func(x, t, *parms)` See also -------- - box_grid(X, Y): construct box-shaped grid of initial conditions + box_grid : construct box-shaped grid of initial conditions Examples -------- + """ # @@ -291,7 +290,7 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, # set(xy, 'AutoScaleFactor', 0); if (scale < 0): - bp = mpl.plot(x1, x2, 'b.'); # add dots at base + bp = mpl.plot(x1, x2, 'b.'); # add dots at base # set(bp, 'MarkerSize', PP_arrow_markersize); return; diff --git a/control/pzmap.py b/control/pzmap.py index 252d10011..a8fb990b5 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -44,9 +44,18 @@ from math import pi from .lti import LTI, isdtime, isctime from .grid import sgrid, zgrid, nogrid +from . import config __all__ = ['pzmap'] + +# Define default parameter values for this module +_pzmap_defaults = { + 'pzmap.grid':False, # Plot omega-damping grid + 'pzmap.Plot':True, # Generate plot using Matplotlib +} + + # TODO: Implement more elegant cross-style axes. See: # http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html # http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html @@ -71,6 +80,10 @@ def pzmap(sys, Plot=True, grid=False, title='Pole Zero Map'): zeros: array The system's zeros. """ + # Get parameter values + Plot = config._get_param('rlocus', 'Plot', Plot, True) + grid = config._get_param('rlocus', 'grid', grid, False) + if not isinstance(sys, LTI): raise TypeError('Argument ``sys``: must be a linear system.') diff --git a/control/rlocus.py b/control/rlocus.py index a6870159b..0c115c26e 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -46,6 +46,7 @@ # $Id$ # Packages used by this module +from functools import partial import numpy as np import matplotlib import matplotlib.pyplot as plt @@ -55,15 +56,22 @@ from .xferfcn import _convert_to_transfer_function from .exception import ControlMIMONotImplemented from .sisotool import _SisotoolUpdate -from functools import partial -from .lti import isdtime -from .grid import sgrid, zgrid, nogrid +from . import config __all__ = ['root_locus', 'rlocus'] +# Default values for module parameters +_rlocus_defaults = { + 'rlocus.grid':True, + 'rlocus.plotstr':'b' if int(matplotlib.__version__[0]) == 1 else 'C0', + 'rlocus.PrintGain':True, + 'rlocus.Plot':True +} + + # Main function: compute a root locus diagram -def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplotlib.__version__[0]) == 1 else 'C0', Plot=True, - PrintGain=True, grid=False, **kwargs): +def root_locus(sys, kvect=None, xlim=None, ylim=None, + plotstr=None, Plot=True, PrintGain=None, grid=None, **kwargs): """Root locus plot @@ -74,28 +82,33 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot Parameters ---------- sys : LTI object - Linear input/output systems (SISO only, for now) + Linear input/output systems (SISO only, for now). kvect : list or ndarray, optional - List of gains to use in computing diagram + List of gains to use in computing diagram. xlim : tuple or list, optional - control of x-axis range, normally with tuple (see matplotlib.axes) + Set limits of x axis, normally with tuple (see matplotlib.axes). ylim : tuple or list, optional - control of y-axis range - Plot : boolean, optional (default = True) - If True, plot root locus diagram. - PrintGain: boolean (default = True) - If True, report mouse clicks when close to the root-locus branches, - calculate gain, damping and print - grid: boolean (default = False) - If True plot omega-damping grid. + Set limits of y axis, normally with tuple (see matplotlib.axes). + Plot : boolean, optional + If True (default), plot root locus diagram. + PrintGain : bool + If True (default), report mouse clicks when close to the root locus + branches, calculate gain, damping and print. + grid : bool + If True plot omega-damping grid. Default is False. Returns ------- rlist : ndarray - Computed root locations, given as a 2d array + Computed root locations, given as a 2D array klist : ndarray or list Gains used. Same as klist keyword argument if provided. """ + # Get parameter values + plotstr = config._get_param('rlocus', 'plotstr', plotstr, _rlocus_defaults) + grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) + PrintGain = config._get_param( + 'rlocus', 'PrintGain', PrintGain, _rlocus_defaults) # Convert numerator and denominator to polynomials if they aren't (nump, denp) = _systopoly1d(sys) @@ -119,7 +132,9 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot else: figure_number = pylab.get_fignums() - figure_title = [pylab.figure(numb).canvas.get_window_title() for numb in figure_number] + figure_title = [ + pylab.figure(numb).canvas.get_window_title() + for numb in figure_number] new_figure_name = "Root Locus" rloc_num = 1 while new_figure_name in figure_title: @@ -128,15 +143,38 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot f = pylab.figure(new_figure_name) ax = pylab.axes() - if PrintGain and sisotool == False: + if PrintGain and not sisotool: f.canvas.mpl_connect( - 'button_release_event', partial(_RLClickDispatcher,sys=sys, fig=f,ax_rlocus=f.axes[0],plotstr=plotstr)) - - elif sisotool == True: - f.axes[1].plot([root.real for root in start_mat], [root.imag for root in start_mat], 'm.', marker='s', markersize=8,zorder=20,label='gain_point') - f.suptitle("Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % (start_mat[0][0].real, start_mat[0][0].imag, 1, -1 * start_mat[0][0].real / abs(start_mat[0][0])),fontsize = 12 if int(matplotlib.__version__[0]) == 1 else 10) + 'button_release_event', + partial(_RLClickDispatcher, sys=sys, fig=f, + ax_rlocus=f.axes[0], plotstr=plotstr)) + + elif sisotool: + f.axes[1].plot( + [root.real for root in start_mat], + [root.imag for root in start_mat], + 'm.', marker='s', markersize=8, zorder=20, label='gain_point') + f.suptitle( + "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % + (start_mat[0][0].real, start_mat[0][0].imag, + 1, -1 * start_mat[0][0].real / abs(start_mat[0][0])), + fontsize=12 if int(matplotlib.__version__[0]) == 1 else 10) f.canvas.mpl_connect( - 'button_release_event',partial(_RLClickDispatcher,sys=sys, fig=f,ax_rlocus=f.axes[1],plotstr=plotstr, sisotool=sisotool, bode_plot_params=kwargs['bode_plot_params'],tvect=kwargs['tvect'])) + 'button_release_event', + partial(_RLClickDispatcher, sys=sys, fig=f, + ax_rlocus=f.axes[1], plotstr=plotstr, + sisotool=sisotool, + bode_plot_params=kwargs['bode_plot_params'], + tvect=kwargs['tvect'])) + + # zoom update on xlim/ylim changed, only then data on new limits + # is available, i.e., cannot combine with _RLClickDispatcher + dpfun = partial( + _RLZoomDispatcher, sys=sys, ax_rlocus=ax, plotstr=plotstr) + # TODO: the next too lines seem to take a long time to execute + # TODO: is there a way to speed them up? (RMM, 6 Jun 2019) + ax.callbacks.connect('xlim_changed', dpfun) + ax.callbacks.connect('ylim_changed', dpfun) # plot open loop poles poles = array(denp.r) @@ -148,14 +186,15 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot ax.plot(real(zeros), imag(zeros), 'o') # Now plot the loci - for index,col in enumerate(mymat.T): - ax.plot(real(col), imag(col), plotstr,label='rootlocus') + for index, col in enumerate(mymat.T): + ax.plot(real(col), imag(col), plotstr, label='rootlocus') # Set up plot axes and labels if xlim: ax.set_xlim(xlim) if ylim: ax.set_ylim(ylim) + ax.set_xlabel('Real') ax.set_ylabel('Imaginary') if grid and sisotool: @@ -163,18 +202,21 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot elif grid: _sgrid_func() else: - ax.axhline(0., linestyle=':', color='k',zorder=-20) + ax.axhline(0., linestyle=':', color='k', zorder=-20) ax.axvline(0., linestyle=':', color='k') return mymat, kvect -def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): +def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): """Unsupervised gains calculation for root locus plot. - References: - Ogata, K. (2002). Modern control engineering (4th ed.). Upper Saddle River, NJ : New Delhi: Prentice Hall..""" + References + ---------- + Ogata, K. (2002). Modern control engineering (4th ed.). Upper + Saddle River, NJ : New Delhi: Prentice Hall.. + """ k_break, real_break = _break_points(num, den) kmax = _k_max(num, den, real_break, k_break) kvect = np.hstack((np.linspace(0, kmax, 50), np.real(k_break))) @@ -185,9 +227,12 @@ def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): open_loop_poles = den.roots open_loop_zeros = num.roots - if (open_loop_zeros.size != 0) and (open_loop_zeros.size < open_loop_poles.size): - open_loop_zeros_xl = np.append(open_loop_zeros, - np.ones(open_loop_poles.size - open_loop_zeros.size) * open_loop_zeros[-1]) + if open_loop_zeros.size != 0 and \ + open_loop_zeros.size < open_loop_poles.size: + open_loop_zeros_xl = np.append( + open_loop_zeros, + np.ones(open_loop_poles.size - open_loop_zeros.size) + * open_loop_zeros[-1]) mymat_xl = np.append(mymat, open_loop_zeros_xl) else: mymat_xl = mymat @@ -198,18 +243,22 @@ def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): false_gain = float(den.coeffs[0]) / float(num.coeffs[0]) if false_gain < 0 and not den.order > num.order: - raise ValueError("Not implemented support for 0 degrees root " - "locus with equal order of numerator and denominator.") + # TODO: make error message more understandable + raise ValueError("Not implemented support for 0 degrees root locus " + "with equal order of numerator and denominator.") if xlim is None and false_gain > 0: - x_tolerance = 0.05 * (np.max(np.real(mymat_xl)) - np.min(np.real(mymat_xl))) + x_tolerance = 0.05 * (np.max(np.real(mymat_xl)) + - np.min(np.real(mymat_xl))) xlim = _ax_lim(mymat_xl) elif xlim is None and false_gain < 0: - axmin = np.min(np.real(important_points)) - ( - np.max(np.real(important_points)) - np.min(np.real(important_points))) + axmin = np.min(np.real(important_points)) \ + - (np.max(np.real(important_points)) + - np.min(np.real(important_points))) axmin = np.min(np.array([axmin, np.min(np.real(mymat_xl))])) - axmax = np.max(np.real(important_points)) + np.max(np.real(important_points)) - np.min( - np.real(important_points)) + axmax = np.max(np.real(important_points)) \ + + np.max(np.real(important_points)) \ + - np.min(np.real(important_points)) axmax = np.max(np.array([axmax, np.max(np.real(mymat_xl))])) xlim = [axmin, axmax] x_tolerance = 0.05 * (axmax - axmin) @@ -217,16 +266,26 @@ def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): x_tolerance = 0.05 * (xlim[1] - xlim[0]) if ylim is None: - y_tolerance = 0.05 * (np.max(np.imag(mymat_xl)) - np.min(np.imag(mymat_xl))) + y_tolerance = 0.05 * (np.max(np.imag(mymat_xl)) + - np.min(np.imag(mymat_xl))) ylim = _ax_lim(mymat_xl * 1j) else: y_tolerance = 0.05 * (ylim[1] - ylim[0]) - tolerance = np.min([x_tolerance, y_tolerance]) - indexes_too_far = _indexes_filt(mymat,tolerance,zoom_xlim,zoom_ylim) + # Figure out which points are spaced too far apart + if x_tolerance == 0: + # Root locus is on imaginary axis (rare), use just y distance + tolerance = y_tolerance + elif y_tolerance == 0: + # Root locus is on imaginary axis (common), use just x distance + tolerance = x_tolerance + else: + tolerance = np.min([x_tolerance, y_tolerance]) + indexes_too_far = _indexes_filt(mymat, tolerance, zoom_xlim, zoom_ylim) - while (len(indexes_too_far) > 0) and (kvect.size < 5000): - for counter,index in enumerate(indexes_too_far): + # Add more points into the root locus for points that are too far apart + while len(indexes_too_far) > 0 and kvect.size < 5000: + for counter, index in enumerate(indexes_too_far): index = index + counter*3 new_gains = np.linspace(kvect[index], kvect[index + 1], 5) new_points = _RLFindRoots(num, den, new_gains[1:4]) @@ -234,7 +293,7 @@ def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): mymat = np.insert(mymat, index + 1, new_points, axis=0) mymat = _RLSortRoots(mymat) - indexes_too_far = _indexes_filt(mymat,tolerance,zoom_xlim,zoom_ylim) + indexes_too_far = _indexes_filt(mymat, tolerance, zoom_xlim, zoom_ylim) new_gains = kvect[-1] * np.hstack((np.logspace(0, 3, 4))) new_points = _RLFindRoots(num, den, new_gains[1:4]) @@ -243,43 +302,54 @@ def _default_gains(num, den, xlim, ylim,zoom_xlim=None,zoom_ylim=None): mymat = _RLSortRoots(mymat) return kvect, mymat, xlim, ylim -def _indexes_filt(mymat,tolerance,zoom_xlim=None,zoom_ylim=None): + +def _indexes_filt(mymat, tolerance, zoom_xlim=None, zoom_ylim=None): """Calculate the distance between points and return the indexes. - Filter the indexes so only the resolution of points within the xlim and ylim is improved when zoom is used""" + + Filter the indexes so only the resolution of points within the xlim and + ylim is improved when zoom is used. + + """ distance_points = np.abs(np.diff(mymat, axis=0)) indexes_too_far = list(np.unique(np.where(distance_points > tolerance)[0])) - if zoom_xlim != None and zoom_ylim != None: + if zoom_xlim is not None and zoom_ylim is not None: x_tolerance_zoom = 0.05 * (zoom_xlim[1] - zoom_xlim[0]) y_tolerance_zoom = 0.05 * (zoom_ylim[1] - zoom_ylim[0]) tolerance_zoom = np.min([x_tolerance_zoom, y_tolerance_zoom]) - indexes_too_far_zoom = list(np.unique(np.where(distance_points > tolerance_zoom)[0])) + indexes_too_far_zoom = list( + np.unique(np.where(distance_points > tolerance_zoom)[0])) indexes_too_far_filtered = [] for index in indexes_too_far_zoom: for point in mymat[index]: - if (zoom_xlim[0] <= point.real <= zoom_xlim[1]) and (zoom_ylim[0] <= point.imag <= zoom_ylim[1]): + if (zoom_xlim[0] <= point.real <= zoom_xlim[1]) and \ + (zoom_ylim[0] <= point.imag <= zoom_ylim[1]): indexes_too_far_filtered.append(index) break - # Check if the zoom box is not overshot and insert points where neccessary - if len(indexes_too_far_filtered) == 0 and len(mymat) <300: - limits = [zoom_xlim[0],zoom_xlim[1],zoom_ylim[0],zoom_ylim[1]] - for index,limit in enumerate(limits): + # Check if zoom box is not overshot & insert points where neccessary + if len(indexes_too_far_filtered) == 0 and len(mymat) < 500: + limits = [zoom_xlim[0], zoom_xlim[1], zoom_ylim[0], zoom_ylim[1]] + for index, limit in enumerate(limits): if index <= 1: asign = np.sign(real(mymat)-limit) else: asign = np.sign(imag(mymat) - limit) - signchange = ((np.roll(asign, 1, axis=0) - asign) != 0).astype(int) + signchange = ((np.roll(asign, 1, axis=0) + - asign) != 0).astype(int) signchange[0] = np.zeros((len(mymat[0]))) - if len(np.where(signchange ==1)[0]) > 0: - indexes_too_far_filtered.append(np.where(signchange == 1)[0][0]-1) + if len(np.where(signchange == 1)[0]) > 0: + indexes_too_far_filtered.append( + np.where(signchange == 1)[0][0]-1) - if len(indexes_too_far_filtered) > 0 : + if len(indexes_too_far_filtered) > 0: if indexes_too_far_filtered[0] != 0: - indexes_too_far_filtered.insert(0,indexes_too_far_filtered[0]-1) - if not indexes_too_far_filtered[-1] +1 >= len(mymat)-2: - indexes_too_far_filtered.append(indexes_too_far_filtered[-1]+1) + indexes_too_far_filtered.insert( + 0, indexes_too_far_filtered[0]-1) + if not indexes_too_far_filtered[-1] + 1 >= len(mymat) - 2: + indexes_too_far_filtered.append( + indexes_too_far_filtered[-1] + 1) indexes_too_far.extend(indexes_too_far_filtered) @@ -287,14 +357,16 @@ def _indexes_filt(mymat,tolerance,zoom_xlim=None,zoom_ylim=None): indexes_too_far.sort() return indexes_too_far + def _break_points(num, den): - """Extract break points over real axis and the gains give these location""" + """Extract break points over real axis and gains given these locations""" # type: (np.poly1d, np.poly1d) -> (np.array, np.array) dnum = num.deriv(m=1) dden = den.deriv(m=1) polynom = den * dnum - num * dden real_break_pts = polynom.r - real_break_pts = real_break_pts[num(real_break_pts) != 0] # don't care about infinite break points + # don't care about infinite break points + real_break_pts = real_break_pts[num(real_break_pts) != 0] k_break = -den(real_break_pts) / num(real_break_pts) idx = k_break >= 0 # only positives gains k_break = k_break[idx] @@ -318,26 +390,35 @@ def _ax_lim(mymat): def _k_max(num, den, real_break_points, k_break_points): - """" Calculate the maximum gain for the root locus shown in the figure""" + """"Calculate the maximum gain for the root locus shown in the figure.""" asymp_number = den.order - num.order singular_points = np.concatenate((num.roots, den.roots), axis=0) - important_points = np.concatenate((singular_points, real_break_points), axis=0) + important_points = np.concatenate( + (singular_points, real_break_points), axis=0) false_gain = den.coeffs[0] / num.coeffs[0] if asymp_number > 0: asymp_center = (np.sum(den.roots) - np.sum(num.roots))/asymp_number distance_max = 4 * np.max(np.abs(important_points - asymp_center)) - asymp_angles = (2 * np.arange(0, asymp_number)-1) * np.pi / asymp_number + asymp_angles = (2 * np.arange(0, asymp_number) - 1) \ + * np.pi / asymp_number if false_gain > 0: - farthest_points = asymp_center + distance_max * np.exp(asymp_angles * 1j) # farthest points over asymptotes + # farthest points over asymptotes + farthest_points = asymp_center \ + + distance_max * np.exp(asymp_angles * 1j) else: asymp_angles = asymp_angles + np.pi - farthest_points = asymp_center + distance_max * np.exp(asymp_angles * 1j) # farthest points over asymptotes - kmax_asymp = np.real(np.abs(den(farthest_points) / num(farthest_points))) + # farthest points over asymptotes + farthest_points = asymp_center \ + + distance_max * np.exp(asymp_angles * 1j) + kmax_asymp = np.real(np.abs(den(farthest_points) + / num(farthest_points))) else: - kmax_asymp = np.abs([np.abs(den.coeffs[0]) / np.abs(num.coeffs[0]) * 3]) + kmax_asymp = np.abs([np.abs(den.coeffs[0]) + / np.abs(num.coeffs[0]) * 3]) - kmax = np.max(np.concatenate((np.real(kmax_asymp), np.real(k_break_points)), axis=0)) + kmax = np.max(np.concatenate((np.real(kmax_asymp), + np.real(k_break_points)), axis=0)) if np.abs(false_gain) > kmax: kmax = np.abs(false_gain) return kmax @@ -380,7 +461,8 @@ def _RLFindRoots(nump, denp, kvect): curpoly = denp + k * nump curroots = curpoly.r if len(curroots) < denp.order: - # if I have fewer poles than open loop, it is because i have one at infinity + # if I have fewer poles than open loop, it is because i have + # one at infinity curroots = np.insert(curroots, len(curroots), np.inf) curroots.sort() @@ -389,6 +471,7 @@ def _RLFindRoots(nump, denp, kvect): mymat = row_stack(roots) return mymat + def _RLSortRoots(mymat): """Sort the roots from sys._RLFindRoots, so that the root locus doesn't show weird pseudo-branches as roots jump from @@ -411,23 +494,33 @@ def _RLSortRoots(mymat): prevrow = sorted[n, :] return sorted -def _RLClickDispatcher(event,sys,fig,ax_rlocus,plotstr,sisotool=False,bode_plot_params=None,tvect=None): - """Rootlocus plot click dispatcher""" - # If zoom is used on the rootlocus plot smooth and update it - if plt.get_current_fig_manager().toolbar.mode in ['zoom rect','pan/zoom'] and event.inaxes == ax_rlocus.axes: - (nump, denp) = _systopoly1d(sys) - xlim,ylim = ax_rlocus.get_xlim(),ax_rlocus.get_ylim() +def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): + """Rootlocus plot zoom dispatcher""" - kvect,mymat, xlim,ylim = _default_gains(nump, denp,xlim=None,ylim=None, zoom_xlim=xlim,zoom_ylim=ylim) - _removeLine('rootlocus', ax_rlocus) + nump, denp = _systopoly1d(sys) + xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim() - for i,col in enumerate(mymat.T): - ax_rlocus.plot(real(col), imag(col), plotstr,label='rootlocus') + kvect, mymat, xlim, ylim = _default_gains( + nump, denp, xlim=None, ylim=None, zoom_xlim=xlim, zoom_ylim=ylim) + _removeLine('rootlocus', ax_rlocus) - # if a point is clicked on the rootlocus plot visually emphasize it - else: - K = _RLFeedbackClicksPoint(event, sys, fig,ax_rlocus,sisotool) + for i, col in enumerate(mymat.T): + ax_rlocus.plot(real(col), imag(col), plotstr, label='rootlocus', + scalex=False, scaley=False) + + +def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, sisotool=False, + bode_plot_params=None, tvect=None): + """Rootlocus plot click dispatcher""" + + # Zoom is handled by specialized callback above, only do gain plot + if event.inaxes == ax_rlocus.axes and \ + plt.get_current_fig_manager().toolbar.mode not in \ + {'zoom rect', 'pan/zoom'}: + + # if a point is clicked on the rootlocus plot visually emphasize it + K = _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool) if sisotool and K is not None: _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect) @@ -435,54 +528,69 @@ def _RLClickDispatcher(event,sys,fig,ax_rlocus,plotstr,sisotool=False,bode_plot_ fig.canvas.draw() -def _RLFeedbackClicksPoint(event,sys,fig,ax_rlocus,sisotool=False): - """Display root-locus gain feedback point for clicks on the root-locus plot""" - +def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): + """Display root-locus gain feedback point for clicks on root-locus plot""" (nump, denp) = _systopoly1d(sys) + xlim = ax_rlocus.get_xlim() + ylim = ax_rlocus.get_ylim() + x_tolerance = 0.05 * abs((xlim[1] - xlim[0])) + y_tolerance = 0.05 * abs((ylim[1] - ylim[0])) + gain_tolerance = np.mean([x_tolerance, y_tolerance])*0.1 + # Catch type error when event click is in the figure but not in an axis try: s = complex(event.xdata, event.ydata) K = -1. / sys.horner(s) + K_xlim = -1. / sys.horner( + complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata)) + K_ylim = -1. / sys.horner( + complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0]))) except TypeError: K = float('inf') + K_xlim = float('inf') + K_ylim = float('inf') - xlim = ax_rlocus.get_xlim() - ylim = ax_rlocus.get_ylim() - x_tolerance = 0.05 * (xlim[1] - xlim[0]) - y_tolerance = 0.05 * (ylim[1] - ylim[0]) - gain_tolerance = np.min([x_tolerance, y_tolerance])*1e-1 + gain_tolerance += 0.1 * max([abs(K_ylim.imag/K_ylim.real), + abs(K_xlim.imag/K_xlim.real)]) - if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and event.inaxes == ax_rlocus.axes: + if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and \ + event.inaxes == ax_rlocus.axes and K.real > 0.: # Display the parameters in the output window and figure print("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" % (s.real, s.imag, K.real, -1 * s.real / abs(s))) - fig.suptitle("Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % - (s.real, s.imag, K.real, -1 * s.real / abs(s)),fontsize = 12 if int(matplotlib.__version__[0]) == 1 else 10) + fig.suptitle( + "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % + (s.real, s.imag, K.real, -1 * s.real / abs(s)), + fontsize=12 if int(matplotlib.__version__[0]) == 1 else 10) # Remove the previous line - _removeLine(label='gain_point',ax=ax_rlocus) + _removeLine(label='gain_point', ax=ax_rlocus) # Visualise clicked point, display all roots for sisotool mode if sisotool: mymat = _RLFindRoots(nump, denp, K.real) - ax_rlocus.plot([root.real for root in mymat], [root.imag for root in mymat], 'm.', marker='s', markersize=8, - zorder=20,label='gain_point') + ax_rlocus.plot( + [root.real for root in mymat], + [root.imag for root in mymat], + 'm.', marker='s', markersize=8, zorder=20, label='gain_point') else: - ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8, zorder=20,label='gain_point') + ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8, + zorder=20, label='gain_point') return K.real[0][0] -def _removeLine(label,ax): +def _removeLine(label, ax): """Remove a line from the ax when a label is specified""" for line in reversed(ax.lines): if line.get_label() == label: line.remove() del line + def _sgrid_func(fig=None, zeta=None, wn=None): if fig is None: fig = pylab.gcf() @@ -524,9 +632,11 @@ def _sgrid_func(fig=None, zeta=None, wn=None): xtext_pos = xtext_pos_lim else: ytext_pos = ytext_pos_lim - ax.annotate(an, textcoords='data', xy=[xtext_pos, ytext_pos], fontsize=8) + ax.annotate(an, textcoords='data', xy=[xtext_pos, ytext_pos], + fontsize=8) index += 1 - ax.plot([0, 0], [ylim[0], ylim[1]], color='gray', linestyle='dashed', linewidth=0.5) + ax.plot([0, 0], [ylim[0], ylim[1]], + color='gray', linestyle='dashed', linewidth=0.5) angules = np.linspace(-90, 90, 20)*np.pi/180 if wn is None: @@ -568,4 +678,5 @@ def _default_wn(xloc, ylim): return wn + rlocus = root_locus diff --git a/control/sisotool.py b/control/sisotool.py index 7a312cf5c..e700875ca 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -131,6 +131,8 @@ def _SisotoolUpdate(sys,fig,K,bode_plot_params,tvect=None): ax_rlocus.get_xaxis().set_label_coords(0.5, -0.15) ax_rlocus.get_yaxis().set_label_coords(-0.15, 0.5) + + # Generate the step response and plot it sys_closed = (K*sys).feedback(1) if tvect is None: diff --git a/control/statefbk.py b/control/statefbk.py index 0fb377a47..c079d9325 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -43,9 +43,11 @@ import numpy as np import scipy as sp from . import statesp +from .mateqn import care +from .statesp import _ssmatrix from .exception import ControlSlycot, ControlArgument, ControlDimension -__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'acker'] +__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'lqe', 'acker'] # Pole placement @@ -110,7 +112,7 @@ def place(A, B, p): result = place_poles(A_mat, B_mat, placed_eigs, method='YT') K = result.gain_matrix - return K + return _ssmatrix(K) def place_varga(A, B, p, dtime=False, alpha=None): @@ -141,7 +143,7 @@ def place_varga(A, B, p, dtime=False, alpha=None): Returns ------- - K : 2-d array + K : 2D array Gain such that A - B K has eigenvalues given in p. @@ -216,7 +218,77 @@ def place_varga(A, B, p, dtime=False, alpha=None): A_mat, B_mat, placed_eigs, DICO) # Return the gain matrix, with MATLAB gain convention - return -F + return _ssmatrix(-F) + +# contributed by Sawyer B. Fuller +def lqe(A, G, C, QN, RN, NN=None): + """lqe(A, G, C, QN, RN, [, N]) + + Linear quadratic estimator design (Kalman filter) for continuous-time + systems. Given the system + + Given the system + .. math:: + x = Ax + Bu + Gw + y = Cx + Du + v + + with unbiased process noise w and measurement noise v with covariances + + .. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN + + The lqe() function computes the observer gain matrix L such that the + stationary (non-time-varying) Kalman filter + + .. math:: x_e = A x_e + B u + L(y - C x_e - D u) + + produces a state estimate that x_e that minimizes the expected squared error + using the sensor measurements y. The noise cross-correlation `NN` is set to + zero when omitted. + + Parameters + ---------- + A, G: 2-d array + Dynamics and noise input matrices + QN, RN: 2-d array + Process and sensor noise covariance matrices + NN: 2-d array, optional + Cross covariance matrix + + Returns + ------- + L: 2D array + Kalman estimator gain + P: 2D array + Solution to Riccati equation + .. math:: + A P + P A^T - (P C^T + G N) R^-1 (C P + N^T G^T) + G Q G^T = 0 + E: 1D array + Eigenvalues of estimator poles eig(A - L C) + + + Examples + -------- + >>> K, P, E = lqe(A, G, C, QN, RN) + >>> K, P, E = lqe(A, G, C, QN, RN, NN) + + See Also + -------- + lqr + """ + + # TODO: incorporate cross-covariance NN, something like this, + # which doesn't work for some reason + #if NN is None: + # NN = np.zeros(QN.size(0),RN.size(1)) + #NG = G @ NN + + #LT, P, E = lqr(A.T, C.T, G @ QN @ G.T, RN) + #P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN) + A, G, C = np.array(A, ndmin=2), np.array(G, ndmin=2), np.array(C, ndmin=2) + QN, RN = np.array(QN, ndmin=2), np.array(RN, ndmin=2) + P, E, LT = care(A.T, C.T, np.dot(np.dot(G, QN), G.T), RN) + return _ssmatrix(LT.T), _ssmatrix(P), _ssmatrix(E) + # Contributed by Roberto Bucher def acker(A, B, poles): @@ -239,8 +311,8 @@ def acker(A, B, poles): """ # Convert the inputs to matrices - a = np.mat(A) - b = np.mat(B) + a = _ssmatrix(A) + b = _ssmatrix(B) # Make sure the system is controllable ct = ctrb(A, B) @@ -251,14 +323,15 @@ def acker(A, B, poles): p = np.real(np.poly(poles)) # Place the poles using Ackermann's method + # TODO: compute pmat using Horner's method (O(n) instead of O(n^2)) n = np.size(p) - pmat = p[n-1]*a**0 + pmat = p[n-1] * np.linalg.matrix_power(a, 0) for i in np.arange(1,n): - pmat = pmat + p[n-i-1]*a**i + pmat = pmat + np.dot(p[n-i-1], np.linalg.matrix_power(a, i)) K = np.linalg.solve(ct, pmat) K = K[-1][:] # Extract the last row - return K + return _ssmatrix(K) def lqr(*args, **keywords): """lqr(A, B, Q, R[, N]) @@ -268,7 +341,7 @@ def lqr(*args, **keywords): The lqr() function computes the optimal state feedback controller that minimizes the quadratic cost - .. math:: J = \int_0^\infty (x' Q x + u' R u + 2 x' N u) dt + .. math:: J = \\int_0^\\infty (x' Q x + u' R u + 2 x' N u) dt The function can be called with either 3, 4, or 5 arguments: @@ -293,11 +366,11 @@ def lqr(*args, **keywords): Returns ------- - K: 2-d array + K: 2D array State feedback gains - S: 2-d array + S: 2D array Solution to Riccati equation - E: 1-d array + E: 1D array Eigenvalues of the closed loop system Examples @@ -305,6 +378,10 @@ def lqr(*args, **keywords): >>> K, S, E = lqr(sys, Q, R, [N]) >>> K, S, E = lqr(A, B, Q, R, [N]) + See Also + -------- + lqe + """ # Make sure that SLICOT is installed @@ -366,9 +443,9 @@ def lqr(*args, **keywords): S = X; E = w[0:nstates]; - return K, S, E + return _ssmatrix(K), _ssmatrix(S), E -def ctrb(A,B): +def ctrb(A, B): """Controllabilty matrix Parameters @@ -388,14 +465,14 @@ def ctrb(A,B): """ # Convert input parameters to matrices (if they aren't already) - amat = np.mat(A) - bmat = np.mat(B) + amat = _ssmatrix(A) + bmat = _ssmatrix(B) n = np.shape(amat)[0] + # Construct the controllability matrix - ctrb = bmat - for i in range(1, n): - ctrb = np.hstack((ctrb, amat**i*bmat)) - return ctrb + ctrb = np.hstack([bmat] + [np.dot(np.linalg.matrix_power(amat, i), bmat) + for i in range(1, n)]) + return _ssmatrix(ctrb) def obsv(A, C): """Observability matrix @@ -417,15 +494,14 @@ def obsv(A, C): """ # Convert input parameters to matrices (if they aren't already) - amat = np.mat(A) - cmat = np.mat(C) + amat = _ssmatrix(A) + cmat = _ssmatrix(C) n = np.shape(amat)[0] - # Construct the controllability matrix - obsv = cmat - for i in range(1, n): - obsv = np.vstack((obsv, cmat*amat**i)) - return obsv + # Construct the observability matrix + obsv = np.vstack([cmat] + [np.dot(cmat, np.linalg.matrix_power(amat, i)) + for i in range(1, n)]) + return _ssmatrix(obsv) def gram(sys,type): """Gramian (controllability or observability) @@ -501,7 +577,7 @@ def gram(sys,type): A = np.array(sys.A) # convert to NumPy array for slycot X,scale,sep,ferr,w = sb03md(n, C, A, U, dico, job='X', fact='N', trana=tra) gram = X - return gram + return _ssmatrix(gram) elif type=='cf' or type=='of': #Compute cholesky factored gramian from slycot routine sb03od @@ -524,4 +600,4 @@ def gram(sys,type): C[0:n,0:m] = sys.C.transpose() X,scale,w = sb03od(n, m, A, Q, C.transpose(), dico, fact='N', trans=tra) gram = X - return gram + return _ssmatrix(gram) diff --git a/control/statesp.py b/control/statesp.py index 9a7a69dd8..85d48882a 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -53,8 +53,8 @@ import math import numpy as np -from numpy import all, angle, any, array, asarray, concatenate, cos, delete, \ - dot, empty, exp, eye, isinf, matrix, ones, pad, shape, sin, zeros, squeeze +from numpy import any, array, asarray, concatenate, cos, delete, \ + dot, empty, exp, eye, isinf, ones, pad, sin, zeros, squeeze from numpy.random import rand, randn from numpy.linalg import solve, eigvals, matrix_rank from numpy.linalg.linalg import LinAlgError @@ -62,32 +62,62 @@ from scipy.signal import lti, cont2discrete from warnings import warn from .lti import LTI, timebase, timebaseEqual, isdtime -from .xferfcn import _convert_to_transfer_function +from . import config from copy import deepcopy __all__ = ['StateSpace', 'ss', 'rss', 'drss', 'tf2ss', 'ssdata'] -def _matrix(a): - """Wrapper around numpy.matrix that reshapes empty matrices to be 0x0 +# Define module default parameter values +_statesp_defaults = { + 'statesp.use_numpy_matrix':True, +} + + +def _ssmatrix(data, axis=1): + """Convert argument to a (possibly empty) state space matrix. Parameters ---------- - a: sequence passed to numpy.matrix + data : array, list, or string + Input data defining the contents of the 2D array + axis : 0 or 1 + If input data is 1D, which axis to use for return object. The default + is 1, corresponding to a row matrix. Returns ------- - am: result of numpy.matrix(a), except if a is empty, am will be 0x0. + arr : 2D array, with shape (0, 0) if a is empty - numpy.matrix([]) has size 1x0; for empty StateSpace objects, we - need 0x0 matrices, so use this instead of numpy.matrix in this - module. """ - from numpy import matrix - am = matrix(a, dtype=float) - if (1, 0) == am.shape: - am.shape = (0, 0) - return am + # Convert the data into an array or matrix, as configured + # If data is passed as a string, use (deprecated?) matrix constructor + if config.defaults['statesp.use_numpy_matrix'] or isinstance(data, str): + arr = np.matrix(data, dtype=float) + else: + arr = np.array(data, dtype=float) + ndim = arr.ndim + shape = arr.shape + + # Change the shape of the array into a 2D array + if (ndim > 2): + raise ValueError("state-space matrix must be 2-dimensional") + + elif (ndim == 2 and shape == (1, 0)) or \ + (ndim == 1 and shape == (0, )): + # Passed an empty matrix or empty vector; change shape to (0, 0) + shape = (0, 0) + + elif ndim == 1: + # Passed a row or column vector + shape = (1, shape[0]) if axis == 1 else (shape[0], 1) + + elif ndim == 0: + # Passed a constant; turn into a matrix + shape = (1, 1) + + # Create the actual object used to store the result + return arr.reshape(shape) class StateSpace(LTI): @@ -104,18 +134,28 @@ class StateSpace(LTI): where u is the input, y is the output, and x is the state. The main data members are the A, B, C, and D matrices. The class also - keeps track of the number of states (i.e., the size of A). - - Discrete-time state space system are implemented by using the 'dt' instance - variable and setting it to the sampling period. If 'dt' is not None, - then it must match whenever two state space systems are combined. + keeps track of the number of states (i.e., the size of A). The data + format used to store state space matrices is set using the value of + `config.defaults['use_numpy_matrix']`. If True (default), the state space + elements are stored as `numpy.matrix` objects; otherwise they are + `numpy.ndarray` objects. The :func:`~control.use_numpy_matrix` function + can be used to set the storage type. + + Discrete-time state space system are implemented by using the 'dt' + instance variable and setting it to the sampling period. If 'dt' is not + None, then it must match whenever two state space systems are combined. Setting dt = 0 specifies a continuous system, while leaving dt = None means the system timebase is not specified. If 'dt' is set to True, the - system will be treated as a discrete time system with unspecified - sampling time. + system will be treated as a discrete time system with unspecified sampling + time. + """ - def __init__(self, *args): + # Allow ndarray * StateSpace to give StateSpace._rmul_() priority + __array_priority__ = 11 # override ndarray and matrix types + + + def __init__(self, *args, **kw): """ StateSpace(A, B, C, D[, dt]) @@ -128,7 +168,6 @@ def __init__(self, *args): call StateSpace(sys), where sys is a StateSpace object. """ - if len(args) == 4: # The user provided A, B, C, and D matrices. (A, B, C, D) = args @@ -152,7 +191,17 @@ def __init__(self, *args): else: raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) - A, B, C, D = [_matrix(M) for M in (A, B, C, D)] + # Process keyword arguments + remove_useless = kw.get('remove_useless', True) + + # Convert all matrices to standard form + A = _ssmatrix(A) + B = _ssmatrix(B, axis=0) + C = _ssmatrix(C, axis=1) + if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0: + # If D is a scalar zero, broadcast it to the proper size + D = np.zeros((C.shape[0], B.shape[1])) + D = _ssmatrix(D) # TODO: use super here? LTI.__init__(self, inputs=D.shape[1], outputs=D.shape[0], dt=dt) @@ -183,7 +232,7 @@ def __init__(self, *args): raise ValueError("C and D must have the same number of rows.") # Check for states that don't do anything, and remove them. - self._remove_useless_states() + if remove_useless: self._remove_useless_states() def _remove_useless_states(self): """Check for states that don't do anything, and remove them. @@ -194,12 +243,15 @@ def _remove_useless_states(self): """ - # Search for useless states and get the indices of these states - # as an array. + # Search for useless states and get indices of these states. + # + # Note: shape from np.where depends on whether we are storing state + # space objects as np.matrix or np.array. Code below will work + # correctly in either case. ax1_A = np.where(~self.A.any(axis=1))[0] ax1_B = np.where(~self.B.any(axis=1))[0] - ax0_A = np.where(~self.A.any(axis=0))[1] - ax0_C = np.where(~self.C.any(axis=0))[1] + ax0_A = np.where(~self.A.any(axis=0))[-1] + ax0_C = np.where(~self.C.any(axis=0))[-1] useless_1 = np.intersect1d(ax1_A, ax1_B, assume_unique=True) useless_2 = np.intersect1d(ax0_A, ax0_C, assume_unique=True) useless = np.union1d(useless_1, useless_2) @@ -324,12 +376,14 @@ def __mul__(self, other): # Concatenate the various arrays A = concatenate( - (concatenate((other.A, zeros((other.A.shape[0], self.A.shape[1]))), - axis=1), - concatenate((self.B * other.C, self.A), axis=1)), axis=0) - B = concatenate((other.B, self.B * other.D), axis=0) - C = concatenate((self.D * other.C, self.C),axis=1) - D = self.D * other.D + (concatenate((other.A, + zeros((other.A.shape[0], self.A.shape[1]))), + axis=1), + concatenate((np.dot(self.B, other.C), self.A), axis=1)), + axis=0) + B = concatenate((other.B, np.dot(self.B, other.D)), axis=0) + C = concatenate((np.dot(self.D, other.C), self.C),axis=1) + D = np.dot(self.D, other.D) return StateSpace(A, B, C, D, dt) @@ -353,9 +407,9 @@ def __rmul__(self, other): # try to treat this as a matrix try: - X = _matrix(other) - C = X * self.C - D = X * self.D + X = _ssmatrix(other) + C = np.dot(X, self.C) + D = np.dot(X, self.D) return StateSpace(self.A, self.B, C, D, self.dt) except Exception as e: @@ -404,14 +458,13 @@ def horner(self, s): Returns a matrix of values evaluated at complex variable s. """ - resp = self.C * solve(s * eye(self.states) - self.A, - self.B) + self.D + resp = np.dot(self.C, solve(s * eye(self.states) - self.A, + self.B)) + self.D return array(resp) # Method for generating the frequency response of the system def freqresp(self, omega): - """ - Evaluate the system's transfer func. at a list of freqs, omega. + """Evaluate the system's transfer func. at a list of freqs, omega. mag, phase, omega = self.freqresp(omega) @@ -424,22 +477,25 @@ def freqresp(self, omega): G(exp(j*omega*dt)) = mag*exp(j*phase). - Inputs - ------ - omega: A list of frequencies in radians/sec at which the system - should be evaluated. The list can be either a python list - or a numpy array and will be sorted before evaluation. + Parameters + ---------- + omega : array + A list of frequencies in radians/sec at which the system should be + evaluated. The list can be either a python list or a numpy array + and will be sorted before evaluation. Returns ------- - mag: The magnitude (absolute value, not dB or log10) of the system + mag : float + The magnitude (absolute value, not dB or log10) of the system frequency response. - phase: The wrapped phase in radians of the system frequency - response. + phase : float + The wrapped phase in radians of the system frequency response. - omega: The list of sorted frequencies at which the response - was evaluated. + omega : array + The list of sorted frequencies at which the response was + evaluated. """ @@ -577,7 +633,7 @@ def feedback(self, other=1, sign=-1): C2 = other.C D2 = other.D - F = eye(self.inputs) - sign * D2 * D1 + F = eye(self.inputs) - sign * np.dot(D2, D1) if matrix_rank(F) != self.inputs: raise ValueError("I - sign * D2 * D1 is singular to working precision.") @@ -590,15 +646,20 @@ def feedback(self, other=1, sign=-1): E_D2 = E_D2_C2[:, :other.inputs] E_C2 = E_D2_C2[:, other.inputs:] - T1 = eye(self.outputs) + sign * D1 * E_D2 - T2 = eye(self.inputs) + sign * E_D2 * D1 - - A = concatenate((concatenate((A1 + sign * B1 * E_D2 * C1, sign * B1 * E_C2), axis=1), - concatenate((B2 * T1 * C1, A2 + sign * B2 * D1 * E_C2), axis=1)), - axis=0) - B = concatenate((B1 * T2, B2 * D1 * T2), axis=0) - C = concatenate((T1 * C1, sign * D1 * E_C2), axis=1) - D = D1 * T2 + T1 = eye(self.outputs) + sign * np.dot(D1, E_D2) + T2 = eye(self.inputs) + sign * np.dot(E_D2, D1) + + A = concatenate( + (concatenate( + (A1 + sign * np.dot(np.dot(B1, E_D2), C1), + sign * np.dot(B1, E_C2)), axis=1), + concatenate( + (np.dot(B2, np.dot(T1, C1)), + A2 + sign * np.dot(np.dot(B2, D1), E_C2)), axis=1)), + axis=0) + B = concatenate((np.dot(B1, T2), np.dot(np.dot(B2, D1), T2)), axis=0) + C = concatenate((np.dot(T1, C1), sign * np.dot(D1, E_C2)), axis=1) + D = np.dot(D1, T2) return StateSpace(A, B, C, D, dt) @@ -614,11 +675,11 @@ def lft(self, other, nu=-1, ny=-1): Parameters ---------- - other: LTI + other : LTI The lower LTI system - ny: int, optional + ny : int, optional Dimension of (plant) measurement output. - nu: int, optional + nu : int, optional Dimension of (plant) control input. """ @@ -666,7 +727,7 @@ def lft(self, other, nu=-1, ny=-1): F = np.block([[np.eye(ny), -D22], [-Dbar11, np.eye(nu)]]) if matrix_rank(F) != ny + nu: raise ValueError("lft not well-posed to working precision.") - + # solve for the resulting ss by solving for [y, u] using [x, # xbar] and [w1, w2]. TH = np.linalg.solve(F, np.block( @@ -681,7 +742,7 @@ def lft(self, other, nu=-1, ny=-1): H12 = TH[:ny, self.states + other.states + self.inputs - nu:] H21 = TH[ny:, self.states + other.states: self.states + other.states + self.inputs - nu] H22 = TH[ny:, self.states + other.states + self.inputs - nu:] - + Ares = np.block([ [A + B2.dot(T21), B2.dot(T22)], [Bbar1.dot(T11), Abar + Bbar1.dot(T12)] @@ -741,7 +802,7 @@ def returnScipySignalLTI(self): for i in range(self.outputs): for j in range(self.inputs): out[i][j] = lti(asarray(self.A), asarray(self.B[:, j]), - asarray(self.C[i, :]), asarray(self.D[i, j])) + asarray(self.C[i, :]), self.D[i, j]) return out @@ -793,19 +854,21 @@ def sample(self, Ts, method='zoh', alpha=None): method : {"gbt", "bilinear", "euler", "backward_diff", "zoh"} Which method to use: - * gbt: generalized bilinear transformation - * bilinear: Tustin's approximation ("gbt" with alpha=0.5) - * euler: Euler (or forward differencing) method ("gbt" with alpha=0) - * backward_diff: Backwards differencing ("gbt" with alpha=1.0) - * zoh: zero-order hold (default) + * gbt: generalized bilinear transformation + * bilinear: Tustin's approximation ("gbt" with alpha=0.5) + * euler: Euler (or forward differencing) method ("gbt" with + alpha=0) + * backward_diff: Backwards differencing ("gbt" with alpha=1.0) + * zoh: zero-order hold (default) alpha : float within [0, 1] The generalized bilinear transformation weighting parameter, which - should only be specified with method="gbt", and is ignored otherwise + should only be specified with method="gbt", and is ignored + otherwise Returns ------- - sysd : StateSpace system + sysd : StateSpace Discrete time system, with sampling rate Ts Notes @@ -939,7 +1002,7 @@ def _convertToStateSpace(sys, **kw): # If this is a matrix, try to create a constant feedthrough try: - D = _matrix(sys) + D = _ssmatrix(sys) return StateSpace([], [], [], D) except Exception as e: print("Failure to assume argument is matrix-like in" \ @@ -1079,18 +1142,18 @@ def _mimo2siso(sys, input, output, warn_conversion=False): Parameters ---------- - sys: StateSpace + sys : StateSpace Linear (MIMO) system that should be converted. - input: int + input : int Index of the input that will become the SISO system's only input. - output: int + output : int Index of the output that will become the SISO system's only output. - warn_conversion: bool - If True: print a warning message when sys is a MIMO system. - Warn that a conversion will take place. + warn_conversion : bool, optional + If `True`, print a message when sys is a MIMO system, + warning that a conversion will take place. Default is False. Returns - sys: StateSpace + sys : StateSpace The converted (SISO) system. """ if not (isinstance(input, int) and isinstance(output, int)): @@ -1341,16 +1404,16 @@ def rss(states=1, outputs=1, inputs=1): Parameters ---------- - states: integer + states : integer Number of state variables - inputs: integer + inputs : integer Number of system inputs - outputs: integer + outputs : integer Number of system outputs Returns ------- - sys: StateSpace + sys : StateSpace The randomly created linear system Raises @@ -1379,16 +1442,16 @@ def drss(states=1, outputs=1, inputs=1): Parameters ---------- - states: integer + states : integer Number of state variables - inputs: integer + inputs : integer Number of system inputs - outputs: integer + outputs : integer Number of system outputs Returns ------- - sys: StateSpace + sys : StateSpace The randomly created linear system Raises @@ -1417,7 +1480,7 @@ def ssdata(sys): Parameters ---------- - sys: LTI (StateSpace, or TransferFunction) + sys : LTI (StateSpace, or TransferFunction) LTI system whose data will be returned Returns diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index 07775cb80..ae687df35 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -253,9 +253,27 @@ def assert_equal(x,y): assert_equal(ref.C, tst.C) assert_equal(ref.D, tst.D) + def test_feedback_args(self): + # Added 25 May 2019 to cover missing exception handling in feedback() + # If first argument is not LTI or convertable, generate an exception + args = ([1], self.sys2) + self.assertRaises(TypeError, ctrl.feedback, *args) + + # If second argument is not LTI or convertable, generate an exception + args = (self.sys1, np.array([1])) + self.assertRaises(TypeError, ctrl.feedback, *args) + + # Convert first argument to FRD, if needed + h = TransferFunction([1], [1, 2, 2]) + omega = np.logspace(-1, 2, 10) + frd = ctrl.FRD(h, omega) + sys = ctrl.feedback(1, frd) + self.assertTrue(isinstance(sys, ctrl.FRD)) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestFeedback) + if __name__ == "__main__": unittest.main() diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index 5c73c1f49..8f0248dc7 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -2,9 +2,10 @@ import unittest import numpy as np -from control import ss -from control.canonical import canonical_form - +from control import ss, tf, tf2ss, ss2tf +from control.canonical import canonical_form, reachable_form, \ + observable_form, modal_form, similarity_transform +from control.exception import ControlNotImplemented class TestCanonical(unittest.TestCase): """Tests for the canonical forms class""" @@ -40,6 +41,11 @@ def test_reachable_form(self): np.testing.assert_array_almost_equal(sys_check.D, D_true) np.testing.assert_array_almost_equal(T_check, T_true) + # Reachable form only supports SISO + sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) + np.testing.assert_raises(ControlNotImplemented, reachable_form, sys) + + def test_unreachable_system(self): """Test reachable canonical form with an unreachable system""" @@ -76,12 +82,84 @@ def test_modal_form(self): sys_check, T_check = canonical_form(ss(A, B, C, D), "modal") # Check against the true values - #TODO: Test in respect to ambiguous transformation (system characteristics?) + # TODO: Test in respect to ambiguous transformation (system characteristics?) np.testing.assert_array_almost_equal(sys_check.A, A_true) #np.testing.assert_array_almost_equal(sys_check.B, B_true) #np.testing.assert_array_almost_equal(sys_check.C, C_true) np.testing.assert_array_almost_equal(sys_check.D, D_true) #np.testing.assert_array_almost_equal(T_check, T_true) + + # Check conversion when there are complex eigenvalues + A_true = np.array([[-1, 1, 0, 0], + [-1, -1, 0, 0], + [ 0, 0, -2, 0], + [ 0, 0, 0, -3]]) + B_true = np.array([[0], [1], [0], [1]]) + C_true = np.array([[1, 0, 0, 1]]) + D_true = np.array([[0]]) + + A = np.linalg.solve(T_true, A_true) * T_true + B = np.linalg.solve(T_true, B_true) + C = C_true * T_true + D = D_true + + # Create state space system and convert to modal canonical form + sys_check, T_check = canonical_form(ss(A, B, C, D), 'modal') + + # Check A and D matrix, which are uniquely defined + np.testing.assert_array_almost_equal(sys_check.A, A_true) + np.testing.assert_array_almost_equal(sys_check.D, D_true) + + # B matrix should be all ones (or zero if not controllable) + # TODO: need to update modal_form() to implement this + if np.allclose(T_check, T_true): + np.testing.assert_array_almost_equal(sys_check.B, B_true) + np.testing.assert_array_almost_equal(sys_check.C, C_true) + + # Make sure Hankel coefficients are OK + from numpy.linalg import matrix_power + for i in range(A.shape[0]): + np.testing.assert_almost_equal( + np.dot(np.dot(C_true, matrix_power(A_true, i)), B_true), + np.dot(np.dot(C, matrix_power(A, i)), B)) + + # Reorder rows to get complete coverage (real eigenvalue cxrtvfirst) + A_true = np.array([[-1, 0, 0, 0], + [ 0, -2, 1, 0], + [ 0, -1, -2, 0], + [ 0, 0, 0, -3]]) + B_true = np.array([[0], [0], [1], [1]]) + C_true = np.array([[0, 1, 0, 1]]) + D_true = np.array([[0]]) + + A = np.linalg.solve(T_true, A_true) * T_true + B = np.linalg.solve(T_true, B_true) + C = C_true * T_true + D = D_true + + # Create state space system and convert to modal canonical form + sys_check, T_check = canonical_form(ss(A, B, C, D), 'modal') + + # Check A and D matrix, which are uniquely defined + np.testing.assert_array_almost_equal(sys_check.A, A_true) + np.testing.assert_array_almost_equal(sys_check.D, D_true) + + # B matrix should be all ones (or zero if not controllable) + # TODO: need to update modal_form() to implement this + if np.allclose(T_check, T_true): + np.testing.assert_array_almost_equal(sys_check.B, B_true) + np.testing.assert_array_almost_equal(sys_check.C, C_true) + + # Make sure Hankel coefficients are OK + from numpy.linalg import matrix_power + for i in range(A.shape[0]): + np.testing.assert_almost_equal( + np.dot(np.dot(C_true, matrix_power(A_true, i)), B_true), + np.dot(np.dot(C, matrix_power(A, i)), B)) + + # Modal form only supports SISO + sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) + np.testing.assert_raises(ControlNotImplemented, modal_form, sys) def test_observable_form(self): """Test the observable canonical form""" @@ -114,6 +192,11 @@ def test_observable_form(self): np.testing.assert_array_almost_equal(sys_check.D, D_true) np.testing.assert_array_almost_equal(T_check, T_true) + # Observable form only supports SISO + sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) + np.testing.assert_raises(ControlNotImplemented, observable_form, sys) + + def test_unobservable_system(self): """Test observable canonical form with an unobservable system""" @@ -126,3 +209,88 @@ def test_unobservable_system(self): # Check if an exception is raised np.testing.assert_raises(ValueError, canonical_form, sys, "observable") + + def test_arguments(self): + # Additional unit tests added on 25 May 2019 to increase coverage + + # Unknown canonical forms should generate exception + sys = tf([1], [1, 2, 1]) + np.testing.assert_raises( + ControlNotImplemented, canonical_form, sys, 'unknown') + + def test_similarity(self): + """Test similarty transform""" + + # Single input, single output systems + siso_ini = tf2ss(tf([1, 1], [1, 1, 1])) + for form in 'reachable', 'observable': + # Convert the system to one of the canonical forms + siso_can, T_can = canonical_form(siso_ini, form) + + # Use a similarity transformation to transform it back + siso_sim = similarity_transform(siso_can, np.linalg.inv(T_can)) + + # Make sure everything goes back to the original form + np.testing.assert_array_almost_equal(siso_sim.A, siso_ini.A) + np.testing.assert_array_almost_equal(siso_sim.B, siso_ini.B) + np.testing.assert_array_almost_equal(siso_sim.C, siso_ini.C) + np.testing.assert_array_almost_equal(siso_sim.D, siso_ini.D) + + # Multi-input, multi-output systems + mimo_ini = ss( + [[-1, 1, 0, 0], [0, -2, 1, 0], [0, 0, -3, 1], [0, 0, 0, -4]], + [[1, 0], [0, 0], [0, 1], [1, 1]], + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], + np.zeros((3, 2))) + + # Simple transformation: row/col flips + scaling + mimo_txf = np.array( + [[0, 1, 0, 0], [2, 0, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) + + # Transform the system and transform it back + mimo_sim = similarity_transform(mimo_ini, mimo_txf) + mimo_new = similarity_transform(mimo_sim, np.linalg.inv(mimo_txf)) + np.testing.assert_array_almost_equal(mimo_new.A, mimo_ini.A) + np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) + np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) + np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) + + # Make sure rescaling by identify does nothing + mimo_new = similarity_transform(mimo_ini, np.eye(4)) + np.testing.assert_array_almost_equal(mimo_new.A, mimo_ini.A) + np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) + np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) + np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) + + # Time rescaling + mimo_tim = similarity_transform(mimo_ini, np.eye(4), timescale=0.3) + mimo_new = similarity_transform(mimo_tim, np.eye(4), timescale=1/0.3) + np.testing.assert_array_almost_equal(mimo_new.A, mimo_ini.A) + np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) + np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) + np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) + + # Time + transformation, in one step + mimo_sim = similarity_transform(mimo_ini, mimo_txf, timescale=0.3) + mimo_new = similarity_transform(mimo_sim, np.linalg.inv(mimo_txf), + timescale=1/0.3) + np.testing.assert_array_almost_equal(mimo_new.A, mimo_ini.A) + np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) + np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) + np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) + + # Time + transformation, in two steps + mimo_sim = similarity_transform(mimo_ini, mimo_txf, timescale=0.3) + mimo_tim = similarity_transform(mimo_sim, np.eye(4), timescale=1/0.3) + mimo_new = similarity_transform(mimo_tim, np.linalg.inv(mimo_txf)) + np.testing.assert_array_almost_equal(mimo_new.A, mimo_ini.A) + np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) + np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) + np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestFeedback) + + +if __name__ == "__main__": + unittest.main() diff --git a/control/tests/config_test.py b/control/tests/config_test.py new file mode 100644 index 000000000..c0fc9755b --- /dev/null +++ b/control/tests/config_test.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# +# config_test.py - test config module +# RMM, 25 may 2019 +# +# This test suite checks the functionality of the config module + +import unittest +import numpy as np +import control as ct +import matplotlib.pyplot as plt +from math import pi, log10 + + +class TestConfig(unittest.TestCase): + def setUp(self): + # Create a simple second order system to use for testing + self.sys = ct.tf([10], [1, 2, 1]) + + def test_set_defaults(self): + ct.config.set_defaults('config', test1=1, test2=2, test3=None) + self.assertEqual(ct.config.defaults['config.test1'], 1) + self.assertEqual(ct.config.defaults['config.test2'], 2) + self.assertEqual(ct.config.defaults['config.test3'], None) + + def test_get_param(self): + self.assertEqual( + ct.config._get_param('bode', 'dB'), + ct.config.defaults['bode.dB']) + self.assertEqual(ct.config._get_param('bode', 'dB', 1), 1) + ct.config.defaults['config.test1'] = 1 + self.assertEqual(ct.config._get_param('config', 'test1', None), 1) + self.assertEqual(ct.config._get_param('config', 'test1', None, 1), 1) + + ct.config.defaults['config.test3'] = None + self.assertEqual(ct.config._get_param('config', 'test3'), None) + self.assertEqual(ct.config._get_param('config', 'test3', 1), 1) + self.assertEqual( + ct.config._get_param('config', 'test3', None, 1), None) + + self.assertEqual(ct.config._get_param('config', 'test4'), None) + self.assertEqual(ct.config._get_param('config', 'test4', 1), 1) + self.assertEqual(ct.config._get_param('config', 'test4', 2, 1), 2) + self.assertEqual(ct.config._get_param('config', 'test4', None, 3), 3) + + self.assertEqual( + ct.config._get_param('config', 'test4', {'test4':1}, None), 1) + + + def test_fbs_bode(self): + ct.use_fbs_defaults(); + + # Generate a Bode plot + plt.figure() + omega = np.logspace(-3, 3, 100) + ct.bode_plot(self.sys, omega) + + # Get the magnitude line + mag_axis = plt.gcf().axes[0] + mag_line = mag_axis.get_lines() + mag_data = mag_line[0].get_data() + mag_x, mag_y = mag_data + + # Make sure the x-axis is in rad/sec and y-axis is in natural units + np.testing.assert_almost_equal(mag_x[0], 0.001, decimal=6) + np.testing.assert_almost_equal(mag_y[0], 10, decimal=3) + + # Get the phase line + phase_axis = plt.gcf().axes[1] + phase_line = phase_axis.get_lines() + phase_data = phase_line[0].get_data() + phase_x, phase_y = phase_data + + # Make sure the x-axis is in rad/sec and y-axis is in degrees + np.testing.assert_almost_equal(phase_x[-1], 1000, decimal=0) + np.testing.assert_almost_equal(phase_y[-1], -180, decimal=0) + + # Override the defaults and make sure that works as well + plt.figure() + ct.bode_plot(self.sys, omega, dB=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) + + plt.figure() + ct.bode_plot(self.sys, omega, Hz=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_x[0], 0.001 / (2*pi), decimal=6) + + plt.figure() + ct.bode_plot(self.sys, omega, deg=False) + phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) + + ct.reset_defaults() + + def test_matlab_bode(self): + ct.use_matlab_defaults(); + + # Generate a Bode plot + plt.figure() + omega = np.logspace(-3, 3, 100) + ct.bode_plot(self.sys, omega) + + # Get the magnitude line + mag_axis = plt.gcf().axes[0] + mag_line = mag_axis.get_lines() + mag_data = mag_line[0].get_data() + mag_x, mag_y = mag_data + + # Make sure the x-axis is in Hertz and y-axis is in dB + np.testing.assert_almost_equal(mag_x[0], 0.001 / (2*pi), decimal=6) + np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) + + # Get the phase line + phase_axis = plt.gcf().axes[1] + phase_line = phase_axis.get_lines() + phase_data = phase_line[0].get_data() + phase_x, phase_y = phase_data + + # Make sure the x-axis is in Hertz and y-axis is in degrees + np.testing.assert_almost_equal(phase_x[-1], 1000 / (2*pi), decimal=1) + np.testing.assert_almost_equal(phase_y[-1], -180, decimal=0) + + # Override the defaults and make sure that works as well + plt.figure() + ct.bode_plot(self.sys, omega, dB=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) + + plt.figure() + ct.bode_plot(self.sys, omega, Hz=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_x[0], 0.001 / (2*pi), decimal=6) + + plt.figure() + ct.bode_plot(self.sys, omega, deg=False) + phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) + + ct.reset_defaults() + + def test_custom_bode_default(self): + ct.config.defaults['bode.dB'] = True + ct.config.defaults['bode.deg'] = True + ct.config.defaults['bode.Hz'] = True + + # Generate a Bode plot + plt.figure() + omega = np.logspace(-3, 3, 100) + ct.bode_plot(self.sys, omega, dB=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) + + # Override defaults + plt.figure() + ct.bode_plot(self.sys, omega, Hz=True, deg=False, dB=True) + mag_x, mag_y = (((plt.gcf().axes[0]).get_lines())[0]).get_data() + phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() + np.testing.assert_almost_equal(mag_x[0], 0.001 / (2*pi), decimal=6) + np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) + np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) + + ct.reset_defaults() + + def test_bode_number_of_samples(self): + # Set the number of samples (default is 50, from np.logspace) + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) + self.assertEqual(len(mag_ret), 87) + + # Change the default number of samples + ct.config.defaults['freqplot.number_of_samples'] = 76 + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys) + self.assertEqual(len(mag_ret), 76) + + # Override the default number of samples + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) + self.assertEqual(len(mag_ret), 87) + + ct.reset_defaults() + + def test_bode_feature_periphery_decade(self): + # Generate a sample Bode plot to figure out the range it uses + ct.reset_defaults() # Make sure starting state is correct + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False) + omega_min, omega_max = omega_ret[[0, -1]] + + # Reset the periphery decade value (should add one decade on each end) + ct.config.defaults['freqplot.feature_periphery_decades'] = 2 + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False) + np.testing.assert_almost_equal(omega_ret[0], omega_min/10) + np.testing.assert_almost_equal(omega_ret[-1], omega_max * 10) + + # Make sure it also works in rad/sec, in opposite direction + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=True) + omega_min, omega_max = omega_ret[[0, -1]] + ct.config.defaults['freqplot.feature_periphery_decades'] = 1 + mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=True) + np.testing.assert_almost_equal(omega_ret[0], omega_min*10) + np.testing.assert_almost_equal(omega_ret[-1], omega_max/10) + + ct.reset_defaults() + + def test_reset_defaults(self): + ct.use_matlab_defaults() + ct.reset_defaults() + self.assertEqual(ct.config.defaults['bode.dB'], False) + self.assertEqual(ct.config.defaults['bode.deg'], True) + self.assertEqual(ct.config.defaults['bode.Hz'], False) + self.assertEqual( + ct.config.defaults['freqplot.number_of_samples'], None) + self.assertEqual( + ct.config.defaults['freqplot.feature_periphery_decades'], 1.0) + + def tearDown(self): + # Get rid of any figures that we created + plt.close('all') + + # Reset the configuration defaults + ct.config.reset_defaults() + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp) + + +if __name__ == '__main__': + unittest.main() diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index f2e705ee5..f08a5fa5e 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -340,6 +340,7 @@ def test_sample_system(self): # Check errors self.assertRaises(ValueError, sample_system, self.siso_ss1d, 1) + self.assertRaises(ValueError, sample_system, self.siso_tf1d, 1) self.assertRaises(ValueError, sample_system, self.siso_ss1, 1, 'unknown') def test_sample_ss(self): diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py new file mode 100644 index 000000000..040d7365a --- /dev/null +++ b/control/tests/flatsys_test.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# +# flatsys_test.py - test flat system module +# RMM, 29 Jun 2019 +# +# This test suite checks to make sure that the basic functions supporting +# differential flat systetms are functioning. It doesn't do exhaustive +# testing of operations on flat systems. Separate unit tests should be +# created for that purpose. + +import unittest +import numpy as np +import scipy as sp +import control as ct +import control.flatsys as fs +from distutils.version import StrictVersion + + +class TestFlatSys(unittest.TestCase): + def setUp(self): + ct.use_numpy_matrix(False) + + def test_double_integrator(self): + # Define a second order integrator + sys = ct.StateSpace([[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], 0) + flatsys = fs.LinearFlatSystem(sys) + + # Define the endpoints of a trajectory + x1 = [0, 0]; u1 = [0]; T1 = 1 + x2 = [1, 0]; u2 = [0]; T2 = 2 + x3 = [0, 1]; u3 = [0]; T3 = 3 + x4 = [1, 1]; u4 = [1]; T4 = 4 + + # Define the basis set + poly = fs.PolyFamily(6) + + # Plan trajectories for various combinations + for x0, u0, xf, uf, Tf in [ + (x1, u1, x2, u2, T2), (x1, u1, x3, u3, T3), (x1, u1, x4, u4, T4)]: + traj = fs.point_to_point(flatsys, x0, u0, xf, uf, Tf, basis=poly) + + # Verify that the trajectory computation is correct + x, u = traj.eval([0, Tf]) + np.testing.assert_array_almost_equal(x0, x[:, 0]) + np.testing.assert_array_almost_equal(u0, u[:, 0]) + np.testing.assert_array_almost_equal(xf, x[:, 1]) + np.testing.assert_array_almost_equal(uf, u[:, 1]) + + # Simulate the system and make sure we stay close to desired traj + T = np.linspace(0, Tf, 100) + xd, ud = traj.eval(T) + + t, y, x = ct.forced_response(sys, T, ud, x0) + np.testing.assert_array_almost_equal(x, xd, decimal=3) + + def test_kinematic_car(self): + """Differential flatness for a kinematic car""" + def vehicle_flat_forward(x, u, params={}): + b = params.get('wheelbase', 3.) # get parameter values + zflag = [np.zeros(3), np.zeros(3)] # list for flag arrays + zflag[0][0] = x[0] # flat outputs + zflag[1][0] = x[1] + zflag[0][1] = u[0] * np.cos(x[2]) # first derivatives + zflag[1][1] = u[0] * np.sin(x[2]) + thdot = (u[0]/b) * np.tan(u[1]) # dtheta/dt + zflag[0][2] = -u[0] * thdot * np.sin(x[2]) # second derivatives + zflag[1][2] = u[0] * thdot * np.cos(x[2]) + return zflag + + def vehicle_flat_reverse(zflag, params={}): + b = params.get('wheelbase', 3.) # get parameter values + x = np.zeros(3); u = np.zeros(2) # vectors to store x, u + x[0] = zflag[0][0] # x position + x[1] = zflag[1][0] # y position + x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # angle + u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2]) + thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2]) + u[1] = np.arctan2(thdot_v, u[0]**2 / b) + return x, u + + def vehicle_update(t, x, u, params): + b = params.get('wheelbase', 3.) # get parameter values + dx = np.array([ + np.cos(x[2]) * u[0], + np.sin(x[2]) * u[0], + (u[0]/b) * np.tan(u[1]) + ]) + return dx + + def vehicle_output(t, x, u, params): return x + + # Create differentially flat input/output system + vehicle_flat = fs.FlatSystem( + vehicle_flat_forward, vehicle_flat_reverse, vehicle_update, + vehicle_output, inputs=('v', 'delta'), outputs=('x', 'y', 'theta'), + states=('x', 'y', 'theta')) + + # Define the endpoints of the trajectory + x0 = [0., -2., 0.]; u0 = [10., 0.] + xf = [100., 2., 0.]; uf = [10., 0.] + Tf = 10 + + # Define a set of basis functions to use for the trajectories + poly = fs.PolyFamily(6) + + # Find trajectory between initial and final conditions + traj = fs.point_to_point(vehicle_flat, x0, u0, xf, uf, Tf, basis=poly) + + # Verify that the trajectory computation is correct + x, u = traj.eval([0, Tf]) + np.testing.assert_array_almost_equal(x0, x[:, 0]) + np.testing.assert_array_almost_equal(u0, u[:, 0]) + np.testing.assert_array_almost_equal(xf, x[:, 1]) + np.testing.assert_array_almost_equal(uf, u[:, 1]) + + # Simulate the system and make sure we stay close to desired traj + T = np.linspace(0, Tf, 500) + xd, ud = traj.eval(T) + + # For SciPy 1.0+, integrate equations and compare to desired + if StrictVersion(sp.__version__) >= "1.0": + t, y, x = ct.input_output_response( + vehicle_flat, T, ud, x0, return_x=True) + np.testing.assert_allclose(x, xd, atol=0.01, rtol=0.01) + + def tearDown(self): + ct.reset_defaults() + + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestFlatSys) + + +if __name__ == '__main__': + unittest.main() diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index dca1c762c..1a6a263f3 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -5,7 +5,9 @@ import unittest +import sys as pysys import numpy as np +import control as ct from control.statesp import StateSpace from control.xferfcn import TransferFunction from control.frdata import FRD, _convertToFRD @@ -22,6 +24,7 @@ class TestFRD(unittest.TestCase): def testBadInputType(self): """Give the constructor invalid input types.""" self.assertRaises(ValueError, FRD) + self.assertRaises(TypeError, FRD, [1]) def testInconsistentDimension(self): self.assertRaises(TypeError, FRD, [1, 1], [1, 2, 3]) @@ -278,7 +281,140 @@ def testAgainstOctave(self): np.exp(1j*f1.freqresp([1.0])[1])).reshape(3, 2), np.matrix('0.4-0.2j 0; 0 0.1-0.2j; 0 0.3-0.1j')) - + def test_string_representation(self): + sys = FRD([1, 2, 3], [4, 5, 6]) + print(sys) # Just print without checking + + def test_frequency_mismatch(self): + # Overlapping but non-equal frequency ranges + sys1 = FRD([1, 2, 3], [4, 5, 6]) + sys2 = FRD([2, 3, 4], [5, 6, 7]) + self.assertRaises(NotImplementedError, FRD.__add__, sys1, sys2) + + # One frequency range is a subset of another + sys1 = FRD([1, 2, 3], [4, 5, 6]) + sys2 = FRD([2, 3], [4, 5]) + self.assertRaises(NotImplementedError, FRD.__add__, sys1, sys2) + + def test_size_mismatch(self): + sys1 = FRD(ct.rss(2, 2, 2), np.logspace(-1, 1, 10)) + + # Different number of inputs + sys2 = FRD(ct.rss(3, 1, 2), np.logspace(-1, 1, 10)) + self.assertRaises(ValueError, FRD.__add__, sys1, sys2) + + # Different number of outputs + sys2 = FRD(ct.rss(3, 2, 1), np.logspace(-1, 1, 10)) + self.assertRaises(ValueError, FRD.__add__, sys1, sys2) + + # Inputs and outputs don't match + self.assertRaises(ValueError, FRD.__mul__, sys2, sys1) + + # Feedback mismatch + self.assertRaises(ValueError, FRD.feedback, sys2, sys1) + + def test_operator_conversion(self): + sys_tf = ct.tf([1], [1, 2, 1]) + frd_tf = FRD(sys_tf, np.logspace(-1, 1, 10)) + frd_2 = FRD(2 * np.ones(10), np.logspace(-1, 1, 10)) + + # Make sure that we can add, multiply, and feedback constants + sys_add = frd_tf + 2 + chk_add = frd_tf + frd_2 + np.testing.assert_array_almost_equal(sys_add.omega, chk_add.omega) + np.testing.assert_array_almost_equal(sys_add.fresp, chk_add.fresp) + + sys_radd = 2 + frd_tf + chk_radd = frd_2 + frd_tf + np.testing.assert_array_almost_equal(sys_radd.omega, chk_radd.omega) + np.testing.assert_array_almost_equal(sys_radd.fresp, chk_radd.fresp) + + sys_sub = frd_tf - 2 + chk_sub = frd_tf - frd_2 + np.testing.assert_array_almost_equal(sys_sub.omega, chk_sub.omega) + np.testing.assert_array_almost_equal(sys_sub.fresp, chk_sub.fresp) + + sys_rsub = 2 - frd_tf + chk_rsub = frd_2 - frd_tf + np.testing.assert_array_almost_equal(sys_rsub.omega, chk_rsub.omega) + np.testing.assert_array_almost_equal(sys_rsub.fresp, chk_rsub.fresp) + + sys_mul = frd_tf * 2 + chk_mul = frd_tf * frd_2 + np.testing.assert_array_almost_equal(sys_mul.omega, chk_mul.omega) + np.testing.assert_array_almost_equal(sys_mul.fresp, chk_mul.fresp) + + sys_rmul = 2 * frd_tf + chk_rmul = frd_2 * frd_tf + np.testing.assert_array_almost_equal(sys_rmul.omega, chk_rmul.omega) + np.testing.assert_array_almost_equal(sys_rmul.fresp, chk_rmul.fresp) + + sys_rdiv = 2 / frd_tf + chk_rdiv = frd_2 / frd_tf + np.testing.assert_array_almost_equal(sys_rdiv.omega, chk_rdiv.omega) + np.testing.assert_array_almost_equal(sys_rdiv.fresp, chk_rdiv.fresp) + + sys_pow = frd_tf**2 + chk_pow = FRD(sys_tf**2, np.logspace(-1, 1, 10)) + np.testing.assert_array_almost_equal(sys_pow.omega, chk_pow.omega) + np.testing.assert_array_almost_equal(sys_pow.fresp, chk_pow.fresp) + + sys_pow = frd_tf**-2 + chk_pow = FRD(sys_tf**-2, np.logspace(-1, 1, 10)) + np.testing.assert_array_almost_equal(sys_pow.omega, chk_pow.omega) + np.testing.assert_array_almost_equal(sys_pow.fresp, chk_pow.fresp) + + # Assertion error if we try to raise to a non-integer power + self.assertRaises(ValueError, FRD.__pow__, frd_tf, 0.5) + + # Selected testing on transfer function conversion + sys_add = frd_2 + sys_tf + chk_add = frd_2 + frd_tf + np.testing.assert_array_almost_equal(sys_add.omega, chk_add.omega) + np.testing.assert_array_almost_equal(sys_add.fresp, chk_add.fresp) + + # Input/output mismatch size mismatch in rmul + sys1 = FRD(ct.rss(2, 2, 2), np.logspace(-1, 1, 10)) + self.assertRaises(ValueError, FRD.__rmul__, frd_2, sys1) + + # Make sure conversion of something random generates exception + self.assertRaises(TypeError, FRD.__add__, frd_tf, 'string') + + def test_eval(self): + sys_tf = ct.tf([1], [1, 2, 1]) + frd_tf = FRD(sys_tf, np.logspace(-1, 1, 3)) + np.testing.assert_almost_equal(sys_tf.evalfr(1), frd_tf.eval(1)) + + # Should get an error if we evaluate at an unknown frequency + self.assertRaises(ValueError, frd_tf.eval, 2) + + # This test only works in Python 3 due to a conflict with the same + # warning type in other test modules (frd_test.py). See + # https://bugs.python.org/issue4180 for more details + @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") + def test_evalfr_deprecated(self): + sys_tf = ct.tf([1], [1, 2, 1]) + frd_tf = FRD(sys_tf, np.logspace(-1, 1, 3)) + + # Deprecated version of the call (should generate warning) + import warnings + with warnings.catch_warnings(): + # Make warnings generate an exception + warnings.simplefilter('error') + + # Make sure that we get a pending deprecation warning + self.assertRaises(PendingDeprecationWarning, frd_tf.evalfr, 1.) + + # FRD.evalfr() is being deprecated + import warnings + with warnings.catch_warnings(): + # Make warnings generate an exception + warnings.simplefilter('error') + + # Make sure that we get a pending deprecation warning + self.assertRaises(PendingDeprecationWarning, frd_tf.evalfr, 1.) + + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestFRD) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index a06e4e8e2..9c1382d8a 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -113,6 +113,7 @@ def test_bode_margin(self): num = [1000] den = [1, 25, 100, 0] sys = ctrl.tf(num, den) + plt.figure() ctrl.bode_plot(sys, margins=True,dB=False,deg = True, Hz=False) fig = plt.gcf() allaxes = fig.get_axes() @@ -198,6 +199,42 @@ def test_discrete(self): # Calling bode should generate a not implemented error self.assertRaises(NotImplementedError, bode, (sys,)) + def test_options(self): + """Test ability to set parameter values""" + # Generate a Bode plot of a transfer function + sys = ctrl.tf([1000], [1, 25, 100, 0]) + fig1 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) + + # Save the parameter values + left1, right1 = fig1.axes[0].xaxis.get_data_interval() + numpoints1 = len(fig1.axes[0].lines[0].get_data()[0]) + + # Same transfer function, but add a decade on each end + ctrl.config.set_defaults('freqplot', feature_periphery_decades=2) + fig2 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) + left2, right2 = fig2.axes[0].xaxis.get_data_interval() + + # Make sure we got an extra decade on each end + self.assertAlmostEqual(left2, 0.1 * left1) + self.assertAlmostEqual(right2, 10 * right1) + + # Same transfer function, but add more points to the plot + ctrl.config.set_defaults( + 'freqplot', feature_periphery_decades=2, number_of_samples=13) + fig3 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) + numpoints3 = len(fig3.axes[0].lines[0].get_data()[0]) + + # Make sure we got the right number of points + self.assertNotEqual(numpoints1, numpoints3) + self.assertEqual(numpoints3, 13) + + # Reset default parameters to avoid contamination + ctrl.config.reset_defaults() + + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py new file mode 100644 index 000000000..aaf2243c1 --- /dev/null +++ b/control/tests/iosys_test.py @@ -0,0 +1,980 @@ +#!/usr/bin/env python +# +# iosys_test.py - test input/output system oeprations +# RMM, 17 Apr 2019 +# +# This test suite checks to make sure that basic input/output class +# operations are working. It doesn't do exhaustive testing of +# operations on input/output systems. Separate unit tests should be +# created for that purpose. + +from __future__ import print_function +import unittest +import warnings +import numpy as np +import scipy as sp +import control as ct +import control.iosys as ios +from distutils.version import StrictVersion + +class TestIOSys(unittest.TestCase): + def setUp(self): + # Turn off numpy matrix warnings + import warnings + warnings.simplefilter('ignore', category=PendingDeprecationWarning) + + # Create a single input/single output linear system + self.siso_linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]]) + + # Create a multi input/multi output linear system + self.mimo_linsys1 = ct.StateSpace( + [[-1, 1], [0, -2]], [[1, 0], [0, 1]], + [[1, 0], [0, 1]], np.zeros((2,2))) + + # Create a multi input/multi output linear system + self.mimo_linsys2 = ct.StateSpace( + [[-1, 1], [0, -2]], [[0, 1], [1, 0]], + [[1, 0], [0, 1]], np.zeros((2,2))) + + # Create simulation parameters + self.T = np.linspace(0, 10, 100) + self.U = np.sin(self.T) + self.X0 = [0, 0] + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_linear_iosys(self): + # Create an input/output system from the linear system + linsys = self.siso_linsys + iosys = ios.LinearIOSystem(linsys) + + # Make sure that the right hand side matches linear system + for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)): + np.testing.assert_array_almost_equal( + np.reshape(iosys._rhs(0, x, u), (-1,1)), + linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u) + + # Make sure that simulations also line up + T, U, X0 = self.T, self.U, self.X0 + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_tf2io(self): + # Create a transfer function from the state space system + linsys = self.siso_linsys + tfsys = ct.ss2tf(linsys) + iosys = ct.tf2io(tfsys) + + # Verify correctness via simulation + T, U, X0 = self.T, self.U, self.X0 + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + def test_ss2io(self): + # Create an input/output system from the linear system + linsys = self.siso_linsys + iosys = ct.ss2io(linsys) + np.testing.assert_array_equal(linsys.A, iosys.A) + np.testing.assert_array_equal(linsys.B, iosys.B) + np.testing.assert_array_equal(linsys.C, iosys.C) + np.testing.assert_array_equal(linsys.D, iosys.D) + + # Try adding names to things + iosys_named = ct.ss2io(linsys, inputs='u', outputs='y', + states=['x1', 'x2'], name='iosys_named') + self.assertEqual(iosys_named.find_input('u'), 0) + self.assertEqual(iosys_named.find_input('x'), None) + self.assertEqual(iosys_named.find_output('y'), 0) + self.assertEqual(iosys_named.find_output('u'), None) + self.assertEqual(iosys_named.find_state('x0'), None) + self.assertEqual(iosys_named.find_state('x1'), 0) + self.assertEqual(iosys_named.find_state('x2'), 1) + np.testing.assert_array_equal(linsys.A, iosys_named.A) + np.testing.assert_array_equal(linsys.B, iosys_named.B) + np.testing.assert_array_equal(linsys.C, iosys_named.C) + np.testing.assert_array_equal(linsys.D, iosys_named.D) + + # Make sure unspecified inputs/outputs/states are handled properly + def test_iosys_unspecified(self): + # System with unspecified inputs and outputs + sys = ios.NonlinearIOSystem(secord_update, secord_output) + np.testing.assert_raises(TypeError, sys.__mul__, sys) + + # Make sure we can print various types of I/O systems + def test_iosys_print(self): + # Send the output to /dev/null + import os + f = open(os.devnull,"w") + + # Simple I/O system + iosys = ct.ss2io(self.siso_linsys) + print(iosys, file=f) + + # I/O system without ninputs, noutputs + ios_unspecified = ios.NonlinearIOSystem(secord_update, secord_output) + print(ios_unspecified, file=f) + + # I/O system with derived inputs and outputs + ios_linearized = ios.linearize(ios_unspecified, [0, 0], [0]) + print(ios_linearized, file=f) + + f.close() + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_nonlinear_iosys(self): + # Create a simple nonlinear I/O system + nlsys = ios.NonlinearIOSystem(predprey) + T = self.T + + # Start by simulating from an equilibrium point + X0 = [0, 0] + ios_t, ios_y = ios.input_output_response(nlsys, T, 0, X0) + np.testing.assert_array_almost_equal(ios_y, np.zeros(np.shape(ios_y))) + + # Now simulate from a nonzero point + X0 = [0.5, 0.5] + ios_t, ios_y = ios.input_output_response(nlsys, T, 0, X0) + + # + # Simulate a linear function as a nonlinear function and compare + # + # Create a single input/single output linear system + linsys = self.siso_linsys + + # Create a nonlinear system with the same dynamics + nlupd = lambda t, x, u, params: \ + np.reshape(linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u, (-1,)) + nlout = lambda t, x, u, params: \ + np.reshape(linsys.C * np.reshape(x, (-1, 1)) + linsys.D * u, (-1,)) + nlsys = ios.NonlinearIOSystem(nlupd, nlout) + + # Make sure that simulations also line up + T, U, X0 = self.T, self.U, self.X0 + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) + ios_t, ios_y = ios.input_output_response(nlsys, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + def test_linearize(self): + # Create a single input/single output linear system + linsys = self.siso_linsys + iosys = ios.LinearIOSystem(linsys) + + # Linearize it and make sure we get back what we started with + linearized = iosys.linearize([0, 0], 0) + np.testing.assert_array_almost_equal(linsys.A, linearized.A) + np.testing.assert_array_almost_equal(linsys.B, linearized.B) + np.testing.assert_array_almost_equal(linsys.C, linearized.C) + np.testing.assert_array_almost_equal(linsys.D, linearized.D) + + # Create a simple nonlinear system to check (kinematic car) + def kincar_update(t, x, u, params): + return np.array([np.cos(x[2]) * u[0], np.sin(x[2]) * u[0], u[1]]) + def kincar_output(t, x, u, params): + return np.array([x[0], x[1]]) + iosys = ios.NonlinearIOSystem(kincar_update, kincar_output) + linearized = iosys.linearize([0, 0, 0], [0, 0]) + np.testing.assert_array_almost_equal(linearized.A, np.zeros((3,3))) + np.testing.assert_array_almost_equal( + linearized.B, [[1, 0], [0, 0], [0, 1]]) + np.testing.assert_array_almost_equal( + linearized.C, [[1, 0, 0], [0, 1, 0]]) + np.testing.assert_array_almost_equal(linearized.D, np.zeros((2,2))) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_connect(self): + # Define a couple of (linear) systems to interconnection + linsys1 = self.siso_linsys + iosys1 = ios.LinearIOSystem(linsys1) + linsys2 = self.siso_linsys + iosys2 = ios.LinearIOSystem(linsys2) + + # Connect systems in different ways and compare to StateSpace + linsys_series = linsys2 * linsys1 + iosys_series = ios.InterconnectedSystem( + (iosys1, iosys2), # systems + ((1, 0),), # interconnection (series) + 0, # input = first system + 1 # output = second system + ) + + # Run a simulation and compare to linear response + T, U = self.T, self.U + X0 = np.concatenate((self.X0, self.X0)) + ios_t, ios_y, ios_x = ios.input_output_response( + iosys_series, T, U, X0, return_x=True) + lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + # Connect systems with different timebases + linsys2c = self.siso_linsys + linsys2c.dt = 0 # Reset the timebase + iosys2c = ios.LinearIOSystem(linsys2c) + iosys_series = ios.InterconnectedSystem( + (iosys1, iosys2c), # systems + ((1, 0),), # interconnection (series) + 0, # input = first system + 1 # output = second system + ) + self.assertTrue(ct.isctime(iosys_series, strict=True)) + ios_t, ios_y, ios_x = ios.input_output_response( + iosys_series, T, U, X0, return_x=True) + lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + # Feedback interconnection + linsys_feedback = ct.feedback(linsys1, linsys2) + iosys_feedback = ios.InterconnectedSystem( + (iosys1, iosys2), # systems + ((1, 0), # input of sys2 = output of sys1 + (0, (1, 0, -1))), # input of sys1 = -output of sys2 + 0, # input = first system + 0 # output = first system + ) + ios_t, ios_y, ios_x = ios.input_output_response( + iosys_feedback, T, U, X0, return_x=True) + lti_t, lti_y, lti_x = ct.forced_response(linsys_feedback, T, U, X0) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_static_nonlinearity(self): + # Linear dynamical system + linsys = self.siso_linsys + ioslin = ios.LinearIOSystem(linsys) + + # Nonlinear saturation + sat = lambda u: u if abs(u) < 1 else np.sign(u) + sat_output = lambda t, x, u, params: sat(u) + nlsat = ios.NonlinearIOSystem(None, sat_output, inputs=1, outputs=1) + + # Set up parameters for simulation + T, U, X0 = self.T, 2 * self.U, self.X0 + Usat = np.vectorize(sat)(U) + + # Make sure saturation works properly by comparing linear system with + # saturated input to nonlinear system with saturation composition + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, Usat, X0) + ios_t, ios_y, ios_x = ios.input_output_response( + ioslin * nlsat, T, U, X0, return_x=True) + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=2) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_algebraic_loop(self): + # Create some linear and nonlinear systems to play with + linsys = self.siso_linsys + lnios = ios.LinearIOSystem(linsys) + nlios = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1) + nlios1 = nlios.copy() + nlios2 = nlios.copy() + + # Set up parameters for simulation + T, U, X0 = self.T, self.U, self.X0 + + # Single nonlinear system - no states + ios_t, ios_y = ios.input_output_response(nlios, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, U*U, decimal=3) + + # Composed nonlinear system (series) + ios_t, ios_y = ios.input_output_response(nlios1 * nlios2, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, U**4, decimal=3) + + # Composed nonlinear system (parallel) + ios_t, ios_y = ios.input_output_response(nlios1 + nlios2, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, 2*U**2, decimal=3) + + # Nonlinear system composed with LTI system (series) + ios_t, ios_y = ios.input_output_response( + nlios * lnios * nlios, T, U, X0) + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U*U, X0) + np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3) + + # Nonlinear system in feeback loop with LTI system + iosys = ios.InterconnectedSystem( + (lnios, nlios), # linear system w/ nonlinear feedback + ((1,), # feedback interconnection (sig to 0) + (0, (1, 0, -1))), + 0, # input to linear system + 0 # output from linear system + ) + ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) + # No easy way to test the result + + # Algebraic loop from static nonlinear system in feedback + # (error will be due to no states) + iosys = ios.InterconnectedSystem( + (nlios1, nlios2), # two copies of a static nonlinear system + ((0, 1), # feedback interconnection + (1, (0, 0, -1))), + 0, 0 + ) + args = (iosys, T, U, X0) + self.assertRaises(RuntimeError, ios.input_output_response, *args) + + # Algebraic loop due to feedthrough term + linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[1]]) + lnios = ios.LinearIOSystem(linsys) + iosys = ios.InterconnectedSystem( + (nlios, lnios), # linear system w/ nonlinear feedback + ((0, 1), # feedback interconnection + (1, (0, 0, -1))), + 0, 0 + ) + args = (iosys, T, U, X0) + # ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) + self.assertRaises(RuntimeError, ios.input_output_response, *args) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_summer(self): + # Construct a MIMO system for testing + linsys = self.mimo_linsys1 + linio = ios.LinearIOSystem(linsys) + + linsys_parallel = linsys + linsys + iosys_parallel = linio + linio + + # Set up parameters for simulation + T = self.T + U = [np.sin(T), np.cos(T)] + X0 = 0 + + lin_t, lin_y, lin_x = ct.forced_response(linsys_parallel, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys_parallel, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_rmul(self): + # Test right multiplication + # TODO: replace with better tests when conversions are implemented + + # Set up parameters for simulation + T, U, X0 = self.T, self.U, self.X0 + + # Linear system with input and output nonlinearities + # Also creates a nested interconnected system + ioslin = ios.LinearIOSystem(self.siso_linsys) + nlios = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1) + sys1 = nlios * ioslin + sys2 = ios.InputOutputSystem.__rmul__(nlios, sys1) + + # Make sure we got the right thing (via simulation comparison) + ios_t, ios_y = ios.input_output_response(sys2, T, U, X0) + lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0) + np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_neg(self): + """Test negation of a system""" + + # Set up parameters for simulation + T, U, X0 = self.T, self.U, self.X0 + + # Static nonlinear system + nlios = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1) + ios_t, ios_y = ios.input_output_response(-nlios, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, -U*U, decimal=3) + + # Linear system with input nonlinearity + # Also creates a nested interconnected system + ioslin = ios.LinearIOSystem(self.siso_linsys) + sys = (ioslin) * (-nlios) + + # Make sure we got the right thing (via simulation comparison) + ios_t, ios_y = ios.input_output_response(sys, T, U, X0) + lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0) + np.testing.assert_array_almost_equal(ios_y, -lti_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_feedback(self): + # Set up parameters for simulation + T, U, X0 = self.T, self.U, self.X0 + + # Linear system with constant feedback (via "nonlinear" mapping) + ioslin = ios.LinearIOSystem(self.siso_linsys) + nlios = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u, inputs=1, outputs=1) + iosys = ct.feedback(ioslin, nlios) + linsys = ct.feedback(self.siso_linsys, 1) + + ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lti_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_bdalg_functions(self): + """Test block diagram functions algebra on I/O systems""" + # Set up parameters for simulation + T = self.T + U = [np.sin(T), np.cos(T)] + X0 = 0 + + # Set up systems to be composed + linsys1 = self.mimo_linsys1 + linio1 = ios.LinearIOSystem(linsys1) + linsys2 = self.mimo_linsys2 + linio2 = ios.LinearIOSystem(linsys2) + + # Series interconnection + linsys_series = ct.series(linsys1, linsys2) + iosys_series = ct.series(linio1, linio2) + lin_t, lin_y, lin_x = ct.forced_response(linsys_series, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys_series, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Make sure that systems don't commute + linsys_series = ct.series(linsys2, linsys1) + lin_t, lin_y, lin_x = ct.forced_response(linsys_series, T, U, X0) + self.assertFalse((np.abs(lin_y - ios_y) < 1e-3).all()) + + # Parallel interconnection + linsys_parallel = ct.parallel(linsys1, linsys2) + iosys_parallel = ct.parallel(linio1, linio2) + lin_t, lin_y, lin_x = ct.forced_response(linsys_parallel, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys_parallel, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Negation + linsys_negate = ct.negate(linsys1) + iosys_negate = ct.negate(linio1) + lin_t, lin_y, lin_x = ct.forced_response(linsys_negate, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys_negate, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Feedback interconnection + linsys_feedback = ct.feedback(linsys1, linsys2) + iosys_feedback = ct.feedback(linio1, linio2) + lin_t, lin_y, lin_x = ct.forced_response(linsys_feedback, T, U, X0) + ios_t, ios_y = ios.input_output_response(iosys_feedback, T, U, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_nonsquare_bdalg(self): + # Set up parameters for simulation + T = self.T + U2 = [np.sin(T), np.cos(T)] + U3 = [np.sin(T), np.cos(T), T] + X0 = 0 + + # Set up systems to be composed + linsys_2i3o = ct.StateSpace( + [[-1, 1, 0], [0, -2, 0], [0, 0, -3]], [[1, 0], [0, 1], [1, 1]], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], np.zeros((3, 2))) + iosys_2i3o = ios.LinearIOSystem(linsys_2i3o) + + linsys_3i2o = ct.StateSpace( + [[-1, 1, 0], [0, -2, 0], [0, 0, -3]], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + [[1, 0, 1], [0, 1, -1]], np.zeros((2, 3))) + iosys_3i2o = ios.LinearIOSystem(linsys_3i2o) + + # Multiplication + linsys_multiply = linsys_3i2o * linsys_2i3o + iosys_multiply = iosys_3i2o * iosys_2i3o + lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U2, X0) + ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U2, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + linsys_multiply = linsys_2i3o * linsys_3i2o + iosys_multiply = iosys_2i3o * iosys_3i2o + lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U3, X0) + ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Right multiplication + # TODO: add real tests once conversion from other types is supported + iosys_multiply = ios.InputOutputSystem.__rmul__(iosys_3i2o, iosys_2i3o) + ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Feedback + linsys_multiply = ct.feedback(linsys_3i2o, linsys_2i3o) + iosys_multiply = iosys_3i2o.feedback(iosys_2i3o) + lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U3, X0) + ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Mismatch should generate exception + args = (iosys_3i2o, iosys_3i2o) + self.assertRaises(ValueError, ct.series, *args) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_discrete(self): + """Test discrete time functionality""" + # Create some linear and nonlinear systems to play with + linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]], True) + lnios = ios.LinearIOSystem(linsys) + + # Set up parameters for simulation + T, U, X0 = self.T, self.U, self.X0 + + # Simulate and compare to LTI output + ios_t, ios_y = ios.input_output_response(lnios, T, U, X0) + lin_t, lin_y, lin_x = ct.forced_response(linsys, T, U, X0) + np.testing.assert_array_almost_equal(ios_t, lin_t, decimal=3) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + # Test MIMO system, converted to discrete time + linsys = ct.StateSpace(self.mimo_linsys1) + linsys.dt = self.T[1] - self.T[0] + lnios = ios.LinearIOSystem(linsys) + + # Set up parameters for simulation + T = self.T + U = [np.sin(T), np.cos(T)] + X0 = 0 + + # Simulate and compare to LTI output + ios_t, ios_y = ios.input_output_response(lnios, T, U, X0) + lin_t, lin_y, lin_x = ct.forced_response(linsys, T, U, X0) + np.testing.assert_array_almost_equal(ios_t, lin_t, decimal=3) + np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3) + + def test_find_eqpts(self): + """Test find_eqpt function""" + # Simple equilibrium point with no inputs + nlsys = ios.NonlinearIOSystem(predprey) + xeq, ueq, result = ios.find_eqpt( + nlsys, [1.6, 1.2], None, return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal(xeq, [1.64705879, 1.17923874]) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((2,))) + + # Ducted fan dynamics with output = velocity + nlsys = ios.NonlinearIOSystem(pvtol, lambda t, x, u, params: x[0:2]) + + # Make sure the origin is a fixed point + xeq, ueq, result = ios.find_eqpt( + nlsys, [0, 0, 0, 0], [0, 4*9.8], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((4,))) + np.testing.assert_array_almost_equal(xeq, [0, 0, 0, 0]) + + # Use a small lateral force to cause motion + xeq, ueq, result = ios.find_eqpt( + nlsys, [0, 0, 0, 0], [0.01, 4*9.8], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + + # Equilibrium point with fixed output + xeq, ueq, result = ios.find_eqpt( + nlsys, [0, 0, 0, 0], [0.01, 4*9.8], + y0=[0.1, 0.1], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + + # Specify outputs to constrain (replicate previous) + xeq, ueq, result = ios.find_eqpt( + nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], + iy = [0, 1], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + + # Specify inputs to constrain (replicate previous), w/ no result + xeq, ueq = ios.find_eqpt( + nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], iu = []) + np.testing.assert_array_almost_equal( + nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + np.testing.assert_array_almost_equal( + nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + + # Now solve the problem with the original PVTOL variables + # Constrain the output angle and x velocity + nlsys_full = ios.NonlinearIOSystem(pvtol_full, None) + xeq, ueq, result = ios.find_eqpt( + nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8], + y0=[0, 0, 0.1, 0.1, 0, 0], iy = [2, 3], + idx=[2, 3, 4, 5], ix=[0, 1], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5) + np.testing.assert_array_almost_equal( + nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) + + # Fix one input and vary the other + nlsys_full = ios.NonlinearIOSystem(pvtol_full, None) + xeq, ueq, result = ios.find_eqpt( + nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8], + y0=[0, 0, 0.1, 0.1, 0, 0], iy=[3], iu=[1], + idx=[2, 3, 4, 5], ix=[0, 1], return_result=True) + self.assertTrue(result.success) + np.testing.assert_almost_equal(ueq[1], 4*9.8, decimal=5) + np.testing.assert_array_almost_equal( + nlsys_full._out(0, xeq, ueq)[[3]], [0.1], decimal=5) + np.testing.assert_array_almost_equal( + nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) + + # PVTOL with output = y velocity + xeq, ueq, result = ios.find_eqpt( + nlsys_full, [0, 0, 0, 0.1, 0, 0], [0.01, 4*9.8], + y0=[0, 0, 0, 0.1, 0, 0], iy=[3], + dx0=[0.1, 0, 0, 0, 0, 0], idx=[1, 2, 3, 4, 5], + ix=[0, 1], return_result=True) + self.assertTrue(result.success) + np.testing.assert_array_almost_equal( + nlsys_full._out(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5) + np.testing.assert_array_almost_equal( + nlsys_full._rhs(0, xeq, ueq)[-5:], np.zeros((5,)), decimal=5) + + # Unobservable system + linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[0, 0]], [[0]]) + lnios = ios.LinearIOSystem(linsys) + + # If result is returned, user has to check + xeq, ueq, result = ios.find_eqpt( + lnios, [0, 0], [0], y0=[1], return_result=True) + self.assertFalse(result.success) + + # If result is not returned, find_eqpt should return None + xeq, ueq = ios.find_eqpt(lnios, [0, 0], [0], y0=[1]) + self.assertEqual(xeq, None) + self.assertEqual(ueq, None) + + @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", + "requires SciPy 1.0 or greater") + def test_params(self): + # Start with the default set of parameters + ios_secord_default = ios.NonlinearIOSystem( + secord_update, secord_output, inputs=1, outputs=1, states=2) + lin_secord_default = ios.linearize(ios_secord_default, [0, 0], [0]) + w_default, v_default = np.linalg.eig(lin_secord_default.A) + + # New copy, with modified parameters + ios_secord_update = ios.NonlinearIOSystem( + secord_update, secord_output, inputs=1, outputs=1, states=2, + params={'omega0':2, 'zeta':0}) + + # Make sure the default parameters haven't changed + lin_secord_check = ios.linearize(ios_secord_default, [0, 0], [0]) + w, v = np.linalg.eig(lin_secord_check.A) + np.testing.assert_array_almost_equal(np.sort(w), np.sort(w_default)) + + # Make sure updated system parameters got set correctly + lin_secord_update = ios.linearize(ios_secord_update, [0, 0], [0]) + w, v = np.linalg.eig(lin_secord_update.A) + np.testing.assert_array_almost_equal(np.sort(w), np.sort([2j, -2j])) + + # Change the parameters of the default sys just for the linearization + lin_secord_local = ios.linearize(ios_secord_default, [0, 0], [0], + params={'zeta':0}) + w, v = np.linalg.eig(lin_secord_local.A) + np.testing.assert_array_almost_equal(np.sort(w), np.sort([1j, -1j])) + + # Change the parameters of the updated sys just for the linearization + lin_secord_local = ios.linearize(ios_secord_update, [0, 0], [0], + params={'zeta':0, 'omega0':3}) + w, v = np.linalg.eig(lin_secord_local.A) + np.testing.assert_array_almost_equal(np.sort(w), np.sort([3j, -3j])) + + # Make sure that changes propagate through interconnections + ios_series_default_local = ios_secord_default * ios_secord_update + lin_series_default_local = ios.linearize( + ios_series_default_local, [0, 0, 0, 0], [0]) + w, v = np.linalg.eig(lin_series_default_local.A) + np.testing.assert_array_almost_equal( + np.sort(w), np.sort(np.concatenate((w_default, [2j, -2j])))) + + # Show that we can change the parameters at linearization + lin_series_override = ios.linearize( + ios_series_default_local, [0, 0, 0, 0], [0], + params={'zeta':0, 'omega0':4}) + w, v = np.linalg.eig(lin_series_override.A) + np.testing.assert_array_almost_equal(w, [4j, -4j, 4j, -4j]) + + # Check for warning if we try to set params for LinearIOSystem + linsys = self.siso_linsys + iosys = ios.LinearIOSystem(linsys) + T, U, X0 = self.T, self.U, self.X0 + lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) + with warnings.catch_warnings(record=True) as warnval: + # Turn off deprecation warnings + warnings.simplefilter("ignore", category=DeprecationWarning) + warnings.simplefilter("ignore", category=PendingDeprecationWarning) + + # Trigger a warning + ios_t, ios_y = ios.input_output_response( + iosys, T, U, X0, params={'something':0}) + + # Verify that we got a warning + self.assertEqual(len(warnval), 1) + self.assertTrue(issubclass(warnval[-1].category, UserWarning)) + self.assertTrue("LinearIOSystem" in str(warnval[-1].message)) + self.assertTrue("ignored" in str(warnval[-1].message)) + + # Check to make sure results are OK + np.testing.assert_array_almost_equal(lti_t, ios_t) + np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) + + def test_named_signals(self): + sys1 = ios.NonlinearIOSystem( + updfcn = lambda t, x, u, params: np.array( + np.dot(self.mimo_linsys1.A, np.reshape(x, (-1, 1))) \ + + np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1))) + ).reshape(-1,), + outfcn = lambda t, x, u, params: np.array( + self.mimo_linsys1.C * np.reshape(x, (-1, 1)) \ + + self.mimo_linsys1.D * np.reshape(u, (-1, 1)) + ).reshape(-1,), + inputs = ('u[0]', 'u[1]'), + outputs = ('y[0]', 'y[1]'), + states = self.mimo_linsys1.states, + name = 'sys1') + sys2 = ios.LinearIOSystem(self.mimo_linsys2, + inputs = ('u[0]', 'u[1]'), + outputs = ('y[0]', 'y[1]'), + name = 'sys2') + + # Series interconnection (sys1 * sys2) using __mul__ + ios_mul = sys1 * sys2 + ss_series = self.mimo_linsys1 * self.mimo_linsys2 + lin_series = ct.linearize(ios_mul, 0, 0) + for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B), + (ss_series.C, lin_series.C), (ss_series.D, lin_series.D)): + np.testing.assert_array_almost_equal(M, N) + + # Series interconnection (sys1 * sys2) using series + ios_series = ct.series(sys2, sys1) + ss_series = ct.series(self.mimo_linsys2, self.mimo_linsys1) + lin_series = ct.linearize(ios_series, 0, 0) + for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B), + (ss_series.C, lin_series.C), (ss_series.D, lin_series.D)): + np.testing.assert_array_almost_equal(M, N) + + # Series interconnection (sys1 * sys2) using named + mixed signals + ios_connect = ios.InterconnectedSystem( + (sys2, sys1), + connections=( + (('sys1', 'u[0]'), 'sys2.y[0]'), + ('sys1.u[1]', 'sys2.y[1]') + ), + inplist=('sys2.u[0]', ('sys2', 1)), + outlist=((1, 'y[0]'), 'sys1.y[1]') + ) + lin_series = ct.linearize(ios_connect, 0, 0) + for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B), + (ss_series.C, lin_series.C), (ss_series.D, lin_series.D)): + np.testing.assert_array_almost_equal(M, N) + + # Make sure that we can use input signal names as system outputs + ios_connect = ios.InterconnectedSystem( + (sys1, sys2), + connections=( + ('sys2.u[0]', 'sys1.y[0]'), ('sys2.u[1]', 'sys1.y[1]'), + ('sys1.u[0]', '-sys2.y[0]'), ('sys1.u[1]', '-sys2.y[1]') + ), + inplist=('sys1.u[0]', 'sys1.u[1]'), + outlist=('sys2.u[0]', 'sys2.u[1]') # = sys1.y[0], sys1.y[1] + ) + ss_feedback = ct.feedback(self.mimo_linsys1, self.mimo_linsys2) + lin_feedback = ct.linearize(ios_connect, 0, 0) + np.testing.assert_array_almost_equal(ss_feedback.A, lin_feedback.A) + np.testing.assert_array_almost_equal(ss_feedback.B, lin_feedback.B) + np.testing.assert_array_almost_equal(ss_feedback.C, lin_feedback.C) + np.testing.assert_array_almost_equal(ss_feedback.D, lin_feedback.D) + + def test_lineariosys_statespace(self): + """Make sure that a LinearIOSystem is also a StateSpace object""" + iosys_siso = ct.LinearIOSystem(self.siso_linsys) + self.assertTrue(isinstance(iosys_siso, ct.StateSpace)) + + # Make sure that state space functions work for LinearIOSystems + np.testing.assert_array_equal( + iosys_siso.pole(), self.siso_linsys.pole()) + omega = np.logspace(.1, 10, 100) + mag_io, phase_io, omega_io = iosys_siso.freqresp(omega) + mag_ss, phase_ss, omega_ss = self.siso_linsys.freqresp(omega) + np.testing.assert_array_equal(mag_io, mag_ss) + np.testing.assert_array_equal(phase_io, phase_ss) + np.testing.assert_array_equal(omega_io, omega_ss) + + # LinearIOSystem methods should override StateSpace methods + io_mul = iosys_siso * iosys_siso + self.assertTrue(isinstance(io_mul, ct.InputOutputSystem)) + + # But also retain linear structure + self.assertTrue(isinstance(io_mul, ct.StateSpace)) + + # And make sure the systems match + ss_series = self.siso_linsys * self.siso_linsys + np.testing.assert_array_equal(io_mul.A, ss_series.A) + np.testing.assert_array_equal(io_mul.B, ss_series.B) + np.testing.assert_array_equal(io_mul.C, ss_series.C) + np.testing.assert_array_equal(io_mul.D, ss_series.D) + + # Make sure that series does the same thing + io_series = ct.series(iosys_siso, iosys_siso) + self.assertTrue(isinstance(io_series, ct.InputOutputSystem)) + self.assertTrue(isinstance(io_series, ct.StateSpace)) + np.testing.assert_array_equal(io_series.A, ss_series.A) + np.testing.assert_array_equal(io_series.B, ss_series.B) + np.testing.assert_array_equal(io_series.C, ss_series.C) + np.testing.assert_array_equal(io_series.D, ss_series.D) + + # Test out feedback as well + io_feedback = ct.feedback(iosys_siso, iosys_siso) + self.assertTrue(isinstance(io_series, ct.InputOutputSystem)) + + # But also retain linear structure + self.assertTrue(isinstance(io_series, ct.StateSpace)) + + # And make sure the systems match + ss_feedback = ct.feedback(self.siso_linsys, self.siso_linsys) + np.testing.assert_array_equal(io_feedback.A, ss_feedback.A) + np.testing.assert_array_equal(io_feedback.B, ss_feedback.B) + np.testing.assert_array_equal(io_feedback.C, ss_feedback.C) + np.testing.assert_array_equal(io_feedback.D, ss_feedback.D) + + def test_duplicates(self): + nlios = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1) + + # Turn off deprecation warnings + warnings.simplefilter("ignore", category=DeprecationWarning) + warnings.simplefilter("ignore", category=PendingDeprecationWarning) + + # Duplicate objects + with warnings.catch_warnings(record=True) as warnval: + # Trigger a warning + ios_series = nlios * nlios + + # Verify that we got a warning + self.assertEqual(len(warnval), 1) + self.assertTrue(issubclass(warnval[-1].category, UserWarning)) + self.assertTrue("Duplicate object" in str(warnval[-1].message)) + + # Nonduplicate objects + nlios1 = nlios.copy() + nlios2 = nlios.copy() + with warnings.catch_warnings(record=True) as warnval: + ios_series = nlios1 * nlios2 + self.assertEqual(len(warnval), 0) + + # Duplicate names + iosys_siso = ct.LinearIOSystem(self.siso_linsys) + nlios1 = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys") + nlios2 = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys") + with warnings.catch_warnings(record=True) as warnval: + # Trigger a warning + iosys = ct.InterconnectedSystem( + (nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0) + + # Verify that we got a warning + self.assertEqual(len(warnval), 1) + self.assertTrue(issubclass(warnval[-1].category, UserWarning)) + self.assertTrue("Duplicate name" in str(warnval[-1].message)) + + # Same system, different names => everything should be OK + nlios1 = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios1") + nlios2 = ios.NonlinearIOSystem(None, \ + lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios2") + with warnings.catch_warnings(record=True) as warnval: + iosys = ct.InterconnectedSystem( + (nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0) + self.assertEqual(len(warnval), 0) + + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp) + + +# Predator prey dynamics +def predprey(t, x, u, params={}): + r = params.get('r', 2) + d = params.get('d', 0.7) + b = params.get('b', 0.3) + k = params.get('k', 10) + a = params.get('a', 8) + c = params.get('c', 4) + + # Dynamics for the system + dx0 = r * x[0] * (1 - x[0]/k) - a * x[1] * x[0]/(c + x[0]) + dx1 = b * a * x[1] * x[0] / (c + x[0]) - d * x[1] + + return np.array([dx0, dx1]) + + +# Reduced planar vertical takeoff and landing dynamics +def pvtol(t, x, u, params={}): + from math import sin, cos + m = params.get('m', 4.) # kg, system mass + J = params.get('J', 0.0475) # kg m^2, system inertia + r = params.get('r', 0.25) # m, thrust offset + g = params.get('g', 9.8) # m/s, gravitational constant + c = params.get('c', 0.05) # N s/m, rotational damping + l = params.get('c', 0.1) # m, pivot location + return np.array([ + x[3], + -c/m * x[1] + 1/m * cos(x[0]) * u[0] - 1/m * sin(x[0]) * u[1], + -g - c/m * x[2] + 1/m * sin(x[0]) * u[0] + 1/m * cos(x[0]) * u[1], + -l/J * sin(x[0]) + r/J * u[0] + ]) + +def pvtol_full(t, x, u, params={}): + from math import sin, cos + m = params.get('m', 4.) # kg, system mass + J = params.get('J', 0.0475) # kg m^2, system inertia + r = params.get('r', 0.25) # m, thrust offset + g = params.get('g', 9.8) # m/s, gravitational constant + c = params.get('c', 0.05) # N s/m, rotational damping + l = params.get('c', 0.1) # m, pivot location + return np.array([ + x[3], x[4], x[5], + -c/m * x[3] + 1/m * cos(x[2]) * u[0] - 1/m * sin(x[2]) * u[1], + -g - c/m * x[4] + 1/m * sin(x[2]) * u[0] + 1/m * cos(x[2]) * u[1], + -l/J * sin(x[2]) + r/J * u[0] + ]) + + +# Second order system dynamics +def secord_update(t, x, u, params={}): + omega0 = params.get('omega0', 1.) + zeta = params.get('zeta', 0.5) + u = np.array(u, ndmin=1) + return np.array([ + x[1], + -2 * zeta * omega0 * x[1] - omega0*omega0 * x[0] + u[0] + ]) +def secord_output(t, x, u, params={}): + return np.array([x[0]]) + + +if __name__ == '__main__': + unittest.main() diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 1fc05a852..65023302a 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -5,7 +5,8 @@ from control.lti import * from control.xferfcn import tf from control import c2d -import numpy as np +from control.matlab import tf2ss +from control.exception import slycot_check class TestUtils(unittest.TestCase): def test_pole(self): @@ -18,6 +19,33 @@ def test_zero(self): np.testing.assert_equal(sys.zero(), 42) np.testing.assert_equal(zero(sys), 42) + def test_issiso(self): + self.assertEqual(issiso(1), True) + self.assertRaises(ValueError, issiso, 1, strict=True) + + # SISO transfer function + sys = tf([-1, 42], [1, 10]) + self.assertEqual(issiso(sys), True) + self.assertEqual(issiso(sys, strict=True), True) + + # SISO state space system + sys = tf2ss(sys) + self.assertEqual(issiso(sys), True) + self.assertEqual(issiso(sys, strict=True), True) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_issiso_mimo(self): + # MIMO transfer function + sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]], + [[[1, 10], [1, 20]], [[1, 30], [1, 40]]]); + self.assertEqual(issiso(sys), False) + self.assertEqual(issiso(sys, strict=True), False) + + # MIMO state space system + sys = tf2ss(sys) + self.assertEqual(issiso(sys), False) + self.assertEqual(issiso(sys, strict=True), False) + def test_damp(self): # Test the continuous time case. zeta = 0.1 @@ -41,3 +69,9 @@ def test_dcgain(self): sys = tf(84, [1, 2]) np.testing.assert_equal(sys.dcgain(), 42) np.testing.assert_equal(dcgain(sys), 42) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestUtils) + +if __name__ == "__main__": + unittest.main() diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 5eadcfefa..a5b609067 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -43,198 +43,262 @@ """ import unittest -from numpy import matrix -from numpy.testing import assert_array_almost_equal, assert_array_less +from numpy import array +from numpy.testing import assert_array_almost_equal, assert_array_less, \ + assert_raises # need scipy version of eigvals for generalized eigenvalue problem from scipy.linalg import eigvals, solve from scipy import zeros,dot from control.mateqn import lyap,dlyap,care,dare -from control.exception import slycot_check +from control.exception import slycot_check, ControlArgument @unittest.skipIf(not slycot_check(), "slycot not installed") class TestMatrixEquations(unittest.TestCase): """These are tests for the matrix equation solvers in mateqn.py""" def test_lyap(self): - A = matrix([[-1, 1],[-1, 0]]) - Q = matrix([[1,0],[0,1]]) + A = array([[-1, 1],[-1, 0]]) + Q = array([[1,0],[0,1]]) X = lyap(A,Q) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X + X * A.T + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X) + X.dot(A.T) + Q, zeros((2,2))) - A = matrix([[1, 2],[-3, -4]]) - Q = matrix([[3, 1],[1, 1]]) + A = array([[1, 2],[-3, -4]]) + Q = array([[3, 1],[1, 1]]) X = lyap(A,Q) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X + X * A.T + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X) + X.dot(A.T) + Q, zeros((2,2))) def test_lyap_sylvester(self): A = 5 - B = matrix([[4, 3], [4, 3]]) - C = matrix([2, 1]) + B = array([[4, 3], [4, 3]]) + C = array([2, 1]) X = lyap(A,B,C) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X + X * B + C, zeros((1,2))) + assert_array_almost_equal(A * X + X.dot(B) + C, zeros((1,2))) - A = matrix([[2,1],[1,2]]) - B = matrix([[1,2],[0.5,0.1]]) - C = matrix([[1,0],[0,1]]) + A = array([[2,1],[1,2]]) + B = array([[1,2],[0.5,0.1]]) + C = array([[1,0],[0,1]]) X = lyap(A,B,C) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X + X * B + C, zeros((2,2))) + assert_array_almost_equal(A.dot(X) + X.dot(B) + C, zeros((2,2))) def test_lyap_g(self): - A = matrix([[-1, 2],[-3, -4]]) - Q = matrix([[3, 1],[1, 1]]) - E = matrix([[1,2],[2,1]]) + A = array([[-1, 2],[-3, -4]]) + Q = array([[3, 1],[1, 1]]) + E = array([[1,2],[2,1]]) X = lyap(A,Q,None,E) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * E.T + E * X * A.T + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(E.T) + E.dot(X).dot(A.T) + Q, zeros((2,2))) def test_dlyap(self): - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[1,0],[0,1]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[1,0],[0,1]]) X = dlyap(A,Q) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * A.T - X + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(A.T) - X + Q, zeros((2,2))) - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[3, 1],[1, 1]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[3, 1],[1, 1]]) X = dlyap(A,Q) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * A.T - X + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(A.T) - X + Q, zeros((2,2))) def test_dlyap_g(self): - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[3, 1],[1, 1]]) - E = matrix([[1, 1],[2, 1]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[3, 1],[1, 1]]) + E = array([[1, 1],[2, 1]]) X = dlyap(A,Q,None,E) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * A.T - E * X * E.T + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(A.T) - E.dot(X).dot(E.T) + Q, zeros((2,2))) def test_dlyap_sylvester(self): A = 5 - B = matrix([[4, 3], [4, 3]]) - C = matrix([2, 1]) + B = array([[4, 3], [4, 3]]) + C = array([2, 1]) X = dlyap(A,B,C) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * B.T - X + C, zeros((1,2))) + assert_array_almost_equal(A * X.dot(B.T) - X + C, zeros((1,2))) - A = matrix([[2,1],[1,2]]) - B = matrix([[1,2],[0.5,0.1]]) - C = matrix([[1,0],[0,1]]) + A = array([[2,1],[1,2]]) + B = array([[1,2],[0.5,0.1]]) + C = array([[1,0],[0,1]]) X = dlyap(A,B,C) # print("The solution obtained is ", X) - assert_array_almost_equal(A * X * B.T - X + C, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(B.T) - X + C, zeros((2,2))) def test_care(self): - A = matrix([[-2, -1],[-1, -1]]) - Q = matrix([[0, 0],[0, 1]]) - B = matrix([[1, 0],[0, 4]]) + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1, 0],[0, 4]]) X,L,G = care(A,B,Q) # print("The solution obtained is", X) - assert_array_almost_equal(A.T * X + X * A - X * B * B.T * X + Q, + assert_array_almost_equal(A.T.dot(X) + X.dot(A) - X.dot(B).dot(B.T).dot(X) + Q, zeros((2,2))) - assert_array_almost_equal(B.T * X, G) + assert_array_almost_equal(B.T.dot(X), G) def test_care_g(self): - A = matrix([[-2, -1],[-1, -1]]) - Q = matrix([[0, 0],[0, 1]]) - B = matrix([[1, 0],[0, 4]]) - R = matrix([[2, 0],[0, 1]]) - S = matrix([[0, 0],[0, 0]]) - E = matrix([[2, 1],[1, 2]]) + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1, 0],[0, 4]]) + R = array([[2, 0],[0, 1]]) + S = array([[0, 0],[0, 0]]) + E = array([[2, 1],[1, 2]]) X,L,G = care(A,B,Q,R,S,E) # print("The solution obtained is", X) + Gref = solve(R, B.T.dot(X).dot(E) + S.T) + assert_array_almost_equal(Gref, G) assert_array_almost_equal( - A.T * X * E + E.T * X * A - - (E.T * X * B + S) * solve(R, B.T * X * E + S.T) + Q, zeros((2,2))) - assert_array_almost_equal(solve(R, B.T * X * E + S.T), G) + A.T.dot(X).dot(E) + E.T.dot(X).dot(A) + - (E.T.dot(X).dot(B) + S).dot(Gref) + Q, + zeros((2,2))) - A = matrix([[-2, -1],[-1, -1]]) - Q = matrix([[0, 0],[0, 1]]) - B = matrix([[1],[0]]) + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1],[0]]) R = 1 - S = matrix([[1],[0]]) - E = matrix([[2, 1],[1, 2]]) + S = array([[1],[0]]) + E = array([[2, 1],[1, 2]]) X,L,G = care(A,B,Q,R,S,E) # print("The solution obtained is", X) + Gref = 1/R * (B.T.dot(X).dot(E) + S.T) assert_array_almost_equal( - A.T * X * E + E.T * X * A - - (E.T * X * B + S) / R * (B.T * X * E + S.T) + Q , zeros((2,2))) - assert_array_almost_equal(dot( 1/R , dot(B.T,dot(X,E)) + S.T) , G) + A.T.dot(X).dot(E) + E.T.dot(X).dot(A) + - (E.T.dot(X).dot(B) + S).dot(Gref) + Q , + zeros((2,2))) + assert_array_almost_equal(Gref , G) def test_dare(self): - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[2, 1],[1, 0]]) - B = matrix([[2, 1],[0, 1]]) - R = matrix([[1, 0],[0, 1]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 0]]) + B = array([[2, 1],[0, 1]]) + R = array([[1, 0],[0, 1]]) X,L,G = dare(A,B,Q,R) # print("The solution obtained is", X) + Gref = solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A)) + assert_array_almost_equal(Gref, G) assert_array_almost_equal( - A.T * X * A - X - - A.T * X * B * solve(B.T * X * B + R, B.T * X * A) + Q, zeros((2,2))) - assert_array_almost_equal(solve(B.T * X * B + R, B.T * X * A), G) + A.T.dot(X).dot(A) - X - + A.T.dot(X).dot(B).dot(Gref) + Q, + zeros((2,2))) # check for stable closed loop - lam = eigvals(A - B * G) + lam = eigvals(A - B.dot(G)) assert_array_less(abs(lam), 1.0) - A = matrix([[1, 0],[-1, 1]]) - Q = matrix([[0, 1],[1, 1]]) - B = matrix([[1],[0]]) + A = array([[1, 0],[-1, 1]]) + Q = array([[0, 1],[1, 1]]) + B = array([[1],[0]]) R = 2 X,L,G = dare(A,B,Q,R) # print("The solution obtained is", X) assert_array_almost_equal( - A.T * X * A - X - - A.T * X * B * solve(B.T * X * B + R, B.T * X * A) + Q, zeros((2,2))) - assert_array_almost_equal(B.T * X * A / (B.T * X * B + R), G) + A.T.dot(X).dot(A) - X - + A.T.dot(X).dot(B) * solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A)) + Q, zeros((2,2))) + assert_array_almost_equal(B.T.dot(X).dot(A) / (B.T.dot(X).dot(B) + R), G) # check for stable closed loop - lam = eigvals(A - B * G) + lam = eigvals(A - B.dot(G)) assert_array_less(abs(lam), 1.0) def test_dare_g(self): - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[2, 1],[1, 3]]) - B = matrix([[1, 5],[2, 4]]) - R = matrix([[1, 0],[0, 1]]) - S = matrix([[1, 0],[2, 0]]) - E = matrix([[2, 1],[1, 2]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 3]]) + B = array([[1, 5],[2, 4]]) + R = array([[1, 0],[0, 1]]) + S = array([[1, 0],[2, 0]]) + E = array([[2, 1],[1, 2]]) X,L,G = dare(A,B,Q,R,S,E) # print("The solution obtained is", X) + Gref = solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A) + S.T) + assert_array_almost_equal(Gref,G) assert_array_almost_equal( - A.T * X * A - E.T * X * E - - (A.T * X * B + S) * solve(B.T * X * B + R, B.T * X * A + S.T) + Q, + A.T.dot(X).dot(A) - E.T.dot(X).dot(E) + - (A.T.dot(X).dot(B) + S).dot(Gref) + Q, zeros((2,2)) ) - assert_array_almost_equal(solve(B.T * X * B + R, B.T * X * A + S.T), G) # check for stable closed loop - lam = eigvals(A - B * G, E) + lam = eigvals(A - B.dot(G), E) assert_array_less(abs(lam), 1.0) - A = matrix([[-0.6, 0],[-0.1, -0.4]]) - Q = matrix([[2, 1],[1, 3]]) - B = matrix([[1],[2]]) + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 3]]) + B = array([[1],[2]]) R = 1 - S = matrix([[1],[2]]) - E = matrix([[2, 1],[1, 2]]) + S = array([[1],[2]]) + E = array([[2, 1],[1, 2]]) X,L,G = dare(A,B,Q,R,S,E) # print("The solution obtained is", X) assert_array_almost_equal( - A.T * X * A - E.T * X * E - - (A.T * X * B + S) * solve(B.T * X * B + R, B.T * X * A + S.T) + Q, + A.T.dot(X).dot(A) - E.T.dot(X).dot(E) - + (A.T.dot(X).dot(B) + S).dot(solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A) + S.T)) + Q, zeros((2,2)) ) - assert_array_almost_equal((B.T * X * A + S.T) / (B.T * X * B + R), G) + assert_array_almost_equal((B.T.dot(X).dot(A) + S.T) / (B.T.dot(X).dot(B) + R), G) # check for stable closed loop - lam = eigvals(A - B * G, E) + lam = eigvals(A - B.dot(G), E) assert_array_less(abs(lam), 1.0) + def test_raise(self): + """ Test exception raise for invalid inputs """ + + # correct shapes and forms + A = array([[1, 0], [-1, -1]]) + Q = array([[2, 1], [1, 2]]) + C = array([[1, 0], [0, 1]]) + E = array([[2, 1], [1, 2]]) + + # these fail + Afq = array([[1, 0, 0], [-1, -1, 0]]) + Qfq = array([[2, 1, 0], [1, 2, 0]]) + Qfs = array([[2, 1], [-1, 2]]) + Cfd = array([[1, 0, 0], [0, 1, 0]]) + Efq = array([[2, 1, 0], [1, 2, 0]]) + + for cdlyap in [lyap, dlyap]: + assert_raises(ControlArgument, cdlyap, Afq, Q) + assert_raises(ControlArgument, cdlyap, A, Qfq) + assert_raises(ControlArgument, cdlyap, A, Qfs) + assert_raises(ControlArgument, cdlyap, Afq, Q, C) + assert_raises(ControlArgument, cdlyap, A, Qfq, C) + assert_raises(ControlArgument, cdlyap, A, Q, Cfd) + assert_raises(ControlArgument, cdlyap, A, Qfq, None, E) + assert_raises(ControlArgument, cdlyap, A, Q, None, Efq) + assert_raises(ControlArgument, cdlyap, A, Qfs, None, E) + assert_raises(ControlArgument, cdlyap, A, Q, C, E) + + B = array([[1, 0], [0, 1]]) + Bf = array([[1, 0], [0, 1], [1, 1]]) + R = Q + Rfs = Qfs + Rfq = Qfq + S = array([[0, 0], [0, 0]]) + Sf = array([[0, 0, 0], [0, 0, 0]]) + E = array([[2, 1], [1, 2]]) + Ef = array([[2, 1], [1, 2], [1, 2]]) + + assert_raises(ControlArgument, care, Afq, B, Q) + assert_raises(ControlArgument, care, A, B, Qfq) + assert_raises(ControlArgument, care, A, Bf, Q) + assert_raises(ControlArgument, care, 1, B, 1) + assert_raises(ControlArgument, care, A, B, Qfs) + assert_raises(ValueError, dare, A, B, Q, Rfs) + for cdare in [care, dare]: + assert_raises(ControlArgument, cdare, Afq, B, Q, R, S, E) + assert_raises(ControlArgument, cdare, A, B, Qfq, R, S, E) + assert_raises(ControlArgument, cdare, A, Bf, Q, R, S, E) + assert_raises(ControlArgument, cdare, A, B, Q, R, S, Ef) + assert_raises(ControlArgument, cdare, A, B, Q, Rfq, S, E) + assert_raises(ControlArgument, cdare, A, B, Q, R, Sf, E) + assert_raises(ControlArgument, cdare, A, B, Qfs, R, S, E) + assert_raises(ControlArgument, cdare, A, B, Q, Rfs, S, E) + assert_raises(ControlArgument, cdare, A, B, Q, R, S) + + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestMatrixEquations) diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index d187f6125..0e7060bea 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -664,6 +664,22 @@ def testCombi01(self): self.assertAlmostEqual(wg, 0.176469728448) self.assertAlmostEqual(wp, 0.0616288455466) + def test_tf_string_args(self): + # Make sure that the 's' variable is defined properly + s = tf('s') + G = (s + 1)/(s**2 + 2*s + 1) + np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) + np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) + self.assertTrue(isctime(G, strict=True)) + + # Make sure that the 'z' variable is defined properly + z = tf('z') + G = (z + 1)/(z**2 + 2*z + 1) + np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) + np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) + self.assertTrue(isdtime(G, strict=True)) + + #! TODO: not yet implemented # def testMIMOtfdata(self): # sisotf = ss2tf(self.siso_ss1) diff --git a/control/tests/modelsimp_array_test.py b/control/tests/modelsimp_array_test.py new file mode 100644 index 000000000..f56f492a8 --- /dev/null +++ b/control/tests/modelsimp_array_test.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# +# modelsimp_test.py - test model reduction functions +# RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) + +import unittest +import numpy as np +import warnings +import control +from control.modelsimp import * +from control.matlab import * +from control.exception import slycot_check + +class TestModelsimp(unittest.TestCase): + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + control.use_numpy_matrix(False) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testHSVD(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + C = np.array([[6., 8.]]) + D = np.array([[9.]]) + sys = ss(A,B,C,D) + hsv = hsvd(sys) + hsvtrue = np.array([24.42686, 0.5731395]) # from MATLAB + np.testing.assert_array_almost_equal(hsv, hsvtrue) + + # Make sure default type values are correct + self.assertTrue(isinstance(hsv, np.ndarray)) + self.assertFalse(isinstance(hsv, np.matrix)) + + # Check that using numpy.matrix does *not* affect answer + with warnings.catch_warnings(record=True) as w: + control.use_numpy_matrix(True) + self.assertTrue(issubclass(w[-1].category, UserWarning)) + + # Redefine the system (using np.matrix for storage) + sys = ss(A, B, C, D) + + # Compute the Hankel singular value decomposition + hsv = hsvd(sys) + + # Make sure that return type is correct + self.assertTrue(isinstance(hsv, np.ndarray)) + self.assertFalse(isinstance(hsv, np.matrix)) + + # Go back to using the normal np.array representation + control.use_numpy_matrix(False) + + def testMarkov(self): + U = np.array([[1.], [1.], [1.], [1.], [1.]]) + Y = U + M = 3 + H = markov(Y,U,M) + Htrue = np.array([[1.], [0.], [0.]]) + np.testing.assert_array_almost_equal( H, Htrue ) + + def testModredMatchDC(self): + #balanced realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = np.array( + [[-1.958, -1.194, 1.824, -1.464], + [-1.194, -0.8344, 2.563, -1.351], + [-1.824, -2.563, -1.124, 2.704], + [-1.464, -1.351, -2.704, -11.08]]) + B = np.array([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) + C = np.array([[-0.9057, -0.4068, 0.3263, -0.3474]]) + D = np.array([[0.]]) + sys = ss(A,B,C,D) + rsys = modred(sys,[2, 3],'matchdc') + Artrue = np.array([[-4.431, -4.552], [-4.552, -5.361]]) + Brtrue = np.array([[-1.362], [-1.031]]) + Crtrue = np.array([[-1.362, -1.031]]) + Drtrue = np.array([[-0.08384]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=3) + np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=3) + np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=3) + np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=2) + + def testModredUnstable(self): + # Check if an error is thrown when an unstable system is given + A = np.array( + [[4.5418, 3.3999, 5.0342, 4.3808], + [0.3890, 0.3599, 0.4195, 0.1760], + [-4.2117, -3.2395, -4.6760, -4.2180], + [0.0052, 0.0429, 0.0155, 0.2743]]) + B = np.array([[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]]) + C = np.array([[1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]]) + D = np.array([[0.0, 0.0], [0.0, 0.0]]) + sys = ss(A,B,C,D) + np.testing.assert_raises(ValueError, modred, sys, [2, 3]) + + def testModredTruncate(self): + #balanced realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = np.array( + [[-1.958, -1.194, 1.824, -1.464], + [-1.194, -0.8344, 2.563, -1.351], + [-1.824, -2.563, -1.124, 2.704], + [-1.464, -1.351, -2.704, -11.08]]) + B = np.array([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) + C = np.array([[-0.9057, -0.4068, 0.3263, -0.3474]]) + D = np.array([[0.]]) + sys = ss(A,B,C,D) + rsys = modred(sys,[2, 3],'truncate') + Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) + Brtrue = np.array([[-0.9057], [-0.4068]]) + Crtrue = np.array([[-0.9057, -0.4068]]) + Drtrue = np.array([[0.]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue) + np.testing.assert_array_almost_equal(rsys.B, Brtrue) + np.testing.assert_array_almost_equal(rsys.C, Crtrue) + np.testing.assert_array_almost_equal(rsys.D, Drtrue) + + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testBalredTruncate(self): + #controlable canonical realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = np.array( + [[-15., -7.5, -6.25, -1.875], + [8., 0., 0., 0.], + [0., 4., 0., 0.], + [0., 0., 1., 0.]]) + B = np.array([[2.], [0.], [0.], [0.]]) + C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) + D = np.array([[0.]]) + sys = ss(A,B,C,D) + orders = 2 + rsys = balred(sys,orders,method='truncate') + Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) + Brtrue = np.array([[0.9057], [0.4068]]) + Crtrue = np.array([[0.9057, 0.4068]]) + Drtrue = np.array([[0.]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) + np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) + np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) + np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testBalredMatchDC(self): + #controlable canonical realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = np.array( + [[-15., -7.5, -6.25, -1.875], + [8., 0., 0., 0.], + [0., 4., 0., 0.], + [0., 0., 1., 0.]]) + B = np.array([[2.], [0.], [0.], [0.]]) + C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) + D = np.array([[0.]]) + sys = ss(A,B,C,D) + orders = 2 + rsys = balred(sys,orders,method='matchdc') + Artrue = np.array( + [[-4.43094773, -4.55232904], + [-4.55232904, -5.36195206]]) + Brtrue = np.array([[1.36235673], [1.03114388]]) + Crtrue = np.array([[1.36235673, 1.03114388]]) + Drtrue = np.array([[-0.08383902]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) + np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) + np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) + np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) + + def tearDown(self): + # Reset configuration variables to their original settings + control.config.reset_defaults() + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestModelsimp) + + +if __name__ == '__main__': + unittest.main() diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index f4bc0fd98..f79a86357 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -18,7 +18,7 @@ def testHSVD(self): D = np.matrix("9.") sys = ss(A,B,C,D) hsv = hsvd(sys) - hsvtrue = np.matrix("24.42686 0.5731395") # from MATLAB + hsvtrue = [24.42686, 0.5731395] # from MATLAB np.testing.assert_array_almost_equal(hsv, hsvtrue) def testMarkov(self): diff --git a/control/tests/phaseplot_test.py b/control/tests/phaseplot_test.py index e1cc25a35..4f93e6d97 100644 --- a/control/tests/phaseplot_test.py +++ b/control/tests/phaseplot_test.py @@ -46,7 +46,7 @@ def testInvPendAuto(self): [[-2.3056, 2.1], [2.3056, -2.1]], T=6, verbose=False) def testOscillatorParams(self): - m = 1; b = 1; k = 1; # default values + m = 1; b = 1; k = 1; # default values phase_plot(self.oscillator_ode, timepts = [0.3, 1, 2, 3], X0 = [[-1,1], [-0.3,1], [0,1], [0.25,1], [0.5,1], [0.7,1], [1,1], [1.3,1], [1,-1], [0.3,-1], [0,-1], [-0.25,-1], diff --git a/control/tests/robust_array_test.py b/control/tests/robust_array_test.py new file mode 100644 index 000000000..51114f879 --- /dev/null +++ b/control/tests/robust_array_test.py @@ -0,0 +1,392 @@ +import unittest +import numpy as np +import control +import control.robust +from control.exception import slycot_check + +class TestHinf(unittest.TestCase): + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + control.use_numpy_matrix(False) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testHinfsyn(self): + """Test hinfsyn""" + p = control.ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) + k, cl, gam, rcond = control.robust.hinfsyn(p, 1, 1) + # from Octave, which also uses SB10AD: + # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; + # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); + # [k,cl] = hinfsyn(g,1,1); + np.testing.assert_array_almost_equal(k.A, [[-3]]) + np.testing.assert_array_almost_equal(k.B, [[1]]) + np.testing.assert_array_almost_equal(k.C, [[-1]]) + np.testing.assert_array_almost_equal(k.D, [[0]]) + np.testing.assert_array_almost_equal(cl.A, [[-1, -1], [1, -3]]) + np.testing.assert_array_almost_equal(cl.B, [[1], [1]]) + np.testing.assert_array_almost_equal(cl.C, [[1, -1]]) + np.testing.assert_array_almost_equal(cl.D, [[0]]) + + # TODO: add more interesting examples + + def tearDown(self): + control.config.reset_defaults() + + +class TestH2(unittest.TestCase): + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + control.use_numpy_matrix(False) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testH2syn(self): + """Test h2syn""" + p = control.ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) + k = control.robust.h2syn(p, 1, 1) + # from Octave, which also uses SB10HD for H-2 synthesis: + # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; + # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); + # k = h2syn(g,1,1); + # the solution is the same as for the hinfsyn test + np.testing.assert_array_almost_equal(k.A, [[-3]]) + np.testing.assert_array_almost_equal(k.B, [[1]]) + np.testing.assert_array_almost_equal(k.C, [[-1]]) + np.testing.assert_array_almost_equal(k.D, [[0]]) + + def tearDown(self): + control.config.reset_defaults() + + +class TestAugw(unittest.TestCase): + """Test control.robust.augw""" + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + control.use_numpy_matrix(False) + + # tolerance for system equality + TOL = 1e-8 + + def siso_almost_equal(self, g, h): + """siso_almost_equal(g,h) -> None + Raises AssertionError if g and h, two SISO LTI objects, are not almost equal""" + from control import tf, minreal + gmh = tf(minreal(g - h, verbose=False)) + if not (gmh.num[0][0] < self.TOL).all(): + maxnum = max(abs(gmh.num[0][0])) + raise AssertionError( + 'systems not approx equal; max num. coeff is {}\nsys 1:\n{}\nsys 2:\n{}'.format( + maxnum, g, h)) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testSisoW1(self): + """SISO plant with S weighting""" + from control import augw, ss + g = ss([-1.], [1.], [1.], [1.]) + w1 = ss([-2], [2.], [1.], [2.]) + p = augw(g, w1) + self.assertEqual(2, p.outputs) + self.assertEqual(2, p.inputs) + # w->z1 should be w1 + self.siso_almost_equal(w1, p[0, 0]) + # w->v should be 1 + self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) + # u->z1 should be -w1*g + self.siso_almost_equal(-w1 * g, p[0, 1]) + # u->v should be -g + self.siso_almost_equal(-g, p[1, 1]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testSisoW2(self): + """SISO plant with KS weighting""" + from control import augw, ss + g = ss([-1.], [1.], [1.], [1.]) + w2 = ss([-2], [1.], [1.], [2.]) + p = augw(g, w2=w2) + self.assertEqual(2, p.outputs) + self.assertEqual(2, p.inputs) + # w->z2 should be 0 + self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) + # w->v should be 1 + self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) + # u->z2 should be w2 + self.siso_almost_equal(w2, p[0, 1]) + # u->v should be -g + self.siso_almost_equal(-g, p[1, 1]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testSisoW3(self): + """SISO plant with T weighting""" + from control import augw, ss + g = ss([-1.], [1.], [1.], [1.]) + w3 = ss([-2], [1.], [1.], [2.]) + p = augw(g, w3=w3) + self.assertEqual(2, p.outputs) + self.assertEqual(2, p.inputs) + # w->z3 should be 0 + self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) + # w->v should be 1 + self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) + # u->z3 should be w3*g + self.siso_almost_equal(w3 * g, p[0, 1]) + # u->v should be -g + self.siso_almost_equal(-g, p[1, 1]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testSisoW123(self): + """SISO plant with all weights""" + from control import augw, ss + g = ss([-1.], [1.], [1.], [1.]) + w1 = ss([-2.], [2.], [1.], [2.]) + w2 = ss([-3.], [3.], [1.], [3.]) + w3 = ss([-4.], [4.], [1.], [4.]) + p = augw(g, w1, w2, w3) + self.assertEqual(4, p.outputs) + self.assertEqual(2, p.inputs) + # w->z1 should be w1 + self.siso_almost_equal(w1, p[0, 0]) + # w->z2 should be 0 + self.siso_almost_equal(0, p[1, 0]) + # w->z3 should be 0 + self.siso_almost_equal(0, p[2, 0]) + # w->v should be 1 + self.siso_almost_equal(ss([], [], [], [1]), p[3, 0]) + # u->z1 should be -w1*g + self.siso_almost_equal(-w1 * g, p[0, 1]) + # u->z2 should be w2 + self.siso_almost_equal(w2, p[1, 1]) + # u->z3 should be w3*g + self.siso_almost_equal(w3 * g, p[2, 1]) + # u->v should be -g + self.siso_almost_equal(-g, p[3, 1]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testMimoW1(self): + """MIMO plant with S weighting""" + from control import augw, ss + g = ss([[-1., -2], [-3, -4]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]]) + w1 = ss([-2], [2.], [1.], [2.]) + p = augw(g, w1) + self.assertEqual(4, p.outputs) + self.assertEqual(4, p.inputs) + # w->z1 should be diag(w1,w1) + self.siso_almost_equal(w1, p[0, 0]) + self.siso_almost_equal(0, p[0, 1]) + self.siso_almost_equal(0, p[1, 0]) + self.siso_almost_equal(w1, p[1, 1]) + # w->v should be I + self.siso_almost_equal(1, p[2, 0]) + self.siso_almost_equal(0, p[2, 1]) + self.siso_almost_equal(0, p[3, 0]) + self.siso_almost_equal(1, p[3, 1]) + # u->z1 should be -w1*g + self.siso_almost_equal(-w1 * g[0, 0], p[0, 2]) + self.siso_almost_equal(-w1 * g[0, 1], p[0, 3]) + self.siso_almost_equal(-w1 * g[1, 0], p[1, 2]) + self.siso_almost_equal(-w1 * g[1, 1], p[1, 3]) + # # u->v should be -g + self.siso_almost_equal(-g[0, 0], p[2, 2]) + self.siso_almost_equal(-g[0, 1], p[2, 3]) + self.siso_almost_equal(-g[1, 0], p[3, 2]) + self.siso_almost_equal(-g[1, 1], p[3, 3]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testMimoW2(self): + """MIMO plant with KS weighting""" + from control import augw, ss + g = ss([[-1., -2], [-3, -4]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]]) + w2 = ss([-2], [2.], [1.], [2.]) + p = augw(g, w2=w2) + self.assertEqual(4, p.outputs) + self.assertEqual(4, p.inputs) + # w->z2 should be 0 + self.siso_almost_equal(0, p[0, 0]) + self.siso_almost_equal(0, p[0, 1]) + self.siso_almost_equal(0, p[1, 0]) + self.siso_almost_equal(0, p[1, 1]) + # w->v should be I + self.siso_almost_equal(1, p[2, 0]) + self.siso_almost_equal(0, p[2, 1]) + self.siso_almost_equal(0, p[3, 0]) + self.siso_almost_equal(1, p[3, 1]) + # u->z2 should be w2 + self.siso_almost_equal(w2, p[0, 2]) + self.siso_almost_equal(0, p[0, 3]) + self.siso_almost_equal(0, p[1, 2]) + self.siso_almost_equal(w2, p[1, 3]) + # # u->v should be -g + self.siso_almost_equal(-g[0, 0], p[2, 2]) + self.siso_almost_equal(-g[0, 1], p[2, 3]) + self.siso_almost_equal(-g[1, 0], p[3, 2]) + self.siso_almost_equal(-g[1, 1], p[3, 3]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testMimoW3(self): + """MIMO plant with T weighting""" + from control import augw, ss + g = ss([[-1., -2], [-3, -4]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]]) + w3 = ss([-2], [2.], [1.], [2.]) + p = augw(g, w3=w3) + self.assertEqual(4, p.outputs) + self.assertEqual(4, p.inputs) + # w->z3 should be 0 + self.siso_almost_equal(0, p[0, 0]) + self.siso_almost_equal(0, p[0, 1]) + self.siso_almost_equal(0, p[1, 0]) + self.siso_almost_equal(0, p[1, 1]) + # w->v should be I + self.siso_almost_equal(1, p[2, 0]) + self.siso_almost_equal(0, p[2, 1]) + self.siso_almost_equal(0, p[3, 0]) + self.siso_almost_equal(1, p[3, 1]) + # u->z3 should be w3*g + self.siso_almost_equal(w3 * g[0, 0], p[0, 2]) + self.siso_almost_equal(w3 * g[0, 1], p[0, 3]) + self.siso_almost_equal(w3 * g[1, 0], p[1, 2]) + self.siso_almost_equal(w3 * g[1, 1], p[1, 3]) + # # u->v should be -g + self.siso_almost_equal(-g[0, 0], p[2, 2]) + self.siso_almost_equal(-g[0, 1], p[2, 3]) + self.siso_almost_equal(-g[1, 0], p[3, 2]) + self.siso_almost_equal(-g[1, 1], p[3, 3]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testMimoW123(self): + """MIMO plant with all weights""" + from control import augw, ss, append + g = ss([[-1., -2], [-3, -4]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]], + [[1., 0.], [0., 1.]]) + # this should be expaned to w1*I + w1 = ss([-2.], [2.], [1.], [2.]) + # diagonal weighting + w2 = append(ss([-3.], [3.], [1.], [3.]), ss([-4.], [4.], [1.], [4.])) + # full weighting + w3 = ss([[-4., -5], [-6, -7]], + [[2., 3.], [5., 7.]], + [[11., 13.], [17., 19.]], + [[23., 29.], [31., 37.]]) + p = augw(g, w1, w2, w3) + self.assertEqual(8, p.outputs) + self.assertEqual(4, p.inputs) + # w->z1 should be w1 + self.siso_almost_equal(w1, p[0, 0]) + self.siso_almost_equal(0, p[0, 1]) + self.siso_almost_equal(0, p[1, 0]) + self.siso_almost_equal(w1, p[1, 1]) + # w->z2 should be 0 + self.siso_almost_equal(0, p[2, 0]) + self.siso_almost_equal(0, p[2, 1]) + self.siso_almost_equal(0, p[3, 0]) + self.siso_almost_equal(0, p[3, 1]) + # w->z3 should be 0 + self.siso_almost_equal(0, p[4, 0]) + self.siso_almost_equal(0, p[4, 1]) + self.siso_almost_equal(0, p[5, 0]) + self.siso_almost_equal(0, p[5, 1]) + # w->v should be I + self.siso_almost_equal(1, p[6, 0]) + self.siso_almost_equal(0, p[6, 1]) + self.siso_almost_equal(0, p[7, 0]) + self.siso_almost_equal(1, p[7, 1]) + + # u->z1 should be -w1*g + self.siso_almost_equal(-w1 * g[0, 0], p[0, 2]) + self.siso_almost_equal(-w1 * g[0, 1], p[0, 3]) + self.siso_almost_equal(-w1 * g[1, 0], p[1, 2]) + self.siso_almost_equal(-w1 * g[1, 1], p[1, 3]) + # u->z2 should be w2 + self.siso_almost_equal(w2[0, 0], p[2, 2]) + self.siso_almost_equal(w2[0, 1], p[2, 3]) + self.siso_almost_equal(w2[1, 0], p[3, 2]) + self.siso_almost_equal(w2[1, 1], p[3, 3]) + # u->z3 should be w3*g + w3g = w3 * g; + self.siso_almost_equal(w3g[0, 0], p[4, 2]) + self.siso_almost_equal(w3g[0, 1], p[4, 3]) + self.siso_almost_equal(w3g[1, 0], p[5, 2]) + self.siso_almost_equal(w3g[1, 1], p[5, 3]) + # u->v should be -g + self.siso_almost_equal(-g[0, 0], p[6, 2]) + self.siso_almost_equal(-g[0, 1], p[6, 3]) + self.siso_almost_equal(-g[1, 0], p[7, 2]) + self.siso_almost_equal(-g[1, 1], p[7, 3]) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testErrors(self): + """Error cases handled""" + from control import augw, ss + # no weights + g1by1 = ss(-1, 1, 1, 0) + g2by2 = ss(-np.eye(2), np.eye(2), np.eye(2), np.zeros((2, 2))) + self.assertRaises(ValueError, augw, g1by1) + # mismatched size of weight and plant + self.assertRaises(ValueError, augw, g1by1, w1=g2by2) + self.assertRaises(ValueError, augw, g1by1, w2=g2by2) + self.assertRaises(ValueError, augw, g1by1, w3=g2by2) + + def tearDown(self): + control.config.reset_defaults() + + +class TestMixsyn(unittest.TestCase): + """Test control.robust.mixsyn""" + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + control.use_numpy_matrix(False) + + # it's a relatively simple wrapper; compare results with augw, hinfsyn + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testSiso(self): + """mixsyn with SISO system""" + from control import tf, augw, hinfsyn, mixsyn + from control import ss + # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 + s = tf([1, 0], 1) + # plant + g = 200 / (10 * s + 1) / (0.05 * s + 1) ** 2 + # sensitivity weighting + M = 1.5 + wb = 10 + A = 1e-4 + w1 = (s / M + wb) / (s + wb * A) + # KS weighting + w2 = tf(1, 1) + + p = augw(g, w1, w2) + kref, clref, gam, rcond = hinfsyn(p, 1, 1) + ktest, cltest, info = mixsyn(g, w1, w2) + # check similar to S+P's example + np.testing.assert_allclose(gam, 1.37, atol=1e-2) + + # mixsyn is a convenience wrapper around augw and hinfsyn, so + # results will be exactly the same. Given than, use the lazy + # but fragile testing option. + np.testing.assert_allclose(ktest.A, kref.A) + np.testing.assert_allclose(ktest.B, kref.B) + np.testing.assert_allclose(ktest.C, kref.C) + np.testing.assert_allclose(ktest.D, kref.D) + + np.testing.assert_allclose(cltest.A, clref.A) + np.testing.assert_allclose(cltest.B, clref.B) + np.testing.assert_allclose(cltest.C, clref.C) + np.testing.assert_allclose(cltest.D, clref.D) + + np.testing.assert_allclose(gam, info[0]) + + np.testing.assert_allclose(rcond, info[1]) + + def tearDown(self): + control.config.reset_defaults() + +if __name__ == "__main__": + unittest.main() diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index 9a3419f0b..b23f06c52 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -245,7 +245,7 @@ def testMimoW3(self): @unittest.skipIf(not slycot_check(), "slycot not installed") def testMimoW123(self): """MIMO plant with all weights""" - from control import augw, ss, append + from control import augw, ss, append, minreal g = ss([[-1., -2], [-3, -4]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]], @@ -295,10 +295,10 @@ def testMimoW123(self): self.siso_almost_equal(w2[1, 1], p[3, 3]) # u->z3 should be w3*g w3g = w3 * g; - self.siso_almost_equal(w3g[0, 0], p[4, 2]) - self.siso_almost_equal(w3g[0, 1], p[4, 3]) - self.siso_almost_equal(w3g[1, 0], p[5, 2]) - self.siso_almost_equal(w3g[1, 1], p[5, 3]) + self.siso_almost_equal(w3g[0, 0], minreal(p[4, 2])) + self.siso_almost_equal(w3g[0, 1], minreal(p[4, 3])) + self.siso_almost_equal(w3g[1, 0], minreal(p[5, 2])) + self.siso_almost_equal(w3g[1, 1], minreal(p[5, 3])) # u->v should be -g self.siso_almost_equal(-g[0, 0], p[6, 2]) self.siso_almost_equal(-g[0, 1], p[6, 3]) diff --git a/control/tests/statefbk_array_test.py b/control/tests/statefbk_array_test.py new file mode 100644 index 000000000..941488978 --- /dev/null +++ b/control/tests/statefbk_array_test.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python +# +# statefbk_test.py - test state feedback functions +# RMM, 30 Mar 2011 (based on TestStatefbk from v0.4a) + +from __future__ import print_function +import unittest +import sys as pysys +import numpy as np +import warnings +from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker +from control.matlab import * +from control.exception import slycot_check, ControlDimension +from control.mateqn import care, dare +from control.config import use_numpy_matrix, reset_defaults + +class TestStatefbk(unittest.TestCase): + """Test state feedback functions""" + + def setUp(self): + # Use array instead of matrix (and save old value to restore at end) + use_numpy_matrix(False) + + # Maximum number of states to test + 1 + self.maxStates = 5 + # Maximum number of inputs and outputs to test + 1 + self.maxTries = 4 + # Set to True to print systems to the output. + self.debug = False + # get consistent test results + np.random.seed(0) + + def testCtrbSISO(self): + A = np.array([[1., 2.], [3., 4.]]) + B = np.array([[5.], [7.]]) + Wctrue = np.array([[5., 19.], [7., 43.]]) + + Wc = ctrb(A, B) + np.testing.assert_array_almost_equal(Wc, Wctrue) + self.assertTrue(isinstance(Wc, np.ndarray)) + self.assertFalse(isinstance(Wc, np.matrix)) + + # This test only works in Python 3 due to a conflict with the same + # warning type in other test modules (frd_test.py). See + # https://bugs.python.org/issue4180 for more details + @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") + def test_ctrb_siso_deprecated(self): + A = np.array([[1., 2.], [3., 4.]]) + B = np.array([[5.], [7.]]) + + # Check that default using np.matrix generates a warning + # TODO: remove this check with matrix type is deprecated + warnings.resetwarnings() + with warnings.catch_warnings(record=True) as w: + use_numpy_matrix(True) + self.assertTrue(issubclass(w[-1].category, UserWarning)) + + Wc = ctrb(A, B) + self.assertTrue(isinstance(Wc, np.matrix)) + self.assertTrue(issubclass(w[-1].category, + PendingDeprecationWarning)) + use_numpy_matrix(False) + + def testCtrbMIMO(self): + A = np.array([[1., 2.], [3., 4.]]) + B = np.array([[5., 6.], [7., 8.]]) + Wctrue = np.array([[5., 6., 19., 22.], [7., 8., 43., 50.]]) + Wc = ctrb(A, B) + np.testing.assert_array_almost_equal(Wc, Wctrue) + + # Make sure default type values are correct + self.assertTrue(isinstance(Wc, np.ndarray)) + + def testObsvSISO(self): + A = np.array([[1., 2.], [3., 4.]]) + C = np.array([[5., 7.]]) + Wotrue = np.array([[5., 7.], [26., 38.]]) + Wo = obsv(A, C) + np.testing.assert_array_almost_equal(Wo, Wotrue) + + # Make sure default type values are correct + self.assertTrue(isinstance(Wo, np.ndarray)) + + # This test only works in Python 3 due to a conflict with the same + # warning type in other test modules (frd_test.py). See + # https://bugs.python.org/issue4180 for more details + @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") + def test_obsv_siso_deprecated(self): + A = np.array([[1., 2.], [3., 4.]]) + C = np.array([[5., 7.]]) + + # Check that default type generates a warning + # TODO: remove this check with matrix type is deprecated + with warnings.catch_warnings(record=True) as w: + use_numpy_matrix(True, warn=False) # warnings off + self.assertEqual(len(w), 0) + + Wo = obsv(A, C) + self.assertTrue(isinstance(Wo, np.matrix)) + use_numpy_matrix(False) + + def testObsvMIMO(self): + A = np.array([[1., 2.], [3., 4.]]) + C = np.array([[5., 6.], [7., 8.]]) + Wotrue = np.array([[5., 6.], [7., 8.], [23., 34.], [31., 46.]]) + Wo = obsv(A, C) + np.testing.assert_array_almost_equal(Wo, Wotrue) + + def testCtrbObsvDuality(self): + A = np.array([[1.2, -2.3], [3.4, -4.5]]) + B = np.array([[5.8, 6.9], [8., 9.1]]) + Wc = ctrb(A, B) + A = np.transpose(A) + C = np.transpose(B) + Wo = np.transpose(obsv(A, C)); + np.testing.assert_array_almost_equal(Wc,Wo) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testGramWc(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5., 6.], [7., 8.]]) + C = np.array([[4., 5.], [6., 7.]]) + D = np.array([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Wctrue = np.array([[18.5, 24.5], [24.5, 32.5]]) + Wc = gram(sys, 'c') + np.testing.assert_array_almost_equal(Wc, Wctrue) + + # This test only works in Python 3 due to a conflict with the same + # warning type in other test modules (frd_test.py). See + # https://bugs.python.org/issue4180 for more details + @unittest.skipIf(pysys.version_info < (3, 0) or not slycot_check(), + "test requires Python 3+ and slycot") + def test_gram_wc_deprecated(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5., 6.], [7., 8.]]) + C = np.array([[4., 5.], [6., 7.]]) + D = np.array([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + + # Check that default type generates a warning + # TODO: remove this check with matrix type is deprecated + with warnings.catch_warnings(record=True) as w: + use_numpy_matrix(True) + self.assertTrue(issubclass(w[-1].category, UserWarning)) + + Wc = gram(sys, 'c') + self.assertTrue(isinstance(Wc, np.ndarray)) + use_numpy_matrix(False) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testGramRc(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5., 6.], [7., 8.]]) + C = np.array([[4., 5.], [6., 7.]]) + D = np.array([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Rctrue = np.array([[4.30116263, 5.6961343], [0., 0.23249528]]) + Rc = gram(sys, 'cf') + np.testing.assert_array_almost_equal(Rc, Rctrue) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testGramWo(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5., 6.], [7., 8.]]) + C = np.array([[4., 5.], [6., 7.]]) + D = np.array([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Wotrue = np.array([[257.5, -94.5], [-94.5, 56.5]]) + Wo = gram(sys, 'o') + np.testing.assert_array_almost_equal(Wo, Wotrue) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testGramWo2(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + C = np.array([[6., 8.]]) + D = np.array([[9.]]) + sys = ss(A,B,C,D) + Wotrue = np.array([[198., -72.], [-72., 44.]]) + Wo = gram(sys, 'o') + np.testing.assert_array_almost_equal(Wo, Wotrue) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testGramRo(self): + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5., 6.], [7., 8.]]) + C = np.array([[4., 5.], [6., 7.]]) + D = np.array([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Rotrue = np.array([[16.04680654, -5.8890222], [0., 4.67112593]]) + Ro = gram(sys, 'of') + np.testing.assert_array_almost_equal(Ro, Rotrue) + + def testGramsys(self): + num =[1.] + den = [1., 1., 1.] + sys = tf(num,den) + self.assertRaises(ValueError, gram, sys, 'o') + self.assertRaises(ValueError, gram, sys, 'c') + + def testAcker(self): + for states in range(1, self.maxStates): + for i in range(self.maxTries): + # start with a random SS system and transform to TF then + # back to SS, check that the matrices are the same. + sys = rss(states, 1, 1) + if (self.debug): + print(sys) + + # Make sure the system is not degenerate + Cmat = ctrb(sys.A, sys.B) + if np.linalg.matrix_rank(Cmat) != states: + if (self.debug): + print(" skipping (not reachable or ill conditioned)") + continue + + # Place the poles at random locations + des = rss(states, 1, 1); + poles = pole(des) + + # Now place the poles using acker + K = acker(sys.A, sys.B, poles) + new = ss(sys.A - sys.B * K, sys.B, sys.C, sys.D) + placed = pole(new) + + # Debugging code + # diff = np.sort(poles) - np.sort(placed) + # if not all(diff < 0.001): + # print("Found a problem:") + # print(sys) + # print("desired = ", poles) + + np.testing.assert_array_almost_equal(np.sort(poles), + np.sort(placed), decimal=4) + + def testPlace(self): + # Matrices shamelessly stolen from scipy example code. + A = np.array([[1.380, -0.2077, 6.715, -5.676], + [-0.5814, -4.290, 0, 0.6750], + [1.067, 4.273, -6.654, 5.893], + [0.0480, 4.273, 1.343, -2.104]]) + + B = np.array([[0, 5.679], + [1.136, 1.136], + [0, 0,], + [-3.146, 0]]) + P = np.array([-0.5+1j, -0.5-1j, -5.0566, -8.6659]) + K = place(A, B, P) + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + P.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P, P_placed) + + # Test that the dimension checks work. + np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) + np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) + + # Check that we get an error if we ask for too many poles in the same + # location. Here, rank(B) = 2, so lets place three at the same spot. + P_repeated = np.array([-0.5, -0.5, -0.5, -8.6659]) + np.testing.assert_raises(ValueError, place, A, B, P_repeated) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testPlace_varga_continuous(self): + """ + Check that we can place eigenvalues for dtime=False + """ + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + + P = np.array([-2., -2.]) + K = place_varga(A, B, P) + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + P.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P, P_placed) + + # Test that the dimension checks work. + np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) + np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) + + # Regression test against bug #177 + # https://github.com/python-control/python-control/issues/177 + A = np.array([[0, 1], [100, 0]]) + B = np.array([[0], [1]]) + P = np.array([-20 + 10*1j, -20 - 10*1j]) + K = place_varga(A, B, P) + P_placed = np.linalg.eigvals(A - B.dot(K)) + + # No guarantee of the ordering, so sort them + P.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P, P_placed) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testPlace_varga_continuous_partial_eigs(self): + """ + Check that we are able to use the alpha parameter to only place + a subset of the eigenvalues, for the continous time case. + """ + # A matrix has eigenvalues at s=-1, and s=-2. Choose alpha = -1.5 + # and check that eigenvalue at s=-2 stays put. + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + + P = np.array([-3.]) + P_expected = np.array([-2.0, -3.0]) + alpha = -1.5 + K = place_varga(A, B, P, alpha=alpha) + + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + P_expected.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P_expected, P_placed) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testPlace_varga_discrete(self): + """ + Check that we can place poles using dtime=True (discrete time) + """ + A = np.array([[1., 0], [0, 0.5]]) + B = np.array([[5.], [7.]]) + + P = np.array([0.5, 0.5]) + K = place_varga(A, B, P, dtime=True) + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + P.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P, P_placed) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def testPlace_varga_discrete_partial_eigs(self): + """" + Check that we can only assign a single eigenvalue in the discrete + time case. + """ + # A matrix has eigenvalues at 1.0 and 0.5. Set alpha = 0.51, and + # check that the eigenvalue at 0.5 is not moved. + A = np.array([[1., 0], [0, 0.5]]) + B = np.array([[5.], [7.]]) + P = np.array([0.2, 0.6]) + P_expected = np.array([0.5, 0.6]) + alpha = 0.51 + K = place_varga(A, B, P, dtime=True, alpha=alpha) + P_placed = np.linalg.eigvals(A - B.dot(K)) + P_expected.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P_expected, P_placed) + + + def check_LQR(self, K, S, poles, Q, R): + S_expected = np.array(np.sqrt(Q * R)) + K_expected = S_expected / R + poles_expected = np.array([-K_expected]) + np.testing.assert_array_almost_equal(S, S_expected) + np.testing.assert_array_almost_equal(K, K_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_LQR_integrator(self): + A, B, Q, R = 0., 1., 10., 2. + K, S, poles = lqr(A, B, Q, R) + self.check_LQR(K, S, poles, Q, R) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_LQR_3args(self): + sys = ss(0., 1., 1., 0.) + Q, R = 10., 2. + K, S, poles = lqr(sys, Q, R) + self.check_LQR(K, S, poles, Q, R) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_care(self): + #unit test for stabilizing and anti-stabilizing feedbacks + #continuous-time + + A = np.diag([1,-1]) + B = np.identity(2) + Q = np.identity(2) + R = np.identity(2) + S = 0 * B + E = np.identity(2) + X, L , G = care(A, B, Q, R, S, E, stabilizing=True) + assert np.all(np.real(L) < 0) + X, L , G = care(A, B, Q, R, S, E, stabilizing=False) + assert np.all(np.real(L) > 0) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_dare(self): + #discrete-time + A = np.diag([0.5,2]) + B = np.identity(2) + Q = np.identity(2) + R = np.identity(2) + S = 0 * B + E = np.identity(2) + X, L , G = dare(A, B, Q, R, S, E, stabilizing=True) + assert np.all(np.abs(L) < 1) + X, L , G = dare(A, B, Q, R, S, E, stabilizing=False) + assert np.all(np.abs(L) > 1) + + def tearDown(self): + reset_defaults() + + +def test_suite(): + + status1 = unittest.TestLoader().loadTestsFromTestCase(TestStatefbk) + status2 = unittest.TestLoader().loadTestsFromTestCase(TestStatefbk) + return status1 and status2 + +if __name__ == '__main__': + unittest.main() diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 66dce2b12..133631232 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -6,7 +6,7 @@ from __future__ import print_function import unittest import numpy as np -from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker +from control.statefbk import ctrb, obsv, place, place_varga, lqr, lqe, gram, acker from control.matlab import * from control.exception import slycot_check, ControlDimension from control.mateqn import care, dare @@ -299,6 +299,20 @@ def test_LQR_3args(self): K, S, poles = lqr(sys, Q, R) self.check_LQR(K, S, poles, Q, R) + def check_LQE(self, L, P, poles, G, QN, RN): + P_expected = np.array(np.sqrt(G*QN*G * RN)) + L_expected = P_expected / RN + poles_expected = np.array([-L_expected], ndmin=2) + np.testing.assert_array_almost_equal(P, P_expected) + np.testing.assert_array_almost_equal(L, L_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_LQE(self): + A, G, C, QN, RN = 0., .1, 1., 10., 2. + L, P, poles = lqe(A, G, C, QN, RN) + self.check_LQE(L, P, poles, G, QN, RN) + @unittest.skipIf(not slycot_check(), "slycot not installed") def test_care(self): #unit test for stabilizing and anti-stabilizing feedbacks diff --git a/control/tests/statesp_array_test.py b/control/tests/statesp_array_test.py new file mode 100644 index 000000000..a45e008bc --- /dev/null +++ b/control/tests/statesp_array_test.py @@ -0,0 +1,637 @@ +#!/usr/bin/env python +# +# statesp_test.py - test state space class with use_numpy_matrix(False) +# RMM, 14 Jun 2019 (coverted from statesp_test.py) + +import unittest +import numpy as np +from numpy.linalg import solve +from scipy.linalg import eigvals, block_diag +from control import matlab +from control.statesp import StateSpace, _convertToStateSpace, tf2ss +from control.xferfcn import TransferFunction, ss2tf +from control.lti import evalfr +from control.exception import slycot_check +from control.config import use_numpy_matrix, reset_defaults + +class TestStateSpace(unittest.TestCase): + """Tests for the StateSpace class.""" + + def setUp(self): + """Set up a MIMO system to test operations on.""" + use_numpy_matrix(False) + + # sys1: 3-states square system (2 inputs x 2 outputs) + A322 = [[-3., 4., 2.], + [-1., -3., 0.], + [2., 5., 3.]] + B322 = [[1., 4.], + [-3., -3.], + [-2., 1.]] + C322 = [[4., 2., -3.], + [1., 4., 3.]] + D322 = [[-2., 4.], + [0., 1.]] + self.sys322 = StateSpace(A322, B322, C322, D322) + + # sys1: 2-states square system (2 inputs x 2 outputs) + A222 = [[4., 1.], + [2., -3]] + B222 = [[5., 2.], + [-3., -3.]] + C222 = [[2., -4], + [0., 1.]] + D222 = [[3., 2.], + [1., -1.]] + self.sys222 = StateSpace(A222, B222, C222, D222) + + # sys3: 6 states non square system (2 inputs x 3 outputs) + A623 = np.array([[1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0], + [0, 0, 0, -4, 0, 0], + [0, 0, 0, 0, -1, 0], + [0, 0, 0, 0, 0, 3]]) + B623 = np.array([[0, -1], + [-1, 0], + [1, -1], + [0, 0], + [0, 1], + [-1, -1]]) + C623 = np.array([[1, 0, 0, 1, 0, 0], + [0, 1, 0, 1, 0, 1], + [0, 0, 1, 0, 0, 1]]) + D623 = np.zeros((3, 2)) + self.sys623 = StateSpace(A623, B623, C623, D623) + + def test_matlab_style_constructor(self): + # Use (deprecated?) matrix-style construction string (w/ warnings off) + import warnings + warnings.filterwarnings("ignore") # turn off warnings + sys = StateSpace("-1 1; 0 2", "0; 1", "1, 0", "0") + warnings.resetwarnings() # put things back to original state + self.assertEqual(sys.A.shape, (2, 2)) + self.assertEqual(sys.B.shape, (2, 1)) + self.assertEqual(sys.C.shape, (1, 2)) + self.assertEqual(sys.D.shape, (1, 1)) + for X in [sys.A, sys.B, sys.C, sys.D]: + self.assertTrue(isinstance(X, np.matrix)) + + def test_pole(self): + """Evaluate the poles of a MIMO system.""" + + p = np.sort(self.sys322.pole()) + true_p = np.sort([3.34747678408874, + -3.17373839204437 + 1.47492908003839j, + -3.17373839204437 - 1.47492908003839j]) + + np.testing.assert_array_almost_equal(p, true_p) + + def test_zero_empty(self): + """Test to make sure zero() works with no zeros in system.""" + sys = _convertToStateSpace(TransferFunction([1], [1, 2, 1])) + np.testing.assert_array_equal(sys.zero(), np.array([])) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_zero_siso(self): + """Evaluate the zeros of a SISO system.""" + # extract only first input / first output system of sys222. This system is denoted sys111 + # or tf111 + tf111 = ss2tf(self.sys222) + sys111 = tf2ss(tf111[0, 0]) + + # compute zeros as root of the characteristic polynomial at the numerator of tf111 + # this method is simple and assumed as valid in this test + true_z = np.sort(tf111[0, 0].zero()) + # Compute the zeros through ab08nd, which is tested here + z = np.sort(sys111.zero()) + + np.testing.assert_almost_equal(true_z, z) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_zero_mimo_sys322_square(self): + """Evaluate the zeros of a square MIMO system.""" + + z = np.sort(self.sys322.zero()) + true_z = np.sort([44.41465, -0.490252, -5.924398]) + np.testing.assert_array_almost_equal(z, true_z) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_zero_mimo_sys222_square(self): + """Evaluate the zeros of a square MIMO system.""" + + z = np.sort(self.sys222.zero()) + true_z = np.sort([-10.568501, 3.368501]) + np.testing.assert_array_almost_equal(z, true_z) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_zero_mimo_sys623_non_square(self): + """Evaluate the zeros of a non square MIMO system.""" + + z = np.sort(self.sys623.zero()) + true_z = np.sort([2., -1.]) + np.testing.assert_array_almost_equal(z, true_z) + + def test_add_ss(self): + """Add two MIMO systems.""" + + A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], + [2., 5., 3., 0., 0.], [0., 0., 0., 4., 1.], [0., 0., 0., 2., -3.]] + B = [[1., 4.], [-3., -3.], [-2., 1.], [5., 2.], [-3., -3.]] + C = [[4., 2., -3., 2., -4.], [1., 4., 3., 0., 1.]] + D = [[1., 6.], [1., 0.]] + + sys = self.sys322 + self.sys222 + + np.testing.assert_array_almost_equal(sys.A, A) + np.testing.assert_array_almost_equal(sys.B, B) + np.testing.assert_array_almost_equal(sys.C, C) + np.testing.assert_array_almost_equal(sys.D, D) + + def test_subtract_ss(self): + """Subtract two MIMO systems.""" + + A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], + [2., 5., 3., 0., 0.], [0., 0., 0., 4., 1.], [0., 0., 0., 2., -3.]] + B = [[1., 4.], [-3., -3.], [-2., 1.], [5., 2.], [-3., -3.]] + C = [[4., 2., -3., -2., 4.], [1., 4., 3., 0., -1.]] + D = [[-5., 2.], [-1., 2.]] + + sys = self.sys322 - self.sys222 + + np.testing.assert_array_almost_equal(sys.A, A) + np.testing.assert_array_almost_equal(sys.B, B) + np.testing.assert_array_almost_equal(sys.C, C) + np.testing.assert_array_almost_equal(sys.D, D) + + def test_multiply_ss(self): + """Multiply two MIMO systems.""" + + A = [[4., 1., 0., 0., 0.], [2., -3., 0., 0., 0.], [2., 0., -3., 4., 2.], + [-6., 9., -1., -3., 0.], [-4., 9., 2., 5., 3.]] + B = [[5., 2.], [-3., -3.], [7., -2.], [-12., -3.], [-5., -5.]] + C = [[-4., 12., 4., 2., -3.], [0., 1., 1., 4., 3.]] + D = [[-2., -8.], [1., -1.]] + + sys = self.sys322 * self.sys222 + + np.testing.assert_array_almost_equal(sys.A, A) + np.testing.assert_array_almost_equal(sys.B, B) + np.testing.assert_array_almost_equal(sys.C, C) + np.testing.assert_array_almost_equal(sys.D, D) + + def test_evalfr(self): + """Evaluate the frequency response at one frequency.""" + + A = [[-2, 0.5], [0.5, -0.3]] + B = [[0.3, -1.3], [0.1, 0.]] + C = [[0., 0.1], [-0.3, -0.2]] + D = [[0., -0.8], [-0.3, 0.]] + sys = StateSpace(A, B, C, D) + + resp = [[4.37636761487965e-05 - 0.0152297592997812j, + -0.792603938730853 + 0.0261706783369803j], + [-0.331544857768052 + 0.0576105032822757j, + 0.128919037199125 - 0.143824945295405j]] + + # Correct versions of the call + np.testing.assert_almost_equal(evalfr(sys, 1j), resp) + np.testing.assert_almost_equal(sys._evalfr(1.), resp) + + # Deprecated version of the call (should generate warning) + import warnings + with warnings.catch_warnings(record=True) as w: + # Set up warnings filter to only show warnings in control module + warnings.filterwarnings("ignore") + warnings.filterwarnings("always", module="control") + + # Make sure that we get a pending deprecation warning + sys.evalfr(1.) + assert len(w) == 1 + assert issubclass(w[-1].category, PendingDeprecationWarning) + + # Leave the warnings filter like we found it + warnings.resetwarnings() + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_freq_resp(self): + """Evaluate the frequency response at multiple frequencies.""" + + A = [[-2, 0.5], [0.5, -0.3]] + B = [[0.3, -1.3], [0.1, 0.]] + C = [[0., 0.1], [-0.3, -0.2]] + D = [[0., -0.8], [-0.3, 0.]] + sys = StateSpace(A, B, C, D) + + true_mag = [[[0.0852992637230322, 0.00103596611395218], + [0.935374692849736, 0.799380720864549]], + [[0.55656854563842, 0.301542699860857], + [0.609178071542849, 0.0382108097985257]]] + true_phase = [[[-0.566195599644593, -1.68063565332582], + [3.0465958317514, 3.14141384339534]], + [[2.90457947657161, 3.10601268291914], + [-0.438157380501337, -1.40720969147217]]] + true_omega = [0.1, 10.] + + mag, phase, omega = sys.freqresp(true_omega) + + np.testing.assert_almost_equal(mag, true_mag) + np.testing.assert_almost_equal(phase, true_phase) + np.testing.assert_equal(omega, true_omega) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_minreal(self): + """Test a minreal model reduction.""" + # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] + A = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + # B = [0.3, -1.3; 0.1, 0; 1, 0] + B = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + # C = [0, 0.1, 0; -0.3, -0.2, 0] + C = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + # D = [0 -0.8; -0.3 0] + D = [[0., -0.8], [-0.3, 0.]] + # sys = ss(A, B, C, D) + + sys = StateSpace(A, B, C, D) + sysr = sys.minreal() + self.assertEqual(sysr.states, 2) + self.assertEqual(sysr.inputs, sys.inputs) + self.assertEqual(sysr.outputs, sys.outputs) + np.testing.assert_array_almost_equal( + eigvals(sysr.A), [-2.136154, -0.1638459]) + + def test_append_ss(self): + """Test appending two state-space systems.""" + A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + B1 = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + C1 = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + D1 = [[0., -0.8], [-0.3, 0.]] + A2 = [[-1.]] + B2 = [[1.2]] + C2 = [[0.5]] + D2 = [[0.4]] + A3 = [[-2, 0.5, 0, 0], [0.5, -0.3, 0, 0], [0, 0, -0.1, 0], + [0, 0, 0., -1.]] + B3 = [[0.3, -1.3, 0], [0.1, 0., 0], [1.0, 0.0, 0], [0., 0, 1.2]] + C3 = [[0., 0.1, 0.0, 0.0], [-0.3, -0.2, 0.0, 0.0], [0., 0., 0., 0.5]] + D3 = [[0., -0.8, 0.], [-0.3, 0., 0.], [0., 0., 0.4]] + sys1 = StateSpace(A1, B1, C1, D1) + sys2 = StateSpace(A2, B2, C2, D2) + sys3 = StateSpace(A3, B3, C3, D3) + sys3c = sys1.append(sys2) + np.testing.assert_array_almost_equal(sys3.A, sys3c.A) + np.testing.assert_array_almost_equal(sys3.B, sys3c.B) + np.testing.assert_array_almost_equal(sys3.C, sys3c.C) + np.testing.assert_array_almost_equal(sys3.D, sys3c.D) + + def test_append_tf(self): + """Test appending a state-space system with a tf""" + A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + B1 = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + C1 = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + D1 = [[0., -0.8], [-0.3, 0.]] + s = TransferFunction([1, 0], [1]) + h = 1 / (s + 1) / (s + 2) + sys1 = StateSpace(A1, B1, C1, D1) + sys2 = _convertToStateSpace(h) + sys3c = sys1.append(sys2) + np.testing.assert_array_almost_equal(sys1.A, sys3c.A[:3, :3]) + np.testing.assert_array_almost_equal(sys1.B, sys3c.B[:3, :2]) + np.testing.assert_array_almost_equal(sys1.C, sys3c.C[:2, :3]) + np.testing.assert_array_almost_equal(sys1.D, sys3c.D[:2, :2]) + np.testing.assert_array_almost_equal(sys2.A, sys3c.A[3:, 3:]) + np.testing.assert_array_almost_equal(sys2.B, sys3c.B[3:, 2:]) + np.testing.assert_array_almost_equal(sys2.C, sys3c.C[2:, 3:]) + np.testing.assert_array_almost_equal(sys2.D, sys3c.D[2:, 2:]) + np.testing.assert_array_almost_equal(sys3c.A[:3, 3:], np.zeros((3, 2))) + np.testing.assert_array_almost_equal(sys3c.A[3:, :3], np.zeros((2, 3))) + + def test_array_access_ss(self): + + sys1 = StateSpace([[1., 2.], [3., 4.]], + [[5., 6.], [6., 8.]], + [[9., 10.], [11., 12.]], + [[13., 14.], [15., 16.]], 1) + + sys1_11 = sys1[0, 1] + np.testing.assert_array_almost_equal(sys1_11.A, + sys1.A) + np.testing.assert_array_almost_equal(sys1_11.B, + sys1.B[:, [1]]) + np.testing.assert_array_almost_equal(sys1_11.C, + sys1.C[[0], :]) + np.testing.assert_array_almost_equal(sys1_11.D, sys1.D[0,1]) + + assert sys1.dt == sys1_11.dt + + def test_dc_gain_cont(self): + """Test DC gain for continuous-time state-space systems.""" + sys = StateSpace(-2., 6., 5., 0) + np.testing.assert_equal(sys.dcgain(), 15.) + + sys2 = StateSpace(-2, [[6., 4.]], [[5.], [7.], [11]], np.zeros((3, 2))) + expected = np.array([[15., 10.], [21., 14.], [33., 22.]]) + np.testing.assert_array_equal(sys2.dcgain(), expected) + + sys3 = StateSpace(0., 1., 1., 0.) + np.testing.assert_equal(sys3.dcgain(), np.nan) + + def test_dc_gain_discr(self): + """Test DC gain for discrete-time state-space systems.""" + # static gain + sys = StateSpace([], [], [], 2, True) + np.testing.assert_equal(sys.dcgain(), 2) + + # averaging filter + sys = StateSpace(0.5, 0.5, 1, 0, True) + np.testing.assert_almost_equal(sys.dcgain(), 1) + + # differencer + sys = StateSpace(0, 1, -1, 1, True) + np.testing.assert_equal(sys.dcgain(), 0) + + # summer + sys = StateSpace(1, 1, 1, 0, True) + np.testing.assert_equal(sys.dcgain(), np.nan) + + def test_dc_gain_integrator(self): + """DC gain when eigenvalue at DC returns appropriately sized array of nan.""" + # the SISO case is also tested in test_dc_gain_{cont,discr} + import itertools + # iterate over input and output sizes, and continuous (dt=None) and discrete (dt=True) time + for inputs, outputs, dt in itertools.product(range(1, 6), range(1, 6), [None, True]): + states = max(inputs, outputs) + + # a matrix that is singular at DC, and has no "useless" states as in + # _remove_useless_states + a = np.triu(np.tile(2, (states, states))) + # eigenvalues all +2, except for ... + a[0, 0] = 0 if dt is None else 1 + b = np.eye(max(inputs, states))[:states, :inputs] + c = np.eye(max(outputs, states))[:outputs, :states] + d = np.zeros((outputs, inputs)) + sys = StateSpace(a, b, c, d, dt) + dc = np.squeeze(np.tile(np.nan, (outputs, inputs))) + np.testing.assert_array_equal(dc, sys.dcgain()) + + def test_scalar_static_gain(self): + """Regression: can we create a scalar static gain?""" + g1 = StateSpace([], [], [], [2]) + g2 = StateSpace([], [], [], [3]) + + # make sure StateSpace internals, specifically ABC matrix + # sizes, are OK for LTI operations + g3 = g1 * g2 + self.assertEqual(6, g3.D[0, 0]) + g4 = g1 + g2 + self.assertEqual(5, g4.D[0, 0]) + g5 = g1.feedback(g2) + np.testing.assert_array_almost_equal(2. / 7, g5.D[0, 0]) + g6 = g1.append(g2) + np.testing.assert_array_equal(np.diag([2, 3]), g6.D) + + def test_matrix_static_gain(self): + """Regression: can we create matrix static gains?""" + d1 = np.array([[1, 2, 3], [4, 5, 6]]) + d2 = np.array([[7, 8], [9, 10], [11, 12]]) + g1 = StateSpace([], [], [], d1) + + # _remove_useless_states was making A = [[0]] + self.assertEqual((0, 0), g1.A.shape) + + g2 = StateSpace([], [], [], d2) + g3 = StateSpace([], [], [], d2.T) + + h1 = g1 * g2 + np.testing.assert_array_equal(np.dot(d1, d2), h1.D) + h2 = g1 + g3 + np.testing.assert_array_equal(d1 + d2.T, h2.D) + h3 = g1.feedback(g2) + np.testing.assert_array_almost_equal( + solve(np.eye(2) + np.dot(d1, d2), d1), h3.D) + h4 = g1.append(g2) + np.testing.assert_array_equal(block_diag(d1, d2), h4.D) + + def test_remove_useless_states(self): + """Regression: _remove_useless_states gives correct ABC sizes.""" + g1 = StateSpace(np.zeros((3, 3)), + np.zeros((3, 4)), + np.zeros((5, 3)), + np.zeros((5, 4))) + self.assertEqual((0, 0), g1.A.shape) + self.assertEqual((0, 4), g1.B.shape) + self.assertEqual((5, 0), g1.C.shape) + self.assertEqual((5, 4), g1.D.shape) + self.assertEqual(0, g1.states) + + def test_bad_empty_matrices(self): + """Mismatched ABCD matrices when some are empty.""" + self.assertRaises(ValueError, StateSpace, [1], [], [], [1]) + self.assertRaises(ValueError, StateSpace, [1], [1], [], [1]) + self.assertRaises(ValueError, StateSpace, [1], [], [1], [1]) + self.assertRaises(ValueError, StateSpace, [], [1], [], [1]) + self.assertRaises(ValueError, StateSpace, [], [1], [1], [1]) + self.assertRaises(ValueError, StateSpace, [], [], [1], [1]) + self.assertRaises(ValueError, StateSpace, [1], [1], [1], []) + + def test_minreal_static_gain(self): + """Regression: minreal on static gain was failing.""" + g1 = StateSpace([], [], [], [1]) + g2 = g1.minreal() + np.testing.assert_array_equal(g1.A, g2.A) + np.testing.assert_array_equal(g1.B, g2.B) + np.testing.assert_array_equal(g1.C, g2.C) + np.testing.assert_array_equal(g1.D, g2.D) + + def test_empty(self): + """Regression: can we create an empty StateSpace object?""" + g1 = StateSpace([], [], [], []) + self.assertEqual(0, g1.states) + self.assertEqual(0, g1.inputs) + self.assertEqual(0, g1.outputs) + + def test_matrix_to_state_space(self): + """_convertToStateSpace(matrix) gives ss([],[],[],D)""" + D = np.array([[1, 2, 3], [4, 5, 6]]) + g = _convertToStateSpace(D) + + def empty(shape): + m = np.array([]) + m.shape = shape + return m + np.testing.assert_array_equal(empty((0, 0)), g.A) + np.testing.assert_array_equal(empty((0, D.shape[1])), g.B) + np.testing.assert_array_equal(empty((D.shape[0], 0)), g.C) + np.testing.assert_array_equal(D, g.D) + + def test_lft(self): + """ test lft function with result obtained from matlab implementation""" + # test case + A = [[1, 2, 3], + [1, 4, 5], + [2, 3, 4]] + B = [[0, 2], + [5, 6], + [5, 2]] + C = [[1, 4, 5], + [2, 3, 0]] + D = [[0, 0], + [3, 0]] + P = StateSpace(A, B, C, D) + Ak = [[0, 2, 3], + [2, 3, 5], + [2, 1, 9]] + Bk = [[1, 1], + [2, 3], + [9, 4]] + Ck = [[1, 4, 5], + [2, 3, 6]] + Dk = [[0, 2], + [0, 0]] + K = StateSpace(Ak, Bk, Ck, Dk) + + # case 1 + pk = P.lft(K, 2, 1) + Amatlab = [1, 2, 3, 4, 6, 12, 1, 4, 5, 17, 38, 61, 2, 3, 4, 9, 26, 37, 2, 3, 0, 3, 14, 18, 4, 6, 0, 8, 27, 35, 18, 27, 0, 29, 109, 144] + Bmatlab = [0, 10, 10, 7, 15, 58] + Cmatlab = [1, 4, 5, 0, 0, 0] + Dmatlab = [0] + np.testing.assert_allclose(np.array(pk.A).reshape(-1), Amatlab) + np.testing.assert_allclose(np.array(pk.B).reshape(-1), Bmatlab) + np.testing.assert_allclose(np.array(pk.C).reshape(-1), Cmatlab) + np.testing.assert_allclose(np.array(pk.D).reshape(-1), Dmatlab) + + # case 2 + pk = P.lft(K) + Amatlab = [1, 2, 3, 4, 6, 12, -3, -2, 5, 11, 14, 31, -2, -3, 4, 3, 2, 7, 0.6, 3.4, 5, -0.6, -0.4, 0, 0.8, 6.2, 10, 0.2, -4.2, -4, 7.4, 33.6, 45, -0.4, -8.6, -3] + Bmatlab = [] + Cmatlab = [] + Dmatlab = [] + np.testing.assert_allclose(np.array(pk.A).reshape(-1), Amatlab) + np.testing.assert_allclose(np.array(pk.B).reshape(-1), Bmatlab) + np.testing.assert_allclose(np.array(pk.C).reshape(-1), Cmatlab) + np.testing.assert_allclose(np.array(pk.D).reshape(-1), Dmatlab) + + def test_horner(self): + """Test horner() function""" + # Make sure we can compute the transfer function at a complex value + self.sys322.horner(1.+1.j) + + # Make sure result agrees with frequency response + mag, phase, omega = self.sys322.freqresp([1]) + np.testing.assert_array_almost_equal( + self.sys322.horner(1.j), + mag[:,:,0] * np.exp(1.j * phase[:,:,0])) + + def tearDown(self): + reset_defaults() # reset configuration defaults + + +class TestRss(unittest.TestCase): + """These are tests for the proper functionality of statesp.rss.""" + + def setUp(self): + use_numpy_matrix(False) + + # Number of times to run each of the randomized tests. + self.numTests = 100 + # Maxmimum number of states to test + 1 + self.maxStates = 10 + # Maximum number of inputs and outputs to test + 1 + self.maxIO = 5 + + def test_shape(self): + """Test that rss outputs have the right state, input, and output size.""" + + for states in range(1, self.maxStates): + for inputs in range(1, self.maxIO): + for outputs in range(1, self.maxIO): + sys = matlab.rss(states, outputs, inputs) + self.assertEqual(sys.states, states) + self.assertEqual(sys.inputs, inputs) + self.assertEqual(sys.outputs, outputs) + + def test_pole(self): + """Test that the poles of rss outputs have a negative real part.""" + + for states in range(1, self.maxStates): + for inputs in range(1, self.maxIO): + for outputs in range(1, self.maxIO): + sys = matlab.rss(states, outputs, inputs) + p = sys.pole() + for z in p: + self.assertTrue(z.real < 0) + + def tearDown(self): + reset_defaults() # reset configuration defaults + + +class TestDrss(unittest.TestCase): + """These are tests for the proper functionality of statesp.drss.""" + + def setUp(self): + use_numpy_matrix(False) + + # Number of times to run each of the randomized tests. + self.numTests = 100 + # Maximum number of states to test + 1 + self.maxStates = 10 + # Maximum number of inputs and outputs to test + 1 + self.maxIO = 5 + + def test_shape(self): + """Test that drss outputs have the right state, input, and output size.""" + + for states in range(1, self.maxStates): + for inputs in range(1, self.maxIO): + for outputs in range(1, self.maxIO): + sys = matlab.drss(states, outputs, inputs) + self.assertEqual(sys.states, states) + self.assertEqual(sys.inputs, inputs) + self.assertEqual(sys.outputs, outputs) + + def test_pole(self): + """Test that the poles of drss outputs have less than unit magnitude.""" + + for states in range(1, self.maxStates): + for inputs in range(1, self.maxIO): + for outputs in range(1, self.maxIO): + sys = matlab.drss(states, outputs, inputs) + p = sys.pole() + for z in p: + self.assertTrue(abs(z) < 1) + + def test_pole_static(self): + """Regression: pole() of static gain is empty array.""" + np.testing.assert_array_equal(np.array([]), + StateSpace([], [], [], [[1]]).pole()) + + def test_copy_constructor(self): + # Create a set of matrices for a simple linear system + A = np.array([[-1]]) + B = np.array([[1]]) + C = np.array([[1]]) + D = np.array([[0]]) + + # Create the first linear system and a copy + linsys = StateSpace(A, B, C, D) + cpysys = StateSpace(linsys) + + # Change the original A matrix + A[0, 0] = -2 + np.testing.assert_array_equal(linsys.A, [[-1]]) # original value + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + + # Change the A matrix for the original system + linsys.A[0, 0] = -3 + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + + def tearDown(self): + reset_defaults() # reset configuration defaults + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestStateSpace) + + +if __name__ == "__main__": + unittest.main() diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 4ef2f9eb9..191271da4 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -63,6 +63,29 @@ def setUp(self): D623 = np.zeros((3, 2)) self.sys623 = StateSpace(A623, B623, C623, D623) + def test_D_broadcast(self): + """Test broadcast of D=0 to the right shape""" + # Giving D as a scalar 0 should broadcast to the right shape + sys = StateSpace(self.sys623.A, self.sys623.B, self.sys623.C, 0) + np.testing.assert_array_equal(self.sys623.D, sys.D) + + # Giving D as a matrix of the wrong size should generate an error + with self.assertRaises(ValueError): + sys = StateSpace(sys.A, sys.B, sys.C, np.array([[0]])) + + # Make sure that empty systems still work + sys = StateSpace([], [], [], 1) + np.testing.assert_array_equal(sys.D, [[1]]) + + sys = StateSpace([], [], [], [[0]]) + np.testing.assert_array_equal(sys.D, [[0]]) + + sys = StateSpace([], [], [], [0]) + np.testing.assert_array_equal(sys.D, [[0]]) + + sys = StateSpace([], [], [], 0) + np.testing.assert_array_equal(sys.D, [[0]]) + def test_pole(self): """Evaluate the poles of a MIMO system.""" @@ -568,6 +591,26 @@ def test_pole_static(self): np.testing.assert_array_equal(np.array([]), StateSpace([], [], [], [[1]]).pole()) + def test_copy_constructor(self): + # Create a set of matrices for a simple linear system + A = np.array([[-1]]) + B = np.array([[1]]) + C = np.array([[1]]) + D = np.array([[0]]) + + # Create the first linear system and a copy + linsys = StateSpace(A, B, C, D) + cpysys = StateSpace(linsys) + + # Change the original A matrix + A[0, 0] = -2 + np.testing.assert_array_equal(linsys.A, [[-1]]) # original value + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + + # Change the A matrix for the original system + linsys.A[0, 0] = -3 + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestStateSpace) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 10001dd96..4087f530f 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -10,7 +10,6 @@ import unittest import numpy as np -# import scipy as sp from control.timeresp import * from control.statesp import * from control.xferfcn import TransferFunction, _convert_to_transfer_function @@ -253,14 +252,14 @@ def test_forced_response(self): # first system: initial value, second system: step response u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) - x0 = np.matrix(".5; 1; 0; 0") + x0 = np.array([[.5], [1], [0], [0]]) youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391], [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]]) _t, yout, _xout = forced_response(self.mimo_ss1, t, u, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - + # Test discrete MIMO system to use correct convention for input sysc = self.mimo_ss1 dt=t[1]-t[0] @@ -271,6 +270,17 @@ def test_forced_response(self): np.testing.assert_array_equal(youtc.shape, youtd.shape) np.testing.assert_array_almost_equal(youtc, youtd, decimal=4) + # Test discrete MIMO system without default T argument + u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) + x0 = np.array([[.5], [1], [0], [0]]) + youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, + 1.1508, 0.5833, 0.1645, -0.1391], + [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, + 42.3227, 44.9694, 47.1599, 48.9776]]) + _t, yout, _xout = forced_response(sysd, U=u, X0=x0) + np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + def test_lsim_double_integrator(self): # Note: scipy.signal.lsim fails if A is not invertible A = np.mat("0. 1.;0. 0.") @@ -309,7 +319,7 @@ def check(u, x0, xtrue): def test_discrete_initial(self): h1 = TransferFunction([1.], [1., 0.], 1.) t, yout = impulse_response(h1, np.arange(4)) - np.testing.assert_array_equal(yout[0], [0., 1., 0., 0.]) + np.testing.assert_array_equal(yout, [0., 1., 0., 0.]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_step_robustness(self): @@ -345,27 +355,32 @@ def test_time_vector(self): # No timebase in system => output should match input # # Initial response - tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0) + tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Impulse response - tout, yout = impulse_response(self.siso_dtf1, Tin2) + tout, yout = impulse_response(self.siso_dtf1, Tin2, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Step response - tout, yout = step_response(self.siso_dtf1, Tin2) + tout, yout = step_response(self.siso_dtf1, Tin2, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with specified time vector - tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2)) + tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2), + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with no time vector, no sample time (should use 1) - tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1)) + tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1), + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) @@ -380,49 +395,58 @@ def test_time_vector(self): # Matching timebase in system => output should match input # # Initial response - tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0) + tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Impulse response - tout, yout = impulse_response(self.siso_dtf2, Tin2) + tout, yout = impulse_response(self.siso_dtf2, Tin2, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Step response - tout, yout = step_response(self.siso_dtf2, Tin2) + tout, yout = step_response(self.siso_dtf2, Tin2, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response - tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2)) + tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2), + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with no time vector, use sample time - tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2)) + tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2), + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Compatible timebase in system => output should match input # # Initial response - tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0) + tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Impulse response - tout, yout = impulse_response(self.siso_dtf2, Tin1) + tout, yout = impulse_response(self.siso_dtf2, Tin1, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Step response - tout, yout = step_response(self.siso_dtf2, Tin1) + tout, yout = step_response(self.siso_dtf2, Tin1, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Forced response - tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1)) + tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1), + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) @@ -431,7 +455,8 @@ def test_time_vector(self): # # Initial response tout, yout, xout = forced_response(self.siso_dtf2, Tin1, - np.sin(Tin1), interpolate=True) + np.sin(Tin1), interpolate=True, + squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) self.assertTrue(np.allclose(tout[1:] - tout[:-1], self.siso_dtf2.dt)) @@ -442,9 +467,101 @@ def test_time_vector(self): # # Initial response with self.assertRaises(Exception) as context: - tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0) + tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0, + squeeze=False) self.assertTrue(isinstance(context.exception, ValueError)) + def test_discrete_time_steps(self): + """Make sure rounding errors in sample time are handled properly""" + # See https://github.com/python-control/python-control/issues/332) + # + # These tests play around with the input time vector to make sure that + # small rounding errors don't generate spurious errors. + + # Discrete time system to use for simulation + # self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2) + + # Set up a time range and simulate + T = np.arange(0, 100, 0.2) + tout1, yout1 = step_response(self.siso_dtf2, T) + + # Simulate every other time step + T = np.arange(0, 100, 0.4) + tout2, yout2 = step_response(self.siso_dtf2, T) + np.testing.assert_array_almost_equal(tout1[::2], tout2) + np.testing.assert_array_almost_equal(yout1[::2], yout2) + + # Add a small error into some of the time steps + T = np.arange(0, 100, 0.2) + T[1:-2:2] -= 1e-12 # tweak second value and a few others + tout3, yout3 = step_response(self.siso_dtf2, T) + np.testing.assert_array_almost_equal(tout1, tout3) + np.testing.assert_array_almost_equal(yout1, yout3) + + # Add a small error into some of the time steps (w/ skipping) + T = np.arange(0, 100, 0.4) + T[1:-2:2] -= 1e-12 # tweak second value and a few others + tout4, yout4 = step_response(self.siso_dtf2, T) + np.testing.assert_array_almost_equal(tout2, tout4) + np.testing.assert_array_almost_equal(yout2, yout4) + + # Make sure larger errors *do* generate an error + T = np.arange(0, 100, 0.2) + T[1:-2:2] -= 1e-3 # change second value and a few others + self.assertRaises(ValueError, step_response, self.siso_dtf2, T) + + def test_time_series_data_convention(self): + """Make sure time series data matches documentation conventions""" + # SISO continuous time + t, y = step_response(self.siso_ss1) + self.assertTrue(isinstance(t, np.ndarray) + and not isinstance(t, np.matrix)) + self.assertTrue(len(t.shape) == 1) + self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output + self.assertTrue(len(t) == len(y)) # Allows direct plotting of output + + # SISO discrete time + t, y = step_response(self.siso_dss1) + self.assertTrue(isinstance(t, np.ndarray) + and not isinstance(t, np.matrix)) + self.assertTrue(len(t.shape) == 1) + self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output + self.assertTrue(len(t) == len(y)) # Allows direct plotting of output + + # MIMO continuous time + tin = np.linspace(0, 10, 100) + uin = [np.sin(tin), np.cos(tin)] + t, y, x = forced_response(self.mimo_ss1, tin, uin) + self.assertTrue(isinstance(t, np.ndarray) + and not isinstance(t, np.matrix)) + self.assertTrue(len(t.shape) == 1) + self.assertTrue(len(y[0].shape) == 1) + self.assertTrue(len(y[1].shape) == 1) + self.assertTrue(len(t) == len(y[0])) + self.assertTrue(len(t) == len(y[1])) + + # MIMO discrete time + tin = np.linspace(0, 10, 100) + uin = [np.sin(tin), np.cos(tin)] + t, y, x = forced_response(self.mimo_dss1, tin, uin) + self.assertTrue(isinstance(t, np.ndarray) + and not isinstance(t, np.matrix)) + self.assertTrue(len(t.shape) == 1) + self.assertTrue(len(y[0].shape) == 1) + self.assertTrue(len(y[1].shape) == 1) + self.assertTrue(len(t) == len(y[0])) + self.assertTrue(len(t) == len(y[1])) + + # Allow input time as 2D array (output should be 1D) + tin = np.array(np.linspace(0, 10, 100), ndmin=2) + t, y = step_response(self.siso_ss1, tin) + self.assertTrue(isinstance(t, np.ndarray) + and not isinstance(t, np.matrix)) + self.assertTrue(len(t.shape) == 1) + self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output + self.assertTrue(len(t) == len(y)) # Allows direct plotting of output + + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp) diff --git a/control/tests/xferfcn_input_test.py b/control/tests/xferfcn_input_test.py index 0471e885e..0d6ca56fe 100644 --- a/control/tests/xferfcn_input_test.py +++ b/control/tests/xferfcn_input_test.py @@ -7,7 +7,7 @@ import numpy as np from numpy import int, int8, int16, int32, int64 -from numpy import float, float16, float32, float64, float128 +from numpy import float, float16, float32, float64, longdouble from numpy import all, ndarray, array from control.xferfcn import _clean_part @@ -73,7 +73,7 @@ def test_clean_part_tuple(self): def test_clean_part_all_scalar_types(self): """Test single scalar value for all valid data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = dtype(1) num_ = _clean_part(num) @@ -92,7 +92,7 @@ def test_clean_part_np_array(self): def test_clean_part_all_np_array_types(self): """Test scalar value in numpy array of ndim=0 for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = np.array(1, dtype=dtype) num_ = _clean_part(num) @@ -102,7 +102,7 @@ def test_clean_part_all_np_array_types(self): def test_clean_part_all_np_array_types2(self): """Test numpy array for all types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = np.array([1, 2], dtype=dtype) num_ = _clean_part(num) @@ -112,7 +112,7 @@ def test_clean_part_all_np_array_types2(self): def test_clean_part_list_all_types(self): """Test list of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = [dtype(1)] num_ = _clean_part(num) assert isinstance(num_, list) @@ -121,7 +121,7 @@ def test_clean_part_list_all_types(self): def test_clean_part_list_all_types2(self): """List of list of numbers of all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = [dtype(1), dtype(2)] num_ = _clean_part(num) assert isinstance(num_, list) @@ -130,7 +130,7 @@ def test_clean_part_list_all_types2(self): def test_clean_part_tuple_all_types(self): """Test tuple of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = (dtype(1),) num_ = _clean_part(num) assert isinstance(num_, list) @@ -139,7 +139,7 @@ def test_clean_part_tuple_all_types(self): def test_clean_part_tuple_all_types2(self): """Test tuple of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: num = (dtype(1), dtype(2)) num_ = _clean_part(num) assert isinstance(num_, list) @@ -184,7 +184,7 @@ def test_clean_part_list_list_list_floats(self): def test_clean_part_list_list_array(self): """List of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = [[array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)]] num_ = _clean_part(num) @@ -195,7 +195,7 @@ def test_clean_part_list_list_array(self): def test_clean_part_tuple_list_array(self): """Tuple of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = ([array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)],) num_ = _clean_part(num) @@ -206,7 +206,7 @@ def test_clean_part_tuple_list_array(self): def test_clean_part_list_tuple_array(self): """List of tuple of numpy array for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = [(array([1, 1], dtype=dtype), array([2, 2], dtype=dtype))] num_ = _clean_part(num) @@ -217,7 +217,7 @@ def test_clean_part_list_tuple_array(self): def test_clean_part_tuple_tuples_arrays(self): """Tuple of tuples of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = ((array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)), (array([3, 4], dtype=dtype), array([4, 4], dtype=dtype))) num_ = _clean_part(num) @@ -229,7 +229,7 @@ def test_clean_part_tuple_tuples_arrays(self): def test_clean_part_list_tuples_arrays(self): """List of tuples of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = [(array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)), (array([3, 4], dtype=dtype), array([4, 4], dtype=dtype))] num_ = _clean_part(num) @@ -241,7 +241,7 @@ def test_clean_part_list_tuples_arrays(self): def test_clean_part_list_list_arrays(self): """List of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: num = [[array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)], [array([3, 3], dtype=dtype), array([4, 4], dtype=dtype)]] num_ = _clean_part(num) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 35e411bea..0a1778d1d 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -4,12 +4,15 @@ # RMM, 30 Mar 2011 (based on TestXferFcn from v0.4a) import unittest +import sys as pysys import numpy as np from control.statesp import StateSpace, _convertToStateSpace, rss -from control.xferfcn import TransferFunction, _convert_to_transfer_function, ss2tf +from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ + ss2tf from control.lti import evalfr from control.exception import slycot_check -# from control.lti import isdtime +from control.lti import isctime, isdtime +from control.dtime import sample_system class TestXferFcn(unittest.TestCase): @@ -23,23 +26,53 @@ class TestXferFcn(unittest.TestCase): def test_constructor_bad_input_type(self): """Give the constructor invalid input types.""" - self.assertRaises(TypeError, TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]]) + # MIMO requires lists of lists of vectors (not lists of vectors) + self.assertRaises( + TypeError, + TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]]) + TransferFunction([[ [0., 1.], [2., 3.] ]], [[ [5., 2.], [3., 0.] ]]) + + # Single argument of the wrong type + self.assertRaises(TypeError, TransferFunction, [1]) + + # Too many arguments + self.assertRaises(ValueError, TransferFunction, 1, 2, 3, 4) + + # Different numbers of elements in numerator rows + self.assertRaises( + ValueError, + TransferFunction, [ [[0, 1], [2, 3]], + [[4, 5]] ], + [ [[6, 7], [4, 5]], + [[2, 3], [0, 1]] ]) + self.assertRaises( + ValueError, + TransferFunction, [ [[0, 1], [2, 3]], + [[4, 5], [6, 7]] ], + [ [[6, 7], [4, 5]], + [[2, 3]] ]) + TransferFunction( # This version is OK + [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], + [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ]) def test_constructor_inconsistent_dimension(self): - """Give the constructor a numerator and denominator of different - sizes.""" + """Give constructor numerators, denominators of different sizes.""" - self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.], [2., 3.]]]) - self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.]], [[2., 3.]]]) - self.assertRaises(ValueError, TransferFunction, [[[1.]]], - [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]]) + self.assertRaises(ValueError, TransferFunction, + [[[1.]]], [[[1.], [2., 3.]]]) + self.assertRaises(ValueError, TransferFunction, + [[[1.]]], [[[1.]], [[2., 3.]]]) + self.assertRaises(ValueError, TransferFunction, + [[[1.]]], [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]]) def test_constructor_inconsistent_columns(self): """Give the constructor inputs that do not have the same number of columns in each row.""" - self.assertRaises(ValueError, TransferFunction, 1., [[[1.]], [[2.], [3.]]]) - self.assertRaises(ValueError, TransferFunction, [[[1.]], [[2.], [3.]]], 1.) + self.assertRaises(ValueError, TransferFunction, + 1., [[[1.]], [[2.], [3.]]]) + self.assertRaises(ValueError, TransferFunction, + [[[1.]], [[2.], [3.]]], 1.) def test_constructor_zero_denominator(self): """Give the constructor a transfer function with a zero denominator.""" @@ -53,7 +86,8 @@ def test_add_inconsistent_dimension(self): """Add two transfer function matrices of different sizes.""" sys1 = TransferFunction([[[1., 2.]]], [[[4., 5.]]]) - sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]], [[[1., 6.]], [[2., 4.]]]) + sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]], + [[[1., 6.]], [[2., 4.]]]) self.assertRaises(ValueError, sys1.__add__, sys2) self.assertRaises(ValueError, sys1.__sub__, sys2) self.assertRaises(ValueError, sys1.__radd__, sys2) @@ -64,7 +98,8 @@ def test_mul_inconsistent_dimension(self): sys1 = TransferFunction([[[1., 2.], [4., 5.]], [[2., 5.], [4., 3.]]], [[[6., 2.], [4., 1.]], [[6., 7.], [2., 4.]]]) - sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]], [[[4.]], [[5.]], [[6.]]]) + sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]], + [[[4.]], [[5.]], [[6.]]]) self.assertRaises(ValueError, sys1.__mul__, sys2) self.assertRaises(ValueError, sys2.__mul__, sys1) self.assertRaises(ValueError, sys1.__rmul__, sys2) @@ -312,6 +347,45 @@ def test_divide_siso(self): np.testing.assert_array_equal(sys4.num, sys3.den) np.testing.assert_array_equal(sys4.den, sys3.num) + def test_div(self): + # Make sure that sampling times work correctly + sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) + sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], True) + sys3 = sys1 / sys2 + self.assertEqual(sys3.dt, True) + + sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], 0.5) + sys3 = sys1 / sys2 + self.assertEqual(sys3.dt, 0.5) + + sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) + self.assertRaises(ValueError, TransferFunction.__truediv__, sys1, sys2) + + sys1 = sample_system(rss(4, 1, 1), 0.5) + sys3 = TransferFunction.__rtruediv__(sys2, sys1) + self.assertEqual(sys3.dt, 0.5) + + def test_pow(self): + sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) + self.assertRaises(ValueError, TransferFunction.__pow__, sys1, 0.5) + + def test_slice(self): + sys = TransferFunction( + [ [ [1], [2], [3]], [ [3], [4], [5]] ], + [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ]) + sys1 = sys[1:, 1:] + self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) + + sys2 = sys[:2, :2] + self.assertEqual((sys2.inputs, sys2.outputs), (2, 2)) + + sys = TransferFunction( + [ [ [1], [2], [3]], [ [3], [4], [5]] ], + [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5) + sys1 = sys[1:, 1:] + self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) + self.assertEqual(sys1.dt, 0.5) + def test_evalfr_siso(self): """Evaluate the frequency response of a SISO system at one frequency.""" @@ -319,26 +393,42 @@ def test_evalfr_siso(self): np.testing.assert_array_almost_equal(evalfr(sys, 1j), np.array([[-0.5 - 0.5j]])) - np.testing.assert_array_almost_equal(evalfr(sys, 32j), - np.array([[0.00281959302585077 - 0.030628473607392j]])) + np.testing.assert_array_almost_equal( + evalfr(sys, 32j), + np.array([[0.00281959302585077 - 0.030628473607392j]])) # Test call version as well np.testing.assert_almost_equal(sys(1.j), -0.5 - 0.5j) - np.testing.assert_almost_equal(sys(32.j), 0.00281959302585077 - 0.030628473607392j) + np.testing.assert_almost_equal( + sys(32.j), 0.00281959302585077 - 0.030628473607392j) # Test internal version (with real argument) - np.testing.assert_array_almost_equal(sys._evalfr(1.), - np.array([[-0.5 - 0.5j]])) - np.testing.assert_array_almost_equal(sys._evalfr(32.), - np.array([[0.00281959302585077 - 0.030628473607392j]])) + np.testing.assert_array_almost_equal( + sys._evalfr(1.), np.array([[-0.5 - 0.5j]])) + np.testing.assert_array_almost_equal( + sys._evalfr(32.), + np.array([[0.00281959302585077 - 0.030628473607392j]])) + + # This test only works in Python 3 due to a conflict with the same + # warning type in other test modules (frd_test.py). See + # https://bugs.python.org/issue4180 for more details + @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") + def test_evalfr_deprecated(self): + sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) # Deprecated version of the call (should generate warning) import warnings - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - sys.evalfr(1.) - assert len(w) == 1 - assert issubclass(w[-1].category, PendingDeprecationWarning) + with warnings.catch_warnings(): + # Make warnings generate an exception + warnings.simplefilter('error') + + # Make sure that we get a pending deprecation warning + self.assertRaises(PendingDeprecationWarning, sys.evalfr, 1.) + + @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") + def test_evalfr_dtime(self): + sys = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) + np.testing.assert_array_almost_equal(sys(1j), -0.5 - 0.5j) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_evalfr_mimo(self): @@ -359,7 +449,8 @@ def test_evalfr_mimo(self): np.testing.assert_array_almost_equal(sys(2.j), resp) def test_freqresp_siso(self): - """Evaluate the magnitude and phase of a SISO system at multiple frequencies.""" + """Evaluate the magnitude and phase of a SISO system at + multiple frequencies.""" sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) @@ -376,7 +467,8 @@ def test_freqresp_siso(self): @unittest.skipIf(not slycot_check(), "slycot not installed") def test_freqresp_mimo(self): - """Evaluate the magnitude and phase of a MIMO system at multiple frequencies.""" + """Evaluate the magnitude and phase of a MIMO system at + multiple frequencies.""" num = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] @@ -385,16 +477,17 @@ def test_freqresp_mimo(self): sys = TransferFunction(num, den) true_omega = [0.1, 1., 10.] - true_mag = [[[0.496287094505259, 0.307147558416976, 0.0334738176210382], + true_mag = [[[0.49628709, 0.30714755, 0.03347381], [300., 3., 0.03], [1., 1., 1.]], - [[33.3333333333333, 0.333333333333333, 0.00333333333333333], - [0.390285696125482, 1.26491106406735, 0.198759144198533], - [3.01663720059274, 4.47213595499958, 104.92378186093]]] - true_phase = [[[3.7128711165168e-4, 0.185347949995695, 1.30770596539255], - [-np.pi, -np.pi, -np.pi], [0., 0., 0.]], + [[33.333333, 0.33333333, 0.00333333], + [0.39028569, 1.26491106, 0.19875914], + [3.01663720, 4.47213595, 104.92378186]]] + true_phase = [[[3.7128711e-4, 0.18534794, + 1.30770596], [-np.pi, -np.pi, -np.pi], + [0., 0., 0.]], [[-np.pi, -np.pi, -np.pi], - [-1.66852323415362, -1.89254688119154, -1.62050658356412], - [-0.132989648369409, -1.1071487177940, -2.7504672066207]]] + [-1.66852323, -1.89254688, -1.62050658], + [-0.13298964, -1.10714871, -2.75046720]]] mag, phase, omega = sys.freqresp(true_omega) @@ -403,24 +496,73 @@ def test_freqresp_mimo(self): np.testing.assert_array_equal(omega, true_omega) # Tests for TransferFunction.pole and TransferFunction.zero. - + def test_common_den(self): + """ Test the helper function to compute common denomitators.""" + + # _common_den() computes the common denominator per input/column. + # The testing columns are: + # 0: no common poles + # 1: regular common poles + # 2: poles with multiplicity, + # 3: complex poles + # 4: complex poles below threshold + + eps = np.finfo(float).eps + tol_imag = np.sqrt(eps*5*2*2)*0.9 + + numin = [[[1.], [1.], [1.], [1.], [1.]], + [[1.], [1.], [1.], [1.], [1.]]] + denin = [[[1., 3., 2.], # 0: poles: [-1, -2] + [1., 6., 11., 6.], # 1: poles: [-1, -2, -3] + [1., 6., 11., 6.], # 2: poles: [-1, -2, -3] + [1., 6., 11., 6.], # 3: poles: [-1, -2, -3] + [1., 6., 11., 6.]], # 4: poles: [-1, -2, -3], + [[1., 12., 47., 60.], # 0: poles: [-3, -4, -5] + [1., 9., 26., 24.], # 1: poles: [-2, -3, -4] + [1., 7., 16., 12.], # 2: poles: [-2, -2, -3] + [1., 7., 17., 15.], # 3: poles: [-2+1J, -2-1J, -3], + np.poly([-2 + tol_imag * 1J, -2 - tol_imag * 1J, -3])]] + numref = np.array([ + [[0., 0., 1., 12., 47., 60.], + [0., 0., 0., 1., 4., 0.], + [0., 0., 0., 1., 2., 0.], + [0., 0., 0., 1., 4., 5.], + [0., 0., 0., 1., 2., 0.]], + [[0., 0., 0., 1., 3., 2.], + [0., 0., 0., 1., 1., 0.], + [0., 0., 0., 1., 1., 0.], + [0., 0., 0., 1., 3., 2.], + [0., 0., 0., 1., 1., 0.]]]) + denref = np.array( + [[1., 15., 85., 225., 274., 120.], + [1., 10., 35., 50., 24., 0.], + [1., 8., 23., 28., 12., 0.], + [1., 10., 40., 80., 79., 30.], + [1., 8., 23., 28., 12., 0.]]) + sys = TransferFunction(numin, denin) + num, den, denorder = sys._common_den() + np.testing.assert_array_almost_equal(num[:2, :, :], numref) + np.testing.assert_array_almost_equal(num[2:, :, :], + np.zeros((3, 5, 6))) + np.testing.assert_array_almost_equal(den, denref) + @unittest.skipIf(not slycot_check(), "slycot not installed") def test_pole_mimo(self): """Test for correct MIMO poles.""" - sys = TransferFunction([[[1.], [1.]], [[1.], [1.]]], - [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) + sys = TransferFunction( + [[[1.], [1.]], [[1.], [1.]]], + [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) p = sys.pole() np.testing.assert_array_almost_equal(p, [-2., -2., -7., -3., -2.]) - @unittest.skipIf(not slycot_check(), "slycot not installed") def test_double_cancelling_poles_siso(self): - + H = TransferFunction([1, 1], [1, 2, 1]) p = H.pole() np.testing.assert_array_almost_equal(p, [-1, -1]) - + # Tests for TransferFunction.feedback def test_feedback_siso(self): """Test for correct SISO transfer function feedback.""" @@ -597,7 +739,7 @@ def test_dcgain_discr(self): # summer # causes a RuntimeWarning due to the divide by zero - sys = TransferFunction([1,-1], [1], True) + sys = TransferFunction([1, -1], [1], True) np.testing.assert_equal(sys.dcgain(), 0) def test_ss2tf(self): @@ -610,6 +752,80 @@ def test_ss2tf(self): np.testing.assert_almost_equal(sys.num, true_sys.num) np.testing.assert_almost_equal(sys.den, true_sys.den) + def test_class_constants(self): + # Make sure that the 's' variable is defined properly + s = TransferFunction.s + G = (s + 1)/(s**2 + 2*s + 1) + np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) + np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) + self.assertTrue(isctime(G, strict=True)) + + # Make sure that the 'z' variable is defined properly + z = TransferFunction.z + G = (z + 1)/(z**2 + 2*z + 1) + np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) + np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) + self.assertTrue(isdtime(G, strict=True)) + + def test_printing(self): + # SISO, continuous time + sys = ss2tf(rss(4, 1, 1)) + self.assertTrue(isinstance(str(sys), str)) + self.assertTrue(isinstance(sys._repr_latex_(), str)) + + # SISO, discrete time + sys = sample_system(sys, 1) + self.assertTrue(isinstance(str(sys), str)) + self.assertTrue(isinstance(sys._repr_latex_(), str)) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_printing_mimo(self): + # MIMO, continuous time + sys = ss2tf(rss(4, 2, 3)) + self.assertTrue(isinstance(str(sys), str)) + self.assertTrue(isinstance(sys._repr_latex_(), str)) + + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_size_mismatch(self): + sys1 = ss2tf(rss(2, 2, 2)) + + # Different number of inputs + sys2 = ss2tf(rss(3, 1, 2)) + self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) + + # Different number of outputs + sys2 = ss2tf(rss(3, 2, 1)) + self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) + + # Inputs and outputs don't match + self.assertRaises(ValueError, TransferFunction.__mul__, sys2, sys1) + + # Feedback mismatch (MIMO not implemented) + self.assertRaises(NotImplementedError, + TransferFunction.feedback, sys2, sys1) + + def test_latex_repr(self): + """ Test latex printout for TransferFunction """ + Hc = TransferFunction([1e-5, 2e5, 3e-4], + [1.2e34, 2.3e-4, 2.3e-45]) + Hd = TransferFunction([1e-5, 2e5, 3e-4], + [1.2e34, 2.3e-4, 2.3e-45], + .1) + # TODO: make the multiplication sign configurable + expmul = r'\times' + for var, H, suffix in zip(['s', 'z'], + [Hc, Hd], + ['', r'\quad dt = 0.1']): + ref = (r'$$\frac{' + r'1 ' + expmul + ' 10^{-5} ' + var + '^2 ' + r'+ 2 ' + expmul + ' 10^{5} ' + var + ' + 0.0003' + r'}{' + r'1.2 ' + expmul + ' 10^{34} ' + var + '^2 ' + r'+ 0.00023 ' + var + ' ' + r'+ 2.3 ' + expmul + ' 10^{-45}' + r'}' + suffix + '$$') + self.assertEqual(H._repr_latex_(), ref) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestXferFcn) diff --git a/control/timeresp.py b/control/timeresp.py index 14e7072d2..0521fcc74 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -1,11 +1,25 @@ -# timeresp.py - time-domain simulation routes -"""Time domain simulation. +# timeresp.py - time-domain simulation routines +# +# This file contains a collection of functions that calculate time +# responses for linear systems. -This file contains a collection of functions that calculate time -responses for linear systems. +"""The :mod:`~control.timeresp` module contains a collection of +functions that are used to compute time-domain simulations of LTI +systems. -See doc/conventions.rst#time-series-conventions_ for more information -on how time series data are represented. +Arguments to time-domain simulations include a time vector, an input +vector (when needed), and an initial condition vector. The most +general function for simulating LTI systems the +:func:`forced_response` function, which has the form:: + + t, y = forced_response(sys, T, U, X0) + +where `T` is a vector of times at which the response should be +evaluated, `U` is a vector of inputs (one for each time point) and +`X0` is the initial condition for the system. + +See :ref:`time-series-convention` for more information on how time +series data are represented. """ @@ -61,6 +75,7 @@ __all__ = ['forced_response', 'step_response', 'step_info', 'initial_response', 'impulse_response'] + # Helper function for checking array-like parameters def _check_convert_array(in_obj, legal_shapes, err_msg_start, squeeze=False, transpose=False): @@ -170,7 +185,7 @@ def shape_matches(s_legal, s_actual): # Forced response of a linear system def forced_response(sys, T=None, U=0., X0=0., transpose=False, - interpolate=False): + interpolate=False, squeeze=True): """Simulate the output of a linear system. As a convenience for parameters `U`, `X0`: @@ -185,29 +200,35 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, sys: LTI (StateSpace, or TransferFunction) LTI system to simulate - T: array-like + T: array-like, optional for discrete LTI `sys` Time steps at which the input is defined; values must be evenly spaced. U: array-like or number, optional Input array giving input at each time `T` (default = 0). If `U` is ``None`` or ``0``, a special algorithm is used. This special - algorithm is faster than the general algorithm, which is used otherwise. + algorithm is faster than the general algorithm, which is used + otherwise. X0: array-like or number, optional Initial condition (default = 0). - transpose: bool + transpose: bool, optional (default=False) If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) - interpolate:bool + interpolate: bool, optional (default=False) If True and system is a discrete time system, the input will be interpolated between the given time steps and the output will be given at system sampling rate. Otherwise, only return the output at the times given in `T`. No effect on continuous time simulations (default = False). + squeeze: bool, optional (default=True) + If True, remove single-dimensional entries from the shape of + the output. For single output systems, this converts the + output response to a 1D array. + Returns ------- T: array @@ -221,11 +242,21 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, -------- step_response, initial_response, impulse_response + Notes + ----- + For discrete time systems, the input/output response is computed using the + :scipy-signal:ref:`scipy.signal.dlsim` function. + + For continuous time systems, the output is computed using the matrix + exponential `exp(A t)` and assuming linear interpolation of the inputs + between time points. + Examples -------- >>> T, yout, xout = forced_response(sys, T, u, X0) See :ref:`time-series-convention`. + """ if not isinstance(sys, LTI): raise TypeError('Parameter ``sys``: must be a ``LTI`` object. ' @@ -238,6 +269,12 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, n_inputs = B.shape[1] n_outputs = C.shape[0] + # Convert inputs to numpy arrays for easier shape checking + if U is not None: + U = np.asarray(U) + if T is not None: + T = np.asarray(T) + # Set and/or check time vector in discrete time case if isdtime(sys, strict=True): if T is None: @@ -245,13 +282,18 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, raise ValueError('Parameters ``T`` and ``U`` can\'t both be' 'zero for discrete-time simulation') # Set T to equally spaced samples with same length as U - T = np.array(range(len(U))) * (1 if sys.dt == True else sys.dt) + if U.ndim == 1: + n_steps = U.shape[0] + else: + n_steps = U.shape[1] + T = np.array(range(n_steps)) * (1 if sys.dt is True else sys.dt) else: # Make sure the input vector and time vector have same length # TODO: allow interpolation of the input vector - if len(U) != len(T): - ValueError('Pamameter ``T`` must have same length as' - 'input vector ``U``') + if (U.ndim == 1 and U.shape[0] != T.shape[0]) or \ + (U.ndim > 1 and U.shape[1] != T.shape[0]): + ValueError('Pamameter ``T`` must have same elements as' + ' the number of columns in input array ``U``') # Test if T has shape (n,) or (1, n); # T must be array-like and values must be increasing. @@ -264,8 +306,9 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, transpose=transpose) dt = T[1] - T[0] if not np.allclose(T[1:] - T[:-1], dt): - raise ValueError('Parameter ``T``: time values must be equally spaced.') - n_steps = len(T) # number of simulation steps + raise ValueError("Parameter ``T``: time values must be " + "equally spaced.") + n_steps = T.shape[0] # number of simulation steps # create X0 if not given, test if X0 has correct shape X0 = _check_convert_array(X0, [(n_states,), (n_states, 1)], @@ -310,7 +353,7 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, # [ u(dt) ] = exp [ 0 0 I ] [ u0 ] # [u1 - u0] [ 0 0 0 ] [u1 - u0] - M = np.bmat([[A * dt, B * dt, np.zeros((n_states, n_inputs))], + M = np.block([[A * dt, B * dt, np.zeros((n_states, n_inputs))], [np.zeros((n_inputs, n_states + n_inputs)), np.identity(n_inputs)], [np.zeros((n_inputs, n_states + 2 * n_inputs))]]) @@ -323,55 +366,61 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, xout[:, i] = (dot(Ad, xout[:, i-1]) + dot(Bd0, U[:, i-1]) + dot(Bd1, U[:, i])) yout = dot(C, xout) + dot(D, U) - tout = T - yout = squeeze(yout) - xout = squeeze(xout) else: # Discrete type system => use SciPy signal processing toolbox - if (sys.dt != True): + if sys.dt is not True: # Make sure that the time increment is a multiple of sampling time # First make sure that time increment is bigger than sampling time - if dt < sys.dt: + # (with allowance for small precision errors) + if dt < sys.dt and not np.isclose(dt, sys.dt): raise ValueError("Time steps ``T`` must match sampling time") # Now check to make sure it is a multiple (with check against # sys.dt because floating point mod can have small errors elif not (np.isclose(dt % sys.dt, 0) or np.isclose(dt % sys.dt, sys.dt)): - raise ValueError("Time steps ``T`` must be multiples of " \ + raise ValueError("Time steps ``T`` must be multiples of " "sampling time") + sys_dt = sys.dt + else: - sys.dt = dt # For unspecified sampling time, use time incr + sys_dt = dt # For unspecified sampling time, use time incr # Discrete time simulation using signal processing toolbox - dsys = (A, B, C, D, sys.dt) + dsys = (A, B, C, D, sys_dt) # Use signal processing toolbox for the discrete time simulation - # Transpose the input to match toolbox convention + # Transpose the input to match toolbox convention tout, yout, xout = sp.signal.dlsim(dsys, np.transpose(U), T, X0) if not interpolate: # If dt is different from sys.dt, resample the output - inc = int(round(dt / sys.dt)) + inc = int(round(dt / sys_dt)) tout = T # Return exact list of time steps - yout = yout[::inc,:] - xout = xout[::inc,:] + yout = yout[::inc, :] + xout = xout[::inc, :] # Transpose the output and state vectors to match local convention xout = sp.transpose(xout) yout = sp.transpose(yout) + # Get rid of unneeded dimensions + if squeeze: + yout = np.squeeze(yout) + xout = np.squeeze(xout) + # See if we need to transpose the data back into MATLAB form - if (transpose): + if transpose: tout = np.transpose(tout) yout = np.transpose(yout) xout = np.transpose(xout) return tout, yout, xout + def _get_ss_simo(sys, input=None, output=None): """Return a SISO or SIMO state-space version of sys @@ -390,8 +439,9 @@ def _get_ss_simo(sys, input=None, output=None): else: return _mimo2siso(sys_ss, input, output, warn_conversion=warn) + def step_response(sys, T=None, X0=0., input=None, output=None, - transpose=False, return_x=False): + transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Step response of a linear system @@ -430,6 +480,11 @@ def step_response(sys, T=None, X0=0., input=None, output=None, return_x: bool If True, return the state vector (default = False). + squeeze: bool, optional (default=True) + If True, remove single-dimensional entries from the shape of + the output. For single output systems, this converts the + output response to a 1D array. + Returns ------- T: array @@ -445,9 +500,15 @@ def step_response(sys, T=None, X0=0., input=None, output=None, -------- forced_response, initial_response, impulse_response + Notes + ----- + This function uses the `forced_response` function with the input set to a + unit step. + Examples -------- >>> T, yout = step_response(sys, T, X0) + """ sys = _get_ss_simo(sys, input, output) if T is None: @@ -460,15 +521,17 @@ def step_response(sys, T=None, X0=0., input=None, output=None, U = np.ones_like(T) - T, yout, xout = forced_response(sys, T, U, X0, - transpose=transpose) + T, yout, xout = forced_response(sys, T, U, X0, transpose=transpose, + squeeze=squeeze) if return_x: return T, yout, xout return T, yout -def step_info(sys, T=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1,0.9)): + +def step_info(sys, T=None, SettlingTimeThreshold=0.02, + RiseTimeLimits=(0.1, 0.9)): ''' Step response characteristics (Rise time, Settling Time, Peak and others). @@ -561,8 +624,9 @@ def step_info(sys, T=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1,0.9)) return S + def initial_response(sys, T=None, X0=0., input=0, output=None, - transpose=False, return_x=False): + transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Initial condition response of a linear system @@ -601,6 +665,11 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, return_x: bool If True, return the state vector (default = False). + squeeze: bool, optional (default=True) + If True, remove single-dimensional entries from the shape of + the output. For single output systems, this converts the + output response to a 1D array. + Returns ------- T: array @@ -614,6 +683,11 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, -------- forced_response, impulse_response, step_response + Notes + ----- + This function uses the `forced_response` function with the input set to + zero. + Examples -------- >>> T, yout = initial_response(sys, T, X0) @@ -631,7 +705,8 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, T = range(int(np.ceil(max(tvec)))) U = np.zeros_like(T) - T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose) + T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose, + squeeze=squeeze) if return_x: return T, yout, _xout @@ -640,7 +715,7 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, def impulse_response(sys, T=None, X0=0., input=0, output=None, - transpose=False, return_x=False): + transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Impulse response of a linear system @@ -679,6 +754,11 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, return_x: bool If True, return the state vector (default = False). + squeeze: bool, optional (default=True) + If True, remove single-dimensional entries from the shape of + the output. For single output systems, this converts the + output response to a 1D array. + Returns ------- T: array @@ -692,17 +772,26 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, -------- forced_response, initial_response, step_response + Notes + ----- + This function uses the `forced_response` function to compute the time + response. For continuous time systems, the initial condition is altered to + account for the initial impulse. + Examples -------- >>> T, yout = impulse_response(sys, T, X0) + """ sys = _get_ss_simo(sys, input, output) - # System has direct feedthrough, can't simulate impulse response numerically + # System has direct feedthrough, can't simulate impulse response + # numerically if np.any(sys.D != 0) and isctime(sys): - warnings.warn('System has direct feedthrough: ``D != 0``. The infinite ' - 'impulse at ``t=0`` does not appear in the output. \n' - 'Results may be meaningless!') + warnings.warn("System has direct feedthrough: ``D != 0``. The " + "infinite impulse at ``t=0`` does not appear in the " + "output.\n" + "Results may be meaningless!") # create X0 if not given, test if X0 has correct shape. # Must be done here because it is used for computations here. @@ -732,9 +821,8 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, new_X0 = X0 U[0] = 1. - T, yout, _xout = forced_response( - sys, T, U, new_X0, - transpose=transpose) + T, yout, _xout = forced_response(sys, T, U, new_X0, transpose=transpose, + squeeze=squeeze) if return_x: return T, yout, _xout diff --git a/control/xferfcn.py b/control/xferfcn.py index 1ef0661a5..017d90437 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -57,11 +57,11 @@ polyadd, polymul, polyval, roots, sqrt, zeros, squeeze, exp, pi, \ where, delete, real, poly, nonzero import scipy as sp -from numpy.polynomial.polynomial import polyfromroots from scipy.signal import lti, tf2zpk, zpk2tf, cont2discrete from copy import deepcopy from warnings import warn from itertools import chain +from re import sub from .lti import LTI, timebaseEqual, timebase, isdtime __all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata'] @@ -73,8 +73,8 @@ class TransferFunction(LTI): A class for representing transfer functions - The TransferFunction class is used to represent systems in transfer function - form. + The TransferFunction class is used to represent systems in transfer + function form. The main data members are 'num' and 'den', which are 2-D lists of arrays containing MIMO numerator and denominator coefficients. For example, @@ -84,13 +84,21 @@ class TransferFunction(LTI): means that the numerator of the transfer function from the 6th input to the 3rd output is set to s^2 + 4s + 8. - Discrete-time transfer functions are implemented by using the 'dt' instance - variable and setting it to something other than 'None'. If 'dt' has a - non-zero value, then it must match whenever two transfer functions are - combined. If 'dt' is set to True, the system will be treated as a + Discrete-time transfer functions are implemented by using the 'dt' + instance variable and setting it to something other than 'None'. If 'dt' + has a non-zero value, then it must match whenever two transfer functions + are combined. If 'dt' is set to True, the system will be treated as a discrete time system with unspecified sampling time. - """ + The TransferFunction class defines two constants ``s`` and ``z`` that + represent the differentiation and delay operators in continuous and + discrete time. These can be used to create variables that allow algebraic + creation of transfer functions. For example, + + >>> s = TransferFunction.s + >>> G = (s + 1)/(s**2 + 2*s + 1) + + """ def __init__(self, *args): """TransferFunction(num, den[, dt]) @@ -121,9 +129,10 @@ def __init__(self, *args): % type(args[0])) num = args[0].num den = args[0].den + # TODO: not sure this can ever happen since dt is always present try: dt = args[0].dt - except NameError: + except NameError: # pragma: no coverage dt = None else: raise ValueError("Needs 1, 2 or 3 arguments; received %i." @@ -137,21 +146,25 @@ def __init__(self, *args): # Make sure numerator and denominator matrices have consistent sizes if inputs != len(den[0]): - raise ValueError("The numerator has %i input(s), but the denominator has " - "%i\ninput(s)." % (inputs, len(den[0]))) + raise ValueError( + "The numerator has %i input(s), but the denominator has " + "%i input(s)." % (inputs, len(den[0]))) if outputs != len(den): - raise ValueError("The numerator has %i output(s), but the denominator has " - "%i\noutput(s)." % (outputs, len(den))) + raise ValueError( + "The numerator has %i output(s), but the denominator has " + "%i output(s)." % (outputs, len(den))) # Additional checks/updates on structure of the transfer function for i in range(outputs): # Make sure that each row has the same number of columns if len(num[i]) != inputs: - raise ValueError("Row 0 of the numerator matrix has %i elements, but row " - "%i\nhas %i." % (inputs, i, len(num[i]))) + raise ValueError( + "Row 0 of the numerator matrix has %i elements, but row " + "%i has %i." % (inputs, i, len(num[i]))) if len(den[i]) != inputs: - raise ValueError("Row 0 of the denominator matrix has %i elements, but row " - "%i\nhas %i." % (inputs, i, len(den[i]))) + raise ValueError( + "Row 0 of the denominator matrix has %i elements, but row " + "%i has %i." % (inputs, i, len(den[i]))) # Check for zeros in numerator or denominator # TODO: Right now these checks are only done during construction. @@ -165,8 +178,9 @@ def __init__(self, *args): zeroden = False break if zeroden: - raise ValueError("Input %i, output %i has a zero denominator." - % (j + 1, i + 1)) + raise ValueError( + "Input %i, output %i has a zero denominator." + % (j + 1, i + 1)) # If we have zero numerators, set the denominator to 1. zeronum = True @@ -231,7 +245,7 @@ def __str__(self, var=None): mimo = self.inputs > 1 or self.outputs > 1 if var is None: - #! TODO: replace with standard calls to lti functions + # TODO: replace with standard calls to lti functions var = 's' if self.dt is None or self.dt == 0 else 'z' outstr = "" @@ -269,7 +283,7 @@ def __str__(self, var=None): __repr__ = __str__ def _repr_latex_(self, var=None): - """LaTeX representation of the transfer function, for Jupyter notebook""" + """LaTeX representation of transfer function, for Jupyter notebook""" mimo = self.inputs > 1 or self.outputs > 1 @@ -288,6 +302,9 @@ def _repr_latex_(self, var=None): numstr = _tf_polynomial_to_string(self.num[i][j], var=var) denstr = _tf_polynomial_to_string(self.den[i][j], var=var) + numstr = _tf_string_to_latex(numstr, var=var) + denstr = _tf_string_to_latex(denstr, var=var) + out += [r"\frac{", numstr, "}{", denstr, "}"] if mimo and j < self.outputs - 1: @@ -301,7 +318,7 @@ def _repr_latex_(self, var=None): # See if this is a discrete time system with specific sampling time if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: - out += ["\quad dt = ", str(self.dt)] + out += [r"\quad dt = ", str(self.dt)] out.append("$$") @@ -330,16 +347,19 @@ def __add__(self, other): # Check that the input-output sizes are consistent. if self.inputs != other.inputs: - raise ValueError("The first summand has %i input(s), but the second has %i." - % (self.inputs, other.inputs)) + raise ValueError( + "The first summand has %i input(s), but the second has %i." + % (self.inputs, other.inputs)) if self.outputs != other.outputs: - raise ValueError("The first summand has %i output(s), but the second has %i." - % (self.outputs, other.outputs)) + raise ValueError( + "The first summand has %i output(s), but the second has %i." + % (self.outputs, other.outputs)) # Figure out the sampling time to use if self.dt is None and other.dt is not None: dt = other.dt # use dt from second argument - elif (other.dt is None and self.dt is not None) or (timebaseEqual(self, other)): + elif (other.dt is None and self.dt is not None) or \ + (timebaseEqual(self, other)): dt = self.dt # use dt from first argument else: raise ValueError("Systems have different sampling times") @@ -350,9 +370,9 @@ def __add__(self, other): for i in range(self.outputs): for j in range(self.inputs): - num[i][j], den[i][j] = _add_siso(self.num[i][j], self.den[i][j], - other.num[i][j], - other.den[i][j]) + num[i][j], den[i][j] = _add_siso( + self.num[i][j], self.den[i][j], + other.num[i][j], other.den[i][j]) return TransferFunction(num, den, dt) @@ -379,8 +399,9 @@ def __mul__(self, other): # Check that the input-output sizes are consistent. if self.inputs != other.outputs: - raise ValueError("C = A * B: A has %i column(s) (input(s)), but B has %i " - "row(s)\n(output(s))." % (self.inputs, other.outputs)) + raise ValueError( + "C = A * B: A has %i column(s) (input(s)), but B has %i " + "row(s)\n(output(s))." % (self.inputs, other.outputs)) inputs = other.inputs outputs = self.outputs @@ -388,7 +409,8 @@ def __mul__(self, other): # Figure out the sampling time to use if self.dt is None and other.dt is not None: dt = other.dt # use dt from second argument - elif (other.dt is None and self.dt is not None) or (self.dt == other.dt): + elif (other.dt is None and self.dt is not None) or \ + (self.dt == other.dt): dt = self.dt # use dt from first argument else: raise ValueError("Systems have different sampling times") @@ -397,7 +419,8 @@ def __mul__(self, other): num = [[[0] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] - # Temporary storage for the summands needed to find the (i, j)th element of the product. + # Temporary storage for the summands needed to find the (i, j)th + # element of the product. num_summand = [[] for k in range(self.inputs)] den_summand = [[] for k in range(self.inputs)] @@ -405,8 +428,10 @@ def __mul__(self, other): for row in range(outputs): for col in range(inputs): for k in range(self.inputs): - num_summand[k] = polymul(self.num[row][k], other.num[k][col]) - den_summand[k] = polymul(self.den[row][k], other.den[k][col]) + num_summand[k] = polymul( + self.num[row][k], other.num[k][col]) + den_summand[k] = polymul( + self.den[row][k], other.den[k][col]) num[row][col], den[row][col] = _add_siso( num[row][col], den[row][col], num_summand[k], den_summand[k]) @@ -425,8 +450,9 @@ def __rmul__(self, other): # Check that the input-output sizes are consistent. if other.inputs != self.outputs: - raise ValueError("C = A * B: A has %i column(s) (input(s)), but B has %i " - "row(s)\n(output(s))." % (other.inputs, self.outputs)) + raise ValueError( + "C = A * B: A has %i column(s) (input(s)), but B has %i " + "row(s)\n(output(s))." % (other.inputs, self.outputs)) inputs = self.inputs outputs = other.outputs @@ -481,7 +507,8 @@ def __truediv__(self, other): # Figure out the sampling time to use if self.dt is None and other.dt is not None: dt = other.dt # use dt from second argument - elif (other.dt is None and self.dt is not None) or (self.dt == other.dt): + elif (other.dt is None and self.dt is not None) or \ + (self.dt == other.dt): dt = self.dt # use dt from first argument else: raise ValueError("Systems have different sampling times") @@ -508,7 +535,8 @@ def __rtruediv__(self, other): if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): raise NotImplementedError( - "TransferFunction.__rtruediv__ is currently implemented only for SISO systems.") + "TransferFunction.__rtruediv__ is currently implemented only " + "for SISO systems.") return other / self @@ -617,10 +645,11 @@ def freqresp(self, omega): mag, phase, omega = self.freqresp(omega) - reports the value of the magnitude, phase, and angular frequency of the - transfer function matrix evaluated at s = i * omega, where omega is a - list of angular frequencies, and is a sorted - version of the input omega. + reports the value of the magnitude, phase, and angular frequency of + the transfer function matrix evaluated at s = i * omega, where omega + is a list of angular frequencies, and is a sorted version of the input + omega. + """ # Preallocate outputs. @@ -659,8 +688,9 @@ def pole(self): def zero(self): """Compute the zeros of a transfer function.""" if self.inputs > 1 or self.outputs > 1: - raise NotImplementedError("TransferFunction.zero is currently only implemented " - "for SISO systems.") + raise NotImplementedError( + "TransferFunction.zero is currently only implemented " + "for SISO systems.") else: # for now, just give zeros of a SISO tf return roots(self.num[0][0]) @@ -672,13 +702,15 @@ def feedback(self, other=1, sign=-1): if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): # TODO: MIMO feedback - raise NotImplementedError("TransferFunction.feedback is currently only implemented " - "for SISO functions.") + raise NotImplementedError( + "TransferFunction.feedback is currently only implemented " + "for SISO functions.") # Figure out the sampling time to use if self.dt is None and other.dt is not None: dt = other.dt # use dt from second argument - elif (other.dt is None and self.dt is not None) or (self.dt == other.dt): + elif (other.dt is None and self.dt is not None) or \ + (self.dt == other.dt): dt = self.dt # use dt from first argument else: raise ValueError("Systems have different sampling times") @@ -774,8 +806,6 @@ def _common_den(self, imag_tol=None): output numerator array num is modified to use the common denominator for this input/column; the coefficient arrays are also padded with zeros to be the same size for all num/den. - num is an sys.outputs by sys.inputs - by len(d) array. Parameters ---------- @@ -786,17 +816,20 @@ def _common_den(self, imag_tol=None): Returns ------- num: array - Multi-dimensional array of numerator coefficients. num[i][j] - gives the numerator coefficient array for the ith input and jth - output, also prepared for use in td04ad; matches the denorder - order; highest coefficient starts on the left. + n by n by kd where n = max(sys.outputs,sys.inputs) + kd = max(denorder)+1 + Multi-dimensional array of numerator coefficients. num[i,j] + gives the numerator coefficient array for the ith output and jth + input; padded for use in td04ad ('C' option); matches the + denorder order; highest coefficient starts on the left. den: array + sys.inputs by kd Multi-dimensional array of coefficients for common denominator polynomial, one row per input. The array is prepared for use in slycot td04ad, the first element is the highest-order polynomial - coefficiend of s, matching the order in denorder, if denorder < - number of columns in den, the den is padded with zeros + coefficient of s, matching the order in denorder. If denorder < + number of columns in den, the den is padded with zeros. denorder: array of int, orders of den, one per input @@ -810,16 +843,18 @@ def _common_den(self, imag_tol=None): # Machine precision for floats. eps = finfo(float).eps + real_tol = sqrt(eps * self.inputs * self.outputs) - # Decide on the tolerance to use in deciding of a pole is complex + # The tolerance to use in deciding if a pole is complex if (imag_tol is None): - imag_tol = 1e-8 # TODO: figure out the right number to use + imag_tol = 2 * real_tol # A list to keep track of cumulative poles found as we scan # self.den[..][..] poles = [[] for j in range(self.inputs)] # RvP, new implementation 180526, issue #194 + # BG, modification, issue #343, PR #354 # pre-calculate the poles for all num, den # has zeros, poles, gain, list for pole indices not in den, @@ -838,30 +873,37 @@ def _common_den(self, imag_tol=None): poleset[-1].append([z, p, k, [], 0]) # collect all individual poles - epsnm = eps * self.inputs * self.outputs for j in range(self.inputs): for i in range(self.outputs): currentpoles = poleset[i][j][1] nothave = ones(currentpoles.shape, dtype=bool) for ip, p in enumerate(poles[j]): - idx, = nonzero( - (abs(currentpoles - p) < epsnm) * nothave) - if len(idx): - nothave[idx[0]] = False + collect = (np.isclose(currentpoles.real, p.real, + atol=real_tol) & + np.isclose(currentpoles.imag, p.imag, + atol=imag_tol) & + nothave) + if np.any(collect): + # mark first found pole as already collected + nothave[nonzero(collect)[0][0]] = False else: # remember id of pole not in tf poleset[i][j][3].append(ip) for h, c in zip(nothave, currentpoles): if h: + if abs(c.imag) < imag_tol: + c = c.real poles[j].append(c) # remember how many poles now known poleset[i][j][4] = len(poles[j]) # figure out maximum number of poles, for sizing the den - npmax = max([len(p) for p in poles]) - den = zeros((self.inputs, npmax + 1), dtype=float) + maxindex = max([len(p) for p in poles]) + den = zeros((self.inputs, maxindex + 1), dtype=float) num = zeros((max(1, self.outputs, self.inputs), - max(1, self.outputs, self.inputs), npmax + 1), dtype=float) + max(1, self.outputs, self.inputs), + maxindex + 1), + dtype=float) denorder = zeros((self.inputs,), dtype=int) for j in range(self.inputs): @@ -872,11 +914,10 @@ def _common_den(self, imag_tol=None): num[i, j, 0] = poleset[i][j][2] else: # create the denominator matching this input - # polyfromroots gives coeffs in opposite order from what we use - # coefficients should be padded on right, ending at np - np = len(poles[j]) - den[j, np::-1] = polyfromroots(poles[j]).real - denorder[j] = np + # coefficients should be padded on right, ending at maxindex + maxindex = len(poles[j]) + den[j, :maxindex+1] = poly(poles[j]) + denorder[j] = maxindex # now create the numerator, also padded on the right for i in range(self.outputs): @@ -885,22 +926,15 @@ def _common_den(self, imag_tol=None): # add all poles not found in the original denominator, # and the ones later added from other denominators for ip in chain(poleset[i][j][3], - range(poleset[i][j][4], np)): + range(poleset[i][j][4], maxindex)): nwzeros.append(poles[j][ip]) - numpoly = poleset[i][j][2] * polyfromroots(nwzeros).real - # print(numpoly, den[j]) - # polyfromroots gives coeffs in opposite order => invert + numpoly = poleset[i][j][2] * np.atleast_1d(poly(nwzeros)) # numerator polynomial should be padded on left and right - # ending at np to line up with what td04ad expects... - num[i, j, np + 1 - len(numpoly):np + 1] = numpoly[::-1] + # ending at maxindex to line up with what td04ad expects. + num[i, j, maxindex+1-len(numpoly):maxindex+1] = numpoly # print(num[i, j]) - if (abs(den.imag) > epsnm).any(): - print("Warning: The denominator has a nontrivial imaginary part: %f" - % abs(den.imag).max()) - den = den.real - return num, den, denorder def sample(self, Ts, method='zoh', alpha=None): @@ -913,18 +947,20 @@ def sample(self, Ts, method='zoh', alpha=None): ---------- Ts : float Sampling period - method : {"gbt", "bilinear", "euler", "backward_diff", "zoh", "matched"} - Which method to use: + method : {"gbt", "bilinear", "euler", "backward_diff", + "zoh", "matched"} + Method to use for sampling: - * gbt: generalized bilinear transformation - * bilinear: Tustin's approximation ("gbt" with alpha=0.5) - * euler: Euler (or forward differencing) method ("gbt" with alpha=0) - * backward_diff: Backwards differencing ("gbt" with alpha=1.0) - * zoh: zero-order hold (default) + * gbt: generalized bilinear transformation + * bilinear: Tustin's approximation ("gbt" with alpha=0.5) + * euler: Euler (or forward difference) method ("gbt" with alpha=0) + * backward_diff: Backwards difference ("gbt" with alpha=1.0) + * zoh: zero-order hold (default) alpha : float within [0, 1] The generalized bilinear transformation weighting parameter, which - should only be specified with method="gbt", and is ignored otherwise + should only be specified with method="gbt", and is ignored + otherwise. Returns ------- @@ -989,9 +1025,8 @@ def _dcgain_cont(self): gain[i][j] = np.nan return np.squeeze(gain) -# c2d function contributed by Benjamin White, Oct 2012 - +# c2d function contributed by Benjamin White, Oct 2012 def _c2d_matched(sysC, Ts): # Pole-zero match method of continuous to discrete time conversion szeros, spoles, sgain = tf2zpk(sysC.num[0][0], sysC.den[0][0]) @@ -1067,6 +1102,18 @@ def _tf_polynomial_to_string(coeffs, var='s'): return thestr +def _tf_string_to_latex(thestr, var='s'): + """ make sure to superscript all digits in a polynomial string + and convert float coefficients in scientific notation + to prettier LaTeX representation """ + # TODO: make the multiplication sign configurable + expmul = r' \\times' + thestr = sub(var + r'\^(\d{2,})', var + r'^{\1}', thestr) + thestr = sub(r'[eE]\+0*(\d+)', expmul + r' 10^{\1}', thestr) + thestr = sub(r'[eE]\-0*(\d+)', expmul + r' 10^{-\1}', thestr) + return thestr + + def _add_siso(num1, den1, num2, den2): """Return num/den = num1/den1 + num2/den2. @@ -1118,8 +1165,10 @@ def _convert_to_transfer_function(sys, **kw): # Slycot doesn't like static SS->TF conversion, so handle # it first. Can't join this with the no-Slycot branch, # since that doesn't handle general MIMO systems - num = [[[sys.D[i, j]] for j in range(sys.inputs)] for i in range(sys.outputs)] - den = [[[1.] for j in range(sys.inputs)] for i in range(sys.outputs)] + num = [[[sys.D[i, j]] for j in range(sys.inputs)] + for i in range(sys.outputs)] + den = [[[1.] for j in range(sys.inputs)] + for i in range(sys.outputs)] else: try: from slycot import tb04ad @@ -1130,12 +1179,15 @@ def _convert_to_transfer_function(sys, **kw): # Use Slycot to make the transformation # Make sure to convert system matrices to numpy arrays - tfout = tb04ad(sys.states, sys.inputs, sys.outputs, array(sys.A), - array(sys.B), array(sys.C), array(sys.D), tol1=0.0) + tfout = tb04ad( + sys.states, sys.inputs, sys.outputs, array(sys.A), + array(sys.B), array(sys.C), array(sys.D), tol1=0.0) # Preallocate outputs. - num = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] - den = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] + num = [[[] for j in range(sys.inputs)] + for i in range(sys.outputs)] + den = [[[] for j in range(sys.inputs)] + for i in range(sys.outputs)] for i in range(sys.outputs): for j in range(sys.inputs): @@ -1213,6 +1265,10 @@ def tf(*args): positive number indicating the sampling time or 'True' if no specific timebase is given. + ``tf('s')`` or ``tf('z')`` + Create a transfer function representing the differential operator + ('s') or delay operator ('z'). + Parameters ---------- sys: LTI (StateSpace or TransferFunction) @@ -1249,6 +1305,9 @@ def tf(*args): The list ``[2, 3, 4]`` denotes the polynomial :math:`2s^2 + 3s + 4`. + The special forms ``tf('s')`` and ``tf('z')`` can be used to create + transfer functions for differentiation and unit delays. + Examples -------- >>> # Create a MIMO transfer function object @@ -1258,6 +1317,10 @@ def tf(*args): >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]] >>> sys1 = tf(num, den) + >>> # Create a variable 's' to allow algebra operations for SISO systems + >>> s = tf('s') + >>> G = (s + 1)/(s**2 + 2*s + 1) + >>> # Convert a StateSpace to a TransferFunction object. >>> sys_ss = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> sys2 = tf(sys1) @@ -1267,6 +1330,12 @@ def tf(*args): if len(args) == 2 or len(args) == 3: return TransferFunction(*args) elif len(args) == 1: + # Look for special cases defining differential/delay operator + if args[0] == 's': + return TransferFunction.s + elif args[0] == 'z': + return TransferFunction.z + from .statesp import StateSpace sys = args[0] if isinstance(sys, StateSpace): @@ -1274,8 +1343,8 @@ def tf(*args): elif isinstance(sys, TransferFunction): return deepcopy(sys) else: - raise TypeError("tf(sys): sys must be a StateSpace or TransferFunction object. " - "It is %s." % type(sys)) + raise TypeError("tf(sys): sys must be a StateSpace or " + "TransferFunction object. It is %s." % type(sys)) else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) @@ -1352,7 +1421,9 @@ def ss2tf(*args): if isinstance(sys, StateSpace): return _convert_to_transfer_function(sys) else: - raise TypeError("ss2tf(sys): sys must be a StateSpace object. It is %s." % type(sys)) + raise TypeError( + "ss2tf(sys): sys must be a StateSpace object. It is %s." + % type(sys)) else: raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) @@ -1416,8 +1487,9 @@ def _clean_part(data): else: # If the user passed in anything else, then it's unclear what # the meaning is. - raise TypeError("The numerator and denominator inputs must be scalars or vectors " - "(for\nSISO), or lists of lists of vectors (for SISO or MIMO).") + raise TypeError( + "The numerator and denominator inputs must be scalars or vectors " + "(for\nSISO), or lists of lists of vectors (for SISO or MIMO).") # Check for coefficients that are ints and convert to floats for i in range(len(data)): @@ -1427,3 +1499,8 @@ def _clean_part(data): data[i][j][k] = float(data[i][j][k]) return data + + +# Define constants to represent differentiation, unit delay +TransferFunction.s = TransferFunction([1, 0], [1], 0) +TransferFunction.z = TransferFunction([1, 0], [1], True) diff --git a/doc-requirements.txt b/doc-requirements.txt index b6ac1e58c..112ca8cbe 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,4 +1,7 @@ numpy scipy matplotlib +sphinx_rtd_theme numpydoc +ipykernel +nbsphinx diff --git a/doc/classes.rst b/doc/classes.rst index 9f63a77a7..0981843ca 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -1,9 +1,9 @@ .. _class-ref: .. currentmodule:: control -****************** -LTI system classes -****************** +********************** +Control system classes +********************** The classes listed below are used to represent models of linear time-invariant (LTI) systems. They are usually created from factory functions such as @@ -15,5 +15,19 @@ these directly. TransferFunction StateSpace - FRD + FrequencyResponseData + ~iosys.InputOutputSystem +Input/output system subclasses +============================== +.. currentmodule:: control.iosys + +Input/output systems are accessed primarily via a set of subclasses +that allow for linear, nonlinear, and interconnected elements: + +.. autosummary:: + :toctree: generated/ + + LinearIOSystem + NonlinearIOSystem + InterconnectedSystem diff --git a/doc/conf.py b/doc/conf.py index fa8be8455..f4c260558 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -30,15 +30,15 @@ # -- Project information ----------------------------------------------------- project = u'Python Control Systems Library' -copyright = u'2018, python-control.org' +copyright = u'2019, python-control.org' author = u'Python Control Developers' # Version information - read from the source code import re import control -# The short X.Y version -version = re.sub(r'(\d+\.\d+)\.(.*)', r'\1', control.__version__) +# The short X.Y.Z version +version = re.sub(r'(\d+\.\d+\.\d+)(.*)', r'\1', control.__version__) # The full version, including alpha/beta/rc tags release = control.__version__ @@ -56,7 +56,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.imgmath', - 'sphinx.ext.autosummary', + 'sphinx.ext.autosummary', 'nbsphinx', ] # scan documents for autosummary directives and generate stub pages for each. @@ -88,7 +88,8 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store', + '*.ipynb_checkpoints'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -96,7 +97,7 @@ #This config value contains the locations and names of other projects that #should be linked to in this documentation. intersphinx_mapping = \ - {'scipy':('https://docs.scipy.org/doc/scipy/reference/', None), + {'scipy':('https://docs.scipy.org/doc/scipy/reference', None), 'numpy':('https://docs.scipy.org/doc/numpy', None)} #If this is True, todo and todolist produce output, else they produce nothing. @@ -109,7 +110,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -120,7 +121,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/doc/control.rst b/doc/control.rst index 1d0b14644..8fd3db58a 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -16,7 +16,7 @@ System creation ss tf - FRD + frd rss drss @@ -32,6 +32,9 @@ System interconnections parallel series +See also the :ref:`iosys-module` module, which can be used to create and +interconnect nonlinear input/output systems. + Frequency domain plotting ========================= @@ -61,6 +64,7 @@ Time domain simulation forced_response impulse_response initial_response + input_output_response step_response phase_plot @@ -89,6 +93,7 @@ Control system analysis zero pzmap root_locus + sisotool Matrix computations =================== @@ -127,6 +132,18 @@ Model simplification tools era markov +Nonlinear system support +======================== +.. autosummary:: + :toctree: generated/ + + ~iosys.find_eqpt + ~iosys.linearize + ~iosys.input_output_response + ~iosys.ss2io + ~iosys.tf2io + flatsys.point_to_point + .. _utility-and-conversions: Utility functions and conversions @@ -146,6 +163,7 @@ Utility functions and conversions observable_form pade reachable_form + reset_defaults sample_system ss2tf ssdata @@ -156,3 +174,4 @@ Utility functions and conversions unwrap use_fbs_defaults use_matlab_defaults + use_numpy_matrix diff --git a/doc/conventions.rst b/doc/conventions.rst index 7bdf3c628..c535027be 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -62,8 +62,8 @@ function. A full list of functions can be found in :ref:`function-ref`. FRD (frequency response data) systems ------------------------------------- -The :class:`FRD` class is used to represent systems in frequency response -data form. +The :class:`FrequencyResponseData` (FRD) class is used to represent systems in +frequency response data form. The main data members are `omega` and `fresp`, where `omega` is a 1D array with the frequency points of the response, and `fresp` is a 3D array, with @@ -85,8 +85,9 @@ The timebase argument can be given when a system is constructed: * dt > 0: discrete time system with sampling period 'dt' * dt = True: discrete time with unspecified sampling period -Only the :class:`StateSpace` and :class:`TransferFunction` classes allow -explicit representation of discrete time systems. +Only the :class:`StateSpace`, :class:`TransferFunction`, and +:class:`InputOutputSystem` classes allow explicit representation of +discrete time systems. Systems must have compatible timebases in order to be combined. A system with timebase `None` can be combined with a system having a specified @@ -106,6 +107,7 @@ constructor for the desired data type using the original system as the sole argument or using the explicit conversion functions :func:`ss2tf` and :func:`tf2ss`. +.. currentmodule:: control .. _time-series-convention: Time series data @@ -162,12 +164,12 @@ The initial conditions are either 1D, or 2D with shape (j, 1):: As all simulation functions return *arrays*, plotting is convenient:: - t, y = step(sys) + t, y = step_response(sys) plot(t, y) The output of a MIMO system can be plotted like this:: - t, y, x = lsim(sys, u, t) + t, y, x = forced_response(sys, u, t) plot(t, y[0], label='y_0') plot(t, y[1], label='y_1') @@ -178,28 +180,57 @@ can be computed like this:: ft = D * U -Package configuration -===================== +Package configuration parameters +================================ + +The python-control library can be customized to allow for different default +values for selected parameters. This includes the ability to set the style +for various types of plots and establishing the underlying representation for +state space matrices. + +To set the default value of a configuration variable, set the appropriate +element of the `control.config.defaults` dictionary: + +.. code-block:: python + + control.config.defaults['module.parameter'] = value + +The `~control.config.set_defaults` function can also be used to set multiple +configuration parameters at the same time: + +.. code-block:: python + + control.config.set_defaults('module', param1=val1, param2=val2, ...] + +Finally, there are also functions available set collections of variables based +on standard configurations. -The python-control library can be customized to allow for different plotting -conventions. The currently configurable options allow the units for Bode -plots to be set as dB for gain, degrees for phase and Hertz for frequency -(MATLAB conventions) or the gain can be given in magnitude units (powers of -10), corresponding to the conventions used in `Feedback Systems -`_ (FBS). +Selected variables that can be configured, along with their default values: -Variables that can be configured, along with their default values: - * bode_dB (False): Bode plot magnitude plotted in dB (otherwise powers of 10) - * bode_deg (True): Bode plot phase plotted in degrees (otherwise radians) - * bode_Hz (False): Bode plot frequency plotted in Hertz (otherwise rad/sec) - * bode_number_of_samples (None): Number of frequency points in Bode plots - * bode_feature_periphery_decade (1.0): How many decades to include in the - frequency range on both sides of features (poles, zeros). + * bode.dB (False): Bode plot magnitude plotted in dB (otherwise powers of 10) + + * bode.deg (True): Bode plot phase plotted in degrees (otherwise radians) + + * bode.Hz (False): Bode plot frequency plotted in Hertz (otherwise rad/sec) + + * bode.grid (True): Include grids for magnitude and phase plots + + * freqplot.number_of_samples (None): Number of frequency points in Bode plots + + * freqplot.feature_periphery_decade (1.0): How many decades to include in the + frequency range on both sides of features (poles, zeros). + + * statesp.use_numpy_matrix: set the return type for state space matrices to + `numpy.matrix` (verus numpy.ndarray) + +Additional parameter variables are documented in individual functions Functions that can be used to set standard configurations: .. autosummary:: :toctree: generated/ - + + reset_defaults use_fbs_defaults use_matlab_defaults + use_numpy_matrix diff --git a/doc/cruise-control.py b/doc/cruise-control.py new file mode 120000 index 000000000..cfa1c8195 --- /dev/null +++ b/doc/cruise-control.py @@ -0,0 +1 @@ +../examples/cruise-control.py \ No newline at end of file diff --git a/doc/cruise-control.rst b/doc/cruise-control.rst new file mode 100644 index 000000000..b0b738e03 --- /dev/null +++ b/doc/cruise-control.rst @@ -0,0 +1,14 @@ +Cruise control design example (as a nonlinear I/O system) +--------------------------------------------------------- + +Code +.... +.. literalinclude:: cruise-control.py + :language: python + :linenos: + + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/cruise.ipynb b/doc/cruise.ipynb new file mode 120000 index 000000000..f712e2d5f --- /dev/null +++ b/doc/cruise.ipynb @@ -0,0 +1 @@ +../examples/cruise.ipynb \ No newline at end of file diff --git a/doc/examples.rst b/doc/examples.rst new file mode 100644 index 000000000..b1ffdfce5 --- /dev/null +++ b/doc/examples.rst @@ -0,0 +1,46 @@ +.. _examples: +.. currentmodule:: control + +******** +Examples +******** + +The source code for the examples below are available in the `examples/` +subdirecory of the source code distribution. The can also be accessed online +via the [python-control GitHub repository](https://github.com/python-control/python-control/tree/master/examples). + + +Python scripts +============== + +The following Python scripts document the use of a variety of methods in the +Python Control Toolbox on examples drawn from standard control textbooks and +other sources. + +.. toctree:: + :maxdepth: 1 + + secord-matlab + pvtol-nested + pvtol-lqr + rss-balred + phaseplots + robust_siso + robust_mimo + cruise-control + steering-gainsched + kincar-flatsys + +Jupyter notebooks +================= + +The examples below use `python-control` in a Jupyter notebook environment. +These notebooks demonstrate the use of modeling, anaylsis, and design tools +using running examples in FBS2e. + +.. toctree:: + :maxdepth: 1 + + cruise + steering + pvtol-lqr-nested diff --git a/doc/flatsys.rst b/doc/flatsys.rst new file mode 100644 index 000000000..ed65cfd01 --- /dev/null +++ b/doc/flatsys.rst @@ -0,0 +1,268 @@ +.. _flatsys-module: + +*************************** +Differentially flat systems +*************************** + +.. automodule:: control.flatsys + :no-members: + :no-inherited-members: + +Overview of differential flatness +================================= + +A nonlinear differential equation of the form + +.. math:: + \dot x = f(x, u), \qquad x \in R^n, u \in R^m + +is *differentially flat* if there exists a function :math:`\alpha` such that + +.. math:: + z = \alpha(x, u, \dot u\, \dots, u^{(p)}) + +and we can write the solutions of the nonlinear system as functions of +:math:`z` and a finite number of derivatives + +.. math:: + x &= \beta(z, \dot z, \dots, z^{(q)}) \\ + u &= \gamma(z, \dot z, \dots, z^{(q)}). + +For a differentially flat system, all of the feasible trajectories for +the system can be written as functions of a flat output :math:`z(\cdot)` and +its derivatives. The number of flat outputs is always equal to the +number of system inputs. + +Differentially flat systems are useful in situations where explicit +trajectory generation is required. Since the behavior of a flat system +is determined by the flat outputs, we can plan trajectories in output +space, and then map these to appropriate inputs. Suppose we wish to +generate a feasible trajectory for the the nonlinear system + +.. math:: + \dot x = f(x, u), \qquad x(0) = x_0,\, x(T) = x_f. + +If the system is differentially flat then + +.. math:: + x(0) &= \beta\bigl(z(0), \dot z(0), \dots, z^{(q)}(0) \bigr) = x_0, \\ + x(T) &= \gamma\bigl(z(T), \dot z(T), \dots, z^{(q)}(T) \bigr) = x_f, + +and we see that the initial and final condition in the full state +space depends on just the output :math:`z` and its derivatives at the +initial and final times. Thus any trajectory for :math:`z` that satisfies +these boundary conditions will be a feasible trajectory for the +system, using equation~\eqref{eq:trajgen:flat2state} to determine the +full state space and input trajectories. + +In particular, given initial and final conditions on :math:`z` and its +derivatives that satisfy the initial and final conditions any curve +:math:`z(\cdot)` satisfying those conditions will correspond to a feasible +trajectory of the system. We can parameterize the flat output trajectory +using a set of smooth basis functions :math:`\psi_i(t)`: + +.. math:: + z(t) = \sum_{i=1}^N \alpha_i \psi_i(t), \qquad \alpha_i \in R + +We seek a set of coefficients :math:`\alpha_i`, :math:`i = 1, \dots, N` such +that :math:`z(t)` satisfies the boundary conditions for :math:`x(0)` and +:math:`x(T)`. The derivatives of the flat output can be computed in terms of +the derivatives of the basis functions: + +.. math:: + \dot z(t) &= \sum_{i=1}^N \alpha_i \dot \psi_i(t) \\ + &\,\vdots \\ + \dot z^{(q)}(t) &= \sum_{i=1}^N \alpha_i \psi^{(q)}_i(t). + +We can thus write the conditions on the flat outputs and their +derivatives as + +.. math:: + \begin{bmatrix} + \psi_1(0) & \psi_2(0) & \dots & \psi_N(0) \\ + \dot \psi_1(0) & \dot \psi_2(0) & \dots & \dot \psi_N(0) \\ + \vdots & \vdots & & \vdots \\ + \psi^{(q)}_1(0) & \psi^{(q)}_2(0) & \dots & \psi^{(q)}_N(0) \\[1ex] + \psi_1(T) & \psi_2(T) & \dots & \psi_N(T) \\ + \dot \psi_1(T) & \dot \psi_2(T) & \dots & \dot \psi_N(T) \\ + \vdots & \vdots & & \vdots \\ + \psi^{(q)}_1(T) & \psi^{(q)}_2(T) & \dots & \psi^{(q)}_N(T) \\ + \end{bmatrix} + \begin{bmatrix} \alpha_1 \\ \vdots \\ \alpha_N \end{bmatrix} = + \begin{bmatrix} + z(0) \\ \dot z(0) \\ \vdots \\ z^{(q)}(0) \\[1ex] + z(T) \\ \dot z(T) \\ \vdots \\ z^{(q)}(T) \\ + \end{bmatrix} + +This equation is a *linear* equation of the form + +.. math:: + M \alpha = \begin{bmatrix} \bar z(0) \\ \bar z(T) \end{bmatrix} + +where :math:`\bar z` is called the *flat flag* for the system. +Assuming that :math:`M` has a sufficient number of columns and that it is full +column rank, we can solve for a (possibly non-unique) :math:`\alpha` that +solves the trajectory generation problem. + +Module usage +============ + +To create a trajectory for a differentially flat system, a +:class:`~control.flatsys.FlatSystem` object must be created. This is +done by specifying the `forward` and `reverse` mappings between the +system state/input and the differentially flat outputs and their +derivatives ("flat flag"). + +The :func:`~control.flatsys.FlatSystem.forward` method computes the +flat flag given a state and input: + + zflag = sys.forward(x, u) + +The :func:`~control.flatsys.FlatSystem.reverse` method computes the state +and input given the flat flag: + + x, u = sys.reverse(zflag) + +The flag :math:`\bar z` is implemented as a list of flat outputs :math:`z_i` +and their derivatives up to order :math:`q_i`: + + zflag[i][j] = :math:`z_i^{(j)}` + +The number of flat outputs must match the number of system inputs. + +For a linear system, a flat system representation can be generated using the +:class:`~control.flatsys.LinearFlatSystem` class: + + flatsys = control.flatsys.LinearFlatSystem(linsys) + +For more general systems, the `FlatSystem` object must be created manually + + flatsys = control.flatsys.FlatSystem(nstate, ninputs, forward, reverse) + +In addition to the flat system descriptionn, a set of basis functions +:math:`\phi_i(t)` must be chosen. The `FlatBasis` class is used to represent +the basis functions. A polynomial basis function of the form 1, :math:`t`, +:math:`t^2:, ... can be computed using the `PolyBasis` class, which is +initialized by passing the desired order of the polynomial basis set: + + polybasis = control.flatsys.PolyBasis(N) + +Once the system and basis function have been defined, the +:func:`~control.flatsys.point_to_point` function can be used to compute a +trajectory between initial and final states and inputs: + + traj = control.flatsys.point_to_point(x0, u0, xf, uf, Tf, basis=polybasis) + +The returned object has class :class:`~control.flatsys.SystemTrajectory` and +can be used to compute the state and input trajectory between the initial and +final condition: + + xd, ud = traj.eval(T) + +where `T` is a list of times on which the trajectory should be evaluated +(e.g., `T = numpy.linspace(0, Tf, M)`. + +Example +======= + +To illustrate how we can use a two degree-of-freedom design to improve the +performance of the system, consider the problem of steering a car to change +lanes on a road. We use the non-normalized form of the dynamics, which are +derived *Feedback Systems* by Astrom and Murray, Example 3.11. + +.. code-block:: python + + import control.flatsys as fs + + # Function to take states, inputs and return the flat flag + def vehicle_flat_forward(x, u, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + + # Create a list of arrays to store the flat output and its derivatives + zflag = [np.zeros(3), np.zeros(3)] + + # Flat output is the x, y position of the rear wheels + zflag[0][0] = x[0] + zflag[1][0] = x[1] + + # First derivatives of the flat output + zflag[0][1] = u[0] * np.cos(x[2]) # dx/dt + zflag[1][1] = u[0] * np.sin(x[2]) # dy/dt + + # First derivative of the angle + thdot = (u[0]/b) * np.tan(u[1]) + + # Second derivatives of the flat output (setting vdot = 0) + zflag[0][2] = -u[0] * thdot * np.sin(x[2]) + zflag[1][2] = u[0] * thdot * np.cos(x[2]) + + return zflag + + # Function to take the flat flag and return states, inputs + def vehicle_flat_reverse(zflag, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + + # Create a vector to store the state and inputs + x = np.zeros(3) + u = np.zeros(2) + + # Given the flat variables, solve for the state + x[0] = zflag[0][0] # x position + x[1] = zflag[1][0] # y position + x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # tan(theta) = ydot/xdot + + # And next solve for the inputs + u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2]) + u[1] = np.arctan2( + (zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2])), u[0]/b) + + return x, u + + vehicle_flat = fs.FlatSystem( + 3, 2, forward=vehicle_flat_forward, reverse=vehicle_flat_reverse) + +To find a trajectory from an initial state :math:`x_0` to a final state +:math:`x_\text{f}` in time :math:`T_\text{f}` we solve a point-to-point +trajectory generation problem. We also set the initial and final inputs, whi +ch sets the vehicle velocity :math:`v` and steering wheel angle :math:`\delta` +at the endpoints. + +.. code-block:: python + + # Define the endpoints of the trajectory + x0 = [0., -2., 0.]; u0 = [10., 0.] + xf = [100., 2., 0.]; uf = [10., 0.] + Tf = 10 + + # Define a set of basis functions to use for the trajectories + poly = fs.PolyFamily(6) + + # Find a trajectory between the initial condition and the final condition + traj = fs.point_to_point(vehicle_flat, x0, u0, xf, uf, Tf, basis=poly) + + # Create the trajectory + t = np.linspace(0, Tf, 100) + x, u = traj.eval(t) + +Module classes and functions +============================ + +Flat systems classes +-------------------- +.. autosummary:: + :toctree: generated/ + + BasisFamily + FlatSystem + LinearFlatSystem + PolyFamily + SystemTrajectory + +Flat systems functions +---------------------- +.. autosummary:: + :toctree: generated/ + + point_to_point diff --git a/doc/index.rst b/doc/index.rst index 7ea8fe1dd..3420789d8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -26,6 +26,9 @@ implements basic operations for analysis and design of feedback control systems. control classes matlab + flatsys + iosys + examples * :ref:`genindex` diff --git a/doc/intro.rst b/doc/intro.rst index 9677135c1..9985da7d9 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -23,7 +23,7 @@ Some differences from MATLAB The python-control package makes use of `NumPy `_ and `SciPy `_. A list of general differences between NumPy and MATLAB can be found `here -`_. +`_. In terms of the python-control package more specifically, here are some thing to keep in mind: diff --git a/doc/iosys.rst b/doc/iosys.rst new file mode 100644 index 000000000..0353a01d7 --- /dev/null +++ b/doc/iosys.rst @@ -0,0 +1,217 @@ +.. _iosys-module: + +******************** +Input/output systems +******************** + +.. automodule:: control.iosys + :no-members: + :no-inherited-members: + +Module usage +============ + +An input/output system is defined as a dynamical system that has a system +state as well as inputs and outputs (either inputs or states can be empty). +The dynamics of the system can be in continuous or discrete time. To simulate +an input/output system, use the :func:`~control.input_output_response` +function:: + + t, y = input_output_response(io_sys, T, U, X0, params) + +An input/output system can be linearized around an equilibrium point to obtain +a :class:`~control.StateSpace` linear system. Use the +:func:`~control.find_eqpt` function to obtain an equilibrium point and the +:func:`~control.linearize` function to linearize about that equilibrium point:: + + xeq, ueq = find_eqpt(io_sys, X0, U0) + ss_sys = linearize(io_sys, xeq, ueq) + +Input/output systems can be created from state space LTI systems by using the +:class:`~control.LinearIOSystem` class`:: + + io_sys = LinearIOSystem(ss_sys) + +Nonlinear input/output systems can be created using the +:class:`~control.NonlinearIOSystem` class, which requires the definition of an +update function (for the right hand side of the differential or different +equation) and and output function (computes the outputs from the state):: + + io_sys = NonlinearIOSystem(updfcn, outfcn, inputs=M, outputs=P, states=N) + +More complex input/output systems can be constructed by using the +:class:`~control.InterconnectedSystem` class, which allows a collection of +input/output subsystems to be combined with internal connections between the +subsystems and a set of overall system inputs and outputs that link to the +subsystems:: + + steering = ct.InterconnectedSystem( + (plant, controller), name='system', + connections=(('controller.e', '-plant.y')), + inplist=('controller.e'), inputs='r', + outlist=('plant.y'), outputs='y') + +Interconnected systems can also be created using block diagram manipulations +such as the :func:`~control.series`, :func:`~control.parallel`, and +:func:`~control.feedback` functions. The :class:`~control.InputOutputSystem` +class also supports various algebraic operations such as `*` (series +interconnection) and `+` (parallel interconnection). + +Example +======= + +To illustrate the use of the input/output systems module, we create a +model for a predator/prey system, following the notation and parameter +values in FBS2e. + +We begin by defining the dynamics of the system + +.. code-block:: python + + import control + import numpy as np + import matplotlib.pyplot as plt + + def predprey_rhs(t, x, u, params): + # Parameter setup + a = params.get('a', 3.2) + b = params.get('b', 0.6) + c = params.get('c', 50.) + d = params.get('d', 0.56) + k = params.get('k', 125) + r = params.get('r', 1.6) + + # Map the states into local variable names + H = x[0] + L = x[1] + + # Compute the control action (only allow addition of food) + u_0 = u if u > 0 else 0 + + # Compute the discrete updates + dH = (r + u_0) * H * (1 - H/k) - (a * H * L)/(c + H) + dL = b * (a * H * L)/(c + H) - d * L + + return [dH, dL] + +We now create an input/output system using these dynamics: + +.. code-block:: python + + io_predprey = control.NonlinearIOSystem( + predprey_rhs, None, inputs=('u'), outputs=('H', 'L'), + states=('H', 'L'), name='predprey') + +Note that since we have not specified an output function, the entire state +will be used as the output of the system. + +The `io_predprey` system can now be simulated to obtain the open loop dynamics +of the system: + +.. code-block:: python + + X0 = [25, 20] # Initial H, L + T = np.linspace(0, 70, 500) # Simulation 70 years of time + + # Simulate the system + t, y = control.input_output_response(io_predprey, T, 0, X0) + + # Plot the response + plt.figure(1) + plt.plot(t, y[0]) + plt.plot(t, y[1]) + plt.legend(['Hare', 'Lynx']) + plt.show(block=False) + +We can also create a feedback controller to stabilize a desired population of +the system. We begin by finding the (unstable) equilibrium point for the +system and computing the linearization about that point. + +.. code-block:: python + + eqpt = control.find_eqpt(io_predprey, X0, 0) + xeq = eqpt[0] # choose the nonzero equilibrium point + lin_predprey = control.linearize(io_predprey, xeq, 0) + +We next compute a controller that stabilizes the equilibrium point using +eigenvalue placement and computing the feedforward gain using the number of +lynxes as the desired output (following FBS2e, Example 7.5): + +.. code-block:: python + + K = control.place(lin_predprey.A, lin_predprey.B, [-0.1, -0.2]) + A, B = lin_predprey.A, lin_predprey.B + C = np.array([[0, 1]]) # regulated output = number of lynxes + kf = -1/(C @ np.linalg.inv(A - B @ K) @ B) + +To construct the control law, we build a simple input/output system that +applies a corrective input based on deviations from the equilibrium point. +This system has no dynamics, since it is a static (affine) map, and can +constructed using the `~control.ios.NonlinearIOSystem` class: + +.. code-block:: python + + io_controller = control.NonlinearIOSystem( + None, + lambda t, x, u, params: -K @ (u[1:] - xeq) + kf * (u[0] - xeq[1]), + inputs=('Ld', 'u1', 'u2'), outputs=1, name='control') + +The input to the controller is `u`, consisting of the vector of hare and lynx +populations followed by the desired lynx population. + +To connect the controller to the predatory-prey model, we create an +`InterconnectedSystem`: + +.. code-block:: python + + io_closed = control.InterconnectedSystem( + (io_predprey, io_controller), # systems + connections=( + ('predprey.u', 'control.y[0]'), + ('control.u1', 'predprey.H'), + ('control.u2', 'predprey.L') + ), + inplist=('control.Ld'), + outlist=('predprey.H', 'predprey.L', 'control.y[0]') + ) + +Finally, we simulate the closed loop system: + +.. code-block:: python + + # Simulate the system + t, y = control.input_output_response(io_closed, T, 30, [15, 20]) + + # Plot the response + plt.figure(2) + plt.subplot(2, 1, 1) + plt.plot(t, y[0]) + plt.plot(t, y[1]) + plt.legend(['Hare', 'Lynx']) + plt.subplot(2, 1, 2) + plt.plot(t, y[2]) + plt.legend(['input']) + plt.show(block=False) + +Module classes and functions +============================ + +Input/output system classes +--------------------------- +.. autosummary:: + + InputOutputSystem + InterconnectedSystem + LinearIOSystem + NonlinearIOSystem + +Input/output system functions +----------------------------- +.. autosummary:: + + find_eqpt + linearize + input_output_response + ss2io + tf2io + diff --git a/doc/kincar-flatsys.py b/doc/kincar-flatsys.py new file mode 120000 index 000000000..7ef7d684e --- /dev/null +++ b/doc/kincar-flatsys.py @@ -0,0 +1 @@ +../examples/kincar-flatsys.py \ No newline at end of file diff --git a/doc/kincar-flatsys.rst b/doc/kincar-flatsys.rst new file mode 100644 index 000000000..2b502bcbb --- /dev/null +++ b/doc/kincar-flatsys.rst @@ -0,0 +1,18 @@ +Differentially flat system - kinematic car +------------------------------------------ + +This example demonstrates the use of the `flatsys` module for +generating trajectories for differentially flat systems. The example +is drawn from Chapter 8 of FBS2e. + +Code +.... +.. literalinclude:: kincar-flatsys.py + :language: python + :linenos: + + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/matlab.rst b/doc/matlab.rst index fc0ec3427..ae5688dde 100644 --- a/doc/matlab.rst +++ b/doc/matlab.rst @@ -82,6 +82,7 @@ Compensator design :toctree: generated/ rlocus + sisotool place lqr diff --git a/doc/phaseplots.py b/doc/phaseplots.py new file mode 120000 index 000000000..4b0575c0f --- /dev/null +++ b/doc/phaseplots.py @@ -0,0 +1 @@ +../examples/phaseplots.py \ No newline at end of file diff --git a/doc/phaseplots.rst b/doc/phaseplots.rst new file mode 100644 index 000000000..44beed598 --- /dev/null +++ b/doc/phaseplots.rst @@ -0,0 +1,14 @@ +Phase plot examples +-------------------- + +Code +.... +.. literalinclude:: phaseplots.py + :language: python + :linenos: + + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/pvtol-lqr-nested.ipynb b/doc/pvtol-lqr-nested.ipynb new file mode 120000 index 000000000..fdc3bcd74 --- /dev/null +++ b/doc/pvtol-lqr-nested.ipynb @@ -0,0 +1 @@ +../examples/pvtol-lqr-nested.ipynb \ No newline at end of file diff --git a/doc/pvtol-lqr.py b/doc/pvtol-lqr.py new file mode 120000 index 000000000..a6106b06a --- /dev/null +++ b/doc/pvtol-lqr.py @@ -0,0 +1 @@ +../examples/pvtol-lqr.py \ No newline at end of file diff --git a/doc/pvtol-lqr.rst b/doc/pvtol-lqr.rst new file mode 100644 index 000000000..255fd41c2 --- /dev/null +++ b/doc/pvtol-lqr.rst @@ -0,0 +1,21 @@ +LQR control design for vertical takeoff and landing aircraft +------------------------------------------------------------ + +This script demonstrates the use of the python-control package for +analysis and design of a controller for a vectored thrust aircraft +model that is used as a running example through the text Feedback +Systems by Astrom and Murray. This example makes use of MATLAB +compatible commands. + +Code +.... +.. literalinclude:: pvtol-lqr.py + :language: python + :linenos: + + +Notes +..... + +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/pvtol-nested.py b/doc/pvtol-nested.py new file mode 120000 index 000000000..f72b7c752 --- /dev/null +++ b/doc/pvtol-nested.py @@ -0,0 +1 @@ +../examples/pvtol-nested.py \ No newline at end of file diff --git a/doc/pvtol-nested.rst b/doc/pvtol-nested.rst new file mode 100644 index 000000000..f9a4538a8 --- /dev/null +++ b/doc/pvtol-nested.rst @@ -0,0 +1,24 @@ +Inner/outer control design for vertical takeoff and landing aircraft +-------------------------------------------------------------------- + +This script demonstrates the use of the python-control package for +analysis and design of a controller for a vectored thrust aircraft +model that is used as a running example through the text Feedback +Systems by Astrom and Murray. This example makes use of MATLAB +compatible commands. + +Code +.... +.. literalinclude:: pvtol-nested.py + :language: python + :linenos: + + +Notes +..... + +1. Importing `print_function` from `__future__` in line 11 is only +required if using Python 2.7. + +2. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/robust_mimo.py b/doc/robust_mimo.py new file mode 120000 index 000000000..f49c7abb6 --- /dev/null +++ b/doc/robust_mimo.py @@ -0,0 +1 @@ +../examples/robust_mimo.py \ No newline at end of file diff --git a/doc/robust_mimo.rst b/doc/robust_mimo.rst new file mode 100644 index 000000000..1b0434562 --- /dev/null +++ b/doc/robust_mimo.rst @@ -0,0 +1,14 @@ +MIMO robust control example (SP96, Example 3.8) +----------------------------------------------- + +Code +.... +.. literalinclude:: robust_mimo.py + :language: python + :linenos: + + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/robust_siso.py b/doc/robust_siso.py new file mode 120000 index 000000000..9d770ea2d --- /dev/null +++ b/doc/robust_siso.py @@ -0,0 +1 @@ +../examples/robust_siso.py \ No newline at end of file diff --git a/doc/robust_siso.rst b/doc/robust_siso.rst new file mode 100644 index 000000000..58be639ee --- /dev/null +++ b/doc/robust_siso.rst @@ -0,0 +1,14 @@ +SISO robust control example (SP96, Example 2.1) +----------------------------------------------- + +Code +.... +.. literalinclude:: robust_siso.py + :language: python + :linenos: + + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/rss-balred.py b/doc/rss-balred.py new file mode 120000 index 000000000..04b921134 --- /dev/null +++ b/doc/rss-balred.py @@ -0,0 +1 @@ +../examples/rss-balred.py \ No newline at end of file diff --git a/doc/rss-balred.rst b/doc/rss-balred.rst new file mode 100644 index 000000000..b0eb8bb50 --- /dev/null +++ b/doc/rss-balred.rst @@ -0,0 +1,15 @@ +Balanced model reduction examples +--------------------------------- + +Code +.... +.. literalinclude:: rss-balred.py + :language: python + :linenos: + + +Notes +..... + +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/secord-matlab.py b/doc/secord-matlab.py new file mode 120000 index 000000000..988ec5aca --- /dev/null +++ b/doc/secord-matlab.py @@ -0,0 +1 @@ +../examples/secord-matlab.py \ No newline at end of file diff --git a/doc/secord-matlab.rst b/doc/secord-matlab.rst new file mode 100644 index 000000000..999e6f5e8 --- /dev/null +++ b/doc/secord-matlab.rst @@ -0,0 +1,18 @@ +Secord order system (MATLAB module example) +------------------------------------------- + +This example computes time and frequency responses for a second-order +system using the MATLAB compatibility module. + +Code +.... +.. literalinclude:: secord-matlab.py + :language: python + :linenos: + + +Notes +..... + +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/steering-gainsched.py b/doc/steering-gainsched.py new file mode 120000 index 000000000..200e49543 --- /dev/null +++ b/doc/steering-gainsched.py @@ -0,0 +1 @@ +../examples/steering-gainsched.py \ No newline at end of file diff --git a/doc/steering-gainsched.rst b/doc/steering-gainsched.rst new file mode 100644 index 000000000..511f76b8e --- /dev/null +++ b/doc/steering-gainsched.rst @@ -0,0 +1,12 @@ +Gain scheduled control for vehicle steeering (I/O system) +--------------------------------------------------------- + +Code +.... +.. literalinclude:: steering-gainsched.py + :language: python + :linenos: + + +Notes +..... diff --git a/doc/steering.ipynb b/doc/steering.ipynb new file mode 120000 index 000000000..a7f083b90 --- /dev/null +++ b/doc/steering.ipynb @@ -0,0 +1 @@ +../examples/steering.ipynb \ No newline at end of file diff --git a/examples/bdalg-matlab.py b/examples/bdalg-matlab.py index c3b11b109..8911d6579 100644 --- a/examples/bdalg-matlab.py +++ b/examples/bdalg-matlab.py @@ -10,8 +10,8 @@ sys1ss = ss(A1, B1, C1, 0) sys1tf = ss2tf(sys1ss) -sys2tf = tf([1, 0.5], [1, 5]); -sys2ss = tf2ss(sys2tf); +sys2tf = tf([1, 0.5], [1, 5]) +sys2ss = tf2ss(sys2tf) # Series composition -series1 = sys1ss + sys2ss; +series1 = sys1ss + sys2ss diff --git a/examples/bode-and-nyquist-plots.ipynb b/examples/bode-and-nyquist-plots.ipynb index 5a2e94ad0..8aa0df822 100644 --- a/examples/bode-and-nyquist-plots.ipynb +++ b/examples/bode-and-nyquist-plots.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -14,13 +14,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "%matplotlib nbagg\n", "# only needed when developing python-control\n", - "%load_ext autoreload \n", + "%load_ext autoreload\n", "%autoreload 2" ] }, @@ -33,11 +33,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { + "text/latex": [ + "$$\\frac{1}{s + 1}$$" + ], "text/plain": [ "\n", " 1\n", @@ -50,6 +53,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{1}{0.1592 s + 1}$$" + ], "text/plain": [ "\n", " 1\n", @@ -62,6 +68,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{1}{0.02533 s^2 + 0.1592 s + 1}$$" + ], "text/plain": [ "\n", " 1\n", @@ -74,6 +83,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{s}{0.1592 s + 1}$$" + ], "text/plain": [ "\n", " s\n", @@ -86,6 +98,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{1}{1.021e-10 s^5 + 7.122e-08 s^4 + 4.519e-05 s^3 + 0.003067 s^2 + 0.1767 s + 1}$$" + ], "text/plain": [ "\n", " 1\n", @@ -131,7 +146,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -151,11 +166,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { + "text/latex": [ + "$$\\frac{0.0004998 z + 0.0004998}{z - 0.999}\\quad dt = 0.001$$" + ], "text/plain": [ "\n", "0.0004998 z + 0.0004998\n", @@ -170,6 +188,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{0.003132 z + 0.003132}{z - 0.9937}\\quad dt = 0.001$$" + ], "text/plain": [ "\n", "0.003132 z + 0.003132\n", @@ -184,6 +205,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{6.264 z - 6.264}{z - 0.9937}\\quad dt = 0.001$$" + ], "text/plain": [ "\n", "6.264 z - 6.264\n", @@ -198,6 +222,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{9.839e-06 z^2 + 1.968e-05 z + 9.839e-06}{z^2 - 1.994 z + 0.9937}\\quad dt = 0.001$$" + ], "text/plain": [ "\n", "9.839e-06 z^2 + 1.968e-05 z + 9.839e-06\n", @@ -212,6 +239,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{2.091e-07 z^5 + 1.046e-06 z^4 + 2.091e-06 z^3 + 2.091e-06 z^2 + 1.046e-06 z + 2.091e-07}{z^5 - 4.205 z^4 + 7.155 z^3 - 6.212 z^2 + 2.78 z - 0.5182}\\quad dt = 0.001$$" + ], "text/plain": [ "\n", "2.091e-07 z^5 + 1.046e-06 z^4 + 2.091e-06 z^3 + 2.091e-06 z^2 + 1.046e-06 z + 2.091e-07\n", @@ -226,6 +256,9 @@ }, { "data": { + "text/latex": [ + "$$\\frac{2.731e-10 z^5 + 1.366e-09 z^4 + 2.731e-09 z^3 + 2.731e-09 z^2 + 1.366e-09 z + 2.731e-10}{z^5 - 4.815 z^4 + 9.286 z^3 - 8.968 z^2 + 4.337 z - 0.8405}\\quad dt = 0.00025$$" + ], "text/plain": [ "\n", "2.731e-10 z^5 + 1.366e-09 z^4 + 2.731e-09 z^3 + 2.731e-09 z^2 + 1.366e-09 z + 2.731e-10\n", @@ -270,7 +303,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -830,7 +863,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -1056,7 +1089,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -1080,7 +1113,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -1640,7 +1673,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -1866,7 +1899,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -1890,7 +1923,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -2450,7 +2483,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -2676,7 +2709,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -2700,7 +2733,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -3260,7 +3293,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -3486,7 +3519,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -3510,7 +3543,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -4070,7 +4103,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -4296,7 +4329,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -4321,7 +4354,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -4881,7 +4914,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -5107,7 +5140,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -5131,7 +5164,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -5691,7 +5724,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -5917,7 +5950,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -5943,7 +5976,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -6503,7 +6536,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -6729,7 +6762,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -6754,7 +6787,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -7314,7 +7347,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -7540,7 +7573,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -7564,7 +7597,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -8124,7 +8157,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -8350,7 +8383,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -8370,7 +8403,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -8930,7 +8963,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -9156,7 +9189,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -9181,7 +9214,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -9741,7 +9774,7 @@ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", + "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", "\n", @@ -9967,7 +10000,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -9992,9 +10025,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python (pctest)", "language": "python", - "name": "python3" + "name": "pctest" }, "language_info": { "codemirror_mode": { @@ -10006,7 +10039,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.4.9" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/check-controllability-and-observability.py b/examples/check-controllability-and-observability.py index d20416f1f..399693781 100644 --- a/examples/check-controllability-and-observability.py +++ b/examples/check-controllability-and-observability.py @@ -6,25 +6,25 @@ from __future__ import print_function -from scipy import * # Load the scipy functions +import numpy as np # Load the scipy functions from control.matlab import * # Load the controls systems library # Parameters defining the system m = 250.0 # system mass -k = 40.0 # spring constant -b = 60.0 # damping constant +k = 40.0 # spring constant +b = 60.0 # damping constant # System matrices -A = matrix([[1, -1, 1.], - [1, -k / m, -b / m], - [1, 1, 1]]) +A = np.array([[1, -1, 1.], + [1, -k/m, -b/m], + [1, 1, 1]]) -B = matrix([[0], - [1 / m], - [1]]) +B = np.array([[0], + [1/m], + [1]]) -C = matrix([[1., 0, 1.]]) +C = np.array([[1., 0, 1.]]) sys = ss(A, B, C, 0) diff --git a/examples/cruise-control.py b/examples/cruise-control.py new file mode 100644 index 000000000..8e59c79c7 --- /dev/null +++ b/examples/cruise-control.py @@ -0,0 +1,488 @@ +# cruise-control.py - Cruise control example from FBS +# RMM, 16 May 2019 +# +# The cruise control system of a car is a common feedback system encountered +# in everyday life. The system attempts to maintain a constant velocity in the +# presence of disturbances primarily caused by changes in the slope of a +# road. The controller compensates for these unknowns by measuring the speed +# of the car and adjusting the throttle appropriately. +# +# This file explore the dynamics and control of the cruise control system, +# following the material presenting in Feedback Systems by Astrom and Murray. +# A full nonlinear model of the vehicle dynamics is used, with both PI and +# state space control laws. Different methods of constructing control systems +# are show, all using the InputOutputSystem class (and subclasses). + +import numpy as np +import matplotlib.pyplot as plt +from math import pi +import control as ct + +# +# Section 4.1: Cruise control modeling and control +# + +# Vehicle model: vehicle() +# +# To develop a mathematical model we start with a force balance for +# the car body. Let v be the speed of the car, m the total mass +# (including passengers), F the force generated by the contact of the +# wheels with the road, and Fd the disturbance force due to gravity, +# friction, and aerodynamic drag. + +def vehicle_update(t, x, u, params={}): + """Vehicle dynamics for cruise control system. + + Parameters + ---------- + x : array + System state: car velocity in m/s + u : array + System input: [throttle, gear, road_slope], where throttle is + a float between 0 and 1, gear is an integer between 1 and 5, + and road_slope is in rad. + + Returns + ------- + float + Vehicle acceleration + + """ + from math import copysign, sin + sign = lambda x: copysign(1, x) # define the sign() function + + # Set up the system parameters + m = params.get('m', 1600.) + g = params.get('g', 9.8) + Cr = params.get('Cr', 0.01) + Cd = params.get('Cd', 0.32) + rho = params.get('rho', 1.3) + A = params.get('A', 2.4) + alpha = params.get( + 'alpha', [40, 25, 16, 12, 10]) # gear ratio / wheel radius + + # Define variables for vehicle state and inputs + v = x[0] # vehicle velocity + throttle = np.clip(u[0], 0, 1) # vehicle throttle + gear = u[1] # vehicle gear + theta = u[2] # road slope + + # Force generated by the engine + + omega = alpha[int(gear)-1] * v # engine angular speed + F = alpha[int(gear)-1] * motor_torque(omega, params) * throttle + + # Disturbance forces + # + # The disturbance force Fd has three major components: Fg, the forces due + # to gravity; Fr, the forces due to rolling friction; and Fa, the + # aerodynamic drag. + + # Letting the slope of the road be \theta (theta), gravity gives the + # force Fg = m g sin \theta. + + Fg = m * g * sin(theta) + + # A simple model of rolling friction is Fr = m g Cr sgn(v), where Cr is + # the coefficient of rolling friction and sgn(v) is the sign of v (+/- 1) or + # zero if v = 0. + + Fr = m * g * Cr * sign(v) + + # The aerodynamic drag is proportional to the square of the speed: Fa = + # 1/\rho Cd A |v| v, where \rho is the density of air, Cd is the + # shape-dependent aerodynamic drag coefficient, and A is the frontal area + # of the car. + + Fa = 1/2 * rho * Cd * A * abs(v) * v + + # Final acceleration on the car + Fd = Fg + Fr + Fa + dv = (F - Fd) / m + + return dv + +# Engine model: motor_torque +# +# The force F is generated by the engine, whose torque is proportional to +# the rate of fuel injection, which is itself proportional to a control +# signal 0 <= u <= 1 that controls the throttle position. The torque also +# depends on engine speed omega. + +def motor_torque(omega, params={}): + # Set up the system parameters + Tm = params.get('Tm', 190.) # engine torque constant + omega_m = params.get('omega_m', 420.) # peak engine angular speed + beta = params.get('beta', 0.4) # peak engine rolloff + + return np.clip(Tm * (1 - beta * (omega/omega_m - 1)**2), 0, None) + +# Define the input/output system for the vehicle +vehicle = ct.NonlinearIOSystem( + vehicle_update, None, name='vehicle', + inputs = ('u', 'gear', 'theta'), outputs = ('v'), states=('v')) + +# Figure 1.11: A feedback system for controlling the speed of a vehicle. In +# this example, the speed of the vehicle is measured and compared to the +# desired speed. The controller is a PI controller represented as a transfer +# function. In the textbook, the simulations are done for LTI systems, but +# here we simulate the full nonlinear system. + +# Construct a PI controller with rolloff, as a transfer function +Kp = 0.5 # proportional gain +Ki = 0.1 # integral gain +control_tf = ct.tf2io( + ct.TransferFunction([Kp, Ki], [1, 0.01*Ki/Kp]), + name='control', inputs='u', outputs='y') + +# Construct the closed loop control system +# Inputs: vref, gear, theta +# Outputs: v (vehicle velocity) +cruise_tf = ct.InterconnectedSystem( + (control_tf, vehicle), name='cruise', + connections = ( + ('control.u', '-vehicle.v'), + ('vehicle.u', 'control.y')), + inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'), + inputs = ('vref', 'gear', 'theta'), + outlist = ('vehicle.v', 'vehicle.u'), + outputs = ('v', 'u')) + +# Define the time and input vectors +T = np.linspace(0, 25, 101) +vref = 20 * np.ones(T.shape) +gear = 4 * np.ones(T.shape) +theta0 = np.zeros(T.shape) + +# Now simulate the effect of a hill at t = 5 seconds +plt.figure() +plt.suptitle('Response to change in road slope') +vel_axes = plt.subplot(2, 1, 1) +inp_axes = plt.subplot(2, 1, 2) +theta_hill = np.array([ + 0 if t <= 5 else + 4./180. * pi * (t-5) if t <= 6 else + 4./180. * pi for t in T]) + +for m in (1200, 1600, 2000): + # Compute the equilibrium state for the system + X0, U0 = ct.find_eqpt( + cruise_tf, [0, vref[0]], [vref[0], gear[0], theta0[0]], + iu=[1, 2], y0=[vref[0], 0], iy=[0], params={'m':m}) + + t, y = ct.input_output_response( + cruise_tf, T, [vref, gear, theta_hill], X0, params={'m':m}) + + # Plot the velocity + plt.sca(vel_axes) + plt.plot(t, y[0]) + + # Plot the input + plt.sca(inp_axes) + plt.plot(t, y[1]) + +# Add labels to the plots +plt.sca(vel_axes) +plt.ylabel('Speed [m/s]') +plt.legend(['m = 1000 kg', 'm = 2000 kg', 'm = 3000 kg'], frameon=False) + +plt.sca(inp_axes) +plt.ylabel('Throttle') +plt.xlabel('Time [s]') + +# Figure 4.2: Torque curves for a typical car engine. The graph on the +# left shows the torque generated by the engine as a function of the +# angular velocity of the engine, while the curve on the right shows +# torque as a function of car speed for different gears. + +plt.figure() +plt.suptitle('Torque curves for typical car engine') + +# Figure 4.2a - single torque curve as function of omega +omega_range = np.linspace(0, 700, 701) +plt.subplot(2, 2, 1) +plt.plot(omega_range, [motor_torque(w) for w in omega_range]) +plt.xlabel('Angular velocity $\omega$ [rad/s]') +plt.ylabel('Torque $T$ [Nm]') +plt.grid(True, linestyle='dotted') + +# Figure 4.2b - torque curves in different gears, as function of velocity +plt.subplot(2, 2, 2) +v_range = np.linspace(0, 70, 71) +alpha = [40, 25, 16, 12, 10] +for gear in range(5): + omega_range = alpha[gear] * v_range + plt.plot(v_range, [motor_torque(w) for w in omega_range], + color='blue', linestyle='solid') + +# Set up the axes and style +plt.axis([0, 70, 100, 200]) +plt.grid(True, linestyle='dotted') + +# Add labels +plt.text(11.5, 120, '$n$=1') +plt.text(24, 120, '$n$=2') +plt.text(42.5, 120, '$n$=3') +plt.text(58.5, 120, '$n$=4') +plt.text(58.5, 185, '$n$=5') +plt.xlabel('Velocity $v$ [m/s]') +plt.ylabel('Torque $T$ [Nm]') + +plt.show(block=False) + +# Figure 4.3: Car with cruise control encountering a sloping road + +# PI controller model: control_pi() +# +# We add to this model a feedback controller that attempts to regulate the +# speed of the car in the presence of disturbances. We shall use a +# proportional-integral controller + +def pi_update(t, x, u, params={}): + # Get the controller parameters that we need + ki = params.get('ki', 0.1) + kaw = params.get('kaw', 2) # anti-windup gain + + # Assign variables for inputs and states (for readability) + v = u[0] # current velocity + vref = u[1] # reference velocity + z = x[0] # integrated error + + # Compute the nominal controller output (needed for anti-windup) + u_a = pi_output(t, x, u, params) + + # Compute anti-windup compensation (scale by ki to account for structure) + u_aw = kaw/ki * (np.clip(u_a, 0, 1) - u_a) if ki != 0 else 0 + + # State is the integrated error, minus anti-windup compensation + return (vref - v) + u_aw + +def pi_output(t, x, u, params={}): + # Get the controller parameters that we need + kp = params.get('kp', 0.5) + ki = params.get('ki', 0.1) + + # Assign variables for inputs and states (for readability) + v = u[0] # current velocity + vref = u[1] # reference velocity + z = x[0] # integrated error + + # PI controller + return kp * (vref - v) + ki * z + +control_pi = ct.NonlinearIOSystem( + pi_update, pi_output, name='control', + inputs = ['v', 'vref'], outputs = ['u'], states = ['z'], + params = {'kp':0.5, 'ki':0.1}) + +# Create the closed loop system +cruise_pi = ct.InterconnectedSystem( + (vehicle, control_pi), name='cruise', + connections=( + ('vehicle.u', 'control.u'), + ('control.v', 'vehicle.v')), + inplist=('control.vref', 'vehicle.gear', 'vehicle.theta'), + outlist=('control.u', 'vehicle.v'), outputs=['u', 'v']) + +# Figure 4.3b shows the response of the closed loop system. The figure shows +# that even if the hill is so steep that the throttle changes from 0.17 to +# almost full throttle, the largest speed error is less than 1 m/s, and the +# desired velocity is recovered after 20 s. + +# Define a function for creating a "standard" cruise control plot +def cruise_plot(sys, t, y, t_hill=5, vref=20, antiwindup=False, + linetype='b-', subplots=[None, None]): + # Figure out the plot bounds and indices + v_min = vref-1.2; v_max = vref+0.5; v_ind = sys.find_output('v') + u_min = 0; u_max = 2 if antiwindup else 1; u_ind = sys.find_output('u') + + # Make sure the upper and lower bounds on v are OK + while max(y[v_ind]) > v_max: v_max += 1 + while min(y[v_ind]) < v_min: v_min -= 1 + + # Create arrays for return values + subplot_axes = list(subplots) + + # Velocity profile + if subplot_axes[0] is None: + subplot_axes[0] = plt.subplot(2, 1, 1) + else: + plt.sca(subplots[0]) + plt.plot(t, y[v_ind], linetype) + plt.plot(t, vref*np.ones(t.shape), 'k-') + plt.plot([t_hill, t_hill], [v_min, v_max], 'k--') + plt.axis([0, t[-1], v_min, v_max]) + plt.xlabel('Time $t$ [s]') + plt.ylabel('Velocity $v$ [m/s]') + + # Commanded input profile + if subplot_axes[1] is None: + subplot_axes[1] = plt.subplot(2, 1, 2) + else: + plt.sca(subplots[1]) + plt.plot(t, y[u_ind], 'r--' if antiwindup else linetype) + plt.plot([t_hill, t_hill], [u_min, u_max], 'k--') + plt.axis([0, t[-1], u_min, u_max]) + plt.xlabel('Time $t$ [s]') + plt.ylabel('Throttle $u$') + + # Applied input profile + if antiwindup: + # TODO: plot the actual signal from the process? + plt.plot(t, np.clip(y[u_ind], 0, 1), linetype) + plt.legend(['Commanded', 'Applied'], frameon=False) + + return subplot_axes + +# Define the time and input vectors +T = np.linspace(0, 30, 101) +vref = 20 * np.ones(T.shape) +gear = 4 * np.ones(T.shape) +theta0 = np.zeros(T.shape) + +# Compute the equilibrium throttle setting for the desired speed (solve for x +# and u given the gear, slope, and desired output velocity) +X0, U0, Y0 = ct.find_eqpt( + cruise_pi, [vref[0], 0], [vref[0], gear[0], theta0[0]], + y0=[0, vref[0]], iu=[1, 2], iy=[1], return_y=True) + +# Now simulate the effect of a hill at t = 5 seconds +plt.figure() +plt.suptitle('Car with cruise control encountering sloping road') +theta_hill = [ + 0 if t <= 5 else + 4./180. * pi * (t-5) if t <= 6 else + 4./180. * pi for t in T] +t, y = ct.input_output_response(cruise_pi, T, [vref, gear, theta_hill], X0) +cruise_plot(cruise_pi, t, y) + +# +# Example 7.8: State space feedback with integral action +# + +# State space controller model: control_sf_ia() +# +# Construct a state space controller with integral action, linearized around +# an equilibrium point. The controller is constructed around the equilibrium +# point (x_d, u_d) and includes both feedforward and feedback compensation. +# +# Controller inputs: (x, y, r) system states, system output, reference +# Controller state: z integrated error (y - r) +# Controller output: u state feedback control +# +# Note: to make the structure of the controller more clear, we implement this +# as a "nonlinear" input/output module, even though the actual input/output +# system is linear. This also allows the use of parameters to set the +# operating point and gains for the controller. + +def sf_update(t, z, u, params={}): + y, r = u[1], u[2] + return y - r + +def sf_output(t, z, u, params={}): + # Get the controller parameters that we need + K = params.get('K', 0) + ki = params.get('ki', 0) + kf = params.get('kf', 0) + xd = params.get('xd', 0) + yd = params.get('yd', 0) + ud = params.get('ud', 0) + + # Get the system state and reference input + x, y, r = u[0], u[1], u[2] + + return ud - K * (x - xd) - ki * z + kf * (r - yd) + +# Create the input/output system for the controller +control_sf = ct.NonlinearIOSystem( + sf_update, sf_output, name='control', + inputs=('x', 'y', 'r'), + outputs=('u'), + states=('z')) + +# Create the closed loop system for the state space controller +cruise_sf = ct.InterconnectedSystem( + (vehicle, control_sf), name='cruise', + connections=( + ('vehicle.u', 'control.u'), + ('control.x', 'vehicle.v'), + ('control.y', 'vehicle.v')), + inplist=('control.r', 'vehicle.gear', 'vehicle.theta'), + outlist=('control.u', 'vehicle.v'), outputs=['u', 'v']) + +# Compute the linearization of the dynamics around the equilibrium point + +# Y0 represents the steady state with PI control => we can use it to +# identify the steady state velocity and required throttle setting. +xd = Y0[1] +ud = Y0[0] +yd = Y0[1] + +# Compute the linearized system at the eq pt +cruise_linearized = ct.linearize(vehicle, xd, [ud, gear[0], 0]) + +# Construct the gain matrices for the system +A, B, C = cruise_linearized.A, cruise_linearized.B[0, 0], cruise_linearized.C +K = 0.5 +kf = -1 / (C * np.linalg.inv(A - B * K) * B) + +# Response of the system with no integral feedback term +plt.figure() +plt.suptitle('Cruise control with proportional and PI control') +theta_hill = [ + 0 if t <= 8 else + 4./180. * pi * (t-8) if t <= 9 else + 4./180. * pi for t in T] +t, y = ct.input_output_response( + cruise_sf, T, [vref, gear, theta_hill], [X0[0], 0], + params={'K':K, 'kf':kf, 'ki':0.0, 'kf':kf, 'xd':xd, 'ud':ud, 'yd':yd}) +subplots = cruise_plot(cruise_sf, t, y, t_hill=8, linetype='b--') + +# Response of the system with state feedback + integral action +t, y = ct.input_output_response( + cruise_sf, T, [vref, gear, theta_hill], [X0[0], 0], + params={'K':K, 'kf':kf, 'ki':0.1, 'kf':kf, 'xd':xd, 'ud':ud, 'yd':yd}) +cruise_plot(cruise_sf, t, y, t_hill=8, linetype='b-', subplots=subplots) + +# Add a legend +plt.legend(['Proportional', 'PI control'], frameon=False) + +# Example 11.5: simulate the effect of a (steeper) hill at t = 5 seconds +# +# The windup effect occurs when a car encounters a hill that is so steep (6 +# deg) that the throttle saturates when the cruise controller attempts to +# maintain speed. + +plt.figure() +plt.suptitle('Cruise control with integrator windup') +T = np.linspace(0, 70, 101) +vref = 20 * np.ones(T.shape) +theta_hill = [ + 0 if t <= 5 else + 6./180. * pi * (t-5) if t <= 6 else + 6./180. * pi for t in T] +t, y = ct.input_output_response( + cruise_pi, T, [vref, gear, theta_hill], X0, + params={'kaw':0}) +cruise_plot(cruise_pi, t, y, antiwindup=True) + +# Example 11.6: add anti-windup compensation +# +# Anti-windup can be applied to the system to improve the response. Because of +# the feedback from the actuator model, the output of the integrator is +# quickly reset to a value such that the controller output is at the +# saturation limit. + +plt.figure() +plt.suptitle('Cruise control with integrator anti-windup protection') +t, y = ct.input_output_response( + cruise_pi, T, [vref, gear, theta_hill], X0, + params={'kaw':2.}) +cruise_plot(cruise_pi, t, y, antiwindup=True) + +# If running as a standalone program, show plots and wait before closing +import os +if __name__ == '__main__' and 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() +else: + plt.show(block=False) diff --git a/examples/cruise.ipynb b/examples/cruise.ipynb new file mode 100644 index 000000000..8da7cee83 --- /dev/null +++ b/examples/cruise.ipynb @@ -0,0 +1,906 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cruise control\n", + "\n", + "Richard M. Murray and Karl J. Åström \n", + "17 Jun 2019\n", + "\n", + "The cruise control system of a car is a common feedback system encountered in everyday life. The system attempts to maintain a constant velocity in the presence of disturbances primarily caused by changes in the slope of a road. The controller compensates for these unknowns by measuring the speed of the car and adjusting the throttle appropriately.\n", + "\n", + "This notebook explores the dynamics and control of the cruise control system, following the material presenting in Feedback Systems by Astrom and Murray. A nonlinear model of the vehicle dynamics is used, with both state space and frequency domain control laws. The process model is presented in Section 1, and a controller based on state feedback is discussed in Section 2, where we also add integral action to the controller. In Section 3 we explore the behavior with PI control including the effect of actuator saturation and how it is avoided by windup protection. Different methods of constructing control systems are shown, all using the InputOutputSystem class (and subclasses)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from math import pi\n", + "import control as ct" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Process Model\n", + "\n", + "### Vehicle Dynamics\n", + "\n", + "To develop a mathematical model we start with a force balance for the car body. Let $v$ be the speed of the car, $m$ the total mass (including passengers), $F$ the force generated by the contact of the wheels with the road, and $F_d$ the disturbance force due to gravity, friction, and aerodynamic drag." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def vehicle_update(t, x, u, params={}):\n", + " \"\"\"Vehicle dynamics for cruise control system.\n", + "\n", + " Parameters\n", + " ----------\n", + " x : array\n", + " System state: car velocity in m/s\n", + " u : array\n", + " System input: [throttle, gear, road_slope], where throttle is\n", + " a float between 0 and 1, gear is an integer between 1 and 5,\n", + " and road_slope is in rad.\n", + "\n", + " Returns\n", + " -------\n", + " float\n", + " Vehicle acceleration\n", + "\n", + " \"\"\"\n", + " from math import copysign, sin\n", + " sign = lambda x: copysign(1, x) # define the sign() function\n", + " \n", + " # Set up the system parameters\n", + " m = params.get('m', 1600.) # vehicle mass, kg\n", + " g = params.get('g', 9.8) # gravitational constant, m/s^2\n", + " Cr = params.get('Cr', 0.01) # coefficient of rolling friction\n", + " Cd = params.get('Cd', 0.32) # drag coefficient\n", + " rho = params.get('rho', 1.3) # density of air, kg/m^3\n", + " A = params.get('A', 2.4) # car area, m^2\n", + " alpha = params.get(\n", + " 'alpha', [40, 25, 16, 12, 10]) # gear ratio / wheel radius\n", + "\n", + " # Define variables for vehicle state and inputs\n", + " v = x[0] # vehicle velocity\n", + " throttle = np.clip(u[0], 0, 1) # vehicle throttle\n", + " gear = u[1] # vehicle gear\n", + " theta = u[2] # road slope\n", + "\n", + " # Force generated by the engine\n", + "\n", + " omega = alpha[int(gear)-1] * v # engine angular speed\n", + " F = alpha[int(gear)-1] * motor_torque(omega, params) * throttle\n", + "\n", + " # Disturbance forces\n", + " #\n", + " # The disturbance force Fd has three major components: Fg, the forces due\n", + " # to gravity; Fr, the forces due to rolling friction; and Fa, the\n", + " # aerodynamic drag.\n", + "\n", + " # Letting the slope of the road be \\theta (theta), gravity gives the\n", + " # force Fg = m g sin \\theta.\n", + " \n", + " Fg = m * g * sin(theta)\n", + "\n", + " # A simple model of rolling friction is Fr = m g Cr sgn(v), where Cr is\n", + " # the coefficient of rolling friction and sgn(v) is the sign of v (±1) or\n", + " # zero if v = 0.\n", + " \n", + " Fr = m * g * Cr * sign(v)\n", + "\n", + " # The aerodynamic drag is proportional to the square of the speed: Fa =\n", + " # 1/2 \\rho Cd A |v| v, where \\rho is the density of air, Cd is the\n", + " # shape-dependent aerodynamic drag coefficient, and A is the frontal area\n", + " # of the car.\n", + "\n", + " Fa = 1/2 * rho * Cd * A * abs(v) * v\n", + " \n", + " # Final acceleration on the car\n", + " Fd = Fg + Fr + Fa\n", + " dv = (F - Fd) / m\n", + " \n", + " return dv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Engine model\n", + "\n", + "The force F is generated by the engine, whose torque is proportional to the rate of fuel injection, which is itself proportional to a control signal 0 <= u <= 1 that controls the throttle position. The torque also depends on engine speed omega." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def motor_torque(omega, params={}):\n", + " # Set up the system parameters\n", + " Tm = params.get('Tm', 190.) # engine torque constant\n", + " omega_m = params.get('omega_m', 420.) # peak engine angular speed\n", + " beta = params.get('beta', 0.4) # peak engine rolloff\n", + "\n", + " return np.clip(Tm * (1 - beta * (omega/omega_m - 1)**2), 0, None)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Torque curves for a typical car engine. The graph on the left shows the torque generated by the engine as a function of the angular velocity of the engine, while the curve on the right shows torque as a function of car speed for different gears." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Figure 4.2a - single torque curve as function of omega\n", + "omega_range = np.linspace(0, 700, 701)\n", + "plt.subplot(2, 2, 1)\n", + "plt.plot(omega_range, [motor_torque(w) for w in omega_range])\n", + "plt.xlabel('Angular velocity $\\omega$ [rad/s]')\n", + "plt.ylabel('Torque $T$ [Nm]')\n", + "plt.grid(True, linestyle='dotted')\n", + "\n", + "# Figure 4.2b - torque curves in different gears, as function of velocity\n", + "plt.subplot(2, 2, 2)\n", + "v_range = np.linspace(0, 70, 71)\n", + "alpha = [40, 25, 16, 12, 10]\n", + "for gear in range(5):\n", + " omega_range = alpha[gear] * v_range\n", + " plt.plot(v_range, [motor_torque(w) for w in omega_range],\n", + " color='blue', linestyle='solid')\n", + "\n", + "# Set up the axes and style\n", + "plt.axis([0, 70, 100, 200])\n", + "plt.grid(True, linestyle='dotted')\n", + "\n", + "# Add labels\n", + "plt.text(11.5, 120, '$n$=1')\n", + "plt.text(24, 120, '$n$=2')\n", + "plt.text(42.5, 120, '$n$=3')\n", + "plt.text(58.5, 120, '$n$=4')\n", + "plt.text(58.5, 185, '$n$=5')\n", + "plt.xlabel('Velocity $v$ [m/s]')\n", + "plt.ylabel('Torque $T$ [Nm]')\n", + "\n", + "plt.tight_layout()\n", + "plt.suptitle('Torque curves for typical car engine');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Input/ouput model for the vehicle system\n", + "\n", + "We now create an input/output model for the vehicle system that takes the throttle input $u$, the gear and the angle of the road $\\theta$ as input. The output of this model is the current vehicle velocity $v$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "vehicle = ct.NonlinearIOSystem(\n", + " vehicle_update, None, name='vehicle',\n", + " inputs = ('u', 'gear', 'theta'), outputs = ('v'), states=('v'))\n", + "\n", + "# Define a generator for creating a \"standard\" cruise control plot\n", + "def cruise_plot(sys, t, y, t_hill=5, vref=20, antiwindup=False, linetype='b-',\n", + " subplots=[None, None]):\n", + " # Figure out the plot bounds and indices\n", + " v_min = vref-1.2; v_max = vref+0.5; v_ind = sys.find_output('v')\n", + " u_min = 0; u_max = 2 if antiwindup else 1; u_ind = sys.find_output('u')\n", + "\n", + " # Make sure the upper and lower bounds on v are OK\n", + " while max(y[v_ind]) > v_max: v_max += 1\n", + " while min(y[v_ind]) < v_min: v_min -= 1\n", + " \n", + " # Create arrays for return values\n", + " subplot_axes = subplots.copy()\n", + "\n", + " # Velocity profile\n", + " if subplot_axes[0] is None:\n", + " subplot_axes[0] = plt.subplot(2, 1, 1)\n", + " else:\n", + " plt.sca(subplots[0])\n", + " plt.plot(t, y[v_ind], linetype)\n", + " plt.plot(t, vref*np.ones(t.shape), 'k-')\n", + " plt.plot([t_hill, t_hill], [v_min, v_max], 'k--')\n", + " plt.axis([0, t[-1], v_min, v_max])\n", + " plt.xlabel('Time $t$ [s]')\n", + " plt.ylabel('Velocity $v$ [m/s]')\n", + "\n", + " # Commanded input profile\n", + " if subplot_axes[1] is None:\n", + " subplot_axes[1] = plt.subplot(2, 1, 2)\n", + " else:\n", + " plt.sca(subplots[1])\n", + " plt.plot(t, y[u_ind], 'r--' if antiwindup else linetype)\n", + " plt.plot([t_hill, t_hill], [u_min, u_max], 'k--')\n", + " plt.axis([0, t[-1], u_min, u_max])\n", + " plt.xlabel('Time $t$ [s]')\n", + " plt.ylabel('Throttle $u$')\n", + "\n", + " # Applied input profile\n", + " if antiwindup:\n", + " plt.plot(t, np.clip(y[u_ind], 0, 1), linetype)\n", + " plt.legend(['Commanded', 'Applied'], frameon=False)\n", + " \n", + " return subplot_axes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## State space controller\n", + "\n", + "Construct a state space controller with integral action, linearized around an equilibrium point. The controller is constructed around the equilibrium point $(x_d, u_d)$ and includes both feedforward and feedback compensation.\n", + "\n", + "* Controller inputs - $(x, y, r)$: system states, system output, reference\n", + "* Controller state - $z$: integrated error $(y - r)$\n", + "* Controller output - $u$: state feedback control\n", + "\n", + "Note: to make the structure of the controller more clear, we implement this as a \"nonlinear\" input/output module, even though the actual input/output system is linear. This also allows the use of parameters to set the operating point and gains for the controller.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Xeq = [20.]\n", + "Ueq = [0.16874874 4. 0. ]\n" + ] + } + ], + "source": [ + "def sf_update(t, z, u, params={}):\n", + " y, r = u[1], u[2]\n", + " return y - r\n", + "\n", + "def sf_output(t, z, u, params={}):\n", + " # Get the controller parameters that we need\n", + " K = params.get('K', 0)\n", + " ki = params.get('ki', 0)\n", + " kf = params.get('kf', 0)\n", + " xd = params.get('xd', 0)\n", + " yd = params.get('yd', 0)\n", + " ud = params.get('ud', 0)\n", + "\n", + " # Get the system state and reference input\n", + " x, y, r = u[0], u[1], u[2]\n", + "\n", + " return ud - K * (x - xd) - ki * z + kf * (r - yd)\n", + "\n", + "# Create the input/output system for the controller\n", + "control_sf = ct.NonlinearIOSystem(\n", + " sf_update, sf_output, name='control',\n", + " inputs=('x', 'y', 'r'),\n", + " outputs=('u'),\n", + " states=('z'))\n", + "\n", + "# Create the closed loop system for the state space controller\n", + "cruise_sf = ct.InterconnectedSystem(\n", + " (vehicle, control_sf), name='cruise',\n", + " connections=(\n", + " ('vehicle.u', 'control.u'),\n", + " ('control.x', 'vehicle.v'),\n", + " ('control.y', 'vehicle.v')),\n", + " inplist=('control.r', 'vehicle.gear', 'vehicle.theta'),\n", + " outlist=('control.u', 'vehicle.v'), outputs=['u', 'v'])\n", + "\n", + "# Define the time and input vectors\n", + "T = np.linspace(0, 25, 501)\n", + "vref = 20 * np.ones(T.shape)\n", + "gear = 4 * np.ones(T.shape)\n", + "theta0 = np.zeros(T.shape)\n", + "\n", + "# Find the equilibrium point for the system\n", + "Xeq, Ueq = ct.find_eqpt(\n", + " vehicle, [vref[0]], [0, gear[0], theta0[0]], y0=[vref[0]], iu=[1, 2])\n", + "print(\"Xeq = \", Xeq)\n", + "print(\"Ueq = \", Ueq)\n", + "\n", + "# Compute the linearized system at the eq pt\n", + "cruise_linearized = ct.linearize(vehicle, Xeq, [Ueq[0], gear[0], 0])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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51mUxLzUblmiTp0UPfPM2YRfYufOUK0arFgBP/2UM023blCpEixfDivDFg8O5dGjh/UlLFkCa9ZklSUkmBzMC2pdNleCpCQ4M/BDmzcPNmzImn/VqnDGGbY/ezYEA1sP5V+zpjVHAXzzja1cGH5tdepAly62/cUXsGuXyWfMOIGMjCTq1FnItm1w2mnwzDPQoUOexec4pZoePXowYcIEjjjiiJjSTZ8+nccee4wpU6YUiR7p6ekk5jIp3dChQ6lZsyZ33nlnoc5Rs2bNHDMWFxcFGUcS9xl6izNAR7VXaUUPZyqcrk2aqL79tmp6ep6TfzpOXHn00Ud15MiRqqp6++23a48ePVRV9eOPP9arr75aVVV37Nihp512Wo60//73v/WWW25RVZstd/DgwXrqqafqMccco2+++aaqqnbu3FkPO+wwbdeunT7xxBOalpamd955p6akpGibNm30X//6l6qqpqen680336wnnnii9u3bV3v37n0oj6ZNm+rf/vY37dq1q44fP15Hjx6tKSkp2rZtW73kkkt09+7dqpp1RuFwJk+erKeccoq2b99ee/bsqb/88ouq2szHAwcO1JNOOknbtGmjEydO1L/85S+akJCg7dq106uuukpVVWvUqKGqqhkZGXrnnXdq69at9aSTTtLXX39dVVWnTZumZ555pl566aV6wgkn6FVXXRX1bMEU8+y/ZY42beC9oEcm5JFUo4bVHA4csOadECF5tWq2nZZmHdHhMhGrVYjYLLmqOdMnJtp2uCxcnn07O/l5ThUk7Y8/Pk9iIrRqlXfejpOd22+HuXOLNs/27fNeLO2MM87g8ccf57bbbmP27Nns37+fgwcP8uWXX3L66acD8PHHH9OzZ/5D29avX8+XX37JDz/8wIUXXshll13G8OHDs9RIRo8eTe3atZk1axb79++na9eunHvuuaSmprJixQoWLFjAxo0badWqFdddd92hvKtWrcqXX34JwJYtW7jhhhsAuO+++3jxxRcZPHhwrnp169aNGTNmICKMGTOGESNG8PjjjzNs2DBq167NggULANi2bRuXXnopzzzzDHMj/BFvvfUWc+fOZd68eWzevJlOnTpxRtCsMWfOHBYtWkSDBg3o2rUrX331Fd26dcu3zApCuTYklStDo0b/3969x0hVnnEc//4AiwYqQYlAxRZajbjd7m7BrJGy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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Construct the gain matrices for the system\n", + "A, B, C = cruise_linearized.A, cruise_linearized.B[0, 0], cruise_linearized.C\n", + "K = 0.5\n", + "kf = -1 / (C * np.linalg.inv(A - B * K) * B)\n", + "\n", + "# Compute the steady state velocity and throttle setting\n", + "xd = Xeq[0]\n", + "ud = Ueq[0]\n", + "yd = vref[-1]\n", + "\n", + "# Response of the system with no integral feedback term\n", + "plt.figure()\n", + "theta_hill = [\n", + " 0 if t <= 5 else\n", + " 4./180. * pi * (t-5) if t <= 6 else\n", + " 4./180. * pi for t in T]\n", + "t, y_sfb = ct.input_output_response(\n", + " cruise_sf, T, [vref, gear, theta_hill], [Xeq[0], 0],\n", + " params={'K':K, 'ki':0.0, 'kf':kf, 'xd':xd, 'ud':ud, 'yd':yd})\n", + "subplots = cruise_plot(cruise_sf, t, y_sfb, t_hill=5, linetype='b--')\n", + "\n", + "# Response of the system with state feedback + integral action\n", + "t, y_sfb_int = ct.input_output_response(\n", + " cruise_sf, T, [vref, gear, theta_hill], [Xeq[0], 0],\n", + " params={'K':K, 'ki':0.1, 'kf':kf, 'xd':xd, 'ud':ud, 'yd':yd})\n", + "cruise_plot(cruise_sf, t, y_sfb_int, t_hill=5, linetype='b-', subplots=subplots)\n", + "\n", + "# Add title and legend\n", + "plt.suptitle('Cruise control with state feedback, integral action')\n", + "import matplotlib.lines as mlines\n", + "p_line = mlines.Line2D([], [], color='blue', linestyle='--', label='State feedback')\n", + "pi_line = mlines.Line2D([], [], color='blue', linestyle='-', label='w/ integral action')\n", + "plt.legend(handles=[p_line, pi_line], frameon=False, loc='lower right');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pole/zero cancellation\n", + "\n", + "The transfer function for the linearized dynamics of the cruise control system is given by $P(s) = b/(s+a)$. A simple (but not necessarily good) way to design a PI controller is to choose the parameters of the PI controller as $k_\\text{i}=ak_\\text{p}$. The controller transfer function is then $C(s)=k_\\text{p}+k_\\text{i}/s=k_\\text{i}(s+a)/s$. It has a zero at $s = -k_\\text{i}/k_\\text{p}=-a$ that cancels the process pole at $s = -a$. We have $P(s)C(s)=k_\\text{i}/s$ giving the transfer function from reference to vehicle velocity as $G_{yr}(s)=b k_\\text{p}/(s + b k_\\text{p})$, and control design is then simply a matter of choosing the gain $k_\\text{p}$. The closed loop system dynamics are of first order with the time constant $1/(b k_\\text{p})$." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "system: a = 0.010124405669387215 , b = 1.3203061238159202\n", + "pzcancel: kp = 0.5 , ki = 0.005062202834693608 , 1/(kp b) = 1.5148002148317266\n", + "sfb_int: K = 0.5 , ki = 0.1\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Get the transfer function from throttle input + hill to vehicle speed\n", + "P = ct.ss2tf(cruise_linearized[0, 0])\n", + "\n", + "# Construction a controller that cancels the pole\n", + "kp = 0.5\n", + "a = -P.pole()[0]\n", + "b = np.real(P(0)) * a\n", + "ki = a * kp\n", + "C = ct.tf2ss(ct.TransferFunction([kp, ki], [1, 0]))\n", + "control_pz = ct.LinearIOSystem(C, name='control', inputs='u', outputs='y')\n", + "print(\"system: a = \", a, \", b = \", b)\n", + "print(\"pzcancel: kp =\", kp, \", ki =\", ki, \", 1/(kp b) = \", 1/(kp * b))\n", + "print(\"sfb_int: K = \", K, \", ki = 0.1\")\n", + "\n", + "# Construct the closed loop system and plot the response\n", + "# Create the closed loop system for the state space controller\n", + "cruise_pz = ct.InterconnectedSystem(\n", + " (vehicle, control_pz), name='cruise_pz',\n", + " connections = (\n", + " ('control.u', '-vehicle.v'),\n", + " ('vehicle.u', 'control.y')),\n", + " inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'),\n", + " inputs = ('vref', 'gear', 'theta'),\n", + " outlist = ('vehicle.v', 'vehicle.u'),\n", + " outputs = ('v', 'u'))\n", + "\n", + "# Find the equilibrium point\n", + "X0, U0 = ct.find_eqpt(\n", + " cruise_pz, [vref[0], 0], [vref[0], gear[0], theta0[0]], \n", + " iu=[1, 2], y0=[vref[0], 0], iy=[0])\n", + "\n", + "# Response of the system with PI controller canceling process pole\n", + "t, y_pzcancel = ct.input_output_response(\n", + " cruise_pz, T, [vref, gear, theta_hill], X0)\n", + "subplots = cruise_plot(cruise_pz, t, y_pzcancel, t_hill=5, linetype='b-')\n", + "cruise_plot(cruise_sf, t, y_sfb_int, t_hill=5, linetype='b--', subplots=subplots);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PI Controller\n", + "\n", + "In this example, the speed of the vehicle is measured and compared to the desired speed. The controller is a PI controller represented as a transfer function. In the textbook, the simulations are done for LTI systems, but here we simulate the full nonlinear system." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Parameter design through pole placement\n", + "\n", + "To illustrate the design of a PI controller, we choose the gains $k_\\text{p}$ and $k_\\text{i}$ so that the characteristic polynomial has the form\n", + "\n", + "$$\n", + "s^2 + 2 \\zeta \\omega_0 s + \\omega_0^2\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Values of the first order transfer function P(s) = b/(s + a) are set above\n", + "\n", + "# Define the input that we want to track\n", + "T = np.linspace(0, 40, 101)\n", + "vref = 20 * np.ones(T.shape)\n", + "gear = 4 * np.ones(T.shape)\n", + "theta_hill = np.array([\n", + " 0 if t <= 5 else\n", + " 4./180. * pi * (t-5) if t <= 6 else\n", + " 4./180. * pi for t in T])\n", + "\n", + "# Fix \\omega_0 and vary \\zeta\n", + "w0 = 0.5\n", + "subplots = [None, None]\n", + "for zeta in [0.5, 1, 2]:\n", + " # Create the controller transfer function (as an I/O system)\n", + " kp = (2*zeta*w0 - a)/b\n", + " ki = w0**2 / b\n", + " control_tf = ct.tf2io(\n", + " ct.TransferFunction([kp, ki], [1, 0.01*ki/kp]),\n", + " name='control', inputs='u', outputs='y')\n", + " \n", + " # Construct the closed loop system by interconnecting process and controller\n", + " cruise_tf = ct.InterconnectedSystem(\n", + " (vehicle, control_tf), name='cruise',\n", + " connections = [('control.u', '-vehicle.v'), ('vehicle.u', 'control.y')],\n", + " inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'), \n", + " inputs = ('vref', 'gear', 'theta'),\n", + " outlist = ('vehicle.v', 'vehicle.u'), outputs = ('v', 'u'))\n", + "\n", + " # Plot the velocity response\n", + " X0, U0 = ct.find_eqpt(\n", + " cruise_tf, [vref[0], 0], [vref[0], gear[0], theta_hill[0]], \n", + " iu=[1, 2], y0=[vref[0], 0], iy=[0])\n", + "\n", + " t, y = ct.input_output_response(cruise_tf, T, [vref, gear, theta_hill], X0)\n", + " subplots = cruise_plot(cruise_tf, t, y, t_hill=5, subplots=subplots)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Fix \\zeta and vary \\omega_0\n", + "zeta = 1\n", + "subplots = [None, None]\n", + "for w0 in [0.2, 0.5, 1]:\n", + " # Create the controller transfer function (as an I/O system)\n", + " kp = (2*zeta*w0 - a)/b\n", + " ki = w0**2 / b\n", + " control_tf = ct.tf2io(\n", + " ct.TransferFunction([kp, ki], [1, 0.01*ki/kp]),\n", + " name='control', inputs='u', outputs='y')\n", + " \n", + " # Construct the closed loop system by interconnecting process and controller\n", + " cruise_tf = ct.InterconnectedSystem(\n", + " (vehicle, control_tf), name='cruise',\n", + " connections = [('control.u', '-vehicle.v'), ('vehicle.u', 'control.y')],\n", + " inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'), \n", + " inputs = ('vref', 'gear', 'theta'),\n", + " outlist = ('vehicle.v', 'vehicle.u'), outputs = ('v', 'u'))\n", + "\n", + " # Plot the velocity response\n", + " X0, U0 = ct.find_eqpt(\n", + " cruise_tf, [vref[0], 0], [vref[0], gear[0], theta_hill[0]], \n", + " iu=[1, 2], y0=[vref[0], 0], iy=[0])\n", + "\n", + " t, y = ct.input_output_response(cruise_tf, T, [vref, gear, theta_hill], X0)\n", + " subplots = cruise_plot(cruise_tf, t, y, t_hill=5, subplots=subplots)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Robustness to change in mass" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# Nominal controller design for remaining analyses\n", + "# Construct a PI controller with rolloff, as a transfer function\n", + "Kp = 0.5 # proportional gain\n", + "Ki = 0.1 # integral gain\n", + "control_tf = ct.tf2io(\n", + " ct.TransferFunction([Kp, Ki], [1, 0.01*Ki/Kp]),\n", + " name='control', inputs='u', outputs='y')\n", + "\n", + "cruise_tf = ct.InterconnectedSystem(\n", + " (vehicle, control_tf), name='cruise',\n", + " connections = [('control.u', '-vehicle.v'), ('vehicle.u', 'control.y')],\n", + " inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'), inputs = ('vref', 'gear', 'theta'),\n", + " outlist = ('vehicle.v', 'vehicle.u'), outputs = ('v', 'u'))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Define the time and input vectors\n", + "T = np.linspace(0, 25, 101)\n", + "vref = 20 * np.ones(T.shape)\n", + "gear = 4 * np.ones(T.shape)\n", + "theta0 = np.zeros(T.shape)\n", + "\n", + "# Now simulate the effect of a hill at t = 5 seconds\n", + "plt.figure()\n", + "plt.suptitle('Response to change in road slope')\n", + "theta_hill = np.array([\n", + " 0 if t <= 5 else\n", + " 4./180. * pi * (t-5) if t <= 6 else\n", + " 4./180. * pi for t in T])\n", + "\n", + "subplots = [None, None]\n", + "linecolor = ['red', 'blue', 'green']\n", + "handles = []\n", + "for i, m in enumerate([1200, 1600, 2000]):\n", + " # Compute the equilibrium state for the system\n", + " X0, U0 = ct.find_eqpt(\n", + " cruise_tf, [vref[0], 0], [vref[0], gear[0], theta0[0]], \n", + " iu=[1, 2], y0=[vref[0], 0], iy=[0], params={'m':m})\n", + "\n", + " t, y = ct.input_output_response(\n", + " cruise_tf, T, [vref, gear, theta_hill], X0, params={'m':m})\n", + "\n", + " subplots = cruise_plot(cruise_tf, t, y, t_hill=5, subplots=subplots,\n", + " linetype=linecolor[i][0] + '-')\n", + " handles.append(mlines.Line2D([], [], color=linecolor[i], linestyle='-', \n", + " label=\"m = %d\" % m))\n", + "\n", + "# Add labels to the plots\n", + "plt.sca(subplots[0])\n", + "plt.ylabel('Speed [m/s]')\n", + "plt.legend(handles=handles, frameon=False, loc='lower right');\n", + "\n", + "plt.sca(subplots[1])\n", + "plt.ylabel('Throttle')\n", + "plt.xlabel('Time [s]');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PI controller with antiwindup protection\n", + "\n", + "We now create a more complicated feedback controller that includes anti-windup protection." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def pi_update(t, x, u, params={}):\n", + " # Get the controller parameters that we need\n", + " ki = params.get('ki', 0.1)\n", + " kaw = params.get('kaw', 2) # anti-windup gain\n", + "\n", + " # Assign variables for inputs and states (for readability)\n", + " v = u[0] # current velocity\n", + " vref = u[1] # reference velocity\n", + " z = x[0] # integrated error\n", + "\n", + " # Compute the nominal controller output (needed for anti-windup)\n", + " u_a = pi_output(t, x, u, params)\n", + "\n", + " # Compute anti-windup compensation (scale by ki to account for structure)\n", + " u_aw = kaw/ki * (np.clip(u_a, 0, 1) - u_a) if ki != 0 else 0\n", + "\n", + " # State is the integrated error, minus anti-windup compensation\n", + " return (vref - v) + u_aw\n", + "\n", + "def pi_output(t, x, u, params={}):\n", + " # Get the controller parameters that we need\n", + " kp = params.get('kp', 0.5)\n", + " ki = params.get('ki', 0.1)\n", + "\n", + " # Assign variables for inputs and states (for readability)\n", + " v = u[0] # current velocity\n", + " vref = u[1] # reference velocity\n", + " z = x[0] # integrated error\n", + "\n", + " # PI controller\n", + " return kp * (vref - v) + ki * z\n", + "\n", + "control_pi = ct.NonlinearIOSystem(\n", + " pi_update, pi_output, name='control',\n", + " inputs = ['v', 'vref'], outputs = ['u'], states = ['z'],\n", + " params = {'kp':0.5, 'ki':0.1})\n", + "\n", + "# Create the closed loop system\n", + "cruise_pi = ct.InterconnectedSystem(\n", + " (vehicle, control_pi), name='cruise',\n", + " connections=(\n", + " ('vehicle.u', 'control.u'),\n", + " ('control.v', 'vehicle.v')),\n", + " inplist=('control.vref', 'vehicle.gear', 'vehicle.theta'),\n", + " outlist=('control.u', 'vehicle.v'), outputs=['u', 'v'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Response to a small hill\n", + "\n", + "Figure 4.3b shows the response of the closed loop system. The figure shows that even if the hill is so steep that the throttle changes from 0.17 to almost full throttle, the largest speed error is less than 1 m/s, and the desired velocity is recovered after 20 s." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Compute the equilibrium throttle setting for the desired speed\n", + "X0, U0, Y0 = ct.find_eqpt(\n", + " cruise_pi, [vref[0], 0], [vref[0], gear[0], theta0[0]],\n", + " y0=[0, vref[0]], iu=[1, 2], iy=[1], return_y=True)\n", + "\n", + "# Now simulate the effect of a hill at t = 5 seconds\n", + "plt.figure()\n", + "plt.suptitle('Car with cruise control encountering sloping road')\n", + "theta_hill = [\n", + " 0 if t <= 5 else\n", + " 4./180. * pi * (t-5) if t <= 6 else\n", + " 4./180. * pi for t in T]\n", + "t, y = ct.input_output_response(\n", + " cruise_pi, T, [vref, gear, theta_hill], X0)\n", + "cruise_plot(cruise_pi, t, y);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Effect of Windup\n", + "\n", + "The windup effect occurs when a car encounters a hill that is so steep ($6^\\circ$) that the throttle saturates when the cruise controller attempts to maintain speed." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "plt.suptitle('Cruise control with integrator windup')\n", + "T = np.linspace(0, 50, 101)\n", + "vref = 20 * np.ones(T.shape)\n", + "theta_hill = [\n", + " 0 if t <= 5 else\n", + " 6./180. * pi * (t-5) if t <= 6 else\n", + " 6./180. * pi for t in T]\n", + "t, y = ct.input_output_response(\n", + " cruise_pi, T, [vref, gear, theta_hill], X0,\n", + " params={'kaw':0})\n", + "cruise_plot(cruise_pi, t, y, antiwindup=True);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PI controller with anti-windup compensation\n", + "\n", + "Anti-windup can be applied to the system to improve the response. Because of the feedback from the actuator model, the output of the integrator is quickly reset to a value such that the controller output is at the saturation limit." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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p+/cuZNevXrRvn17brrpJnzHkHrnnXfo0qUL7dq146abbiInJweAN954g+bNm9OjRw9mzpwZsbLMY4kkgjIzM8nMzIx1GMaUbenpx95eesnNy8z0P3/MGDd/x45j55Vg2bJldOzY8ZjpkyZNIiMjg8WLF/PFF19w3333sXmz60Bj8eLFPPfccyxdupS3336bH374gblz53LDDTfw73//O38bu3fv5quvvuJf//oX/fr1489//jPLly9n6dKlZGS4660fffRR5s+fz5IlS/jmm29YsmRJ/vppaWksXLiQIUOG8PTTTwPw8MMP0717dxYtWsQll1zC+vXrAVixYgXjx49n5syZZGRkkJyczNixY9m8eTPDhw9n5syZTJ06le+/9x2RIzIskURQnz596NOnT6zDMMYEYMaMGQwcOJDk5GTq1atHjx49mDdvHgCdO3emfv36VKpUiVNOOYVevXoB0KZNG9auXZu/jX79+iEitGnThnr16tGmTRuSkpI47bTT8pebMGECHTp0oH379ixfvvyoH/orrrgCgI4dO+YvP336dK699loALr74YmrVqgXAl19+yYIFC+jcuTPt2rXjyy+/ZM2aNcyZM4f09HTq1KlDxYoVufrqqyNZbIC1kRhjYq243rGrVCl+flpa8fP9OO2005g4ceIx04sbdrxSpUr5j5OSkvKfJyUlkZ2dfcxyvsv4Lvfzzz/z9NNPM2/ePGrVqsXgwYM5dOjQMesnJycftV3XJeGx8Q4aNIjHH3/8qOkffPCB3+UjyWokxpiEcv7553P48GFeffXV/Gl5P+zjx48nJyeH7du3M336dLp06RLWfe/du5eqVatSs2ZNtm7dyieffFLiOueeey5jx44F4JNPPmH37t0A9OzZk4kTJ7Jt2zYAdu3axbp16zjzzDOZNm0aO3fuJCsri//+979hfQ3+WI3EGJNQRIT333+fu+66iyeeeILU1FSaNGnCyJEj2b9/P23btkVEeOqppzjhhBPyG73DoW3btrRv357TTjuNk08+mbPPPrvEdYYPH87AgQPp0KEDPXr0oFGjRgC0bt2aESNG0KtXL3Jzc0lJSeHFF1+ka9euPPTQQ3Tr1o369evToUOH/Eb4SJHiqnPlnQ1sZYwxwRGRBaraKZh1rEYSQb6n/BljTLyyRBJBlkiMMYmgTDS2i8jrIrJNRJYVMT9d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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "plt.suptitle('Cruise control with integrator anti-windup protection')\n", + "t, y = ct.input_output_response(\n", + " cruise_pi, T, [vref, gear, theta_hill], X0,\n", + " params={'kaw':2.})\n", + "cruise_plot(cruise_pi, t, y, antiwindup=True);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/genswitch.py b/examples/genswitch.py index 11f79a36c..e65e40110 100644 --- a/examples/genswitch.py +++ b/examples/genswitch.py @@ -8,75 +8,76 @@ import os import numpy as np -import matplotlib.pyplot as mpl +import matplotlib.pyplot as plt from scipy.integrate import odeint from control import phase_plot, box_grid # Simple model of a genetic switch -# + # This function implements the basic model of the genetic switch # Parameters taken from Gardner, Cantor and Collins, Nature, 2000 def genswitch(y, t, mu=4, n=2): - return (mu / (1 + y[1]**n) - y[0], mu / (1 + y[0]**n) - y[1]) + return mu/(1 + y[1]**n) - y[0], mu/(1 + y[0]**n) - y[1] # Run a simulation from an initial condition tim1 = np.linspace(0, 10, 100) sol1 = odeint(genswitch, [1, 5], tim1) -# Extract the equlibirum points -mu = 4; n = 2; # switch parameters -eqpt = np.empty(3); -eqpt[0] = sol1[0,-1] -eqpt[1] = sol1[1,-1] -eqpt[2] = 0; # fzero(@(x) mu/(1+x^2) - x, 2); +# Extract the equilibrium points +mu = 4; n = 2 # switch parameters +eqpt = np.empty(3) +eqpt[0] = sol1[0, -1] +eqpt[1] = sol1[1, -1] +eqpt[2] = 0 # fzero(@(x) mu/(1+x^2) - x, 2) # Run another simulation showing switching behavior -tim2 = np.linspace(11, 25, 100); -sol2 = odeint(genswitch, sol1[-1,:] + [2, -2], tim2) +tim2 = np.linspace(11, 25, 100) +sol2 = odeint(genswitch, sol1[-1, :] + [2, -2], tim2) # First plot out the curves that define the equilibria u = np.linspace(0, 4.5, 46) -f = np.divide(mu, (1 + u**n)) # mu / (1 + u^n), elementwise +f = np.divide(mu, (1 + u**n)) # mu/(1 + u^n), element-wise -mpl.figure(1); mpl.clf(); -mpl.axis([0, 5, 0, 5]); # box on; -mpl.plot(u, f, '-', f, u, '--') # 'LineWidth', AM_data_linewidth); -mpl.legend(('z1, f(z1)', 'z2, f(z2)')) # legend(lgh, 'boxoff'); -mpl.plot([0, 3], [0, 3], 'k-') # 'LineWidth', AM_ref_linewidth); -mpl.plot(eqpt[0], eqpt[1], 'k.', eqpt[1], eqpt[0], 'k.', - eqpt[2], eqpt[2], 'k.') # 'MarkerSize', AM_data_markersize*3); -mpl.xlabel('z1, f(z2)'); -mpl.ylabel('z2, f(z1)'); +plt.figure(1); plt.clf() +plt.axis([0, 5, 0, 5]) # box on; +plt.plot(u, f, '-', f, u, '--') # 'LineWidth', AM_data_linewidth) +plt.legend(('z1, f(z1)', 'z2, f(z2)')) # legend(lgh, 'boxoff') +plt.plot([0, 3], [0, 3], 'k-') # 'LineWidth', AM_ref_linewidth) +plt.plot(eqpt[0], eqpt[1], 'k.', eqpt[1], eqpt[0], 'k.', + eqpt[2], eqpt[2], 'k.') # 'MarkerSize', AM_data_markersize*3) +plt.xlabel('z1, f(z2)') +plt.ylabel('z2, f(z1)') # Time traces -mpl.figure(3); mpl.clf(); # subplot(221); -mpl.plot(tim1, sol1[:,0], 'b-', tim1, sol1[:,1], 'g--'); -# set(pl, 'LineWidth', AM_data_linewidth); -mpl.plot([tim1[-1], tim1[-1]+1], - [sol1[-1,0], sol2[0,1]], 'ko:', - [tim1[-1], tim1[-1]+1], [sol1[-1,1], sol2[0,0]], 'ko:'); -# set(pl, 'LineWidth', AM_data_linewidth, 'MarkerSize', AM_data_markersize); -mpl.plot(tim2, sol2[:,0], 'b-', tim2, sol2[:,1], 'g--'); -# set(pl, 'LineWidth', AM_data_linewidth); -mpl.axis([0, 25, 0, 5]); +plt.figure(3); plt.clf() # subplot(221) +plt.plot(tim1, sol1[:, 0], 'b-', tim1, sol1[:, 1], 'g--') +# set(pl, 'LineWidth', AM_data_linewidth) +plt.plot([tim1[-1], tim1[-1] + 1], + [sol1[-1, 0], sol2[0, 1]], 'ko:', + [tim1[-1], tim1[-1] + 1], [sol1[-1, 1], sol2[0, 0]], 'ko:') +# set(pl, 'LineWidth', AM_data_linewidth, 'MarkerSize', AM_data_markersize) +plt.plot(tim2, sol2[:, 0], 'b-', tim2, sol2[:, 1], 'g--') +# set(pl, 'LineWidth', AM_data_linewidth) +plt.axis([0, 25, 0, 5]) -mpl.xlabel('Time {\itt} [scaled]'); -mpl.ylabel('Protein concentrations [scaled]'); -mpl.legend(('z1 (A)', 'z2 (B)')) # 'Orientation', 'horizontal'); -# legend(legh, 'boxoff'); +plt.xlabel('Time {\itt} [scaled]') +plt.ylabel('Protein concentrations [scaled]') +plt.legend(('z1 (A)', 'z2 (B)')) # 'Orientation', 'horizontal') +# legend(legh, 'boxoff') # Phase portrait -mpl.figure(2); mpl.clf(); # subplot(221); -mpl.axis([0, 5, 0, 5]); # set(gca, 'DataAspectRatio', [1, 1, 1]); -phase_plot(genswitch, X0 = box_grid([0, 5, 6], [0, 5, 6]), T = 10, - timepts = [0.2, 0.6, 1.2]) +plt.figure(2) +plt.clf() # subplot(221) +plt.axis([0, 5, 0, 5]) # set(gca, 'DataAspectRatio', [1, 1, 1]) +phase_plot(genswitch, X0=box_grid([0, 5, 6], [0, 5, 6]), T=10, + timepts=[0.2, 0.6, 1.2]) # Add the stable equilibrium points -mpl.plot(eqpt[0], eqpt[1], 'k.', eqpt[1], eqpt[0], 'k.', - eqpt[2], eqpt[2], 'k.') # 'MarkerSize', AM_data_markersize*3); +plt.plot(eqpt[0], eqpt[1], 'k.', eqpt[1], eqpt[0], 'k.', + eqpt[2], eqpt[2], 'k.') # 'MarkerSize', AM_data_markersize*3) -mpl.xlabel('Protein A [scaled]'); -mpl.ylabel('Protein B [scaled]'); # 'Rotation', 90); +plt.xlabel('Protein A [scaled]') +plt.ylabel('Protein B [scaled]') # 'Rotation', 90) if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: - mpl.show() + plt.show() diff --git a/examples/kincar-flatsys.py b/examples/kincar-flatsys.py new file mode 100644 index 000000000..17a1b71b9 --- /dev/null +++ b/examples/kincar-flatsys.py @@ -0,0 +1,133 @@ +# kincar-flatsys.py - differentially flat systems example +# RMM, 3 Jul 2019 +# +# This example demonstrates the use of the `flatsys` module for generating +# trajectories for differnetially flat systems by computing a trajectory for a +# kinematic (bicycle) model of a car changing lanes. + +import os +import numpy as np +import matplotlib.pyplot as plt +import control as ct +import control.flatsys as fs + + +# Function to take states, inputs and return the flat flag +def vehicle_flat_forward(x, u, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + + # Create a list of arrays to store the flat output and its derivatives + zflag = [np.zeros(3), np.zeros(3)] + + # Flat output is the x, y position of the rear wheels + zflag[0][0] = x[0] + zflag[1][0] = x[1] + + # First derivatives of the flat output + zflag[0][1] = u[0] * np.cos(x[2]) # dx/dt + zflag[1][1] = u[0] * np.sin(x[2]) # dy/dt + + # First derivative of the angle + thdot = (u[0]/b) * np.tan(u[1]) + + # Second derivatives of the flat output (setting vdot = 0) + zflag[0][2] = -u[0] * thdot * np.sin(x[2]) + zflag[1][2] = u[0] * thdot * np.cos(x[2]) + + return zflag + + +# Function to take the flat flag and return states, inputs +def vehicle_flat_reverse(zflag, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + + # Create a vector to store the state and inputs + x = np.zeros(3) + u = np.zeros(2) + + # Given the flat variables, solve for the state + x[0] = zflag[0][0] # x position + x[1] = zflag[1][0] # y position + x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # tan(theta) = ydot/xdot + + # And next solve for the inputs + u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2]) + thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2]) + u[1] = np.arctan2(thdot_v, u[0]**2 / b) + + return x, u + + +# Function to compute the RHS of the system dynamics +def vehicle_update(t, x, u, params): + b = params.get('wheelbase', 3.) # get parameter values + dx = np.array([ + np.cos(x[2]) * u[0], + np.sin(x[2]) * u[0], + (u[0]/b) * np.tan(u[1]) + ]) + return dx + + +# Create differentially flat input/output system +vehicle_flat = fs.FlatSystem( + vehicle_flat_forward, vehicle_flat_reverse, vehicle_update, + inputs=('v', 'delta'), outputs=('x', 'y', 'theta'), + states=('x', 'y', 'theta')) + +# Define the endpoints of the trajectory +x0 = [0., -2., 0.]; u0 = [10., 0.] +xf = [40., 2., 0.]; uf = [10., 0.] +Tf = 4 + +# Define a set of basis functions to use for the trajectories +poly = fs.PolyFamily(6) + +# Find a trajectory between the initial condition and the final condition +traj = fs.point_to_point(vehicle_flat, x0, u0, xf, uf, Tf, basis=poly) + +# Create the desired trajectory between the initial and final condition +T = np.linspace(0, Tf, 500) +xd, ud = traj.eval(T) + +# Simulation the open system dynamics with the full input +t, y, x = ct.input_output_response( + vehicle_flat, T, ud, x0, return_x=True) + +# Plot the open loop system dynamics +plt.figure() +plt.suptitle("Open loop trajectory for kinematic car lane change") + +# Plot the trajectory in xy coordinates +plt.subplot(4, 1, 2) +plt.plot(x[0], x[1]) +plt.xlabel('x [m]') +plt.ylabel('y [m]') +plt.axis([x0[0], xf[0], x0[1]-1, xf[1]+1]) + +# Time traces of the state and input +plt.subplot(2, 4, 5) +plt.plot(t, x[1]) +plt.ylabel('y [m]') + +plt.subplot(2, 4, 6) +plt.plot(t, x[2]) +plt.ylabel('theta [rad]') + +plt.subplot(2, 4, 7) +plt.plot(t, ud[0]) +plt.xlabel('Time t [sec]') +plt.ylabel('v [m/s]') +plt.axis([0, Tf, u0[0] - 1, uf[0] + 1]) + +plt.subplot(2, 4, 8) +plt.plot(t, ud[1]) +plt.xlabel('Ttime t [sec]') +plt.ylabel('$\delta$ [rad]') +plt.tight_layout() + +# Show the results unless we are running in batch mode +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() diff --git a/examples/phaseplots.py b/examples/phaseplots.py index 0c86522de..cf05c384a 100644 --- a/examples/phaseplots.py +++ b/examples/phaseplots.py @@ -7,12 +7,12 @@ import os import numpy as np -import matplotlib.pyplot as mpl +import matplotlib.pyplot as plt from control.phaseplot import phase_plot -from numpy import pi +from numpy import pi # Clear out any figures that are present -mpl.close('all') +plt.close('all') # # Inverted pendulum @@ -20,52 +20,65 @@ # Define the ODEs for a damped (inverted) pendulum def invpend_ode(x, t, m=1., l=1., b=0.2, g=1): - return (x[1], -b/m*x[1] + (g*l/m) * np.sin(x[0])) + return x[1], -b/m*x[1] + (g*l/m)*np.sin(x[0]) + # Set up the figure the way we want it to look -mpl.figure(); mpl.clf(); -mpl.axis([-2*pi, 2*pi, -2.1, 2.1]); -mpl.title('Inverted pendulum') +plt.figure() +plt.clf() +plt.axis([-2*pi, 2*pi, -2.1, 2.1]) +plt.title('Inverted pendulum') # Outer trajectories -phase_plot(invpend_ode, - X0 = [ [-2*pi, 1.6], [-2*pi, 0.5], [-1.8, 2.1], - [-1, 2.1], [4.2, 2.1], [5, 2.1], - [2*pi, -1.6], [2*pi, -0.5], [1.8, -2.1], - [1, -2.1], [-4.2, -2.1], [-5, -2.1] ], - T = np.linspace(0, 40, 200), - logtime = (3, 0.7) ) +phase_plot( + invpend_ode, + X0=[[-2*pi, 1.6], [-2*pi, 0.5], [-1.8, 2.1], + [-1, 2.1], [4.2, 2.1], [5, 2.1], + [2*pi, -1.6], [2*pi, -0.5], [1.8, -2.1], + [1, -2.1], [-4.2, -2.1], [-5, -2.1]], + T=np.linspace(0, 40, 200), + logtime=(3, 0.7) +) # Separatrices -phase_plot(invpend_ode, X0 = [[-2.3056, 2.1], [2.3056, -2.1]], T=6, lingrid=0) +phase_plot(invpend_ode, X0=[[-2.3056, 2.1], [2.3056, -2.1]], T=6, lingrid=0) # # Systems of ODEs: damped oscillator example (simulation + phase portrait) # def oscillator_ode(x, t, m=1., b=1, k=1): - return (x[1], -k/m*x[0] - b/m*x[1]) + return x[1], -k/m*x[0] - b/m*x[1] + # Generate a vector plot for the damped oscillator -mpl.figure(); mpl.clf(); -phase_plot(oscillator_ode, [-1, 1, 10], [-1, 1, 10], 0.15); -#mpl.plot([0], [0], '.'); -# a=gca; set(a,'FontSize',20); set(a,'DataAspectRatio',[1,1,1]); -mpl.xlabel('$x_1$'); mpl.ylabel('$x_2$'); -mpl.title('Damped oscillator, vector field') +plt.figure() +plt.clf() +phase_plot(oscillator_ode, [-1, 1, 10], [-1, 1, 10], 0.15) +#plt.plot([0], [0], '.') +# a=gca; set(a,'FontSize',20); set(a,'DataAspectRatio',[1,1,1]) +plt.xlabel('$x_1$') +plt.ylabel('$x_2$') +plt.title('Damped oscillator, vector field') # Generate a phase plot for the damped oscillator -mpl.figure(); mpl.clf(); -mpl.axis([-1, 1, -1, 1]); # set(gca, 'DataAspectRatio', [1, 1, 1]); -phase_plot(oscillator_ode, - X0 = [ - [-1, 1], [-0.3, 1], [0, 1], [0.25, 1], [0.5, 1], [0.75, 1], [1, 1], - [1, -1], [0.3, -1], [0, -1], [-0.25, -1], [-0.5, -1], [-0.75, -1], [-1, -1] - ], T = np.linspace(0, 8, 80), timepts = [0.25, 0.8, 2, 3]) -mpl.plot([0], [0], 'k.'); # 'MarkerSize', AM_data_markersize*3); -# set(gca,'DataAspectRatio',[1,1,1]); -mpl.xlabel('$x_1$'); mpl.ylabel('$x_2$'); -mpl.title('Damped oscillator, vector field and stream lines') +plt.figure() +plt.clf() +plt.axis([-1, 1, -1, 1]) # set(gca, 'DataAspectRatio', [1, 1, 1]); +phase_plot( + oscillator_ode, + X0=[ + [-1, 1], [-0.3, 1], [0, 1], [0.25, 1], [0.5, 1], [0.75, 1], [1, 1], + [1, -1], [0.3, -1], [0, -1], [-0.25, -1], [-0.5, -1], [-0.75, -1], [-1, -1] + ], + T=np.linspace(0, 8, 80), + timepts=[0.25, 0.8, 2, 3] +) +plt.plot([0], [0], 'k.') # 'MarkerSize', AM_data_markersize*3) +# set(gca, 'DataAspectRatio', [1,1,1]) +plt.xlabel('$x_1$') +plt.ylabel('$x_2$') +plt.title('Damped oscillator, vector field and stream lines') # # Stability definitions @@ -73,54 +86,81 @@ def oscillator_ode(x, t, m=1., b=1, k=1): # This set of plots illustrates the various types of equilibrium points. # -# Saddle point vector field + def saddle_ode(x, t): - return (x[0] - 3*x[1], -3*x[0] + x[1]); + """Saddle point vector field""" + return x[0] - 3*x[1], -3*x[0] + x[1] + # Asy stable -m = 1; b = 1; k = 1; # default values -mpl.figure(); mpl.clf(); -mpl.axis([-1, 1, -1, 1]); # set(gca, 'DataAspectRatio', [1 1 1]); -phase_plot(oscillator_ode, - X0 = [ - [-1,1], [-0.3,1], [0,1], [0.25,1], [0.5,1], [0.7,1], [1,1], [1.3,1], - [1,-1], [0.3,-1], [0,-1], [-0.25,-1], [-0.5,-1], [-0.7,-1], [-1,-1], - [-1.3,-1] - ], T = np.linspace(0, 10, 100), - timepts = [0.3, 1, 2, 3], parms = (m, b, k)); -mpl.plot([0], [0], 'k.'); # 'MarkerSize', AM_data_markersize*3); -# set(gca,'FontSize', 16); -mpl.xlabel('$x_1$'); mpl.ylabel('$x_2$'); -mpl.title('Asymptotically stable point') +m = 1 +b = 1 +k = 1 # default values +plt.figure() +plt.clf() +plt.axis([-1, 1, -1, 1]) # set(gca, 'DataAspectRatio', [1 1 1]); +phase_plot( + oscillator_ode, + X0=[ + [-1, 1], [-0.3, 1], [0, 1], [0.25, 1], [0.5, 1], [0.7, 1], [1, 1], [1.3, 1], + [1, -1], [0.3, -1], [0, -1], [-0.25, -1], [-0.5, -1], [-0.7, -1], [-1, -1], + [-1.3, -1] + ], + T=np.linspace(0, 10, 100), + timepts=[0.3, 1, 2, 3], + parms=(m, b, k) +) +plt.plot([0], [0], 'k.') # 'MarkerSize', AM_data_markersize*3) +# plt.set(gca,'FontSize', 16) +plt.xlabel('$x_1$') +plt.ylabel('$x_2$') +plt.title('Asymptotically stable point') # Saddle -mpl.figure(); mpl.clf(); -mpl.axis([-1, 1, -1, 1]); # set(gca, 'DataAspectRatio', [1 1 1]); -phase_plot(saddle_ode, scale = 2, timepts = [0.2, 0.5, 0.8], X0 = - [ [-1, -1], [1, 1], - [-1, -0.95], [-1, -0.9], [-1, -0.8], [-1, -0.6], [-1, -0.4], [-1, -0.2], - [-0.95, -1], [-0.9, -1], [-0.8, -1], [-0.6, -1], [-0.4, -1], [-0.2, -1], - [1, 0.95], [1, 0.9], [1, 0.8], [1, 0.6], [1, 0.4], [1, 0.2], - [0.95, 1], [0.9, 1], [0.8, 1], [0.6, 1], [0.4, 1], [0.2, 1], - [-0.5, -0.45], [-0.45, -0.5], [0.5, 0.45], [0.45, 0.5], - [-0.04, 0.04], [0.04, -0.04] ], T = np.linspace(0, 2, 20)); -mpl.plot([0], [0], 'k.'); # 'MarkerSize', AM_data_markersize*3); -# set(gca,'FontSize', 16); -mpl.xlabel('$x_1$'); mpl.ylabel('$x_2$'); -mpl.title('Saddle point') +plt.figure() +plt.clf() +plt.axis([-1, 1, -1, 1]) # set(gca, 'DataAspectRatio', [1 1 1]) +phase_plot( + saddle_ode, + scale=2, + timepts=[0.2, 0.5, 0.8], + X0=[ + [-1, -1], [1, 1], + [-1, -0.95], [-1, -0.9], [-1, -0.8], [-1, -0.6], [-1, -0.4], [-1, -0.2], + [-0.95, -1], [-0.9, -1], [-0.8, -1], [-0.6, -1], [-0.4, -1], [-0.2, -1], + [1, 0.95], [1, 0.9], [1, 0.8], [1, 0.6], [1, 0.4], [1, 0.2], + [0.95, 1], [0.9, 1], [0.8, 1], [0.6, 1], [0.4, 1], [0.2, 1], + [-0.5, -0.45], [-0.45, -0.5], [0.5, 0.45], [0.45, 0.5], + [-0.04, 0.04], [0.04, -0.04] + ], + T=np.linspace(0, 2, 20) +) +plt.plot([0], [0], 'k.') # 'MarkerSize', AM_data_markersize*3) +# set(gca,'FontSize', 16) +plt.xlabel('$x_1$') +plt.ylabel('$x_2$') +plt.title('Saddle point') # Stable isL -m = 1; b = 0; k = 1; # zero damping -mpl.figure(); mpl.clf(); -mpl.axis([-1, 1, -1, 1]); # set(gca, 'DataAspectRatio', [1 1 1]); -phase_plot(oscillator_ode, timepts = - [pi/6, pi/3, pi/2, 2*pi/3, 5*pi/6, pi, 7*pi/6, 4*pi/3, 9*pi/6, 5*pi/3, 11*pi/6, 2*pi], - X0 = [ [0.2,0], [0.4,0], [0.6,0], [0.8,0], [1,0], [1.2,0], [1.4,0] ], - T = np.linspace(0, 20, 200), parms = (m, b, k)); -mpl.plot([0], [0], 'k.') # 'MarkerSize', AM_data_markersize*3); -# set(gca,'FontSize', 16); -mpl.xlabel('$x_1$'); mpl.ylabel('$x_2$'); -mpl.title('Undamped system\nLyapunov stable, not asympt. stable') +m = 1 +b = 0 +k = 1 # zero damping +plt.figure() +plt.clf() +plt.axis([-1, 1, -1, 1]) # set(gca, 'DataAspectRatio', [1 1 1]); +phase_plot( + oscillator_ode, + timepts=[pi/6, pi/3, pi/2, 2*pi/3, 5*pi/6, pi, 7*pi/6, + 4*pi/3, 9*pi/6, 5*pi/3, 11*pi/6, 2*pi], + X0=[[0.2, 0], [0.4, 0], [0.6, 0], [0.8, 0], [1, 0], [1.2, 0], [1.4, 0]], + T=np.linspace(0, 20, 200), + parms=(m, b, k) +) +plt.plot([0], [0], 'k.') # 'MarkerSize', AM_data_markersize*3) +# plt.set(gca,'FontSize', 16) +plt.xlabel('$x_1$') +plt.ylabel('$x_2$') +plt.title('Undamped system\nLyapunov stable, not asympt. stable') if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: - mpl.show() + plt.show() diff --git a/examples/pvtol-lqr-nested.ipynb b/examples/pvtol-lqr-nested.ipynb index bb9a51b07..bd55f8abb 100644 --- a/examples/pvtol-lqr-nested.ipynb +++ b/examples/pvtol-lqr-nested.ipynb @@ -1 +1,564 @@ -{"nbformat_minor": 0, "cells": [{"source": "#`python-control` Example: Vertical takeoff and landing aircraft\n\nhttp://www.cds.caltech.edu/~murray/wiki/index.php/Python-control/Example:_Vertical_takeoff_and_landing_aircraft\n\nThis page demonstrates the use of the python-control package for analysis and design of a controller for a vectored thrust aircraft model that is used as a running example through the text *Feedback Systems* by Astrom and Murray. This example makes use of MATLAB compatible commands. ", "cell_type": "markdown", "metadata": {}}, {"source": "##System Description\nThis example uses a simplified model for a (planar) vertical takeoff and landing aircraft (PVTOL), as shown below:\n \n\nThe position and orientation of the center of mass of the aircraft is denoted by $(x,y,\\theta)$, $m$ is the mass of the vehicle, $J$ the moment of inertia, $g$ the gravitational constant and $c$ the damping coefficient. The forces generated by the main downward thruster and the maneuvering thrusters are modeled as a pair of forces $F_1$ and $F_2$ acting at a distance $r$ below the aircraft (determined by the geometry of the thrusters).\n\nIt is convenient to redefine the inputs so that the origin is an equilibrium point of the system with zero input. Letting $u_1 =\nF_1$ and $u_2 = F_2 - mg$, the equations can be written in state space form as:\n\n\n##LQR state feedback controller\nThis section demonstrates the design of an LQR state feedback controller for the vectored thrust aircraft example. This example is pulled from Chapter 6 (State Feedback) of [http:www.cds.caltech.edu/~murray/amwiki Astrom and Murray]. The python code listed here are contained the the file pvtol-lqr.py.\n\nTo execute this example, we first import the libraries for SciPy, MATLAB plotting and the python-control package:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 1, "cell_type": "code", "source": "from numpy import * # Grab all of the NumPy functions\nfrom matplotlib.pyplot import * # Grab MATLAB plotting functions\nfrom control.matlab import * # MATLAB-like functions\n%matplotlib inline", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "The parameters for the system are given by", "cell_type": "markdown", "metadata": {}}, {"execution_count": 2, "cell_type": "code", "source": "m = 4; # mass of aircraft\nJ = 0.0475; # inertia around pitch axis\nr = 0.25; # distance to center of force\ng = 9.8; # gravitational constant\nc = 0.05; # damping factor (estimated)\nprint \"m = %f\" % m\nprint \"J = %f\" % J\nprint \"r = %f\" % r\nprint \"g = %f\" % g\nprint \"c = %f\" % c", "outputs": [{"output_type": "stream", "name": "stdout", "text": "m = 4.000000\nJ = 0.047500\nr = 0.250000\ng = 9.800000\nc = 0.050000\n"}], "metadata": {"collapsed": false, "trusted": true}}, {"source": "The linearization of the dynamics near the equilibrium point $x_e = (0, 0, 0, 0, 0, 0)$, $u_e = (0, mg)$ are given by", "cell_type": "markdown", "metadata": {}}, {"execution_count": 3, "cell_type": "code", "source": "# State space dynamics\nxe = [0, 0, 0, 0, 0, 0]; # equilibrium point of interest\nue = [0, m*g]; # (note these are lists, not matrices)", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 4, "cell_type": "code", "source": "# Dynamics matrix (use matrix type so that * works for multiplication)\nA = matrix(\n [[ 0, 0, 0, 1, 0, 0],\n [ 0, 0, 0, 0, 1, 0],\n [ 0, 0, 0, 0, 0, 1],\n [ 0, 0, (-ue[0]*sin(xe[2]) - ue[1]*cos(xe[2]))/m, -c/m, 0, 0],\n [ 0, 0, (ue[0]*cos(xe[2]) - ue[1]*sin(xe[2]))/m, 0, -c/m, 0],\n [ 0, 0, 0, 0, 0, 0 ]])\n\n# Input matrix\nB = matrix(\n [[0, 0], [0, 0], [0, 0],\n [cos(xe[2])/m, -sin(xe[2])/m],\n [sin(xe[2])/m, cos(xe[2])/m],\n [r/J, 0]])\n\n# Output matrix \nC = matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]])\nD = matrix([[0, 0], [0, 0]])", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "To compute a linear quadratic regulator for the system, we write the cost function as\n\n\nwhere $z = z - z_e$ and $v = u - u_e$ represent the local coordinates around the desired equilibrium point $(z_e, u_e)$. We begin with diagonal matrices for the state and input costs:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 5, "cell_type": "code", "source": "Qx1 = diag([1, 1, 1, 1, 1, 1]);\nQu1a = diag([1, 1]);\n(K, X, E) = lqr(A, B, Qx1, Qu1a); K1a = matrix(K);", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "This gives a control law of the form $v = -K z$, which can then be used to derive the control law in terms of the original variables:\n\n\n $$u = v + u_d = - K(z - z_d) + u_d.$$\nwhere $u_d = (0, mg)$ and $z_d = (x_d, y_d, 0, 0, 0, 0)$\n\nSince the `python-control` package only supports SISO systems, in order to compute the closed loop dynamics, we must extract the dynamics for the lateral and altitude dynamics as individual systems. In addition, we simulate the closed loop dynamics using the step command with $K x_d$ as the input vector (assumes that the \"input\" is unit size, with $xd$ corresponding to the desired steady state. The following code performs these operations:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 6, "cell_type": "code", "source": "xd = matrix([[1], [0], [0], [0], [0], [0]]); \nyd = matrix([[0], [1], [0], [0], [0], [0]]); ", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 7, "cell_type": "code", "source": "# Indices for the parts of the state that we want\nlat = (0,2,3,5);\nalt = (1,4);\n\n# Decoupled dynamics\nAx = (A[lat, :])[:, lat]; #! not sure why I have to do it this way\nBx = B[lat, 0]; Cx = C[0, lat]; Dx = D[0, 0];\n \nAy = (A[alt, :])[:, alt]; #! not sure why I have to do it this way\nBy = B[alt, 1]; Cy = C[1, alt]; Dy = D[1, 1];\n\n# Step response for the first input\nH1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx);\n(Tx, Yx) = step(H1ax, T=linspace(0,10,100));\n\n# Step response for the second input\nH1ay = ss(Ay - By*K1a[1,alt], By*K1a[1,alt]*yd[alt,:], Cy, Dy);\n(Ty, Yy) = step(H1ay, T=linspace(0,10,100));", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 8, "cell_type": "code", "source": "plot(Yx.T, Tx, '-', Yy.T, Ty, '--'); hold(True);\nplot([0, 10], [1, 1], 'k-'); hold(True);\nylabel('Position');\nxlabel('Time (s)');\ntitle('Step Response for Inputs');\nlegend(('Yx', 'Yy'), loc='lower right');", "outputs": [{"output_type": "display_data", "data": {"image/png": "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\n6v8DfENdKxw8Sq0AAAAASUVORK5CYII=\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}, {"source": "The plot above shows the $x$ and $y$ positions of the aircraft when it is commanded to move 1 m in each direction. The following shows the $x$ motion for control weights $\\rho = 1, 10^2, 10^4$. A higher weight of the input term in the cost function causes a more sluggish response. It is created using the code:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 9, "cell_type": "code", "source": "# Look at different input weightings\nQu1a = diag([1, 1]); (K1a, X, E) = lqr(A, B, Qx1, Qu1a);\nH1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx);\n\nQu1b = (40**2)*diag([1, 1]); (K1b, X, E) = lqr(A, B, Qx1, Qu1b);\nH1bx = ss(Ax - Bx*K1b[0,lat], Bx*K1b[0,lat]*xd[lat,:],Cx, Dx);\n\nQu1c = (200**2)*diag([1, 1]); (K1c, X, E) = lqr(A, B, Qx1, Qu1c);\nH1cx = ss(Ax - Bx*K1c[0,lat], Bx*K1c[0,lat]*xd[lat,:],Cx, Dx);\n\n[T1, Y1] = step(H1ax, T=linspace(0,10,100));\n[T2, Y2] = step(H1bx, T=linspace(0,10,100));\n[T3, Y3] = step(H1cx, T=linspace(0,10,100));", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 10, "cell_type": "code", "source": "plot(Y1.T, T1, 'b-'); hold(True);\nplot(Y2.T, T2, 'r-'); hold(True);\nplot(Y3.T, T3, 'g-'); hold(True);\nplot([0 ,10], [1, 1], 'k-'); hold(True);\ntitle('Step Response for Inputs');\nylabel('Position');\nxlabel('Time (s)');\nlegend(('Y1','Y2','Y3'),loc='lower right');\naxis([0, 10, -0.1, 1.4]); ", "outputs": [{"output_type": "display_data", "data": {"image/png": "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\ni76yKeGdswAAAABJRU5ErkJggg==\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}, {"source": "##Lateral control using inner/outer loop design\nThis section demonstrates the design of loop shaping controller for the vectored thrust aircraft example. This example is pulled from Chapter 11 [Frequency Domain Design](http:www.cds.caltech.edu/~murray/amwiki) of Astrom and Murray. \n\nTo design a controller for the lateral dynamics of the vectored thrust aircraft, we make use of a \"inner/outer\" loop design methodology. We begin by representing the dynamics using the block diagram\n\n\nwhere\n \nThe controller is constructed by splitting the process dynamics and controller into two components: an inner loop consisting of the roll dynamics $P_i$ and control $C_i$ and an outer loop consisting of the lateral position dynamics $P_o$ and controller $C_o$.\n\nThe closed inner loop dynamics $H_i$ control the roll angle of the aircraft using the vectored thrust while the outer loop controller $C_o$ commands the roll angle to regulate the lateral position.\n\nThe following code imports the libraries that are required and defines the dynamics:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 11, "cell_type": "code", "source": "from matplotlib.pyplot import * # Grab MATLAB plotting functions\nfrom control.matlab import * # MATLAB-like functions", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 12, "cell_type": "code", "source": "# System parameters\nm = 4; # mass of aircraft\nJ = 0.0475; # inertia around pitch axis\nr = 0.25; # distance to center of force\ng = 9.8; # gravitational constant\nc = 0.05; # damping factor (estimated)\nprint \"m = %f\" % m\nprint \"J = %f\" % J\nprint \"r = %f\" % r\nprint \"g = %f\" % g\nprint \"c = %f\" % c", "outputs": [{"output_type": "stream", "name": "stdout", "text": "m = 4.000000\nJ = 0.047500\nr = 0.250000\ng = 9.800000\nc = 0.050000\n"}], "metadata": {"collapsed": false, "trusted": true}}, {"execution_count": 13, "cell_type": "code", "source": "# Transfer functions for dynamics\nPi = tf([r], [J, 0, 0]); # inner loop (roll)\nPo = tf([1], [m, c, 0]); # outer loop (position)", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "For the inner loop, use a lead compensator", "cell_type": "markdown", "metadata": {}}, {"execution_count": 14, "cell_type": "code", "source": "k = 200; a = 2; b = 50\nCi = k*tf([1, a], [1, b]) # lead compensator\nLi = Pi*Ci", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "The closed loop dynamics of the inner loop, $H_i$, are given by", "cell_type": "markdown", "metadata": {}}, {"execution_count": 15, "cell_type": "code", "source": "Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1));", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "Finally, we design the lateral compensator using another lead compenstor", "cell_type": "markdown", "metadata": {}}, {"execution_count": 16, "cell_type": "code", "source": "# Now design the lateral control system\na = 0.02; b = 5; K = 2;\nCo = -K*tf([1, 0.3], [1, 10]); # another lead compensator\nLo = -m*g*Po*Co;", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"source": "The performance of the system can be characterized using the sensitivity function and the complementary sensitivity function:", "cell_type": "markdown", "metadata": {}}, {"execution_count": 17, "cell_type": "code", "source": "L = Co*Hi*Po;\nS = feedback(1, L);\nT = feedback(L, 1);", "outputs": [], "metadata": {"collapsed": true, "trusted": true}}, {"execution_count": 18, "cell_type": "code", "source": "t, y = step(T,T=linspace(0,10,100))\nplot(y, t)\ntitle(\"Step Response\")\ngrid()\nxlabel(\"time (s)\")\nylabel(\"y(t)\")", "outputs": [{"execution_count": 18, "output_type": "execute_result", "data": {"text/plain": ""}, "metadata": {}}, {"output_type": "display_data", "data": {"image/png": "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\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}, {"source": "The frequency response and Nyquist plot for the loop transfer function are computed using the commands", "cell_type": "markdown", "metadata": {}}, {"execution_count": 19, "cell_type": "code", "source": "bode(L);", "outputs": [{"output_type": "display_data", "data": {"image/png": "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\nGZxzH3gVd6cS60xK0lf0tb1b+prkH3p91ekVdHIdxq3S1A1h6kzb30npS24MG78Tjh8DvB8u3A/m\nezOc+ReJC2HfZ2HazfbdF7Sir0l4VKwzyc6nqEPuVNp9M7lN0i8lrS9poqQjgNslzQ9tv5J2atHj\nCNOd3L7MbKNs0zIVX27u8tVlGoWr0mbWOW+HXOXL0Nfkmnnpir5a+UeTvgZaZ9bL0yZVtj36Epxw\nV/YmsyN8/Iuw17bwqW8Dr8C6n4Ffny5xlcS+sP27Yd6bG2nJEa55v/Xw+2uUp/C9x+t/jwfmtKkp\n7b6ZTAZ2A/bKwtcA/4/UkHyszToLr3wPgmA0cOsz2cyyi6Qtr4S3zw8PjQEmwXZfhe3fIHEucBEs\n/zT89d/l2jtKcMF50+0e/KdPrnmAe7L4+Uit5Ypt1OsO2DaxSJ56adXxjcJ5zrulrSx9RbV1U1+e\nuGHWV09rJ3+bndO3ybbgXcHngZ9Of390IHixdr+7Xuor8dniItraejORtDzwQ2AlYIEs2raXy1m+\nKyvfK+o/jvDNVTOct3xZ+gZ1TGHY9VWnV9CPvrneZnMI8Cvp3R+HHd4PK6wPzJbO/Cvc8wd479UN\n6hsVYyYV9ZXqm+sa4ADgIOCTpG6vsc4GzbqFpC1I61MWITU2l9bIY8fU4CAIqpBYCNiU5Jl8U5Lf\nv1OAM22eKtO2fqDos7PdxuRG22tJmuW560RutF1rMWPHkbQo8DPbu9RIi8YkCIKGSCwIfIK0lu1j\nwBWkhuU8m+fKtK0sij472x2Af0FpkeHdWdfUw8BCeQur4Ap4YH/S1r716j+Ogt1ctg9plH8krpXy\n9crWClfFVda3F23sHFkRzlW+DH3V+ftJX638o0lfA82v2TNI+rLwaZL+DiscDTt8GKbsApcfI11w\nJ/x8f+Ay0O4l6Gv0/KlZvo6+euHq5wmU6OhxHdLWvUsDxwFnAO9vofx6wJq8fgB+LHA3aQB+XrIB\neGB74GDg7YCAHwMbNKjb7WiqqmNikTz10qrjG4XznHdLW1n6imrrpr48ccOsr57WTv42e6mvvh6/\nFQ7+Bfg68KNw3Ong94HVK30lPltcRFtb3VydQP+5j8kHSL5hJmXhKZm6AyvK7AF8keRqZaazbYSr\n6rWjmysIgoJITAA+T9rt9VXgJOAkm3tKNaxL9LSbS9K5gElvCNXY9ifbNYR8K+APBQ7NYedxFFsB\n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\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}, {"execution_count": 20, "cell_type": "code", "source": "nyquist(L, (0.0001, 1000));", "outputs": [{"output_type": "display_data", "data": {"image/png": "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\nLKv/D2ULb19b6nZgAAAAAElFTkSuQmCC\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}, {"execution_count": 21, "cell_type": "code", "source": "gangof4(Hi*Po, Co);", "outputs": [{"output_type": "display_data", "data": {"image/png": "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\nAAAASUVORK5CYII=\n", "text/plain": ""}, "metadata": {}}], "metadata": {"collapsed": false, "trusted": true}}], "nbformat": 4, "metadata": {"kernelspec": {"display_name": "Python 2", "name": "python2", "language": "python"}, "language_info": {"mimetype": "text/x-python", "nbconvert_exporter": "python", "version": "2.7.6", "name": "python", "file_extension": ".py", "pygments_lexer": "ipython2", "codemirror_mode": {"version": 2, "name": "ipython"}}}} \ No newline at end of file +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vertical takeoff and landing aircraft\n", + "\n", + "This notebook demonstrates the use of the python-control package for analysis and design of a controller for a vectored thrust aircraft model that is used as a running example through the text *Feedback Systems* by Astrom and Murray. This example makes use of MATLAB compatible commands. \n", + "\n", + "Additional information on this system is available at\n", + "\n", + "http://www.cds.caltech.edu/~murray/wiki/index.php/Python-control/Example:_Vertical_takeoff_and_landing_aircraft" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## System Description\n", + "This example uses a simplified model for a (planar) vertical takeoff and landing aircraft (PVTOL), as shown below:\n", + "\n", + "![PVTOL diagram](http://www.cds.caltech.edu/~murray/wiki/images/7/7d/Pvtol-diagram.png)\n", + "\n", + "![PVTOL dynamics](http://www.cds.caltech.edu/~murray/wiki/images/b/b7/Pvtol-dynamics.png)\n", + "\n", + "The position and orientation of the center of mass of the aircraft is denoted by $(x,y,\\theta)$, $m$ is the mass of the vehicle, $J$ the moment of inertia, $g$ the gravitational constant and $c$ the damping coefficient. The forces generated by the main downward thruster and the maneuvering thrusters are modeled as a pair of forces $F_1$ and $F_2$ acting at a distance $r$ below the aircraft (determined by the geometry of the thrusters).\n", + "\n", + "It is convenient to redefine the inputs so that the origin is an equilibrium point of the system with zero input. Letting $u_1 =\n", + "F_1$ and $u_2 = F_2 - mg$, the equations can be written in state space form as:\n", + "![PVTOL state space dynamics](http://www.cds.caltech.edu/~murray/wiki/images/2/21/Pvtol-statespace.png)\n", + "\n", + "## LQR state feedback controller\n", + "This section demonstrates the design of an LQR state feedback controller for the vectored thrust aircraft example. This example is pulled from Chapter 6 (State Feedback) of [Astrom and Murray](https://fbsbook.org). The python code listed here are contained the the file pvtol-lqr.py.\n", + "\n", + "To execute this example, we first import the libraries for SciPy, MATLAB plotting and the python-control package:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from numpy import * # Grab all of the NumPy functions\n", + "from matplotlib.pyplot import * # Grab MATLAB plotting functions\n", + "from control.matlab import * # MATLAB-like functions\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The parameters for the system are given by" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "m = 4.000000\n", + "J = 0.047500\n", + "r = 0.250000\n", + "g = 9.800000\n", + "c = 0.050000\n" + ] + } + ], + "source": [ + "m = 4 # mass of aircraft\n", + "J = 0.0475 # inertia around pitch axis\n", + "r = 0.25 # distance to center of force\n", + "g = 9.8 # gravitational constant\n", + "c = 0.05 # damping factor (estimated)\n", + "print(\"m = %f\" % m)\n", + "print(\"J = %f\" % J)\n", + "print(\"r = %f\" % r)\n", + "print(\"g = %f\" % g)\n", + "print(\"c = %f\" % c)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The linearization of the dynamics near the equilibrium point $x_e = (0, 0, 0, 0, 0, 0)$, $u_e = (0, mg)$ are given by" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# State space dynamics\n", + "xe = [0, 0, 0, 0, 0, 0] # equilibrium point of interest\n", + "ue = [0, m*g] # (note these are lists, not matrices)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Dynamics matrix (use matrix type so that * works for multiplication)\n", + "A = matrix(\n", + " [[ 0, 0, 0, 1, 0, 0],\n", + " [ 0, 0, 0, 0, 1, 0],\n", + " [ 0, 0, 0, 0, 0, 1],\n", + " [ 0, 0, (-ue[0]*sin(xe[2]) - ue[1]*cos(xe[2]))/m, -c/m, 0, 0],\n", + " [ 0, 0, (ue[0]*cos(xe[2]) - ue[1]*sin(xe[2]))/m, 0, -c/m, 0],\n", + " [ 0, 0, 0, 0, 0, 0 ]])\n", + "\n", + "# Input matrix\n", + "B = matrix(\n", + " [[0, 0], [0, 0], [0, 0],\n", + " [cos(xe[2])/m, -sin(xe[2])/m],\n", + " [sin(xe[2])/m, cos(xe[2])/m],\n", + " [r/J, 0]])\n", + "\n", + "# Output matrix \n", + "C = matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]])\n", + "D = matrix([[0, 0], [0, 0]])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To compute a linear quadratic regulator for the system, we write the cost function as\n", + "\n", + "\n", + "where $z = z - z_e$ and $v = u - u_e$ represent the local coordinates around the desired equilibrium point $(z_e, u_e)$. We begin with diagonal matrices for the state and input costs:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "Qx1 = diag([1, 1, 1, 1, 1, 1])\n", + "Qu1a = diag([1, 1])\n", + "(K, X, E) = lqr(A, B, Qx1, Qu1a); K1a = matrix(K)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This gives a control law of the form $v = -K z$, which can then be used to derive the control law in terms of the original variables:\n", + "\n", + "\n", + " $$u = v + u_d = - K(z - z_d) + u_d.$$\n", + "where $u_d = (0, mg)$ and $z_d = (x_d, y_d, 0, 0, 0, 0)$\n", + "\n", + "Since the `python-control` package only supports SISO systems, in order to compute the closed loop dynamics, we must extract the dynamics for the lateral and altitude dynamics as individual systems. In addition, we simulate the closed loop dynamics using the step command with $K x_d$ as the input vector (assumes that the \"input\" is unit size, with $xd$ corresponding to the desired steady state. The following code performs these operations:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "xd = matrix([[1], [0], [0], [0], [0], [0]]) \n", + "yd = matrix([[0], [1], [0], [0], [0], [0]]) " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Indices for the parts of the state that we want\n", + "lat = (0,2,3,5)\n", + "alt = (1,4)\n", + "\n", + "# Decoupled dynamics\n", + "Ax = (A[lat, :])[:, lat] #! not sure why I have to do it this way\n", + "Bx, Cx, Dx = B[lat, 0], C[0, lat], D[0, 0]\n", + " \n", + "Ay = (A[alt, :])[:, alt] #! not sure why I have to do it this way\n", + "By, Cy, Dy = B[alt, 1], C[1, alt], D[1, 1]\n", + "\n", + "# Step response for the first input\n", + "H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx)\n", + "(Tx, Yx) = step(H1ax, T=linspace(0,10,100))\n", + "\n", + "# Step response for the second input\n", + "H1ay = ss(Ay - By*K1a[1,alt], By*K1a[1,alt]*yd[alt,:], Cy, Dy)\n", + "(Ty, Yy) = step(H1ay, T=linspace(0,10,100))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plot(Yx.T, Tx, '-', Yy.T, Ty, '--')\n", + "plot([0, 10], [1, 1], 'k-')\n", + "ylabel('Position')\n", + "xlabel('Time (s)')\n", + "title('Step Response for Inputs')\n", + "legend(('Yx', 'Yy'), loc='lower right')\n", + "show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The plot above shows the $x$ and $y$ positions of the aircraft when it is commanded to move 1 m in each direction. The following shows the $x$ motion for control weights $\\rho = 1, 10^2, 10^4$. A higher weight of the input term in the cost function causes a more sluggish response. It is created using the code:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Look at different input weightings\n", + "Qu1a = diag([1, 1])\n", + "K1a, X, E = lqr(A, B, Qx1, Qu1a)\n", + "H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx)\n", + "\n", + "Qu1b = (40**2)*diag([1, 1])\n", + "K1b, X, E = lqr(A, B, Qx1, Qu1b)\n", + "H1bx = ss(Ax - Bx*K1b[0,lat], Bx*K1b[0,lat]*xd[lat,:],Cx, Dx)\n", + "\n", + "Qu1c = (200**2)*diag([1, 1])\n", + "K1c, X, E = lqr(A, B, Qx1, Qu1c)\n", + "H1cx = ss(Ax - Bx*K1c[0,lat], Bx*K1c[0,lat]*xd[lat,:],Cx, Dx)\n", + "\n", + "[T1, Y1] = step(H1ax, T=linspace(0,10,100))\n", + "[T2, Y2] = step(H1bx, T=linspace(0,10,100))\n", + "[T3, Y3] = step(H1cx, T=linspace(0,10,100))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plot(Y1.T, T1, 'b-')\n", + "plot(Y2.T, T2, 'r-')\n", + "plot(Y3.T, T3, 'g-')\n", + "plot([0 ,10], [1, 1], 'k-')\n", + "title('Step Response for Inputs')\n", + "ylabel('Position')\n", + "xlabel('Time (s)')\n", + "legend(('Y1','Y2','Y3'),loc='lower right')\n", + "axis([0, 10, -0.1, 1.4])\n", + "show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lateral control using inner/outer loop design\n", + "This section demonstrates the design of loop shaping controller for the vectored thrust aircraft example. This example is pulled from Chapter 11 (Frequency Domain Design) of [Astrom and Murray](https://fbsbook.org). \n", + "\n", + "To design a controller for the lateral dynamics of the vectored thrust aircraft, we make use of a \"inner/outer\" loop design methodology. We begin by representing the dynamics using the block diagram\n", + "\n", + "\n", + "where\n", + " \n", + "The controller is constructed by splitting the process dynamics and controller into two components: an inner loop consisting of the roll dynamics $P_i$ and control $C_i$ and an outer loop consisting of the lateral position dynamics $P_o$ and controller $C_o$.\n", + "\n", + "The closed inner loop dynamics $H_i$ control the roll angle of the aircraft using the vectored thrust while the outer loop controller $C_o$ commands the roll angle to regulate the lateral position.\n", + "\n", + "The following code imports the libraries that are required and defines the dynamics:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib.pyplot import * # Grab MATLAB plotting functions\n", + "from control.matlab import * # MATLAB-like functions" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "m = 4.000000\n", + "J = 0.047500\n", + "r = 0.250000\n", + "g = 9.800000\n", + "c = 0.050000\n" + ] + } + ], + "source": [ + "# System parameters\n", + "m = 4 # mass of aircraft\n", + "J = 0.0475 # inertia around pitch axis\n", + "r = 0.25 # distance to center of force\n", + "g = 9.8 # gravitational constant\n", + "c = 0.05 # damping factor (estimated)\n", + "print(\"m = %f\" % m)\n", + "print(\"J = %f\" % J)\n", + "print(\"r = %f\" % r)\n", + "print(\"g = %f\" % g)\n", + "print(\"c = %f\" % c)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Transfer functions for dynamics\n", + "Pi = tf([r], [J, 0, 0]) # inner loop (roll)\n", + "Po = tf([1], [m, c, 0]) # outer loop (position)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the inner loop, use a lead compensator" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "k = 200\n", + "a = 2\n", + "b = 50\n", + "Ci = k*tf([1, a], [1, b]) # lead compensator\n", + "Li = Pi*Ci" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The closed loop dynamics of the inner loop, $H_i$, are given by" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we design the lateral compensator using another lead compenstor" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# Now design the lateral control system\n", + "a = 0.02\n", + "b = 5\n", + "K = 2\n", + "Co = -K*tf([1, 0.3], [1, 10]) # another lead compensator\n", + "Lo = -m*g*Po*Co" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The performance of the system can be characterized using the sensitivity function and the complementary sensitivity function:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "L = Co*Hi*Po\n", + "S = feedback(1, L)\n", + "T = feedback(L, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "t, y = step(T, T=linspace(0,10,100))\n", + "plot(y, t)\n", + "title(\"Step Response\")\n", + "grid()\n", + "xlabel(\"time (s)\")\n", + "ylabel(\"y(t)\")\n", + "show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The frequency response and Nyquist plot for the loop transfer function are computed using the commands" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "bode(L)\n", + "show()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "nyquist(L, (0.0001, 1000))\n", + "show()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "gangof4(Hi*Po, Co)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.6" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/pvtol-lqr.py b/examples/pvtol-lqr.py index 8412dc2ff..611931a9a 100644 --- a/examples/pvtol-lqr.py +++ b/examples/pvtol-lqr.py @@ -8,10 +8,9 @@ # import os - -from numpy import * # Grab all of the NumPy functions -from matplotlib.pyplot import * # Grab MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import numpy as np +import matplotlib.pyplot as plt # MATLAB plotting functions +from control.matlab import * # MATLAB-like functions # # System dynamics @@ -21,35 +20,41 @@ # # System parameters -m = 4; # mass of aircraft -J = 0.0475; # inertia around pitch axis -r = 0.25; # distance to center of force -g = 9.8; # gravitational constant -c = 0.05; # damping factor (estimated) +m = 4 # mass of aircraft +J = 0.0475 # inertia around pitch axis +r = 0.25 # distance to center of force +g = 9.8 # gravitational constant +c = 0.05 # damping factor (estimated) # State space dynamics -xe = [0, 0, 0, 0, 0, 0]; # equilibrium point of interest -ue = [0, m*g]; # (note these are lists, not matrices) +xe = [0, 0, 0, 0, 0, 0] # equilibrium point of interest +ue = [0, m*g] # (note these are lists, not matrices) + +# TODO: The following objects need converting from np.matrix to np.array +# This will involve re-working the subsequent equations as the shapes +# See below. # Dynamics matrix (use matrix type so that * works for multiplication) -A = matrix( - [[ 0, 0, 0, 1, 0, 0], - [ 0, 0, 0, 0, 1, 0], - [ 0, 0, 0, 0, 0, 1], - [ 0, 0, (-ue[0]*sin(xe[2]) - ue[1]*cos(xe[2]))/m, -c/m, 0, 0], - [ 0, 0, (ue[0]*cos(xe[2]) - ue[1]*sin(xe[2]))/m, 0, -c/m, 0], - [ 0, 0, 0, 0, 0, 0 ]]) +A = np.matrix( + [[0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, (-ue[0]*np.sin(xe[2]) - ue[1]*np.cos(xe[2]))/m, -c/m, 0, 0], + [0, 0, (ue[0]*np.cos(xe[2]) - ue[1]*np.sin(xe[2]))/m, 0, -c/m, 0], + [0, 0, 0, 0, 0, 0]] +) # Input matrix -B = matrix( +B = np.matrix( [[0, 0], [0, 0], [0, 0], - [cos(xe[2])/m, -sin(xe[2])/m], - [sin(xe[2])/m, cos(xe[2])/m], - [r/J, 0]]) + [np.cos(xe[2])/m, -np.sin(xe[2])/m], + [np.sin(xe[2])/m, np.cos(xe[2])/m], + [r/J, 0]] +) # Output matrix -C = matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]]) -D = matrix([[0, 0], [0, 0]]) +C = np.matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]]) +D = np.matrix([[0, 0], [0, 0]]) # # Construct inputs and outputs corresponding to steps in xy position @@ -61,16 +66,16 @@ # The way these vectors are used is to compute the closed loop system # dynamics as # -# xdot = Ax + B u => xdot = (A-BK)x + K xd -# u = -K(x - xd) y = Cx +# xdot = Ax + B u => xdot = (A-BK)x + K xd +# u = -K(x - xd) y = Cx # # The closed loop dynamics can be simulated using the "step" command, # with K*xd as the input vector (assumes that the "input" is unit size, # so that xd corresponds to the desired steady state. # -xd = matrix([[1], [0], [0], [0], [0], [0]]); -yd = matrix([[0], [1], [0], [0], [0], [0]]); +xd = np.matrix([[1], [0], [0], [0], [0], [0]]) +yd = np.matrix([[0], [1], [0], [0], [0], [0]]) # # Extract the relevant dynamics for use with SISO library @@ -83,91 +88,127 @@ # # Indices for the parts of the state that we want -lat = (0,2,3,5); -alt = (1,4); +lat = (0, 2, 3, 5) +alt = (1, 4) # Decoupled dynamics -Ax = (A[lat, :])[:, lat]; #! not sure why I have to do it this way -Bx = B[lat, 0]; Cx = C[0, lat]; Dx = D[0, 0]; +Ax = (A[lat, :])[:, lat] # ! not sure why I have to do it this way +Bx = B[lat, 0] +Cx = C[0, lat] +Dx = D[0, 0] -Ay = (A[alt, :])[:, alt]; #! not sure why I have to do it this way -By = B[alt, 1]; Cy = C[1, alt]; Dy = D[1, 1]; +Ay = (A[alt, :])[:, alt] # ! not sure why I have to do it this way +By = B[alt, 1] +Cy = C[1, alt] +Dy = D[1, 1] # Label the plot -clf(); -suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)") +plt.clf() +plt.suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)") # # LQR design # # Start with a diagonal weighting -Qx1 = diag([1, 1, 1, 1, 1, 1]); -Qu1a = diag([1, 1]); -(K, X, E) = lqr(A, B, Qx1, Qu1a); K1a = matrix(K); +Qx1 = np.diag([1, 1, 1, 1, 1, 1]) +Qu1a = np.diag([1, 1]) +K, X, E = lqr(A, B, Qx1, Qu1a) +K1a = np.matrix(K) # Close the loop: xdot = Ax - B K (x-xd) # Note: python-control requires we do this 1 input at a time # H1a = ss(A-B*K1a, B*K1a*concatenate((xd, yd), axis=1), C, D); -# (T, Y) = step(H1a, T=linspace(0,10,100)); +# (T, Y) = step(H1a, T=np.linspace(0,10,100)); + +# TODO: The following equations will need modifying when converting from np.matrix to np.array +# because the results and even intermediate calculations will be different with numpy arrays +# For example: +# Bx = B[lat, 0] +# Will need to be changed to: +# Bx = B[lat, 0].reshape(-1, 1) +# (if we want it to have the same shape as before) + +# For reference, here is a list of the correct shapes of these objects: +# A: (6, 6) +# B: (6, 2) +# C: (2, 6) +# D: (2, 2) +# xd: (6, 1) +# yd: (6, 1) +# Ax: (4, 4) +# Bx: (4, 1) +# Cx: (1, 4) +# Dx: () +# Ay: (2, 2) +# By: (2, 1) +# Cy: (1, 2) # Step response for the first input -H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx); -(Yx, Tx) = step(H1ax, T=linspace(0,10,100)); +H1ax = ss(Ax - Bx*K1a[0, lat], Bx*K1a[0, lat]*xd[lat, :], Cx, Dx) +Yx, Tx = step(H1ax, T=np.linspace(0, 10, 100)) # Step response for the second input -H1ay = ss(Ay - By*K1a[1,alt], By*K1a[1,alt]*yd[alt,:], Cy, Dy); -(Yy, Ty) = step(H1ay, T=linspace(0,10,100)); +H1ay = ss(Ay - By*K1a[1, alt], By*K1a[1, alt]*yd[alt, :], Cy, Dy) +Yy, Ty = step(H1ay, T=np.linspace(0, 10, 100)) -subplot(221); title("Identity weights") -# plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--'); -plot(Tx.T, Yx.T, '-', Ty.T, Yy.T, '--'); -plot([0, 10], [1, 1], 'k-'); +plt.subplot(221) +plt.title("Identity weights") +# plt.plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--') +plt.plot(Tx.T, Yx.T, '-', Ty.T, Yy.T, '--') +plt.plot([0, 10], [1, 1], 'k-') -axis([0, 10, -0.1, 1.4]); -ylabel('position'); -legend(('x', 'y'), loc='lower right'); +plt.axis([0, 10, -0.1, 1.4]) +plt.ylabel('position') +plt.legend(('x', 'y'), loc='lower right') # Look at different input weightings -Qu1a = diag([1, 1]); (K1a, X, E) = lqr(A, B, Qx1, Qu1a); -H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx); +Qu1a = np.diag([1, 1]) +K1a, X, E = lqr(A, B, Qx1, Qu1a) +H1ax = ss(Ax - Bx*K1a[0, lat], Bx*K1a[0, lat]*xd[lat, :], Cx, Dx) -Qu1b = (40**2)*diag([1, 1]); (K1b, X, E) = lqr(A, B, Qx1, Qu1b); -H1bx = ss(Ax - Bx*K1b[0,lat], Bx*K1b[0,lat]*xd[lat,:],Cx, Dx); +Qu1b = (40 ** 2)*np.diag([1, 1]) +K1b, X, E = lqr(A, B, Qx1, Qu1b) +H1bx = ss(Ax - Bx*K1b[0, lat], Bx*K1b[0, lat]*xd[lat, :], Cx, Dx) -Qu1c = (200**2)*diag([1, 1]); (K1c, X, E) = lqr(A, B, Qx1, Qu1c); -H1cx = ss(Ax - Bx*K1c[0,lat], Bx*K1c[0,lat]*xd[lat,:],Cx, Dx); +Qu1c = (200 ** 2)*np.diag([1, 1]) +K1c, X, E = lqr(A, B, Qx1, Qu1c) +H1cx = ss(Ax - Bx*K1c[0, lat], Bx*K1c[0, lat]*xd[lat, :], Cx, Dx) -[Y1, T1] = step(H1ax, T=linspace(0,10,100)); -[Y2, T2] = step(H1bx, T=linspace(0,10,100)); -[Y3, T3] = step(H1cx, T=linspace(0,10,100)); +[Y1, T1] = step(H1ax, T=np.linspace(0, 10, 100)) +[Y2, T2] = step(H1bx, T=np.linspace(0, 10, 100)) +[Y3, T3] = step(H1cx, T=np.linspace(0, 10, 100)) -subplot(222); title("Effect of input weights") -plot(T1.T, Y1.T, 'b-'); -plot(T2.T, Y2.T, 'b-'); -plot(T3.T, Y3.T, 'b-'); -plot([0 ,10], [1, 1], 'k-'); +plt.subplot(222) +plt.title("Effect of input weights") +plt.plot(T1.T, Y1.T, 'b-') +plt.plot(T2.T, Y2.T, 'b-') +plt.plot(T3.T, Y3.T, 'b-') +plt.plot([0, 10], [1, 1], 'k-') -axis([0, 10, -0.1, 1.4]); +plt.axis([0, 10, -0.1, 1.4]) -# arcarrow([1.3, 0.8], [5, 0.45], -6); -text(5.3, 0.4, 'rho'); +# arcarrow([1.3, 0.8], [5, 0.45], -6) +plt.text(5.3, 0.4, 'rho') # Output weighting - change Qx to use outputs -Qx2 = C.T * C; -Qu2 = 0.1 * diag([1, 1]); -(K, X, E) = lqr(A, B, Qx2, Qu2); K2 = matrix(K) +Qx2 = C.T*C +Qu2 = 0.1*np.diag([1, 1]) +K, X, E = lqr(A, B, Qx2, Qu2) +K2 = np.matrix(K) -H2x = ss(Ax - Bx*K2[0,lat], Bx*K2[0,lat]*xd[lat,:], Cx, Dx); -H2y = ss(Ay - By*K2[1,alt], By*K2[1,alt]*yd[alt,:], Cy, Dy); +H2x = ss(Ax - Bx*K2[0, lat], Bx*K2[0, lat]*xd[lat, :], Cx, Dx) +H2y = ss(Ay - By*K2[1, alt], By*K2[1, alt]*yd[alt, :], Cy, Dy) -subplot(223); title("Output weighting") -[Y2x, T2x] = step(H2x, T=linspace(0,10,100)); -[Y2y, T2y] = step(H2y, T=linspace(0,10,100)); -plot(T2x.T, Y2x.T, T2y.T, Y2y.T) -ylabel('position'); -xlabel('time'); ylabel('position'); -legend(('x', 'y'), loc='lower right'); +plt.subplot(223) +plt.title("Output weighting") +[Y2x, T2x] = step(H2x, T=np.linspace(0, 10, 100)) +[Y2y, T2y] = step(H2y, T=np.linspace(0, 10, 100)) +plt.plot(T2x.T, Y2x.T, T2y.T, Y2y.T) +plt.ylabel('position') +plt.xlabel('time') +plt.ylabel('position') +plt.legend(('x', 'y'), loc='lower right') # # Physically motivated weighting @@ -177,21 +218,21 @@ # due to loss in efficiency. # -Qx3 = diag([100, 10, 2*pi/5, 0, 0, 0]); -Qu3 = 0.1 * diag([1, 10]); -(K, X, E) = lqr(A, B, Qx3, Qu3); K3 = matrix(K); +Qx3 = np.diag([100, 10, 2*np.pi/5, 0, 0, 0]) +Qu3 = 0.1*np.diag([1, 10]) +(K, X, E) = lqr(A, B, Qx3, Qu3) +K3 = np.matrix(K) -H3x = ss(Ax - Bx*K3[0,lat], Bx*K3[0,lat]*xd[lat,:], Cx, Dx); -H3y = ss(Ay - By*K3[1,alt], By*K3[1,alt]*yd[alt,:], Cy, Dy); -subplot(224) -# step(H3x, H3y, 10); -[Y3x, T3x] = step(H3x, T=linspace(0,10,100)); -[Y3y, T3y] = step(H3y, T=linspace(0,10,100)); -plot(T3x.T, Y3x.T, T3y.T, Y3y.T) -title("Physically motivated weights") -xlabel('time'); -legend(('x', 'y'), loc='lower right'); +H3x = ss(Ax - Bx*K3[0, lat], Bx*K3[0, lat]*xd[lat, :], Cx, Dx) +H3y = ss(Ay - By*K3[1, alt], By*K3[1, alt]*yd[alt, :], Cy, Dy) +plt.subplot(224) +# step(H3x, H3y, 10) +[Y3x, T3x] = step(H3x, T=np.linspace(0, 10, 100)) +[Y3y, T3y] = step(H3y, T=np.linspace(0, 10, 100)) +plt.plot(T3x.T, Y3x.T, T3y.T, Y3y.T) +plt.title("Physically motivated weights") +plt.xlabel('time') +plt.legend(('x', 'y'), loc='lower right') if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: - show() - + plt.show() diff --git a/examples/pvtol-nested-ss.py b/examples/pvtol-nested-ss.py index 24e173bc8..1af49e425 100644 --- a/examples/pvtol-nested-ss.py +++ b/examples/pvtol-nested-ss.py @@ -8,24 +8,25 @@ # package. # -from matplotlib.pyplot import * # Grab MATLAB plotting functions +import os +import matplotlib.pyplot as plt # MATLAB plotting functions from control.matlab import * # MATLAB-like functions import numpy as np # System parameters -m = 4; # mass of aircraft -J = 0.0475; # inertia around pitch axis -r = 0.25; # distance to center of force -g = 9.8; # gravitational constant -c = 0.05; # damping factor (estimated) +m = 4 # mass of aircraft +J = 0.0475 # inertia around pitch axis +r = 0.25 # distance to center of force +g = 9.8 # gravitational constant +c = 0.05 # damping factor (estimated) # Transfer functions for dynamics -Pi = tf([r], [J, 0, 0]); # inner loop (roll) -Po = tf([1], [m, c, 0]); # outer loop (position) +Pi = tf([r], [J, 0, 0]) # inner loop (roll) +Po = tf([1], [m, c, 0]) # outer loop (position) # Use state space versions -Pi = tf2ss(Pi); -Po = tf2ss(Po); +Pi = tf2ss(Pi) +Po = tf2ss(Po) # # Inner loop control design @@ -36,102 +37,114 @@ # # Design a simple lead controller for the system -k = 200; a = 2; b = 50; -Ci = k*tf([1, a], [1, b]); # lead compensator +k, a, b = 200, 2, 50 +Ci = k*tf([1, a], [1, b]) # lead compensator # Convert to statespace -Ci = tf2ss(Ci); +Ci = tf2ss(Ci) # Compute the loop transfer function for the inner loop -Li = Pi*Ci; +Li = Pi*Ci # Bode plot for the open loop process -figure(1); -bode(Pi); +plt.figure(1) +bode(Pi) # Bode plot for the loop transfer function, with margins -figure(2); -bode(Li); +plt.figure(2) +bode(Li) # Compute out the gain and phase margins #! Not implemented # (gm, pm, wcg, wcp) = margin(Li); # Compute the sensitivity and complementary sensitivity functions -Si = feedback(1, Li); -Ti = Li * Si; +Si = feedback(1, Li) +Ti = Li*Si # Check to make sure that the specification is met -figure(3); gangof4(Pi, Ci); +plt.figure(3) +gangof4(Pi, Ci) # Compute out the actual transfer function from u1 to v1 (see L8.2 notes) # Hi = Ci*(1-m*g*Pi)/(1+Ci*Pi); -Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)); +Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)) -figure(4); clf; subplot(221); -bode(Hi); +plt.figure(4) +plt.clf() +plt.subplot(221) +bode(Hi) # Now design the lateral control system -a = 0.02; b = 5; K = 2; -Co = -K*tf([1, 0.3], [1, 10]); # another lead compensator +a, b, K = 0.02, 5, 2 +Co = -K*tf([1, 0.3], [1, 10]) # another lead compensator # Convert to statespace -Co = tf2ss(Co); +Co = tf2ss(Co) # Compute the loop transfer function for the outer loop -Lo = -m*g*Po*Co; +Lo = -m*g*Po*Co -figure(5); -bode(Lo); # margin(Lo) +plt.figure(5) +bode(Lo) # margin(Lo) # Finally compute the real outer-loop loop gain + responses -L = Co*Hi*Po; -S = feedback(1, L); -T = feedback(L, 1); +L = Co*Hi*Po +S = feedback(1, L) +T = feedback(L, 1) # Compute stability margins #! Not yet implemented # (gm, pm, wgc, wpc) = margin(L); -figure(6); clf; subplot(221); -bode(L, logspace(-4, 3)); - -# Add crossover line -subplot(211); -loglog([1e-4, 1e3], [1, 1], 'k-') - -# Replot phase starting at -90 degrees -(mag, phase, w) = freqresp(L, logspace(-4, 3)); -phase = phase - 360; - -subplot(212); -semilogx([1e-4, 1e3], [-180, -180], 'k-') -semilogx(w, np.squeeze(phase), 'b-') -axis([1e-4, 1e3, -360, 0]); -xlabel('Frequency [deg]'); ylabel('Phase [deg]'); -# set(gca, 'YTick', [-360, -270, -180, -90, 0]); -# set(gca, 'XTick', [10^-4, 10^-2, 1, 100]); +plt.figure(6) +plt.clf() +bode(L, logspace(-4, 3)) + +# Add crossover line to magnitude plot +for ax in plt.gcf().axes: + if ax.get_label() == 'control-bode-magnitude': + break +ax.semilogx([1e-4, 1e3], 20*np.log10([1, 1]), 'k-') + +# Re-plot phase starting at -90 degrees +mag, phase, w = freqresp(L, logspace(-4, 3)) +phase = phase - 360 + +for ax in plt.gcf().axes: + if ax.get_label() == 'control-bode-phase': + break +ax.semilogx([1e-4, 1e3], [-180, -180], 'k-') +ax.semilogx(w, np.squeeze(phase), 'b-') +ax.axis([1e-4, 1e3, -360, 0]) +plt.xlabel('Frequency [deg]') +plt.ylabel('Phase [deg]') +# plt.set(gca, 'YTick', [-360, -270, -180, -90, 0]) +# plt.set(gca, 'XTick', [10^-4, 10^-2, 1, 100]) # # Nyquist plot for complete design # -figure(7); clf; -axis([-700, 5300, -3000, 3000]); -nyquist(L, (0.0001, 1000)); -axis([-700, 5300, -3000, 3000]); +plt.figure(7) +plt.clf() +plt.axis([-700, 5300, -3000, 3000]) +nyquist(L, (0.0001, 1000)) +plt.axis([-700, 5300, -3000, 3000]) # Add a box in the region we are going to expand -plot([-400, -400, 200, 200, -400], [-100, 100, 100, -100, -100], 'r-') +plt.plot([-400, -400, 200, 200, -400], [-100, 100, 100, -100, -100], 'r-') # Expanded region -figure(8); clf; subplot(231); -axis([-10, 5, -20, 20]); -nyquist(L); -axis([-10, 5, -20, 20]); +plt.figure(8) +plt.clf() +plt.subplot(231) +plt.axis([-10, 5, -20, 20]) +nyquist(L) +plt.axis([-10, 5, -20, 20]) # set up the color -color = 'b'; +color = 'b' # Add arrows to the plot # H1 = L.evalfr(0.4); H2 = L.evalfr(0.41); @@ -142,19 +155,23 @@ # arrow([real(H2), -imag(H2)], [real(H1), -imag(H1)], AM_normal_arrowsize, \ # 'EdgeColor', color, 'FaceColor', color); -figure(9); -(Yvec, Tvec) = step(T, linspace(1, 20)); -plot(Tvec.T, Yvec.T); +plt.figure(9) +Yvec, Tvec = step(T, linspace(1, 20)) +plt.plot(Tvec.T, Yvec.T) -(Yvec, Tvec) = step(Co*S, linspace(1, 20)); -plot(Tvec.T, Yvec.T); +Yvec, Tvec = step(Co*S, linspace(1, 20)) +plt.plot(Tvec.T, Yvec.T) #TODO: PZmap for statespace systems has not yet been implemented. -figure(10); clf(); -# (P, Z) = pzmap(T, Plot=True) -# print "Closed loop poles and zeros: ", P, Z +plt.figure(10) +plt.clf() +# P, Z = pzmap(T, Plot=True) +# print("Closed loop poles and zeros: ", P, Z) # Gang of Four -figure(11); clf(); -gangof4(Hi*Po, Co, linspace(-2, 3)); +plt.figure(11) +plt.clf() +gangof4(Hi*Po, Co, linspace(-2, 3)) +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() diff --git a/examples/pvtol-nested.py b/examples/pvtol-nested.py index e02d86352..56685599b 100644 --- a/examples/pvtol-nested.py +++ b/examples/pvtol-nested.py @@ -3,30 +3,32 @@ # # This file works through a fairly complicated control design and # analysis, corresponding to the planar vertical takeoff and landing -# (PVTOL) aircraft in Astrom and Mruray, Chapter 11. It is intended +# (PVTOL) aircraft in Astrom and Murray, Chapter 11. It is intended # to demonstrate the basic functionality of the python-control # package. # from __future__ import print_function -from matplotlib.pyplot import * # Grab MATLAB plotting functions + +import os +import matplotlib.pyplot as plt # MATLAB plotting functions from control.matlab import * # MATLAB-like functions import numpy as np # System parameters -m = 4; # mass of aircraft -J = 0.0475; # inertia around pitch axis -r = 0.25; # distance to center of force -g = 9.8; # gravitational constant -c = 0.05; # damping factor (estimated) +m = 4 # mass of aircraft +J = 0.0475 # inertia around pitch axis +r = 0.25 # distance to center of force +g = 9.8 # gravitational constant +c = 0.05 # damping factor (estimated) # Transfer functions for dynamics -Pi = tf([r], [J, 0, 0]); # inner loop (roll) -Po = tf([1], [m, c, 0]); # outer loop (position) +Pi = tf([r], [J, 0, 0]) # inner loop (roll) +Po = tf([1], [m, c, 0]) # outer loop (position) # Use state space versions -Pi = tf2ss(Pi); -Po = tf2ss(Po); +Pi = tf2ss(Pi) +Po = tf2ss(Po) # # Inner loop control design @@ -37,110 +39,118 @@ # # Design a simple lead controller for the system -k = 200; a = 2; b = 50; -Ci = k*tf([1, a], [1, b]); # lead compensator -Li = Pi*Ci; +k, a, b = 200, 2, 50 +Ci = k*tf([1, a], [1, b]) # lead compensator +Li = Pi*Ci # Bode plot for the open loop process -figure(1); -bode(Pi); +plt.figure(1) +bode(Pi) # Bode plot for the loop transfer function, with margins -figure(2); -bode(Li); +plt.figure(2) +bode(Li) # Compute out the gain and phase margins #! Not implemented -# (gm, pm, wcg, wcp) = margin(Li); +# gm, pm, wcg, wcp = margin(Li) # Compute the sensitivity and complementary sensitivity functions -Si = feedback(1, Li); -Ti = Li * Si; +Si = feedback(1, Li) +Ti = Li*Si # Check to make sure that the specification is met -figure(3); gangof4(Pi, Ci); +plt.figure(3) +gangof4(Pi, Ci) # Compute out the actual transfer function from u1 to v1 (see L8.2 notes) -# Hi = Ci*(1-m*g*Pi)/(1+Ci*Pi); -Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)); +# Hi = Ci*(1-m*g*Pi)/(1+Ci*Pi) +Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)) -figure(4); clf; subplot(221); -bode(Hi); +plt.figure(4) +plt.clf() +plt.subplot(221) +bode(Hi) # Now design the lateral control system -a = 0.02; b = 5; K = 2; -Co = -K*tf([1, 0.3], [1, 10]); # another lead compensator -Lo = -m*g*Po*Co; +a, b, K = 0.02, 5, 2 +Co = -K*tf([1, 0.3], [1, 10]) # another lead compensator +Lo = -m*g*Po*Co -figure(5); -bode(Lo); # margin(Lo) +plt.figure(5) +bode(Lo) # margin(Lo) # Finally compute the real outer-loop loop gain + responses -L = Co*Hi*Po; -S = feedback(1, L); -T = feedback(L, 1); +L = Co*Hi*Po +S = feedback(1, L) +T = feedback(L, 1) # Compute stability margins -(gm, pm, wgc, wpc) = margin(L); +gm, pm, wgc, wpc = margin(L) print("Gain margin: %g at %g" % (gm, wgc)) print("Phase margin: %g at %g" % (pm, wpc)) -figure(6); clf; -bode(L, logspace(-4, 3)); +plt.figure(6) +plt.clf() +bode(L, np.logspace(-4, 3)) # Add crossover line to the magnitude plot # # Note: in matplotlib before v2.1, the following code worked: # -# subplot(211); hold(True); +# plt.subplot(211); hold(True); # loglog([1e-4, 1e3], [1, 1], 'k-') # -# In later versions of matplotlib the call to subplot will clear the +# In later versions of matplotlib the call to plt.subplot will clear the # axes and so we have to extract the axes that we want to use by hand. # In addition, hold() is deprecated so we no longer require it. # -for ax in gcf().axes: +for ax in plt.gcf().axes: if ax.get_label() == 'control-bode-magnitude': break -ax.semilogx([1e-4, 1e3], 20 * np.log10([1, 1]), 'k-') +ax.semilogx([1e-4, 1e3], 20*np.log10([1, 1]), 'k-') # # Replot phase starting at -90 degrees # # Get the phase plot axes -for ax in gcf().axes: +for ax in plt.gcf().axes: if ax.get_label() == 'control-bode-phase': break # Recreate the frequency response and shift the phase -(mag, phase, w) = freqresp(L, logspace(-4, 3)); -phase = phase - 360; +mag, phase, w = freqresp(L, np.logspace(-4, 3)) +phase = phase - 360 # Replot the phase by hand ax.semilogx([1e-4, 1e3], [-180, -180], 'k-') ax.semilogx(w, np.squeeze(phase), 'b-') -ax.axis([1e-4, 1e3, -360, 0]); -xlabel('Frequency [deg]'); ylabel('Phase [deg]'); -# set(gca, 'YTick', [-360, -270, -180, -90, 0]); -# set(gca, 'XTick', [10^-4, 10^-2, 1, 100]); +ax.axis([1e-4, 1e3, -360, 0]) +plt.xlabel('Frequency [deg]') +plt.ylabel('Phase [deg]') +# plt.set(gca, 'YTick', [-360, -270, -180, -90, 0]) +# plt.set(gca, 'XTick', [10^-4, 10^-2, 1, 100]) # # Nyquist plot for complete design # -figure(7); clf; -nyquist(L, (0.0001, 1000)); -axis([-700, 5300, -3000, 3000]); +plt.figure(7) +plt.clf() +nyquist(L, (0.0001, 1000)) +plt.axis([-700, 5300, -3000, 3000]) # Add a box in the region we are going to expand -plot([-400, -400, 200, 200, -400], [-100, 100, 100, -100, -100], 'r-') +plt.plot([-400, -400, 200, 200, -400], [-100, 100, 100, -100, -100], 'r-') # Expanded region -figure(8); clf; subplot(231); -nyquist(L); -axis([-10, 5, -20, 20]); +plt.figure(8) +plt.clf() +plt.subplot(231) +nyquist(L) +plt.axis([-10, 5, -20, 20]) # set up the color -color = 'b'; +color = 'b' # Add arrows to the plot # H1 = L.evalfr(0.4); H2 = L.evalfr(0.41); @@ -151,18 +161,22 @@ # arrow([real(H2), -imag(H2)], [real(H1), -imag(H1)], AM_normal_arrowsize, \ # 'EdgeColor', color, 'FaceColor', color); -figure(9); -(Yvec, Tvec) = step(T, linspace(0, 20)); -plot(Tvec.T, Yvec.T); +plt.figure(9) +Yvec, Tvec = step(T, np.linspace(0, 20)) +plt.plot(Tvec.T, Yvec.T) -(Yvec, Tvec) = step(Co*S, linspace(0, 20)); -plot(Tvec.T, Yvec.T); +Yvec, Tvec = step(Co*S, np.linspace(0, 20)) +plt.plot(Tvec.T, Yvec.T) -figure(10); clf(); -(P, Z) = pzmap(T, Plot=True) +plt.figure(10) +plt.clf() +P, Z = pzmap(T, Plot=True) print("Closed loop poles and zeros: ", P, Z) # Gang of Four -figure(11); clf(); -gangof4(Hi*Po, Co); +plt.figure(11) +plt.clf() +gangof4(Hi*Po, Co) +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() diff --git a/examples/robust_mimo.py b/examples/robust_mimo.py index c7a06ea1c..402d91488 100644 --- a/examples/robust_mimo.py +++ b/examples/robust_mimo.py @@ -10,7 +10,7 @@ import numpy as np import matplotlib.pyplot as plt -from control import tf, ss, mixsyn, feedback, step_response +from control import tf, ss, mixsyn, step_response def weighting(wb, m, a): @@ -21,7 +21,7 @@ def weighting(wb, m, a): wf - SISO LTI object """ s = tf([1, 0], [1]) - return (s / m + wb) / (s + wb * a) + return (s/m + wb) / (s + wb*a) def plant(): @@ -44,7 +44,7 @@ def triv_sigma(g, w): w - frequencies, length m s - (m,n) array of singular values of g(1j*w)""" m, p, _ = g.freqresp(w) - sjw = (m * np.exp(1j * p * np.pi / 180)).transpose(2, 0, 1) + sjw = (m*np.exp(1j*p*np.pi/180)).transpose(2, 0, 1) sv = np.linalg.svd(sjw, compute_uv=False) return sv @@ -135,8 +135,8 @@ def design(): g = plant() w = np.logspace(-2, 2, 101) I = ss([], [], [], np.eye(2)) - s1 = I.feedback(g * k1) - s2 = I.feedback(g * k2) + s1 = I.feedback(g*k1) + s2 = I.feedback(g*k2) # frequency response sv1 = triv_sigma(s1, w) @@ -145,10 +145,10 @@ def design(): plt.figure(2) plt.subplot(1, 2, 1) - plt.semilogx(w, 20 * np.log10(sv1[:, 0]), label=r'$\sigma_1(S_1)$') - plt.semilogx(w, 20 * np.log10(sv1[:, 1]), label=r'$\sigma_2(S_1)$') - plt.semilogx(w, 20 * np.log10(sv2[:, 0]), label=r'$\sigma_1(S_2)$') - plt.semilogx(w, 20 * np.log10(sv2[:, 1]), label=r'$\sigma_2(S_2)$') + plt.semilogx(w, 20*np.log10(sv1[:, 0]), label=r'$\sigma_1(S_1)$') + plt.semilogx(w, 20*np.log10(sv1[:, 1]), label=r'$\sigma_2(S_1)$') + plt.semilogx(w, 20*np.log10(sv2[:, 0]), label=r'$\sigma_1(S_2)$') + plt.semilogx(w, 20*np.log10(sv2[:, 1]), label=r'$\sigma_2(S_2)$') plt.ylim([-60, 10]) plt.ylabel('magnitude [dB]') plt.xlim([1e-2, 1e2]) @@ -162,8 +162,8 @@ def design(): # design 2, output 2 does not, and is very fast, while output 1 # has a larger initial inverse response than in design 1 time = np.linspace(0, 10, 301) - t1 = (g * k1).feedback(I) - t2 = (g * k2).feedback(I) + t1 = (g*k1).feedback(I) + t2 = (g*k2).feedback(I) y1 = step_opposite(t1, time) y2 = step_opposite(t2, time) diff --git a/examples/robust_siso.py b/examples/robust_siso.py index 013ea821d..87fcdb707 100644 --- a/examples/robust_siso.py +++ b/examples/robust_siso.py @@ -11,20 +11,20 @@ import numpy as np import matplotlib.pyplot as plt -from control import tf, ss, mixsyn, feedback, step_response +from control import tf, mixsyn, feedback, step_response s = tf([1, 0], 1) # the plant -g = 200 / (10 * s + 1) / (0.05 * s + 1) ** 2 +g = 200/(10*s + 1) / (0.05*s + 1)**2 # disturbance plant -gd = 100 / (10 * s + 1) +gd = 100/(10*s + 1) # first design # sensitivity weighting M = 1.5 wb = 10 A = 1e-4 -ws1 = (s / M + wb) / (s + wb * A) +ws1 = (s/M + wb) / (s + wb*A) # KS weighting wu = tf(1, 1) @@ -32,21 +32,21 @@ # sensitivity (S) and complementary sensitivity (T) functions for # design 1 -s1 = feedback(1, g * k1) -t1 = feedback(g * k1, 1) +s1 = feedback(1, g*k1) +t1 = feedback(g*k1, 1) # second design # this weighting differs from the text, where A**0.5 is used; if you use that, # the frequency response doesn't match the figure. The time responses # are similar, though. -ws2 = (s / M ** 0.5 + wb) ** 2 / (s + wb * A) ** 2 +ws2 = (s/M ** 0.5 + wb)**2 / (s + wb*A)**2 # the KS weighting is the same as for the first design k2, cl2, info2 = mixsyn(g, ws2, wu) # S and T for design 2 -s2 = feedback(1, g * k2) -t2 = feedback(g * k2, 1) +s2 = feedback(1, g*k2) +t2 = feedback(g*k2, 1) # frequency response omega = np.logspace(-2, 2, 101) @@ -57,11 +57,11 @@ plt.figure(1) # text uses log-scaled absolute, but dB are probably more familiar to most control engineers -plt.semilogx(omega, 20 * np.log10(s1mag.flat), label='$S_1$') -plt.semilogx(omega, 20 * np.log10(s2mag.flat), label='$S_2$') +plt.semilogx(omega, 20*np.log10(s1mag.flat), label='$S_1$') +plt.semilogx(omega, 20*np.log10(s2mag.flat), label='$S_2$') # -1 in logspace is inverse -plt.semilogx(omega, -20 * np.log10(ws1mag.flat), label='$1/w_{P1}$') -plt.semilogx(omega, -20 * np.log10(ws2mag.flat), label='$1/w_{P2}$') +plt.semilogx(omega, -20*np.log10(ws1mag.flat), label='$1/w_{P1}$') +plt.semilogx(omega, -20*np.log10(ws2mag.flat), label='$1/w_{P2}$') plt.ylim([-80, 10]) plt.xlim([1e-2, 1e2]) @@ -77,8 +77,8 @@ # gd injects into the output (that is, g and gd are summed), and the # closed loop mapping from output disturbance->output is S. -_, y1d = step_response(s1 * gd, time) -_, y2d = step_response(s2 * gd, time) +_, y1d = step_response(s1*gd, time) +_, y2d = step_response(s2*gd, time) plt.figure(2) plt.subplot(1, 2, 1) diff --git a/examples/rss-balred.py b/examples/rss-balred.py index 86e499a80..0af0b7ed0 100755 --- a/examples/rss-balred.py +++ b/examples/rss-balred.py @@ -10,21 +10,29 @@ plt.close('all') -#controlable canonical realization computed in matlab for the transfer function: -# num = [1 11 45 32], den = [1 15 60 200 60] -A = np.matrix('-15., -7.5, -6.25, -1.875; \ -8., 0., 0., 0.; \ -0., 4., 0., 0.; \ -0., 0., 1., 0.') -B = np.matrix('2.; 0.; 0.; 0.') -C = np.matrix('0.5, 0.6875, 0.7031, 0.5') -D = np.matrix('0.') +# controllable canonical realization computed in MATLAB for the +# transfer function: num = [1 11 45 32], den = [1 15 60 200 60] +A = np.array([ + [-15., -7.5, -6.25, -1.875], + [8., 0., 0., 0.], + [0., 4., 0., 0.], + [0., 0., 1., 0.] +]) +B = np.array([ + [2.], + [0.], + [0.], + [0.] +]) +C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) +D = np.array([[0.]]) # The full system -fsys = StateSpace(A,B,C,D) +fsys = StateSpace(A, B, C, D) + # The reduced system, truncating the order by 1 -ord = 3 -rsys = msimp.balred(fsys,ord, method = 'truncate') +n = 3 +rsys = msimp.balred(fsys, n, method='truncate') # Comparison of the step responses of the full and reduced systems plt.figure(1) @@ -35,15 +43,13 @@ # Repeat balanced reduction, now with 100-dimensional random state space sysrand = mt.rss(100, 1, 1) -rsysrand = msimp.balred(sysrand,10,method ='truncate') +rsysrand = msimp.balred(sysrand, 10, method='truncate') # Comparison of the impulse responses of the full and reduced random systems plt.figure(2) yrand, trand = mt.impulse(sysrand) yrandr, trandr = mt.impulse(rsysrand) -plt.plot(trand.T, yrand.T, trandr.T, yrandr.T) - +plt.plot(trand.T, yrand.T, trandr.T, yrandr.T) if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: plt.show() - diff --git a/examples/secord-matlab.py b/examples/secord-matlab.py index c3cf08277..25bf1ff79 100644 --- a/examples/secord-matlab.py +++ b/examples/secord-matlab.py @@ -1,33 +1,39 @@ -# secord.py - demonstrate some standard MATLAB commands +# secord.py - demonstrate some standard MATLAB commands # RMM, 25 May 09 -from matplotlib.pyplot import * # Grab MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import os +import matplotlib.pyplot as plt # MATLAB plotting functions +from control.matlab import * # MATLAB-like functions # Parameters defining the system -m = 250.0 # system mass -k = 40.0 # spring constant -b = 60.0 # damping constant +m = 250.0 # system mass +k = 40.0 # spring constant +b = 60.0 # damping constant # System matrices A = [[0, 1.], [-k/m, -b/m]] B = [[0], [1/m]] C = [[1., 0]] -sys = ss(A, B, C, 0); +sys = ss(A, B, C, 0) # Step response for the system -figure(1) +plt.figure(1) yout, T = step(sys) -plot(T.T, yout.T) +plt.plot(T.T, yout.T) +plt.show(block=False) # Bode plot for the system -figure(2) -mag,phase,om = bode(sys, logspace(-2, 2),Plot=True) +plt.figure(2) +mag, phase, om = bode(sys, logspace(-2, 2), Plot=True) +plt.show(block=False) # Nyquist plot for the system -figure(3) +plt.figure(3) nyquist(sys, logspace(-2, 2)) +plt.show(block=False) -# Root lcous plut for the system -figure(4) +# Root lcous plot for the system rlocus(sys) + +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() diff --git a/examples/slycot-import-test.py b/examples/slycot-import-test.py index 7e4f0d9a9..c2c78fa89 100644 --- a/examples/slycot-import-test.py +++ b/examples/slycot-import-test.py @@ -5,19 +5,18 @@ """ import numpy as np -from scipy import * from control.matlab import * from control.exception import slycot_check # Parameters defining the system m = 250.0 # system mass -k = 40.0 # spring constant -b = 60.0 # damping constant +k = 40.0 # spring constant +b = 60.0 # damping constant # System matrices -A = np.matrix([[1, -1, 1.], [1, -k / m, -b / m], [1, 1, 1]]) -B = np.matrix([[0], [1 / m], [1]]) -C = np.matrix([[1., 0, 1.]]) +A = np.array([[1, -1, 1.], [1, -k/m, -b/m], [1, 1, 1]]) +B = np.array([[0], [1/m], [1]]) +C = np.array([[1., 0, 1.]]) sys = ss(A, B, C, 0) # Python control may be used without slycot, for example for a pole placement. @@ -25,7 +24,7 @@ w = [-3, -2, -1] K = place(A, B, w) print("[python-control (from scipy)] K = ", K) -print("[python-control (from scipy)] eigs = ", np.linalg.eig(A - B * K)[0]) +print("[python-control (from scipy)] eigs = ", np.linalg.eig(A - B*K)[0]) # Before using one of its routine, check that slycot is installed. w = np.array([-3, -2, -1]) @@ -33,11 +32,11 @@ # Import routine sb01bd used for pole placement. from slycot import sb01bd - n = 3 # Number of states - m = 1 # Number of inputs - npp = 3 # Number of placed eigen values - alpha = 1 # Maximum threshold for eigen values - dico = 'D' # Discrete system + n = 3 # Number of states + m = 1 # Number of inputs + npp = 3 # Number of placed eigen values + alpha = 1 # Maximum threshold for eigen values + dico = 'D' # Discrete system _, _, _, _, _, K, _ = sb01bd(n, m, npp, alpha, A, B, w, dico, tol=0.0, ldwork=None) print("[slycot] K = ", K) print("[slycot] eigs = ", np.linalg.eig(A + np.dot(B, K))[0]) diff --git a/examples/steering-gainsched.py b/examples/steering-gainsched.py new file mode 100644 index 000000000..8f541ead8 --- /dev/null +++ b/examples/steering-gainsched.py @@ -0,0 +1,195 @@ +# steering-gainsched.py - gain scheduled control for vehicle steering +# RMM, 8 May 2019 +# +# This file works through Example 1.1 in the "Optimization-Based Control" +# course notes by Richard Murray (avaliable at http://fbsbook.org, in the +# optimization-based control supplement). It is intended to demonstrate the +# functionality for nonlinear input/output systems in the python-control +# package. + +import numpy as np +import control as ct +from cmath import sqrt +import matplotlib.pyplot as mpl + +# +# Vehicle steering dynamics +# +# The vehicle dynamics are given by a simple bicycle model. We take the state +# of the system as (x, y, theta) where (x, y) is the position of the vehicle +# in the plane and theta is the angle of the vehicle with respect to +# horizontal. The vehicle input is given by (v, phi) where v is the forward +# velocity of the vehicle and phi is the angle of the steering wheel. The +# model includes saturation of the vehicle steering angle. +# +# System state: x, y, theta +# System input: v, phi +# System output: x, y +# System parameters: wheelbase, maxsteer +# +def vehicle_update(t, x, u, params): + # Get the parameters for the model + l = params.get('wheelbase', 3.) # vehicle wheelbase + phimax = params.get('maxsteer', 0.5) # max steering angle (rad) + + # Saturate the steering input + phi = np.clip(u[1], -phimax, phimax) + + # Return the derivative of the state + return np.array([ + np.cos(x[2]) * u[0], # xdot = cos(theta) v + np.sin(x[2]) * u[0], # ydot = sin(theta) v + (u[0] / l) * np.tan(phi) # thdot = v/l tan(phi) + ]) + +def vehicle_output(t, x, u, params): + return x # return x, y, theta (full state) + +# Define the vehicle steering dynamics as an input/output system +vehicle = ct.NonlinearIOSystem( + vehicle_update, vehicle_output, states=3, name='vehicle', + inputs=('v', 'phi'), + outputs=('x', 'y', 'theta')) + +# +# Gain scheduled controller +# +# For this system we use a simple schedule on the forward vehicle velocity and +# place the poles of the system at fixed values. The controller takes the +# current vehicle position and orientation plus the velocity velocity as +# inputs, and returns the velocity and steering commands. +# +# System state: none +# System input: ex, ey, etheta, vd, phid +# System output: v, phi +# System parameters: longpole, latpole1, latpole2 +# +def control_output(t, x, u, params): + # Get the controller parameters + longpole = params.get('longpole', -2.) + latpole1 = params.get('latpole1', -1/2 + sqrt(-7)/2) + latpole2 = params.get('latpole2', -1/2 - sqrt(-7)/2) + l = params.get('wheelbase', 3) + + # Extract the system inputs + ex, ey, etheta, vd, phid = u + + # Determine the controller gains + alpha1 = -np.real(latpole1 + latpole2) + alpha2 = np.real(latpole1 * latpole2) + + # Compute and return the control law + v = -longpole * ex # Note: no feedfwd (to make plot interesting) + if vd != 0: + phi = phid + (alpha1 * l) / vd * ey + (alpha2 * l) / vd * etheta + else: + # We aren't moving, so don't turn the steering wheel + phi = phid + + return np.array([v, phi]) + +# Define the controller as an input/output system +controller = ct.NonlinearIOSystem( + None, control_output, name='controller', # static system + inputs=('ex', 'ey', 'etheta', 'vd', 'phid'), # system inputs + outputs=('v', 'phi') # system outputs +) + +# +# Reference trajectory subsystem +# +# The reference trajectory block generates a simple trajectory for the system +# given the desired speed (vref) and lateral position (yref). The trajectory +# consists of a straight line of the form (vref * t, yref, 0) with nominal +# input (vref, 0). +# +# System state: none +# System input: vref, yref +# System output: xd, yd, thetad, vd, phid +# System parameters: none +# +def trajgen_output(t, x, u, params): + vref, yref = u + return np.array([vref * t, yref, 0, vref, 0]) + +# Define the trajectory generator as an input/output system +trajgen = ct.NonlinearIOSystem( + None, trajgen_output, name='trajgen', + inputs=('vref', 'yref'), + outputs=('xd', 'yd', 'thetad', 'vd', 'phid')) + +# +# System construction +# +# The input to the full closed loop system is the desired lateral position and +# the desired forward velocity. The output for the system is taken as the +# full vehicle state plus the velocity of the vehicle. The following diagram +# summarizes the interconnections: +# +# +---------+ +---------------> v +# | | | +# [ yref ] | v | +# [ ] ---> trajgen -+-+-> controller -+-> vehicle -+-> [x, y, theta] +# [ vref ] ^ | +# | | +# +----------- [-1] -----------+ +# +# We construct the system using the InterconnectedSystem constructor and using +# signal labels to keep track of everything. + +steering = ct.InterconnectedSystem( + # List of subsystems + (trajgen, controller, vehicle), name='steering', + + # Interconnections between subsystems + connections=( + ('controller.ex', 'trajgen.xd', '-vehicle.x'), + ('controller.ey', 'trajgen.yd', '-vehicle.y'), + ('controller.etheta', 'trajgen.thetad', '-vehicle.theta'), + ('controller.vd', 'trajgen.vd'), + ('controller.phid', 'trajgen.phid'), + ('vehicle.v', 'controller.v'), + ('vehicle.phi', 'controller.phi') + ), + + # System inputs + inplist=['trajgen.vref', 'trajgen.yref'], + inputs=['yref', 'vref'], + + # System outputs + outlist=['vehicle.x', 'vehicle.y', 'vehicle.theta', 'controller.v', + 'controller.phi'], + outputs=['x', 'y', 'theta', 'v', 'phi'] +) + +# Set up the simulation conditions +yref = 1 +T = np.linspace(0, 5, 100) + +# Set up a figure for plotting the results +mpl.figure(); + +# Plot the reference trajectory for the y position +mpl.plot([0, 5], [yref, yref], 'k--') + +# Find the signals we want to plot +y_index = steering.find_output('y') +v_index = steering.find_output('v') + +# Do an iteration through different speeds +for vref in [8, 10, 12]: + # Simulate the closed loop controller response + tout, yout = ct.input_output_response( + steering, T, [vref * np.ones(len(T)), yref * np.ones(len(T))]) + + # Plot the reference speed + mpl.plot([0, 5], [vref, vref], 'k--') + + # Plot the system output + y_line, = mpl.plot(tout, yout[y_index, :], 'r') # lateral position + v_line, = mpl.plot(tout, yout[v_index, :], 'b') # vehicle velocity + +# Add axis labels +mpl.xlabel('Time (s)') +mpl.ylabel('x vel (m/s), y pos (m)') +mpl.legend((v_line, y_line), ('v', 'y'), loc='center right', frameon=False) diff --git a/examples/steering.ipynb b/examples/steering.ipynb new file mode 100644 index 000000000..544d443c5 --- /dev/null +++ b/examples/steering.ipynb @@ -0,0 +1,1192 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vehicle steering\n", + "Karl J. Astrom and Richard M. Murray \n", + "23 Jul 2019\n", + "\n", + "This notebook contains the computations for the vehicle steering running example in *Feedback Systems*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM comments to Karl, 27 Jun 2019\n", + "* I'm using this notebook to walk through all of the vehicle steering examples and make sure that all of the parameters, conditions, and maximum steering angles are consitent and reasonable.\n", + "* Please feel free to send me comments on the contents as well as the bulletted notes, in whatever form is most convenient.\n", + "* Once we have sorted out all of the settings we want to use, I'll copy over the changes into the MATLAB files that we use for creating the figures in the book.\n", + "* These notes will be removed from the notebook once we have finalized everything." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import control as ct\n", + "ct.use_fbs_defaults()\n", + "ct.use_numpy_matrix(False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vehicle steering dynamics (Example 3.11)\n", + "\n", + "The vehicle dynamics are given by a simple bicycle model. We take the state of the system as $(x, y, \\theta)$ where $(x, y)$ is the position of the reference point of the vehicle in the plane and $\\theta$ is the angle of the vehicle with respect to horizontal. The vehicle input is given by $(v, \\delta)$ where $v$ is the forward velocity of the vehicle and $\\delta$ is the angle of the steering wheel. We take as parameters the wheelbase $b$ and the offset $a$ between the rear wheels and the reference point. The model includes saturation of the vehicle steering angle (`maxsteer`).\n", + "\n", + "* System state: `x`, `y`, `theta`\n", + "* System input: `v`, `delta` \n", + "* System output: `x`, `y` \n", + "* System parameters: `wheelbase`, `refoffset`, `maxsteer` \n", + "\n", + "Assuming no slipping of the wheels, the motion of the vehicle is given by a rotation around a point O that depends on the steering angle $\\delta$. To compute the angle $\\alpha$ of the velocity of the reference point with respect to the axis of the vehicle, we let the distance from the center of rotation O to the contact point of the rear wheel be $r_\\text{r}$ and it the follows from Figure 3.17 in FBS that $b = r_\\text{r} \\tan \\delta$ and $a = r_\\text{r} \\tan \\alpha$, which implies that $\\tan \\alpha = (a/b) \\tan \\delta$.\n", + "\n", + "Reasonable limits for the steering angle depend on the speed. The physical limit is given in our model as 0.5 radians (about 30 degrees). However, this limit is rarely possible when the car is driving since it would cause the tires to slide on the pavement. We us a limit of 0.1 radians (about 6 degrees) at 10 m/s ($\\approx$ 35 kph) and 0.05 radians (about 3 degrees) at 30 m/s ($\\approx$ 110 kph). Note that a steering angle of 0.05 rad gives a cross acceleration of $(v^2/b) \\tan \\delta \\approx (100/3) 0.05 = 1.7$ $\\text{m/s}^2$ at 10 m/s and 15 $\\text{m/s}^2$ at 30 m/s ($\\approx$ 1.5 times the force of gravity)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def vehicle_update(t, x, u, params):\n", + " # Get the parameters for the model\n", + " a = params.get('refoffset', 1.5) # offset to vehicle reference point\n", + " b = params.get('wheelbase', 3.) # vehicle wheelbase\n", + " maxsteer = params.get('maxsteer', 0.5) # max steering angle (rad)\n", + "\n", + " # Saturate the steering input\n", + " delta = np.clip(u[1], -maxsteer, maxsteer)\n", + " alpha = np.arctan2(a * np.tan(delta), b)\n", + "\n", + " # Return the derivative of the state\n", + " return np.array([\n", + " u[0] * np.cos(x[2] + alpha), # xdot = cos(theta + alpha) v\n", + " u[0] * np.sin(x[2] + alpha), # ydot = sin(theta + alpha) v\n", + " (u[0] / b) * np.tan(delta) # thdot = v/l tan(phi)\n", + " ])\n", + "\n", + "def vehicle_output(t, x, u, params):\n", + " return x[0:2]\n", + "\n", + "# Default vehicle parameters (including nominal velocity)\n", + "vehicle_params={'refoffset': 1.5, 'wheelbase': 3, 'velocity': 15, \n", + " 'maxsteer': 0.5}\n", + "\n", + "# Define the vehicle steering dynamics as an input/output system\n", + "vehicle = ct.NonlinearIOSystem(\n", + " vehicle_update, vehicle_output, states=3, name='vehicle',\n", + " inputs=('v', 'delta'), outputs=('x', 'y'), params=vehicle_params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vehicle driving on a curvy road (Figure 8.6a)\n", + "\n", + "To illustrate the dynamics of the system, we create an input that correspond to driving down a curvy road. This trajectory will be used in future simulations as a reference trajectory for estimation and control." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM notes, 27 Jun 2019:\n", + "* The figure below appears in Chapter 8 (output feedback) as Example 8.3, but I've put it here in the notebook since it is a good way to demonstrate the dynamics of the vehicle.\n", + "* In the book, this figure is created for the linear model and in a manner that I can't quite understand, since the linear model that is used is only for the lateral dynamics. The original file is `OutputFeedback/figures/steering_obs.m`.\n", + "* To create the figure here, I set the initial vehicle angle to be $\\theta(0) = 0.75$ rad and then used an input that gives a figure approximating Example 8.3 To create the lateral offset, I think subtracted the trajectory from the averaged straight line trajectory, shown as a dashed line in the $xy$ figure below.\n", + "* I find the approach that we used in the MATLAB version to be confusing, but I also think the method of creating the lateral error here is a hart to follow. We might instead consider choosing a trajectory that goes mainly vertically, with the 2D dynamics being the $x$, $\\theta$ dynamics instead of the $y$, $\\theta$ dynamics.\n", + "\n", + "KJA comments, 1 Jul 2019:\n", + "\n", + "0. I think we should point out that the reference point is typically the projection of the center of mass of the whole vehicle.\n", + "\n", + "1. The heading angle $\\theta$ must be marked in Figure 3.17b.\n", + "\n", + "2. I think it is useful to start with a curvy road that you have done here but then to specialized to a trajectory that is essentially horizontal, where $y$ is the deviation from the nominal horizontal $x$ axis. Assuming that $\\alpha$ and $\\theta$ are small we get the natural linearization of (3.26) $\\dot x = v$ and $\\dot y =v(\\alpha + \\theta)$\n", + "\n", + "RMM response, 16 Jul 2019:\n", + "* I've changed the trajectory to be about the horizontal axis, but I am ploting things vertically for better figure layout. This corresponds to what is done in Example 9.10 in the text, which I think looks OK.\n", + "\n", + "KJA response, 20 Jul 2019: Fig 8.6a is fine" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# System parameters\n", + "wheelbase = vehicle_params['wheelbase']\n", + "v0 = vehicle_params['velocity']\n", + "\n", + "# Control inputs\n", + "T_curvy = np.linspace(0, 7, 500)\n", + "v_curvy = v0*np.ones(T_curvy.shape) \n", + "delta_curvy = 0.1*np.sin(T_curvy)*np.cos(4*T_curvy) + 0.0025*np.sin(T_curvy*np.pi/7)\n", + "u_curvy = [v_curvy, delta_curvy]\n", + "X0_curvy = [0, 0.8, 0]\n", + "\n", + "# Simulate the system + estimator\n", + "t_curvy, y_curvy, x_curvy = ct.input_output_response(\n", + " vehicle, T_curvy, u_curvy, X0_curvy, params=vehicle_params, return_x=True)\n", + "\n", + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure(figsize=[9, 4.5])\n", + "\n", + "# Plot the resulting trajectory (and some road boundaries)\n", + "plt.subplot(1, 4, 2)\n", + "plt.plot(y_curvy[1], y_curvy[0])\n", + "plt.plot(y_curvy[1] - 9/np.cos(x_curvy[2]), y_curvy[0], 'k-', linewidth=1)\n", + "plt.plot(y_curvy[1] - 3/np.cos(x_curvy[2]), y_curvy[0], 'k--', linewidth=1)\n", + "plt.plot(y_curvy[1] + 3/np.cos(x_curvy[2]), y_curvy[0], 'k-', linewidth=1)\n", + "\n", + "plt.xlabel('y [m]')\n", + "plt.ylabel('x [m]');\n", + "plt.axis('Equal')\n", + "\n", + "# Plot the lateral position\n", + "plt.subplot(2, 2, 2)\n", + "plt.plot(t_curvy, y_curvy[1])\n", + "plt.ylabel('Lateral position $y$ [m]')\n", + "\n", + "# Plot the steering angle\n", + "plt.subplot(2, 2, 4)\n", + "plt.plot(t_curvy, delta_curvy)\n", + "plt.ylabel('Steering angle $\\\\delta$ [rad]')\n", + "plt.xlabel('Time t [sec]')\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linearization of lateral steering dynamics (Example 6.13)\n", + "\n", + "We are interested in the motion of the vehicle about a straight-line path ($\\theta = \\theta_0$) with constant velocity $v_0 \\neq 0$. To find the relevant equilibrium point, we first set $\\dot\\theta = 0$ and we see that we must have $\\delta = 0$, corresponding to the steering wheel being straight. The motion in the xy plane is by definition not at equilibrium and so we focus on lateral deviation of the vehicle from a straight line. For simplicity, we let $\\theta_\\text{e} = 0$, which corresponds to driving along the $x$ axis. We can then focus on the equations of motion in the $y$ and $\\theta$ directions with input $u = \\delta$." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Linearized system dynamics:\n", + "\n", + "A = [[0. 1.]\n", + " [0. 0.]]\n", + "\n", + "B = [[0.5]\n", + " [1. ]]\n", + "\n", + "C = [[1. 0.]]\n", + "\n", + "D = [[0.]]\n", + "\n" + ] + } + ], + "source": [ + "# Define the lateral dynamics as a subset of the full vehicle steering dynamics\n", + "lateral = ct.NonlinearIOSystem(\n", + " lambda t, x, u, params: vehicle_update(\n", + " t, [0., x[0], x[1]], [params.get('velocity', 1), u[0]], params)[1:],\n", + " lambda t, x, u, params: vehicle_output(\n", + " t, [0., x[0], x[1]], [params.get('velocity', 1), u[0]], params)[1:],\n", + " states=2, name='lateral', inputs=('phi'), outputs=('y', 'theta')\n", + ")\n", + "\n", + "# Compute the linearization at velocity 10 m/sec\n", + "lateral_linearized = ct.linearize(lateral, [0, 0], [0], params=vehicle_params)\n", + "\n", + "# Normalize dynamics using state [x1/b, x2] and timescale v0 t / b\n", + "b = vehicle_params['wheelbase']\n", + "v0 = vehicle_params['velocity']\n", + "lateral_transformed = ct.similarity_transform(\n", + " lateral_linearized, [[1/b, 0], [0, 1]], timescale=v0/b)\n", + "\n", + "# Set the output to be the normalized state x1/b\n", + "lateral_normalized = lateral_transformed[0,:] * (1/b)\n", + "print(\"Linearized system dynamics:\\n\")\n", + "print(lateral_normalized)\n", + "\n", + "# Save the system matrices for later use\n", + "A = lateral_normalized.A\n", + "B = lateral_normalized.B\n", + "C = lateral_normalized.C" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Eigenvalue placement controller design (Example 7.4)\n", + "\n", + "We want to design a controller that stabilizes the dynamics of the vehicle and tracks a given reference value $r$ of the lateral position of the vehicle. We use feedback to design the dynamics of the system to have the characteristic polynomial\n", + "$p(s) = s^2 + 2 \\zeta_\\text{c} \\omega_\\text{c} + \\omega_\\text{c}^2$.\n", + "\n", + "To find reasonable values of $\\omega_\\text{c}$ we observe that the initial response of the steering angle to a unit step change in the steering command is $\\omega_\\text{c}^2 r$, where $r$ is the commanded lateral transition. Recall that the model is normalized so that the length unit is the wheelbase $b$ and the time unit is the time $b/v_0$ to travel one wheelbase. A typical car has a wheelbase of about 3 m and, assuming a speed of 30 m/s, a normalized time unit corresponds to 0.1 s. To determine a reasonable steering angle when making a gentle lane change, we assume that the turning radius is $R$ = 600 m. For a wheelbase of 3 m this corresponds to a steering angle $\\delta \\approx 3/600 = 0.005$ rad and a lateral acceleration of $v^2/R$ = 302/600 = 1.5 m/s$^2$. Assuming that a lane change corresponds to a translation of one wheelbase we find $\\omega_\\text{c} = \\sqrt{0.005}$ = 0.07 rad/s.\n", + "\n", + "The unit step responses for the closed loop system for different values of the design parameters are shown below. The effect of $\\omega_c$ is shown on the left, which shows that the response speed increases with increasing $\\omega_\\text{c}$. All responses have overshoot less than 5% (15 cm), as indicated by the dashed lines. The settling times range from 30 to 60 normalized time units, which corresponds to about 3–6 s, and are limited by the acceptable lateral acceleration of the vehicle. The effect of $\\zeta_\\text{c}$ is shown on the right. The response speed and the overshoot increase with decreasing damping. Using these plots, we conclude that a reasonable design choice is $\\omega_\\text{c} = 0.07$ and $\\zeta_\\text{c} = 0.7$. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM note, 27 Jun 2019: \n", + "* The design guidelines are for $v_0$ = 30 m/s (highway speeds) but most of the examples below are done at lower speed (typically 10 m/s). Also, the eigenvalue locations above are not the same ones that we use in the output feedback example below. We should probably make things more consistent.\n", + "\n", + "KJA comment, 1 Jul 2019: \n", + "* I am all for maikng it consist and choosing e.g. v0 = 30 m/s\n", + "\n", + "RMM comment, 17 Jul 2019:\n", + "* I've updated the examples below to use v0 = 30 m/s for everything except the forward/reverse example. This corresponds to ~105 kph (freeway speeds) and a reasonable bound for the steering angle to avoid slipping is 0.05 rad." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Utility function to place poles for the normalized vehicle steering system\n", + "def normalized_place(wc, zc):\n", + " # Get the dynamics and input matrices, for later use\n", + " A, B = lateral_normalized.A, lateral_normalized.B\n", + " \n", + " # Compute the eigenvalues from the characteristic polynomial\n", + " eigs = np.roots([1, 2*zc*wc, wc**2])\n", + " \n", + " # Compute the feedback gain using eigenvalue placement\n", + " K = ct.place_varga(A, B, eigs)\n", + " \n", + " # Create a new system representing the closed loop response\n", + " clsys = ct.StateSpace(A - B @ K, B, lateral_normalized.C, 0)\n", + " \n", + " # Compute the feedforward gain based on the zero frequency gain of the closed loop\n", + " kf = np.real(1/clsys.evalfr(0))\n", + "\n", + " # Scale the input by the feedforward gain\n", + " clsys *= kf\n", + " \n", + " # Return gains and closed loop system dynamics\n", + " return K, kf, clsys\n", + "\n", + "# Utility function to plot simulation results for normalized vehicle steering system\n", + "def normalized_plot(t, y, u, inpfig, outfig):\n", + " plt.sca(outfig)\n", + " plt.plot(t, y)\n", + " plt.sca(inpfig)\n", + " plt.plot(t, u[0])\n", + " \n", + "# Utility function to label plots of normalized vehicle steering system \n", + "def normalized_label(inpfig, outfig):\n", + " plt.sca(inpfig)\n", + " plt.xlabel('Normalized time $v_0 t / b$')\n", + " plt.ylabel('Steering angle $\\delta$ [rad]')\n", + "\n", + " plt.sca(outfig)\n", + " plt.ylabel('Lateral position $y/b$')\n", + " plt.plot([0, 20], [0.95, 0.95], 'k--')\n", + " plt.plot([0, 20], [1.05, 1.05], 'k--')\n", + "\n", + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure(figsize=[9, 4.5])\n", + "\n", + "# Explore range of values for omega_c, with zeta_c = 0.7\n", + "outfig = plt.subplot(2, 2, 1)\n", + "inpfig = plt.subplot(2, 2, 3)\n", + "zc = 0.7\n", + "for wc in [0.5, 0.7, 1]:\n", + " # Place the poles of the system\n", + " K, kf, clsys = normalized_place(wc, zc)\n", + " \n", + " # Compute the step response\n", + " t, y, x = ct.step_response(clsys, np.linspace(0, 20, 100), return_x=True)\n", + " \n", + " # Compute the input used to generate the control response\n", + " u = -K @ x + kf * 1\n", + "\n", + " # Plot the results\n", + " normalized_plot(t, y, u, inpfig, outfig)\n", + " \n", + "# Add labels to the figure\n", + "normalized_label(inpfig, outfig)\n", + "plt.legend(('$\\omega_c = 0.5$', '$\\omega_c = 0.7$', '$\\omega_c = 0.1$'))\n", + "\n", + "# Explore range of values for zeta_c, with omega_c = 0.07\n", + "outfig = plt.subplot(2, 2, 2)\n", + "inpfig = plt.subplot(2, 2, 4)\n", + "wc = 0.7\n", + "for zc in [0.5, 0.7, 1]:\n", + " # Place the poles of the system\n", + " K, kf, clsys = normalized_place(wc, zc)\n", + " \n", + " # Compute the step response\n", + " t, y, x = ct.step_response(clsys, np.linspace(0, 20, 100), return_x=True)\n", + " \n", + " # Compute the input used to generate the control response\n", + " u = -K @ x + kf * 1\n", + "\n", + " # Plot the results\n", + " normalized_plot(t, y, u, inpfig, outfig)\n", + " \n", + "# Add labels to the figure\n", + "normalized_label(inpfig, outfig)\n", + "plt.legend(('$\\zeta_c = 0.5$', '$\\zeta_c = 0.7$', '$\\zeta_c = 1$'))\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM notes, 17 Jul 2019\n", + "* These step responses are *very* slow. Note that the steering wheel angles are about 10X less than a resonable bound (0.05 rad at 30 m/s). A consequence of these low gains is that the tracking controller in Example 8.4 has to use a different set of gains. We could update, but the gains listed here have a rationale that we would have to update as well.\n", + "* Based on the discussion below, I think we should make $\\omega_\\text{c}$ range from 0.5 to 1 (10X faster).\n", + "\n", + "KJA response, 20 Jul 2019: Makes a lot of sense to make $\\omega_\\text{c}$ range from 0.5 to 1 (10X faster). The plots were still in the range 0.05 to 0.1 in the note you sent me.\n", + "\n", + "RMM response: 23 Jul 2019: Updated $\\omega_\\text{c}$ to 10X faster. Note that this makes size of the inputs for the step response quite large, but that is in part because a unit step in the desired position produces an (instantaneous) error of $b = 3$ m $\\implies$ quite a large error. A lateral error of 10 cm with $\\omega_c = 0.7$ would produce an (initial) input of 0.015 rad." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Eigenvalue placement observer design (Example 8.3)\n", + "\n", + "We construct an estimator for the (normalized) lateral dynamics by assigning the eigenvalues of the estimator dynamics to desired value, specifified in terms of the second order characteristic equation for the estimator dynamics." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "L = [[1.4]\n", + " [1. ]]\n" + ] + } + ], + "source": [ + "# Find the eigenvalue from the characteristic polynomial\n", + "wo = 1 # bandwidth for the observer\n", + "zo = 0.7 # damping ratio for the observer\n", + "eigs = np.roots([1, 2*zo*wo, wo**2])\n", + " \n", + "# Compute the estimator gain using eigenvalue placement\n", + "L = np.transpose(\n", + " ct.place(np.transpose(A), np.transpose(C), eigs))\n", + "print(\"L = \", L)\n", + "\n", + "# Create a linear model of the lateral dynamics driving the estimator\n", + "est = ct.StateSpace(A - L @ C, np.block([[B, L]]), np.eye(2), np.zeros((2,2)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Linear observer applied to nonlinear system output\n", + "\n", + "A simulation of the observer for a vehicle driving on a curvy road is shown below. The first figure shows the trajectory of the vehicle on the road, as viewed from above. The response of the observer is shown on the right, where time is normalized to the vehicle length. We see that the observer error settles in about 4 vehicle lengths." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM note, 27 Jun 2019:\n", + "* As an alternative, we can attempt to estimate the state of the full nonlinear system using a linear estimator. This system does not necessarily converge to zero since there will be errors in the nominal dynamics of the system for the linear estimator.\n", + "* The limits on the $x$ axis for the time plots are different to show the error over the entire trajectory.\n", + "* We should decide whether we want to keep the figure above or the one below for the text.\n", + "\n", + "KJA comment, 1 Jul 2019:\n", + "* I very much like your observation about the nonlinear system. I think it is a very good idea to use your new simulation\n", + "\n", + "RMM comment, 17 Jul 2019: plan to use this version in the text.\n", + "\n", + "KJA comment, 20 Jul 2019: I think this is a big improvement we show that an observer based on a linearized model works on a nonlinear simulation, If possible we could add a line telling why the linear model works and that this is standard procedure in control engineering.\t" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Convert the curvy trajectory into normalized coordinates\n", + "x_ref = x_curvy[0] / wheelbase\n", + "y_ref = x_curvy[1] / wheelbase\n", + "theta_ref = x_curvy[2]\n", + "tau = v0 * T_curvy / b\n", + "\n", + "# Simulate the estimator, with a small initial error in y position\n", + "t, y_est, x_est = ct.forced_response(est, tau, [delta_curvy, y_ref], [0.5, 0])\n", + "\n", + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure(figsize=[9, 4.5])\n", + "\n", + "# Plot the actual and estimated states\n", + "ax = plt.subplot(2, 2, 1)\n", + "plt.plot(t, y_ref)\n", + "plt.plot(t, x_est[0])\n", + "ax.set(xlim=[0, 10])\n", + "plt.legend(['actual', 'estimated'])\n", + "plt.ylabel('Lateral position $y/b$')\n", + "\n", + "ax = plt.subplot(2, 2, 2)\n", + "plt.plot(t, x_est[0] - y_ref)\n", + "ax.set(xlim=[0, 10])\n", + "plt.ylabel('Lateral error')\n", + "\n", + "ax = plt.subplot(2, 2, 3)\n", + "plt.plot(t, theta_ref)\n", + "plt.plot(t, x_est[1])\n", + "ax.set(xlim=[0, 10])\n", + "plt.xlabel('Normalized time $v_0 t / b$')\n", + "plt.ylabel('Vehicle angle $\\\\theta$')\n", + "\n", + "ax = plt.subplot(2, 2, 4)\n", + "plt.plot(t, x_est[1] - theta_ref)\n", + "ax.set(xlim=[0, 10])\n", + "plt.xlabel('Normalized time $v_0 t / b$')\n", + "plt.ylabel('Angle error')\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output Feedback Controller (Example 8.4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM note, 27 Jun 2019\n", + "* The feedback gains for the controller below are different that those computed in the eigenvalue placement example (from Ch 7), where an argument was given for the choice of the closed loop eigenvalues. Should we choose a single, consistent set of gains in both places?\n", + "* This plot does not quite match Example 8.4 because a different reference is being used for the laterial position.\n", + "* The transient in $\\delta$ is quiet large. This appears to be due to the error in $\\theta(0)$, which is initialized to zero intead of to `theta_curvy`.\n", + "\n", + "KJA comment, 1 Jul 2019:\n", + "1. The large initial errors dominate the plots.\n", + "\n", + "2. There is somehing funny happening at the end of the simulation, may be due to the small curvature at the end of the path?\n", + "\n", + "RMM comment, 17 Jul 2019:\n", + "* Updated to use the new trajectory\n", + "* We will have the issue that the gains here are different than the gains that we used in Chapter 7. I think that what we need to do is update the gains in Ch 7 (they are too sluggish, as noted above).\n", + "* Note that unlike the original example in the book, the errors do not converge to zero. This is because we are using pure state feedback (no feedforward) => the controller doesn't apply any input until there is an error.\n", + "\n", + "KJA comment, 20 Jul 2019: We may add that state feedback is a proportional controller which does not guarantee that the error goes to zero for example by changing the line \"The tracking error ...\" to \"The tracking error can be improved by adding integral action (Section7.4), later in this chapter \"Disturbance Modeling\" or feedforward (Section 8,5). Should we do an exercises? \t" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "K = [[0.49 0.7448]]\n", + "kf = [[0.49]]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnkAAAE8CAYAAAC8dKfjAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJzs3Xd8jdcfwPHPyTaSGBFEECOUSJBpz1o/I6qUoIqitKrVaktLqdEqWqMtLTVao6g9ahSxZxJJSBA7YiRBRCI7Ob8/bqRBSCL35kZy3q9XXjc393nO+V7h8b3nOed7hJQSRVEURVEUpXAx0HcAiqIoiqIoivapJE9RFEVRFKUQUkmeoiiKoihKIaSSPEVRFEVRlEJIJXmKoiiKoiiFkEryFEVRFEVRCiGV5CmKoiiKohRCKslTFEVRFEUphFSSpyiKoiiKUggZ6TsAXbOyspJ2dnb6DkNRFB3y9fW9K6Usp+84CgJ1zVOUwi0317tCn+TZ2dnh4+Oj7zAURdEhIcR1fcdQUKhrnqIUbrm53qnbtYqiKIqiKIWQSvIURVEURVEKoUJ/u1ZRFEXJREoI2QU3ToCVPTi+BYbqvwJFKYzUv2ylUEtOTiYsLIyEhAR9h6JogZmZGba2thgbG+s7lFeTlLDxPQhcAwhAgs9S8FoNJcrqOzpFUbRMJXlKoRYWFoa5uTl2dnYIIfQdjpIHUkru3btHWFgY1apV03c4r6ZTv2sSvGafQOsvIWgTbP4A1vSHAZvAyFTfESqKokVqTp5SqCUkJFC2bFmV4BUCQgjKli2rRmVfVtx9+PdrqPk6tJkAhsbg1Au6z4fQo3Dge31HqCiKlqkkTyn0VIJXeKjfZR74LoPkOGg3BQwyXfode0L9vnBkHkSc11t4iqJon0ryFEVRCrvUZDi5CKq3gvJ1n329/VQwKQH/TsjvyBRF0SGV5CmKHsyZM4e4uDitHZfZ+fPnadCgAQ0bNuTy5csvGyIA+/fvp0uXLgBMmjSJWbNm5ak9gIEDB7Ju3bo8t6PkwvUjEHMLXN/N+vUSZaHpR3BxN9w4lb+xKYqiM0UiyfO9fp8+C4/x3nIfbkfH6zscRdFpkrdp0yY8PT05ffo0NWrUeNkQlcLkwg4wMtPMx3se92FQ3ErNzVOUQqRAra4VQnQE5gKGwO9SyunPOa4n8DfgJqV84f49SalpvLPkFMVMDHmUmMKAxSfZ+mEzzIwNtR6/UrB9szWI4FsPtdpmXRsLJnZ1eO7rjx494q233iIsLIzU1FQmTJhAeHg4t27donXr1lhZWeHt7c2IESM4deoU8fHx9OzZk2+++YZ58+Y9c9zu3buZOHEiiYmJ1KhRg6VLl1KyZMmM/v755x/mzJmDoaEhBw8exNvbmxUrVjBv3jySkpLw8PBg/vz5GBoaPretnTt38vHHH2NlZYWzs/MT7ycgIIA2bdpw48YNPv/8c4YOHUpsbCyenp5ERUWRnJzM1KlT8fT0BODPP/9k1qxZCCFwcnJi+fLlT7Q3YcIEbty4wZIlSzAwKBKfOfOflHDhH82tWpPizz/OtCS4D4X938HdS2BVM78iVBRFRwrMVVUIYQj8AnQC6gJeQohnJo8IIcyBUcCJnLQbHp1ASloaG0Y0YX4/Zy5GxLL8mNrmUskfO3fuxMbGhoCAAM6ePUvHjh0ZNWoUNjY2eHt74+3tDcC0adPw8fEhMDCQAwcOEBgY+Mxxd+/eZerUqezZswc/Pz9cXV358ccfn+jvf//7H8OHD2f06NF4e3tz7tw51qxZw5EjR/D398fQ0JCVK1c+t62EhASGDh3K1q1bOXToEHfu3Hmi/cDAQLZv386xY8eYPHkyt27dwszMjI0bN+Ln54e3tzeffvopUkqCgoKYNm0a+/btIyAggLlz5z7R1ueff05ERARLly5VCZ4uRZyDB6FQu1P2x7oMAgNjTakVRVFeeQVpJM8duCSlvAIghFgNeALBTx03BZgBjMlJo9HxyYz0qErlMsWpXKY4zWpa8fvhKwxuVg1DA7VSryh50Yibrjg6OjJmzBi++OILunTpQvPmzbM8bu3atSxcuJCUlBRu375NcHAwTk5OTxxz/PhxgoODadq0KQBJSUk0btz4hf3v3bsXX19f3NzcAIiPj8fa2vq5bZ0/f55q1aphb28PQP/+/Vm4cGFGe56enhQrVoxixYrRunVrTp48SefOnfnyyy85ePAgBgYG3Lx5k/DwcPbt20fPnj2xsrICoEyZMhntTJkyBQ8PjyfaVnTk2mHNY4222R9rXh4cuoP/SmgzXjO6pyjKK6sgfXyuBNzI9Dws/WcZhBANgcpSym0vakgIMUwI4SOE8AF4u1HVjNf6elQh/GEixy7f01rgivI8tWrVwtfXF0dHR8aNG8fkyZOfOebq1avMmjWLvXv3EhgYSOfOnbOsBSelpF27dvj7++Pv709wcDCLFy9+Yf9SSt55552Mcy5cuMCkSZNe2NaLypQ8/ZoQgpUrVxIZGYmvry/+/v6UL1+ehIQEpJTPbcvNzQ1fX1/u37//wvgVLbhxAsxtoFTlnB3v/h4kPoSAv3Qbl6IoOleQkrys/jeQGS8KYQDMBj7NriEp5UIppauU0rWujQV2ViUyXmvzmjXmpkZs8r+pjZgV5YVu3bpF8eLF6d+/P2PGjMHPzw8Ac3NzYmJiAHj48CElSpTA0tKS8PBwduzYkXF+5uMaNWrEkSNHuHTpEgBxcXGEhIS8sP+2bduybt06IiIiALh//z7Xr19/bluvvfYaV69ezViV+9dfT/5Hv3nzZhISErh37x779+/Hzc2N6OhorK2tMTY2xtvbm+vXr2f0vXbtWu7du5fR92MdO3Zk7NixdO7cOeP9KTpy4yRUds/58bauYNMQTi7UzOdTFOWVVZCSvDAg80dNW+BWpufmQD1gvxDiGtAI2CKEcH1RowZPjSSYGRvSsV4Fdp69Q2JKqjbiVpTnOnPmDO7u7jRo0IBp06Yxfvx4AIYNG0anTp1o3bo19evXp2HDhjg4ODB48OCMW6hPH1euXDmWLVuGl5cXTk5ONGrUiPPnX1y8tm7dukydOpX27dvj5OREu3btuH379nPbMjMzY+HChXTu3JlmzZpRtWrVJ9pzd3enc+fONGrUiAkTJmBjY0O/fv3w8fHB1dWVlStX8tprrwHg4ODAV199RcuWLalfvz6ffPLJE2316tWLoUOH0q1bN+Lj1ap3nXh4G6JDobJHzs8RAtyGwN0QCD2uu9gURdE5IQvIJzUhhBEQArQFbgKngL5SyqDnHL8fGJPd6lpXV1fp4/PkIf8GhzP0Tx/+GtqIxjXUptyF2blz56hTp46+w1C0KKvfqRDCV0r5wg98BU121QSEEC2AOYAT0EdKmaPigk9c84I3w9oBMGSvZoQupxJjYVYtcHwTuv2U8/MURdG53FzvCsxInpQyBRgJ7ALOAWullEFCiMlCiG7a7KtR9TIYGgiOXLqrzWYVRVFyJIfVBEKBgcCql+7oph8YGEEFx9ydZ1oS6nrC2Y2QlLs6jYqiFBwFaXUtUsp/gH+e+tnXzzm21cv2Y25mTIPKpTh86S5jOtR+2WYURVFeVrbVBKSU19JfS3vpXiLOgVUtMDLN/bkNvCBgFZzfDk69XjoEpeiIjo5mwYIFHD16lMuXLxMTE0Px4sWxs7PDzc2NLl264O7urvagzkcFZiQvvzWtaUVg2AOi45P1HYqiKEVPttUEciNzRYHIyMj/XogIBuss9qrNiarNwLKKppyKorxAdHQ0AMbGxkyZMoWLFy9Sq1Yt2rRpg6OjI+Hh4Xz77bc0atSI+vXrc+jQIT1HXHQUqJG8/NSkRlnm7b2I7/X7tHmtvL7DURSlABBC2AEfADWA+4A/sFVKqe0K6i+sJpBbUsqFwELQzMkDICEaom+A66CXa9TAAOr3gYMzIfomWL50DqoUYp999hk7duwgICCA4sWLc/PmTUqVKvXMcQ8ePGD9+vXMmjULU1PNyPKLyiwp2lFkR/KcbC0xNBCcDn2g71AURSk4NgPn0cyXawfUBw4KIX4RQrzEPc/nyq6aQN5FnNM8WuehCHgDL0BC4BqthKQUPm3btqV79+6kpmqqVWSV4D3++bvvvktQUBDu7pqSPu+//z4TJkygoCwALYyKbJJX3MSI1yqYqyRPUZTMDKWUi6WUe4H7UsqhaEb1rpE+UqYlpwB7IUQ1IYQJ0AfYosX2ITy9MEH5l7xdC1Cmuqb8SuAaVTNPyfB4j2zQ1LycOnUqJiYmOTr38RaGqampJCQkkJaWpkbzdKjIJnkADauUwv/GA1LT1MVLKTiWLVvGrVsvP6hz7do1Vq16/oLMzz77DAcHBz777LOX7uOxVq1a8bhcR8mSed8C69q1a9SrVy/P7eTBHiHEyPTvJWhW/kspZwIv3kMuF3JSTUAI4SaECAN6Ab8JIbIsJ/VcEefAxBwsc7jTxfM49YbI83A7IG/tKIXCmjVr8PT0ZMWKFSQlJb10O4aGhixdupSpU6cC4OPjw9WrV7UVppKuSCd5zlVKE5uYwqWIWH2HoigZdJ3k/fbbb/j5+TFz5syX7qMQ+wSwTN8S0SZ9QUN/IcQvgFb3QpRS/iOlrCWlrCGlnJb+s6+llFvSvz8lpbSVUpaQUpaVUubuvuu9i2BVU1PcOC8c3gADYwhcm7d2lFfe5s2b6du3L02aNGHv3r05Hr17ESEEqampvP3227Ro0YIrV65oIVLlsSK78AKgQWXN3AH/G1HUrmCu52gUndsxFu6c0W6bFRyh0/QXHvLjjz+yZMkSAIYMGcLHH3/MtWvX6NKlC2fPngVg1qxZxMbGUq9ePXx8fOjXrx/FihXj2LFj1KlTh969e+Pt7Q3AqlWrqFmzJgMHDqRLly707NkT0IykxcbGMnbsWM6dO0eDBg145513GD16dEYs3bp149GjR3h4eDBu3DjatGnD8OHDCQ0NBWDOnDk0bdqUR48e8eGHH3LmzBlSUlKYNGkSnp6exMfHM2jQIIKDg6lTp84zO1V8+umneHt7U7p0aVavXk25cuVYtGgRCxcuJCkpiZo1a7J8+XKKFy9OeHg4w4cPz7ioL1iwABsbm4y2rly5wptvvsnChQtxc3PLy28px6SUacA0IcRs4HWgAVAaOAt8lS9BaMv9K2Cbi+3Mnqd4GajVAc78De0mg2GR/m+jyDpx4gReXl64urryzz//UKJEiexPyiFDQ0P++usv2rZtS+vWrTlw4AB2dnZaa78oK9IjeXZlS1DS1IjgWw/1HYpSSPn6+rJ06VJOnDjB8ePHWbRoEadPn37u8T179szYHszf359ixYoBYGFhwcmTJxk5ciQff/zxC/ucPn06zZs3x9/f/4kED2DLli0UK1YMf39/evfuzUcffcTo0aM5deoU69evZ8iQIQBMmzaNNm3acOrUKby9vfnss8949OgRCxYsoHjx4gQGBvLVV1/h6+ub0fajR49wdnbGz8+Pli1b8s033wDQo0cPTp06RUBAAHXq1GHx4sUAjBo1ipYtWxIQEICfnx8ODv8NVF24cIE333yTpUuX5luCl5mUMk5KuUVKOVlKOVpKuUBK+epM4E1JgugwzZw6bXDqDY8i4Mp+7bSnvFKuXLlC165dqVChAlu3btVqgvdYgwYN2LNnDzExMbRu3Zo7d+5ovY+iqEh/JDMwENSpaE7wbZXkFQnZjLjpwuHDh3njjTcyLoo9evTg0KFDdOuWu01cvLy8Mh6fTtzyYs+ePQQHZ9Tf5eHDh8TExLB79262bNnCrFmzAEhISCA0NJSDBw8yatQoAJycnHBycso418DAgN69ewPQv39/evToAcDZs2cZP348Dx48IDY2lg4dOgCwb98+/vzzT0DzSd7S0pKoqCgiIyPx9PRk/fr1TyR+Si48CAWZpr0kr1YHMCsFgavB/nXttKm8EhITE+nVqxfJycns2LEDa2trnfXVsGFDdu3aRatWrejatSv79+/XSUJZlBTpJA+gbkUL1vvdJC1NYiDI+/wVRcnkeaUBjIyMSEv7byODhISEF7aTefXZ4+8ztyGlfKlJ0GlpaRw7dixjxDBz3OvXr6d27Wd3hMnpSrjHxw0cOJBNmzZRv359li1bxv79+194nqWlJZUrV+bIkSMqyXtZ99PnNWkryTMy1czNC1gNiTFgqqa3FBVjxozBz8+PzZs3Z3k90DY3NzdWr15N9+7d6devH+vXr8fQ0FDn/RZWRfp2LUBdGwtKJd0iZX4TmFENgjbqOySlEGnRogWbNm0iLi6OR48esXHjRpo3b0758uWJiIjg3r17JCYmsm3btoxzzM3NiYmJeaKdNWvWZDw2bqxZ5GlnZ5dxu3Tz5s0kJyc/9/znad++PT///HPGc39/fwA6dOjATz/9lJGkPr7F3KJFC1au1OyAcPbsWQIDAzPOTUtLY926dYBm3mCzZs0AiImJoWLFiiQnJ2ecC5r6WgsWLAA05RQePtSMqJuYmLBp0yb+/PPPFy4g0SWh0V8I8XX68ypCCC1McMsn2k7yQFMYOSUezm3L/lilUIiMjOSvv/5i9OjRub77kBddu3Zl7ty5bN68mQkTJuRbv4WRSvIqWjLdaBEGD65ByQqw4T24e0nfYSmFhLOzMwMHDsTd3R0PDw+GDBlCw4YNMTY25uuvv8bDw4MuXbrw2muvZZwzcOBAhg8fToMGDTIWNiQmJuLh4cHcuXOZPXs2AEOHDuXAgQO4u7tz4sSJjNsaTk5OGBkZUb9+/Yxjn2fevHn4+Pjg5ORE3bp1+fXXXwGYMGECycnJODk5Ua9evYwL7YgRI4iNjcXJyYkZM2ZkFDUFKFGiBEFBQbi4uLBv3z6+/lqz7fSUKVPw8PCgXbt2T7zPuXPn4u3tjaOjIy4uLgQFBT3R1rZt25g9ezabN29+6T//PJiPpmSKV/rzGDQFkl8NUVc15VNKWGmvzcoeUKqq5patUiSUK1eOgIAApk/P/6kuI0eOZOjQoXz33XcEBKjyPS9LFPZK066urvJxHa+sJN44jeniVnhX/YjWPT+An1ygdkd48/d8jFLRlXPnzlGnTh19h5EndnZ2+Pj4YGWlxf+wX2FZ/U6FEL5SSldt9SGE8JNSOgshTkspG6b/LEBKWV9bfeiKq6ur9BldDWLuwHAt7xG6b5pmm7NPgsHCJvvjlVeSlJJt27bRuXPnjOLF+pCYmMiuXbvydRTxVZCb612RH8kzDV5HEkZsSGsJ5uXB5R04uwFiI/QdmqIo+pMshDAkvSCyEKIckPbiUwqQqGtQ2k777dbvA0g4s077bSsFxoEDB+jWrRvLly/XaxympqYZCV5wcHC2c5dlcjxR189yNeAg13x2cS3gAPdCzyOT4vIj3AKpyC+84OK/XC3RgJPh6SOaDfrBsZ/h3BZwG6Lf2BQFTXFjJd/NAzYC1kKIaUBPYLx+Q8qF6JtQs5322y1bAyq5arY5azpK++0rBULLli3ZuHEjXbp00XcoANy8eRMXFxdGjx7Nt99+m/HzlLhoLhzdQsr5HZSNCqBiyk1KC0npp85PkQaEGdoSVcoBUfN17Ju+QQnLsvn7JvSkaCd5D2/B3Qs8qDma8LOJ3ItNpKx1HbCqDUGbVJKnKEWUlHKlEMIXaAsIoLuU8pyew8qZtBRIfgSWlXTTfv0+8M8YuHMWKuh1CzpFB2JiYjA3N6d79+76DiVDpUqV+OWXXzKSzvCQU9zaPY86d3fiQBLRsgQXzJy4at2OtLI1KW5eFmlcDJEcR2LMXeS9K5SICsbu3mHK3N9B8onPCTBvjIHbYOo1744wKLyrd4t2khemmatnWqMpnE3iQngMTWpYaUoFHPheM6fFvIKeg1QURR+klOeB8/qOI9dSNaussbTVTfsOPWDnWM0CjApTddOHohd+fn60bt2aDRs20LZtW32H84TBgwcTfiWAgD/eoeq9I7xmasopi3aYOHtRv0l73E1Ns20jOTmZIF9vonzXUzfyH8p4DybswATuNPiQhv8bgqGRcT68k/xVtOfk3fIDA2Mq1tZU1L8Ynr6HbZ0ugITL+/QXm6Io+U4IESOEeJj++Pj7x89fjarpqen1Ei10lOSVKKu5FXxmHaSl6qYPJd+lpaUxcuRIzMzMcHFx0Xc4T0iKe0jgovcou6wFn/y6j7ZbynDvPX+af7oKj9ZdMctBggdgbGyMQ6P2NPvgN0qMvcCxhjNIxARXv7Hc+rY+gXtXIdNenam3OVHEkzx/KF8X69KWWJgZERKeXlvM2gGKW6ktfBSliJFSmkspLdIfH3//+LmFvuPLkYyRPB3drgWo3xtibsPVg7rrQ8lXK1eu5NixY3z//feUKlVK3+FkCPXdyf1ZLtQLW8OhUt1oP2wSfueus2lD3mrampoVp7Hne1Qf74uPxzxSpcDp0AiCZrxO+NWg7Bt4RRTtJC/iHFg7IISgVnnz/0byDAygeku4cgAKeYkZpWA4dOgQDg4OT9TGU/RHCPFJFl/vCiEa6Du2bKUmgYExlNDd9lPU6gSmlpoFGMorLyEhgfHjx+Pi4sKAAQP0HQ4AMi2VwOVfYLulD0lpBvi2WUnrT5bz+edj6dSpE1988QWXLuW9pq0wMMS10zvYjPXjmP0YqsSfw2JZK07+NY3U1Fd/pLroJnnxURB7B6w1xVnty5sTEhHz3zZU1VpqXo+8oMcglcJESvnEVmaZrVy5kjFjxuDv7//MFmO5bUvRCldgOFAp/WsY0ApYJIT4XI9xZS81WVPDTpf1zYzNwMETgrdA0iPd9aPki/nz5xMaGsqMGTP0WhfvscSYe1z4sRNOl3/laMnXKTbqOG4tOwOa7RIXLVqEsbExAwcO1FoiZmJqSuN+E4h99zAhxerjfmEG56e34Pa1V2O91fPo/7epLxHp86nLaYqq1ipfkgdxyUTGJmp+Xr2l5vGalouJKnrVqlWrbL9mzZr1xPHLli0D4O7du88cm51r165Rp04d3n//fZydnVm+fDmNGzfG2dmZXr16ERsby++//87atWuZPHky/fr1A2DmzJm4ubnh5OTExIkTs2zrxo0b7N69+5n2QFNAeeLEiTg7O+Po6Mj585q/77GxsQwaNAhHR0ecnJxYv349wHPbKcLKAs5Syk+llJ+iSfrKAS2AgfoMLFupSbpbdJGZUx/NKt5zW3Xfl6IzDx48YNq0aXTo0IE2bdroOxyibl4ick5Lqsf48G+NcTT5ZC3lyjxZFKVSpUrMmzePI0eOMHfuXK32b1OlBk6f78a3wRSqJl3GYlkr/Hcs1mof+anoJnmP93YsWwOAWuU1G25n3LItVVWzzdmNk/qITilELly4wIABA/j3339ZvHgxe/bswc/PD1dXV3788UeGDBlCt27dmDlzJitXrmT37t1cvHiRkydP4u/vj6+vLwcPHnyirdOnT1OiRAmmTp36THuPWVlZ4efnx4gRIzIS1ylTpmBpacmZM2cIDAykTZs23L1794XtFFFVgKRMz5OBqlLKeCBRPyHlUH4leVUaQ5ka4LNU930pOvP9998TFRXF999/r+9QiAg5Sdrvr2Oech+fFkto9/ZYDAyzTlPefvttunXrxpdffpnxIVZbhIEBLt1HET3oIKFG1Whw4hOO/zyQhPhXr6hy0S2h8iAUEGBZGQB765IAhITH0LSmFQgBld0gTCV5hcn+/ftf+ngrK6tcnw9QtWpVGjVqxLZt2wgODqZp06YAJCUl0bhx42eO3717N7t376Zhw4aAZvTt4sWLVKlSJaMtgOPHj7+wvR49egDg4uLChg0bANizZw+rV/+392jp0qVzHFcRswo4LoR4vHFuV+AvIUQJIFh/YeVAWnL+bDlmYACug2D3eAgPgvIOuu9T0aqbN28yZ84c+vXrR/36+t2xL+zMAUqt70MMxbjdfQNNGjZ64fFCCH777TccHBx45513OHLkCEZG2k1pKtnVotxnBzix9GMa3VnFpR/OYDFgFdZVamu1H10qwknedc2F0MgEgHLmplgWMyYkPNNtqsoemlsRsRFQUoeTmJVCrUSJEoBmHl27du3466+/Xni8lJJx48bx3nvvPfHza9euZbSVk/ZM08sKGBoakpKSknGOEOKZ/nISV1EipZwihPgHaIamGPJwKeXjTbD76S+yHJASzCvmT18N+sHeKeCzBDr/kD99KlqTkJDA66+/zuTJk/Uax82AfZTe6MV9ShHfdyP1atXN0XkVKlRg/vz59OnTh1WrVulk0YiJqSkewxdwendTahz5jNQlr3Oh00Jqe3TSel+6UKBu1wohOgohLgghLgkhxmbx+nAhxBkhhL8Q4rAQImd/E7LyIBRKVcncNrXKl+Ti4zIqALbumkd1y1bRgkaNGnHkyJGMFWFxcXGEhIQ8c1yHDh1YsmRJxry4mzdvEhHx7F7KOW0vs/bt2/Pzzz9nPI+KinqpdooCKaWvlHKulHJOpgTv1ZBfH0qLl4F6PSBgDSQW+Xmcr5waNWqwdetWqlWrprcYwoMOUXqjF5GUIWXANmrlMMF77K233mLr1q30799fRxFqNGzfn3t9dxIjSlL9n36cXPdqTGkpMEle+mbgvwCdgLqAVxZJ3CoppaOUsgEwA3j5P+UHoZp5d5nYlzcnJDzTCtuK9TWlCNQtW0ULypUrx7Jly/Dy8sLJyYlGjRplOZekffv29O3bl8aNG+Po6EjPnj2JiYl56fYyGz9+PFFRUdSrV4/69evj7e39Uu0UdkIIUyFEXyHEl0KIrx9/6TuuHCuZjzv1uA6GpBg4szb/+lTybNmyZVy5ckWvMURe9qPY3324iyXJ/bdQrbp9rtsQQtClSxcMDAy4c+cOycnJOohUo1rt+lh+eIhzxRrifvYbjv78LsnJSdmfqEdCFpA6cEKIxsAkKWWH9OfjAKSU3z3neC9ggJTyhWOmrq6u0sfnqQ/hqckw1Rqaj4E2X2X8eNmRq0zaGsyJL9tS3sJM88NFbcHQBAbveOn3pujPuXPnqFOnjr7DULQoq9+pEMJXSumqrT6EEDuBaMAXyKjRIKUs8PckXW0Mpc+ZkIxFZTonJfzWHFJTYMRR3ZZuUbQiOjoaW1tbBg8erPXzlaSSAAAgAElEQVTVqTl1LywEubg9qWlwv88W6tRxylN7N2/exNHRkU8//ZSvvvoq+xPyIDUlGd9FI3EPX02gqTNV31uDZZn8m9KVm+tdQZqTVwm4kel5GODx9EFCiA+ATwAT4OXWe0eHgUx74nYtPLnCNiPJq+wOPktIS05kj/cB1q1bR1BQEN26deOLL754qe4VRSnwbKWUHfUdxEsrWV7nXSQmJhIQEEDt2rWxbDyS4z+9y689O5BoakWxYsUoWbIkVatWxd7enoYNG1K5cmWdx6TkjKWlJSEhIVpfqJBTcfdvkrSkG8XSkrnTfT318pjggaasyujRo+nVq5cWInwxQyNj3Ef8hs9GB5z8J3H7p9ZE911NFXtHnfedWwXpI5fI4mfPDDNKKX+RUtYAvgDGZ9mQEMOEED5CCJ/IyMhnD3gQqnks/eztWuC/7c0AbN04djUWV+f6dOjQgbVr12JsbJxRsDY6Opq5c+dSUEZEFUXRiqNCiIJ3xc4JYQimJUlLk8QlpWit2aioKNauXUtgYCAAx44dw8PDg+PHj0O9N4mQZdl34DA+Pj7s3r2bP/74gzFjxuDp6UmVKlWwt7fn/fff58aNG9n0pOhSYmIiUkoqVqxIuXLl8r3/1IRYIn/1xCL1PiGvL6VeNqtoc2PChAnUqlUr34rFu74xisudVmIpH2CxsiNnjmzXeZ+5VZCSvDAg80c9W+DWC45fDXTP6gUp5UIppauU0jXLv8QPrmsenxrJsyppQunixlyM+C/JW7ArmBbL4rgbGcmyZcsIDw9n//79jBo1CoC1a9fy6aefEhRUePa6K2xUAl545OPvshngm74QLDB9wVdgfnWeJ4ZGHLl0l9dnH6Du17to88N+fK7dz3UzUkoCAwOZPn06LVq0oFy5cvTu3TujHI+bmxsbNmzA1dUVDI3pNnQcoR+acHHfSsLCwoiOjubevXscO3aMOXPmULduXVatWoWJiaaiwY0bN0hKKtjzmQqjUaNG0aFDB73smCNTUwhZ0AfbxEuccJmFe/P2Wu8jKSmJnj17MnXqVK23nZU6jToRN+BfHhqUovbutzm2Xj+3v59LSlkgvtDcOr4CVENzKzYAcHjqGPtM33cFfLJr18XFRT7D+zspJ1pImZL0zEu9FhyVb84/IhMTE+V7770nAdn5teLywYrBz7YjpUxNTZUnT57M8jVF/65cuSIjIyNlWlqavkNR8igtLU1GRkbKK1euPPNaTq4FufkCqmb1pc0+dPXVsIq5rPXVP7L1TG85598Q2fz7fbL6uO3ywIWIbP+MHz58KDdu3CiHDh0qK1WqJNHcTZHOzs5y/Pjx8ujRozIlJSXrkxNipPyuipR/9X1u+wkJCVJKze+ycePGslmzZtnGpGjPtWvXpLGxsRwxYoRe+vdfPFLKiRZy1+JJOu2nb9++0sjISJ4+fVqn/WT2MCpSnvmulZQTLeSh+e/L5ORknfWVm+tdgZmTJ6VMEUKMBHYBhsASKWWQEGIymje0BRgphHgdTfX5KOCdl+osNhyKlwVD42desi9fkq2nw2jfvj0HDhzgiy++YJrDZQwfZD1SZ2BggJubGwD//vsvDg4O2NjkQyFSJUdsbW0JCwsjy9v2yivHzMwMW1vd7+YgpbwuhCgN2ANmmV66rq0+hBAdgblorne/SymnP/W6KfAn4ALcA3pLKa9l1+6jFIG5mRGr32uEtbkZg5rZ8davx/jwr9Ns+7AZlcsUzzg2LS2NGzduULWqZuqKi4sLFy9exNzcnPbt2/O///2Pjh075uyaZloS3IfBwRlw5wxUePZu9+PajQATJ04kISEBgJSUFK5du0bNmjWz70cHHiWm4BcaxfV7cTxKTCElTWJTyowqZYrjYGOJmbGhXuLStsc734wd+0yFMp07v20e9UP/ZL9ld14fqNuF6vPmzWPv3r0MHDiQkydPZowe65J5KSvqfLoLn9+G0Sx8Bb4/XMV+xCosLErpvO8XKTCra3Uly9W1q/tptjV7/9gzxz9eYdvXxJd6r9Wkb9++sH+65mvcDTA1z7KfO3fuULVqVby8vDL2OlUUJX/oYHXtEOAjNNNG/IFGwDEppVY290wvGRUCtEMzVeUU4CWlDM50zPuAk5RyuBCiD/CGlLJ3dm3bVSwj5+08S7f6/yVmN+7H0WnuIRpWKsGohqa4uroihGDo0KFs2bKF27dvY2BgwIYNGyhTpgxNmjR5uf8Y46NgbgOwdYX+63N82uzZsxk3bhw//fQTQ4YMeaZgty4kpaSx/cwtVh4P5fSNB6SmZf1/YTFjQ5rbW9Glvg3/q1cBo+dss1XQRUZGUrVqVfr06cOSJUvyte9b/v9SblNv/I0aUG/MDoqZmWZ/Uh5t3ryZ7t278/XXX/PNN9/ovL8MUuKzdjoNg7/nimE1ir/zN5WqavfDy6u6ujb/xIZnWSx0/fr1xCWZAAZ0HvC+ZnszgEougIRb/lCteZZNVqhQgdGjR/P9998zevRovW8RoyhKnnwEuAHHpZSthRCvAdr8n8IduCSlvAIghFgNePLklmmewKT079cBPwshhMzmk3lCisTk9hk2XT1JREQEV69e5cqVK8T4B7Pi0nlWpKVw6dIlatSowYABA2jRogWpqakYGBhkbIX30oqVhhZjNFudXTkA1Vvm6DQvLy927tzJsGHDOHDgAL/99tsTu7tok5SSzUeD2LnvX6wTrtKlRBJjq4J1KXNKl7XGuJQNVGxAmKEtV+7Fc+jiXfacC2d3cDjflyrGsBbV8XKvgonRq5Xs/fLLL8THx/PZZ5/la7+x4VcptvldwqiAzZC/8iXBA/D09KR///58++23eHp64uzsnC/9IgSuvccRfNCeKvtGEr/0dYI6L8PBrVX+9P90OEVyJG+Oo2Zz7R4LM36UnJxM/fr1qVKtBucdhzOxa10GNU2vAv7oHsysDu0mQ9OPntvXgwcPqFKlCl27dmXlypW6eDuKomRBByN5p6SUbkIIf8BDSpkohPCXmkLs2mi/J9BRSjkk/fnb6f2MzHTM2fRjwtKfX04/5m42bT9xUTc2NsbOzo5q1apxIbksFpVrc3Dux5QqZamNt/Ks5AT42VUzJWaod47r5qWmpvLtt98yadIknJ2d2b59O9bWWqw9djuQKJ+/eRiwlaopV598zaQkpCZpvjL/rObr4PAGaTXbs/dyDL8duIzP9ShqWpdksqcDTWpYaS8+HXr06BFVqlShWbNmbN68OfsTtCQtMY4bP7SgTGIYlzy30tDZLd/6Brh//z4ODg6ULVuWU6dOZVTFyC9hF3wxWt0Hy7QHnHb9niZdB2ul3dxc716tjyLaIKVmL9oSmlW3oaGhPHr0CGNjY3bs2MHGdWuxLGbMxYhMW/SUKKvZHeOm7wubLlWqFMOGDWPNmjVcv661qTuKouS/MCFEKWAT8K8QYjMvXu2fWzkpGZWjslLwZNko63JlOXz4MKdPnyY0NJT4+HhCQkLYtWsXP82excOKrhwOfZTnN/BcxmbQZjzc9ofANTk+zdDQkAkTJrBp0yaCgoJo0qRJxlZ7Ly0lEU6vgIWt4bfmWPj8RERKMQJrf0Ravw3w6QWYcA++vAnjI+DL2/D+CXjjN3DsBdePwN/vYDC3Hu0ilrJuQG2WDHQlMSWVvotOMHZ9IPFJqdnHoWdLlizh/v37+VvbVUrO/z6YqkkXOdFwer4neABlypRh6dKlBAUF6aWurW1tF4q/f4AbpjVp4jua/b+PJTU1n1c153SFxqv69czq2vhoKSdayLi9M+X06dNl8eLF5fDhw584pOeCI7LngiNPnrd2oJQ/Osjs3LhxQxoaGsovvvgi22MVRdEOtLy6NvMX0BLoBphosc3GwK5Mz8cB4546ZhfQOP17I+Au6XdfXvSVZUWBdCmpabLtD/tlpzkHdbviPDVVykWvSzm9qpQP7+T69OPHj0srKytZoUIFef78+dz3nxQv5YmFUv5QR8qJFvLe9w3kxC8/lF5ztstbD+Jy3k5qipSXvaVc2VtTkWFqRSkPzpLxcbHyu3/OSbux22TbH/bL4FvRuY8xnyQlJcmqVavKpk2b5mu/wRumSznRQu78ebTeqxt8/PHHEpB79uzRS/9JCY/k6Tk9pZxoIY/O6CFjHz7IU3u5ud4VuZG886ePMflAIna9pjJ27Fjat2//zEqjmtbmhITHPr7QalRygegbmlHAF7C1taVTp078+eefpKRorxCpoij6IaU8IKXcIqXUZlG3U4C9EKKaEMIE6ANseeqYLfxXQaAnsE8+cVHKPUMDweCm1Qi+/RCf61F5aerFDAyg+3xIioPtn2juoOSCh4cHBw8eJC0tjdatW3Px4sWcnSglnN8O8z3gnzHIUlX4u848nO9P4a7DQBaP6EBFy1zcsjMwhOqtoO9qeP+45vu9kzH7rTFj7W+y4l0PouOT6TH/KDvP3s7Ve8wv58+fJyYmJl9HskJ9d2LvP53jJo1pOfT7fFlI8yLTp09nxowZNGvWTC/9G5sWp8GotfjVHIlH7F4iZzfj5kX/fOm70Cd5UVFRDBo0iI4dO2Jra0udJh2ZuD+Rhg618Pb2ZuPGjRnlAx6zty5JdHwyd2MzXdMruWgeb/pl2+fgwYO5ffs2u3fv1uZbURSlkJBSpgCPS0adA9bK9JJRQohu6YctBsoKIS6h2cpRK3Uvuje0wcLMiGVHr2mjueezstfctj2/Dc6sy/XpderUYd++fVSrVi1nc6kiL8DyN2B1XzAqhuy/kR9s5vDZaSu83Kswr09DipnkoRSKdR3wWgUDNoORKazoQdPz0/hneENeq2jO8BV+/LT3InnMw7XO0dGR0NBQOnfunC/9Rd++jMXWoYQKG6oPW4GZybOlyvKbqakpn332GaampsTExOilEDRC4Nx/GmfbLMUiLYrSK9rjt32Rzrst9ElefHw8e/bsISIiglatWvHzl0MI/bgkO/9eRqtWrbI85789bDNtb1bRCYRBtvPyADp37oyVlRXLly/XxltQFKUQklL+I6WsJaWsIaWclv6zr6WmJihSygQpZS8pZU0ppbtMX4mbV8VNjHjLtTK7zt7hTnSCNpp8vsYfgK07bBsNkSG5Pt3BwYHDhw9ja2tLWloasbGxzx4U/wB2joMFTeCWH3SaAcMP82tYVX7ef5k+bpWZ1t0RAwMtjSZVbwXvHYImH4LPUsqtas9qTwveaFiJH/4NYfyms88tx5LfIiIiSEtLo0SJEhjkcAFMXqQkxBK15C0MZApxPf7E2qpgLUy5ffs2Tk5OzJw5U28xOLV8g8QhB7luUgPnU2M48dNAEuJ1N0e20Cd5NjY23LhxAz8/P1asWMEHXV2obGnwwg287cuXBHhy8YVJCbCuq7mIZMPExITRo0fj7u6e5/gVRcl/QqO/EOLr9OdVhBCF5h/0gMZ2pErJqhM6XiBmYAi9lmpGvlZ7waMXLgzO0uNbfYMHD6ZLly4kJydrXkhLA78/4ScXOL4AGr4NH/qBx3v8ExzJ9zvP08WpIt++ocUE7zFjM2g/FQZug8QYTJe248fawYxoVYOVJ0L5eI0/yfk9wT4LvXv3pn177W8dliUpubBoEFWSLnPabSb1nFzyp99cqFChAt27d6d169Z6jaOibXVqfraf4xX64XFvI7dmNuFy4FGd9FXok7xnxIaDgZGmntNzWJubYmFmREjmkTyASs6akbwcDMd/+eWXjB49Oq/RKoqiH/PRLI7wSn8eA/yiv3C0q0rZ4rSpbc2qk6Ekpeg4GbG0hd4rIDoMVrwJcbnfRxegffv2dOrUCSMjI7h6EBa2hC0fam4Lv3cAus6BElb433jA6DX+OFcpxaxe9bWf4GVm10wzqmfritg0gi+MVvNFh1psDbjFsD999LryVkrJ8OHDeffdd/Olv7Prv8Ph3m72VRxKyy7986XP3BJCMHv27IwBmPj4eL3FYmxiSqPh8wlouQiLtAdUXt+FY0vHkpKs3f2ci16S9yi9fMoLhq6FENiXN39yJA/AxllT0T3qatYnPiUuLg5f3+xv7yqKUuB4SCk/ABIApJRRaPbULjT6N6rK3dgk9p1/8WIyrajaGHr9ARHBsKwzPAjNdRN9+/bli3d7IFb3Jf73Lppr8ZuLYdAOqKgpPn8vNpHhy32xtjBl0QDX/NmOzLw8vL0JXAbB4dmMiJzK993s2R8SyaBlJ/WW6Akh6N27N15eXtkfnEfXTv1DnTMzOG7alBbvTs/+hALg66+/pkWLFhlb6+lL/dZvYTTyBGfMm9P4+gKufN+US4HP7sb1sopekhd3X1OkMxv21iW59HSSl4vFFwAjRozgf//7n34meSqKkhfJ6VuPSQAhRDmgUP1Dbm5vhbW5Ket8w/Knw9odod/fmhG9X5tD4N85X3V75wysexd+cefYgT1U+xWOu/0Cjj0h/XZuWprk4zX+3I9L4tf+LpQtmT87KwBgaARdZmtu4QZvpnfQCOZ3r8zJq/cZ8ucpEpLzN9G7ePEi06ZNIzo6Wud9Pbh1iVLbh3HdwJaaw5Zj8ors8+vi4oKPjw8ffPCB3hfLlLKqgMuYzfi6zaJcyh2qre/EsfnDeBj9cqPemRW9JC8+6oW3ah+zL2/O/UdJ3I1N/O+H1nXAqFiOk7wPP/yQFStW6P0vkKIouTYP2AhYCyGmAYeBb/UbknYZGRrwhnMlvC9EEBmTmP0J2lC9lebWapnqsGEI/N5Wk+wlPHzyuLRUuHsRjv0Ci9rAr80gZCc0/oDaXx6muKUVPfv0Izw8POOUX7wvcejiXSZ1dcDBRke7ebyIEJrFGL1XQHgQnU4M5JfO5Th6+R5D//TJ10Rv5syZTJkyReejVCkJsTxY+hYGMpXEN5djVTb7AZSCwtPTkwkTJrBkyRKmTy8Yo48unYdi+KEvvlaeeISvJXG2Myc3/UxqHsqxFb0kL+5+zpI86/TFF+GZRvMMjTWrbHOwwhbA1dWVdu3aYWj4anyyURRFQ0q5Evgc+A64DXSXUv6t36i0r6ezLalpks3+N/Ov0zLVYcge6DoX4u5pkr3pVWBuA1jUFhY0g+9sNVuj7foSUpOh3RQYfRbaT6VMldps2LCBe/fu0adPH1JTU/G9fp/Ze0LwbGCDl3vl/HsvWanTRVNmJe4unU4OZEGHkhy+dJfhK3xJTNF9onfnzh3++OMPBg4cSPnyz19gmGdScm7hIKokXSHQ40fq1Guou750ZNKkSXh5efHll1+yatUqfYcDgEVZa9w//IPLnpt4YGSFu/9XhH7bEL9dy5EvcVew6CV58VFQvEy2h2WUUYl4evGFC9wOgNScZdY+Pj4sWqT7WjiKomiXlPK8lPIXKeXPUspz+o5HF+zLm1O/cin+9gnL3zsOBobgMhA+PA2DdkKrsWDTAEzNwbKS5jXPX2CkLww/BE1HPfHhvEGDBixYsID9+/czeeq3fLI2gIqWxZjavZ7eC+8CUMUDBv4Dqcl0PDmIX9sYsP9CJCNW+Ok80Zs7dy7JycmMGTNGp/0E/j0Nx/u78a40jGb/0/28P10wMDBg6dKltGjRgkGDBhWo2rb2zq2o+eUJTnvMwZBUnI+NJORbD3x3LM1VO0UryZMS4nM2klfewhRzU6MnR/JAk+SlxENkzq75a9as4YMPPsi6vpOiKAWKECJGCPEw0+PDzM/1HZ8u9HKx5UJ4DGdv6uHtGRhoFmW0Ggu9lsGATdB3DXT8Dhr2B6uazz31nXfewcvLiymTvyHkjB8/vFUfczP9F97NUKEeDN4JxiXocGooi1rEs+98BCNXndbZiuaHDx+yYMEC3nzzTWrWfP6fXV6FHFpLvaBZHDdrRovB3+msn/xgamrKpk2bqFOnDt27d+fAgQP6DimDMDCkYadB2Izz55TjN5RMjcblxMe5aqNoJXlJsZCWAsWyH8kTQlCzfMlny6jYpA9J5/CWbYcOHUhOTi5Qf3EURcmalNJcSmmR6dEi83N9x6cLXZ1sMDEyYJ3vDX2HkitCCAaMmYpBybIk7p5DXasClOA9VrYGvLsLLGxo5/s+ixvf5d/gcD78y08ndfQWLlxIdHS0Trcwu3XuBLZ7RxJiWIPaw1dibPTqT0cqXbo0//77L3Z2dnTu3Jljx7S3ulUbjIxNcHvzYyp8Fczpxj/l6tyileQ9rs+Ug5E8gFrW5s+usC1THcxK5TjJa9asGWZmZuzZsyc3kSqKokdCiD+EEKUyPS8thFiiz5h0xbK4Me3rlmdzwK18mTOmLQnJqXy39zpO/ccTHXmLkSNH6jukrFnYaMq8lK9L24BPWOZ6nV1B4Yz667RWE73ExERmz55NmzZtcHV11Vq7mUWHh2K81ouHlKTEO39TulSp7E96RZQrV469e/fi6uqq27mMeWBoZETDDgNydU7RSvLi0zfkzsGcPNDsfHHvURL3Mq+wFUJzy/bm6Ry1YWZmhru7O0eOHMlttIqi6I+TlPLB4yfpdfJevZnlOdTTxZYHccnsPZcPNfO0ZP7+y1y/F8dPo72YMGECq1atIigoSN9hZa1EWRiwBSo3otXZL1nZIIgdZ+/w8Wp/UrSU6C1fvpxbt24xbtw4rbT3tKS4GO7+3oPiabHc7fYnlatW10k/+lSxYkX2799P9erVSUtL4+DBg/oOKc+KWJKXu5E8+4zFF0/Py3PWFPVMytl+c02bNsXPz49Hj3S3P52iKFplIITIuFAIIcoARnqMR6ea25ejvIUpf/u8GrdsL0fG8uv+y3g2sKFpTSvGjx+Pv78/Dg4O+g7t+cwsoP86sG9P0/PTWOtwnO1nbvPxmrwneqmpqcyYMQMXFxfatm2rpYAztZ+cyKWfu2OXdIkzjX6knnMzrfdR0CxatIiWLVty6NAhfYeSITk5OdeLQ4pYkpc+kpeDOXmQqYxKVkWRZSrcDsxRO82aNSM1NZWTJ0/mOFRFUfTqB+CoEGKKEGIycBSYoeeYdMbQQPCmsy0HQiIJf6jfHQCyI6VkwqazmBob8FXnOgAYGRlRr149QFPRoMDWJjUuBn1WQr2euF+ex8ba/7It8BafrA3IU6JnYGDAokWLmD17ttZXF6elpHD2p97UjfPhSN0JNOpUMLcs07bBgwezfPlymjXTJLT62hkjMTGRrVu3MmDAAKytrenQoUOuzi9aSV4u5+RVtDSjpKkRIXeeXnzhrHm8lbOiyI0bN0YIweHDh3MaqaIoeiSl/BPoCYQDkUAPKeVy/UalWz1dbEmTsMEvH2vmvYTN/rc4evken3d8DWtzsyde2717N25ubqxfv15P0eWAoTH0WAiug2l4fSnbq29ka0AYH6zye+mCyUIIWrZsSfPmzbUaqkxLw+/Xd6n/0JsDdqNo0ftTrbZfkBkbG9O/f3+EEFy6dAk7Ozt+++23fNnB6vGHFCklDRo0oFu3bmzbto3u3buzefPmXLVVtJK8+PQpNjlM8oQQvFbBnHO3nyotYF4eLCvnePFF6dKlqVevnkryFOUVIqUMSq+R95OUMljf8eha9XIlca1amr99bxTYkbDouGSmbg+mfuVS9HWv8szrbdu25eeff6ZLly56iC4XDAyh84/Q9GMcbq1jj90K9gXd5N0/TvEoMXe7G+zdu5cPP/xQ61uYybQ0Ti3+CNe7mzhU/m1avDNZq+2/SkxMTHB0dGT48OE0atRIpwspf/31V1xdXUlLS0MIwVdffcWOHTsIDw9n6dKldOvWLVftFbEkLwpMSoJRzvcZr1PRgvN3YkhLe+qiZ9Mwx0keaG7ZhoaGFtiLp6IoIIQ4nP5YZOrkZdbL1ZYrkY/wC32Q/cF6MHP3ee4/SmJa93oYGjx7W9LQ0JAPPvgAMzMzHj16VLCvt0JAu2+g7URq3NnJkUo/E3wllH6/n+BBXFKOmwkICGD79u0UK1ZMa6HJtDROLvwA95t/cqy0J02HzSsYRab1pEqVKuzevZs//viD8PBw2rVrR/PmzVm5cmWebuM+ePCANWvWMGDAAK5evQqAlZUV9vb2PHyoudz079+fjh07Ymz8ciWCiliSdz/H8/Eeq1PRgtjEFMKi4p98oZILRF377xZwNn788UeCg4OL9D8URSnopJTN0h+LTJ28zDo72VDM2LBA1sw7HRrFyhOhvNPEjnqVXrw3bWhoKA4ODvzxxx/5FF0eNP8E3vgN6wf+HC4zjdhbIby54CjX7+Vsod4nn3zCuXPnMDHJ+eDFi6SmpnJi/hA87qziWNk38Ri5DAPDopUqZEUIwYABAwgJCWHu3Lncvn2b/v37U6FCBXr27Mnffz9/10MpJXfv3uXkyZMsXryYoUOH4ujoSJkyZejTpw/bt28nJCQEgJ49e7J69WpKaak8TdH6zcVHQbHc/cHVtdFc14OfvmVbyUXzeDNn8/LMzMxUgqcoSoFW0tSITo4V2Bpwm/ikglMzLyU1ja82nsXa3JRP2tXK9nhbW1uqVq3KRx99RFhYWD5EmEf1+8CAzZRIfciOEpOwiz1N91+OcOLKvReedunSJUCza4M2xMXFcvrHN2h0dz3Hy3vR6IPfVYL3FFNTU0aNGkVISAh79+6lR48enDhxgv379wOaJLl8+fLMnj0bgCtXrlC8eHHKlSuHh4cHQ4YMYf369dja2jJx4kSOHDlCRERErhdU5FShLQmQpbicbWmWWe3y5hgIOHf7IR3rVfjvBZsGgICbPmD/eo7aGjFiBOXKlWPy5KI7t0FRXgVCCFPgTcCOTNdJKWWh/8fby6UyG/xusjPoNm80tNV3OAD8cew6wbcfMr+fc462Lnu8J6mjoyPDhg1j+/btBf9DdtUmMHQvxivf4vf7U1ho1J/+vyfxRae6vNus2jPxBwUFUa9ePZYsWcKgQYPy3P2dO2Hc+70XrinBnLIfjYfX1wgDleA9j4GBAW3atKFNmzZIKTNu28bFxdGnT5+Mcj5lypRh5MiRVKpUiQQrMUoAACAASURBVOrVq/Paa69hb2+fb38fC1SSJ4ToCMwFDIHfpZTTn3r9E2AIkIJmxdtgKeX1HHeQEA0WFXMVUzETQ+ysSjw7kmdqDuXrwfWjOe8+IUFvy7AVRcmVzUA04AskZnNsoeJRrQyVyxTjb5+wApHk3Y6O58fdF2hVuxydMn/Qzkb16tWZPn06o0aN4o8//mDgwIG6C1JbylSHofsQW0byXvAfNCp1gXe3v8PxK/eZ0dOJMiX+uyU7efJkSpYsSdeuXfPc7amDO6i87wNqyoecaToXt/YD89xmUSKEyJgTaW5uzty5czNeK1WqFDNnztRXaAXndq0Q4v/snXd4VGXah+9n0kMIoXcINUCAUAKhCApYQJqKggIuYlcUy6eIu4qurq5lVWy4ioIIWBBWARUFVGog9CSUBEJP6DWBkP58f8ygCIHMmcxkEnjv6zrXzJzzll+SyZxn3vcpPsCHQB+gBXCHiLQ4r9l6IFpVWwMzsZq3KjsdAqy71TSvGXphhC1Ag26wNw5ynTPcJk+ezBtvXLaptgyGy4k6qjpEVd9Q1bfOHt4WVRLYbMJt7esSu/2o035hnuSluZvJK1BeGtDS8urHqFGj6NatG48//jhpaaU7NcwfBIbCbVOgzxu0zl7PsvLPErxtNte9vZjZG9JQVTZu3Mi3337L6NGjqVKlistTpWeeYf4nY2nz6zDU5sfhIXNpZQy8y4pSY+QBHYEUVd2hqjnA18DAcxuo6u+qmul4uRKw9jUzKx0CL+2wWxgtaoaSevwM6Vm5f73QoDvkZUHqakvjlUSeHYPBUCxiRaSVt0V4iyEd6uJrE6atdH6jxBP8nnSIeRsPMLpXE+pVDrbc32azMWnSJHJycnjggQdKd7TtuYhAzAPIA0sIrNqQ93zf42Of13jvmx+587NVPDn2OUJCQvi//3Mtb11BgfLb7/NJe6ML1+/7iJSK3aj05ArqtOjs5h/E4G1Kk5FXGzg3pCvVce5i3APMK+yCiNwvImtEZM3hw4ftJ/PzIPe0Syt5LWra+yTtPy8pcv0uIDbY5VzZk+PHj1O/fn0mTJhgWYPBYPA8IpIoIgnAVcA6EUkWkYRzzl8RVA8N5IbIGsxYk+q1AIwzOfmMm7ORxtVCuK+b63VSGzduzL///W9+/PFHpk4tY/msqzWDexbA9a/QXrayIHAsbdb8nQU/zqZxz8FsPlpwYXqvS5CVm89Pi5by66sDuWbRYGrIMXb3nEDzx74nIMRa5glD2aA0+eQVtg5f6LtXRIYD0cDVhV1X1U+ATwCio6PtY2Q7tlsDrRt5kY4I28S0k3RscM4/QmAFqNkGdi6BHn8vcpywsDCys7NZs2aNZQ0Gg6FEKOVZdEuOOzvX58fE/cyN38fgDnVLfP73f9vG3mNn+Oq+Tvj7Fm894tFHH2XmzJk8/vjjDBw4kAoVrO/oeA0fX+jyCBJ1O7LoNWKffp8KAfDPiGRmTH6Hlyp0oUvzcDo1rEzL2qFUDw3EzxERm5tfwPbDp0jae5ij6+fQMG0uN7COXPFnR9O7aXDT81QsZy0Y0VC2cNrIE5HuwL+AykA8MF5V3VmMNRU495OkDrCvEB3XAv8ArlZV5x2izxp5AeUtC6sWGkiN0EASUgtJENqgO6z4AHJOg3+5S44jIkRHRxsjz2AopZwN5BK789cwoKGqviQi9YAagHf3L0uQmAaVaFo9hC9W7uK26DolGp2adCCdT5bs4JZ2tencqHKxxzu7bZuSklK2DLxzKVeF9bWG8d2Wt3hxRA/6VU2j/6kPyDnzMZvXhpOwKpzPtRqnCcLXz5/yBSepXHCM1rbt9JGdBEguJ/2qsC/iQer0eZLGIdW8/RMZSgArK3mTgIeADUB7YLyIjFfVGW7SshpoIiINgDTgdmDouQ1EpC3wMdBbVQ9ZGj3rrJHnWj7TqLoViN97ESNv+Xh7lG2T64ocJzo6mp9++onTp09TrtyljUKDweA1JgAFQE/gJSADmAV08KaokkREuLNTfZ6fvYl1e07Qvn7JrPjkFyhjZyUSGuTHc33Pj71znSZNmtCkSROAMvv5+8ILLxAWFsbj736HlC8Pe+PwT/6R1qlrabV/BT65p/5sbIM83wDSKzTjdL278Y28ngqNe1DB5uO9H8BQ4lgx8o6o6gLH858d5X9WAm4x8lQ1T0QeAX7BnkJlkqpuEpGXgDWqOgd4EwgBvnV8q9yjqs4VcivGdi1A6zph/LLpICczc6kQfE6epvpdwDcItv7ilJEXFRX1R3RUTEyMS1oMBoPHiVHVdiKyHkBVj4uIe0oKlCFubleHN39J5uPF2/nkb9ElMue0lbvZsPcE7wyJ+kvKEHcxZcoUnnnmGTZs2ECNGs6nZPE2BQUFREVF0aNHjz9XI+t3hvqd7c71BQWQc8p+r8vPgeAq+AaUp1Jpzw9o8ChFGnki8gWwDlgmIuOAV1U1D3vuKLcmfVPVn4Cfzjs37pznzmUdLoxsR9CEqyt5deyVMhLSTtCtSdU/L/gFQaOekDwPbnzTHhV1Cdq0aQPY6w0aI89gKLXkOtI6KYCIVMW+sldsRKQS8A32RMu7gMGqeryQdj8DnYBlquoVX8GQAF/u6hLOe7+lsPVgBk2rW3d3scK+E2d44+ckujWpwk1tLhV35zqdOnXixhtvdFsZsJLCZrPx8ssvX6qBfRHDxYUMw+WJM96sn2H/oKsE3ASkiMhCIInzDLJSzdntWhdSqAC0qmPvl5B68sKLEX0gPRUOJBY5Tnh4OKGhocTHx7ukw2AwlAjvAd8B1UTkFWAZ8Kqbxh4L/KqqTYBfHa8L403gTjfN6TIjuzYg2N+HCb+neHQeVWXc7E3kq/LKTa085gMYERHBpEmTqFSp7ESTzp8/nx9++KHspIAxlBqKNPJUdbGqvquqd6tqO6AR8ATwAhDkaYFuI7t4PnkVgvxoWKVc4X55TW8AxL6aVwQiQuvWrY2RZzCUQkTkAxHpoqrTgTHAv4H9wE2qevEK5NYYCExxPJ+C/cvzBajqr9h9Ab1KxXL+DIupx5z4fR5Njjw3YT8LtxzkiWubupQTzypJSUnccMMNHDhwwONzFZfx48czduxYk2PVYJkijTwR6SznfKVS1XxVTVTVaar6tGfluZEsxwpcMZayW9epQHxhEbYh1aBOB0h2bmEzKiqKhIQE8w9rMJQ+tgFvicguYCSwXFU/UNUtbpyjuqruB3A8FjvMsdDcoG7kvm4N8fWxMeH37W4fG+zbtM99l0jbemHcc1UDj8xxPqrKkiVLGDlyZKlfIZs9ezY//PADPj4maMJgDWe2a0cAa0XkaxG5S0TKjqfquWSng08A+Aa4PESbumEcTM9m34kzF16M6AP7N8DxojMsDBgwgNGjR5OdfUWVxDQYSj2OXYvO2HNwHgMmi8gWERknIk2dHUdEForIxkKOgUX3dkn3J6oararRVatWLbqDRaqFBjIsph4z1u5l075CXFaKQUGB8vTMePIKlHcGt8HXp2Ry9Ddv3pz//Oc//Pzzz7z//vslMqdVjh49SkZGBn5+foSHh3tbjqEM4sx27YOObdoXgYrA5yKyQkReFZHuDufk0k9WerEdUjs4EiGv2nnswostB9kfE4sONr7++uv517/+9UdBY4PBULpQ1d2q+rqqtsWeyulmwOnVPFW9VlVbFnLMBg6KSE0Ax6O1dFBe4vFeTQkL8uOfcza7deXrs2U7WZ5ylOf7tSC8SsmmNXn44Yfp27cvY8aMITGxaJ/qkuaxxx4jKiqKnJwcb0sxlFGc/sqkqkmq+o6q9saeO2oZcBsQ5ylxbiU73WV/vLM0qxFK+UBf4nYevfBixfpQvyvEfwNOfABmZGSQmppaLD0Gg8EziIifiPQXkenYyyduBQa5afg52HdIcDzOdtO4HqVCsB9P3RDBql3H+DFxv1vGXLH9KK/9nMQNkdW53QtVNUSESZMmERYWxtChQzlzppBdGi+xZMkSpk+fzrBhw8pcJLCh9OC0kefYfogCUNUzqvqTqj6qqiWTPKm4uGElz8cmdAyvRNyOQlbyAFoPgaPbILXoihYdO3bkkUceKZYeg8HgXkTkOhGZhL0Cz/3YMwg0UtUhqvq9m6Z5DbhORLYB1zleIyLRIvLpOVqWAt8CvUQkVURucNP8LnN7h3q0qBnKqz9u4VR2XrHG2nfiDI98uY7wysH857aoEq2ocS7VqlXj888/Z+PGjYwZM8YrGs7nzJkz3HfffdSvX59nn33W23IMZRgrzg9jgHdEZPLZrYYyhRtW8gBiGlZix5HTHEovJEVgy1vAvzys+qTIcV599VVGjx5dbD0Gg8Gt/B1YATRX1f6qOl1V3RpSqqpHVbWXqjZxPB5znF+jqvee066bqlZV1SBVraOqv7hThyv42ISXb4rkQHoW/5yzyeVxMrJyeWDqWrLzCvj4zmjKB/oV3cmD9O7dmyeeeIIPPviAGTPcVcTJdcaNG8fWrVv59NNPCQ72fKSx4fLFynbtOlXtCfyAveLFCyJSdpzK3LCSBxDTwF5HMa4wv7yA8tB2OGz6DjIuHZZ/880307Nnz2LrMRgM7kNVe6jqxLOGl+FC2tevxMPXNObbtanMXGvd5eRMTj73fL6GLfvTeff2NjSuFuIBldZ57bXX6NKlC3fffTdpaWle0xEXF8fbb7/Nfffdx7XXup7/32AAayt5Z4t2JwMfAY8C20TE68k6nSI7HQKKX5g6slYo5fx9CvfLA+h4HxTkwYoPLznO6dOnWbBgAQcPHiy2JoPBYChJHru2CZ0bVubv/0skbsdFPgsLISs3n/unrmHN7mO8M6QNvZpX96BKa/j7+zNjxgzGjx9PrVq1vKLh1KlTjBgxglq1avHmm296RYPh8sKKT94yIA14B6gN3AVcA3QUkaL3J71NdoZ9pa2Y+PrYiA6vROz2i3ywVW4ErQfDqomXXM3btWsX119/PQsWLLhoG4PBYCiN+PnYmDCsHXUrBTHy89XEphwpss+Bk1kM+WQly1KO8Pqg1vSP8o4hdSlq167Nvffei4iwbds28vKK53doBVXloYceYtu2bUyZMuXP+rQGQzGwspL3IFBbVa9T1edV9QdVTVHVR4FuHtLnHlTdZuQBXN20KjsOn2bP0czCG1wz1l4g+vdXLjpGkyZN8PX1ZfPmzW7RZDAYDCVJxXL+fHV/J2qHBXHnpFV8+HsKOXkXJnjPL1BmrNnLDeOXsO1gBh8Pb89t0SUfSWuFvXv30q5du0vXinUza9euZdq0abzwwgvGlcfgNnydbaiqGy9xua8btHiO3ExAIcA9vh89m1XjpR8281vSQe7qWkh29koNofPDEPu+PeI2/KoLmvj7+9O0aVM2bXLdedlgMBi8SbXygfzv4S48MyuBN39J5su4PQxsU4sWtUJRhaQD6fyQsJ/dRzNpVy+M/9wWRcOqpcMH71LUrVuXV155hVtvvbXE5oyOjmb58uXExMSU2JyGyx+njbxLoao73DGOx8g+ZX/0d0+izfAq5WhYpRy/JR8u3MgDuObvsGUufPcQ3P87lKtyQZPIyEjWrVvnFk0Gg8HgDcoH+jFhWHsWJR/is2U7+Wjx9j9ShdoEOjaoxDO9m9GnZQ2vpUlxhbPZD/Ly8liyZInHVte2b9/Ozp07ufbaa+nSpYtH5jBcubjFyCv15Jw18tz3DbJns2p8sWI36Vm5hBYW/u8fDLdOhkm94Zs7YfjMC4zMyMhIZs6cSWZmpgmTNxgMZZprIqpxTUQ1MnPy2H00ExGoVymYYP+yfZsZP348Y8aMYfLkyYwYMaLoDhYZM2YMsbGxbN++3dwHDG7HSuBFgIgMFZG/O+o4jhORcZ4U5zZyHGmu3Gjk9WlVk5z8AuZvukR0bO12cNME2LsSpt4Cp/5avSgyMhJVJSkpyW26DAaDwZsE+/vSvGYozWqElnkDD+CRRx6hV69e3HXXXbz++utuLekGMGnSJObNm2cMPINHsBJ4MRsYCOQBp885Sj857t2uBWhXL4zaYUHMjd936YatboVbJ8H+DfBRV1g/DfLtEVuRkZEAJvjCYDAYSimBgYH88MMP3H777YwdO5aHH3642LVkt27dyogRI8jKyqJChQq0adPGTWoNhr9i5WtWHUfd2rLH2ZU8N0XXgr3mYf+oWkxcuoMjp7KpEhJw8caRN0OVCJg9yn4sGw+dH6Zx80H4+fmZ4AuDwWAoxQQEBDB9+nTq1avHG2+8QVxcHNOmTaNFixaWx/ryyy95+OGH8ff3Z/fu3URERHhAscFgx8pKXqyItPKYEk+SnWF/dONKHsCt7WuTX6B8s3pv0Y2rt4D7foPBX4BfEPzwBH7vt6ZprTA2bSi61q3BYDAYvIfNZuP111/n+++/JzU1lXbt2vH8889z/Phxp/qnpaVxxx13MGzYMFq2bElcXJwx8Awex4qRdxWwVkSSRSRBRBJFJMFTwtyKB3zyABpXK89VjaswbeVu8vIvzA91ASLQYiA8sARGzoMG3Zlywxneb7YKvh0Jx3e7VZ/BYDAY3MvAgQNJTExk4MCB/Otf/+LVV18F7FG4BQV/vQ/k5+ezbNkyHnjgARo3bsysWbN4+eWXWbRoEQ0aXCQzg8HgRqwYeX2AJsD1QH+gn+Ox9OMBn7yz/K1zffafzOKHhP3OdxKB+l1gyFTav7aR+r0fgW3z4aMukDjT7RoNBoPB4D6qV6/ON998Q3x8PE899RQAc+fOpVKlSmzbtg2A9957j9DQULp168bUqVMZPnw4ycnJPPfcc/j6lv2AFEPZwGkjT1V3A2HYDbv+QJjjXOnHAylUznJt8+o0rxnKWwuSyc7Lt9Q3IyuXT+KOcM2XuYzUl0jxaQCz7iF78dtu12kwGAwG99K6dWuqV7fX361duzZDhw6lZs2aAISHh3PffffxzTffcODAASZOnGhW7wwljpUUKo8B04FqjmOaiDzqKWFuJfsU+PiDr7/bh7bZhLF9mrH32Bk+W7bTqT45eQV8vnwnV7+5iBe/XcniL95k44407s5/jjn5nQn4/Z9s+8EYegaDwVBW6NixIxMmTCAkxL6YMGDAAMaPH8/gwYMJDQ31sjrDlYqVNeN7gBhVPQ0gIq8DK4D3PSHMreSc9shW7Vm6N6lC78gavLNgK10bVSGqblih7VSVnzce4PWfk9h1NJMujSrz0dDBhL96K9WqVQNg9Y52xH41nI6rX2ZpQDjdrrvFY7oNBoPBYDBcvljxyRPg3P3IfMe50k/OKfB3X/qU8xERXhvUiqohAYz8fDUJqSf+cl1VWZ5yhFv/u4KHpq/D39fG5Ls6MP3eGGIaVaV69eqICCJCx0bVaD36Gw761abZssdZtXmbx3QbDAaDwWC4fLFi5E0G4kTkRRF5EVgJfOZOMSLS2xG9myIiYwu53l1E1olInog4Xzk655RHV/IAwoL9mX5fJ4L8fLh5Qiz/NyOeqSt28e7CbQz8cDnDPo0j9Xgmr93Sip9Gd6NHs2p/1HH8+uuv+cc//vHHWCGhFak4YjoV5RSHv32SwxnZHtVuMBgMBoPh8kOslGgRkfZAV+wreEtUdb3bhIj4AFuB64BUYDVwh6puPqdNOBAKPAXMUdUiQ1Gjo6N1zeg6dkPv3oXukntRjpzK5t2F2/hufRqnsu2VLVrVrsDgDnW5rX0dAv18LujzxBNP8Mknn5CRkYHN9qfdfXTuC1ReO5736r7D6Hvu9rh2g6GsIiJrVTXa2zpKAyKSASR7WUYV4IjRAJQOHUbDn5QGHcXVUF9VqzrT0FIct6quBda6JKloOgIpqroDQES+xl5G7Q8jT1V3Oa45kZTuHDzsk3cuVUICePmmlvxzQCSHT2VTPtC3yPqNERERZGZmkpaWRt26df84X7n3WNI3fk2P3e+ybGs/rmpazdPyDQZD2SfZ2waviKwxGkqPDqOhdOkoSQ1FbteKyDLHY4aIpJ9zZIhIuhu11AbOLR2R6jhXfHJOeyR9yqWw2YTqoYFOFehu1qwZAMnJ53359gsiqPc/aWXbxfK5n7m9MLbBYDAYDIbLlyKNPFW9yvFYXlVDzznKq6o748ILC+JwyaoRkftFZI2IrDl8+DDkZJS4kWeFs6VtkpKSLrjmFzWY9HIN6HfyK37ZaCHhssFgMBgMhisaK3nyXnfmXDFIBeqe87oOsM+VgVT1E1WNVtXoqlWrluh2rSvUqFGD8uXLF2rkYbMR0uv/iLTtZuWCb81qnsFgKIpPvC0Ao+FcSoMOo+FPSoOOEtNgJbr2ukLO9XGXEOyBFk1EpIGI+AO3A3PcMnL2KQgovSt5IkKzZs0u3K51YGs9hNOB1el94itW7TxWwuoMBkNZQlW9fhMzGv6kNOgwGv6kNOgoSQ3O+OQ9JCKJQISIJJxz7AQS3CVEVfOAR4BfgC3ADFXdJCIvicgAh5YOIpIK3AZ8LCKbnBgZ8rNL9XYt2LdsC13JA/D1x7/rI3SybWH+b7+WrDCDwWAwGAxlEmdW8r7EXqt2Dn/Wre0PtFfV4e4Uo6o/qWpTVW2kqq84zo1T1TmO56tVtY6qllPVyqoaWeSgBY5A3FK8XQv24IvU1FROnTpV6HW/9sPJE3/Cd3/LkVMmb57BYDAYDIZL40zgxUlV3aWqd6jq7nOOsrFvqI4iHWVgJa9KlSqkpaUV3iC4EplN+jHQtpQ5q00VDIPB8FeKSiZfAvPXFZHfRWSLiGxy1Dv3GiLiIyLrReQHL80fJiIzRSTJ8Tvp7AUNTzj+FhtF5CsRCSyheSeJyCER2XjOuUoiskBEtjkeK3pBw5uOv0eCiHwnIoXXIPWwjnOuPSUiKiJVPDW/qylUMjyQQsUzaNlYybvllls4fPjwH5G2hRHa9T5C5QxH4r42ARgGpzhz5oy3JRhKAEcy+Q+x+0m3AO4QkRYlLCMP+D9VbQ50AkZ5QcO5PIbd9cdbvAv8rKrNgKiS1iIitYHRQLSqtgR8sPu6lwSfA73POzcW+FVVmwC/Ol6XtIYFQEtVbY29+MKzHtZwMR2ISF3ssQ57PDm5qylUynsghYpnOGvk+QV7V0cRnFvp4qLU60x6SAN6Zv7Cmt3HPS/KBfLz85k4cSL9+/cnO9tsK3uTF198keuu+zNeKi4uznw5uHz5I5m8quYAZ5PJlxiqul9V1zmeZ2A3atyT69QiIlIH6At86qX5Q4HuOEp/qmqOqp64dC+P4AsEiYgvEIyLGSusoqpLgPN3+wYCUxzPpwA3lbQGVZ3v8P8He2nWOp7UcDEdDt4BxuBiqjhnsZJC5TYRKe94/pyI/E9E2npOmpv4YyWvdBt5AE8++STPPnuJLxYiBEYPI9q2lcVxa0pOmJOcOHGC7t27c//995OSkvLHKtJXX33FsWNlY3f/cqJp06a0aNECVWXZsmV06tSJDz74wNuyDJ7Bc8nkXcBRgrItEOclCeOx30CtVUdyHw2Bw8Bkx5bxpyJSottJqpoG/Af7StF+4KSqzi9JDedRXVX3O7TtB7xdwuluYJ43JnYEk6aparyn57KSQuV5Vc0QkauAG7Bb4v/1jCw3UkZW8gAyMjIuGnhxFv+o2wAISP6evHxvfX5dSFZWFn379mX16tVMnTqVzZs3ExYWxqlTp3jggQd44YUXvC3ximPo0KF88skniAhdunShb9++PPXUUyQkuC0o3lB6cFsy+eIiIiHALOBxVS1xlx4R6QcccpTh9Ba+QDvgI1VtC5zG89uTf8Hh8zYQaADUAsqJiFuDJcsqIvIP7O4F070wdzDwD2BcScxnxchzRDDQF/sbdzbg735JbqYMGXkTJ07k/fffv3SjiuEcrxTFtXlLiStFOfNeeuklYmNjmTZtGsOHD0fEfs8JCQnhp59+4vXX3Zk323AxVJUBAwbw4Ycf/uW8zWZj8uTJhIaG8tRTT3lJncGDuC2ZfHEQET/sBt50Vf1fSc/voCswQER2Yd+27iki00pYQyqQqqpnVzJnYjf6SpJrgZ2qelhVc4H/AV1KWMO5HBSRmgCOx0PeECEiI4B+wDD1jv9KI+yGd7zjPVoHWCciNTwxmRUjL01EPgYGAz+JSIDF/t7hbAoVvyDv6nAj5drfQXPbHlatWu5tKQDEx8fzxhtvMHLkSAYPHnzB9auuuorg4GBOnjxJXJy3dm+uDBYsWMDcuXP/MLLPpWrVqowdO5YFCxawZMkSL6gzeBDPJZN3ErG/6T4Dtqjq2yU597mo6rOOVFvh2H8Pv7k73ZcTGg4Ae0XkbCRdL2BzSWrAvk3bSUSCHX+bXng3EGUOMMLxfAQwu6QFiEhv4BlggKpmlvT8AKqaqKrVVDXc8R5NBdo53jNux4qRNhh7ouLeDgfSSsDTnhDlVspIdC1AYmIijRo1YuHChZds5x81iAJslNs6m9xSsGX73HPPERoayltvvXXJdnfddReDBg3i9OnTJaTsyuONN96gTp063HPPPYVef+ihh6hRowavvvpqCSszeJKLJZMvYRldgTuxr5xtcBw3lrCG0sSjwHQRSQDaACX6T+dYRZwJrAMSsd/vS6TSgoh8BazAXkQhVUTuAV4DrhORbdijSl/zgoYPgPLAAsf70+MuZxfRUXKoqtMH9jDwRxxHlJW+3jraR9RVfSFU9cxJLe3s379fAX333XeLbHv0/V66+flIXZ5yuASUXZy4uDgF9NVXXy2y7bJlyxTQ559/vgSUXXmkpKQooC+//PIl240bN05FRHfs2FFCyjwPsEZLweeNOcxhDnOUpsNKdO1j2J0UqzmOaSLyqHtNTg9QhnzyqlevToUKFS5e3uwcykUNpLltL2s3rC8BZRcnLi6OGjVq8MgjjxTZtmvXrtx2222MHz+e48dLZwqYssynn36KzWZj5MiRl2x37733IiJ8+qlXsksYDAaDoYSwsl17DxCj9jJj47Anu7zPM7LciBaAjz/4+HpbSZGIZxbqsgAAIABJREFUCBERESQnJxfZNiCyn71P8k+oei/32aOPPsrOnTspX768U+2fe+45MjIyig4wMViioKCAadOm0adPH2rXvnTmjLp169KnTx8+//xzCgq8v91vMBgMBs9gxcgT/oywxfG8sLD90oUWlKmgC2eNPCo14HhIY6KzVrD9sHd83NLT7dkRAgOdr5TTunVrBgwYwPjx48nIyPCUtCuOVatWkZqaypAhQ5xqP2rUKB588EGTsNpgMBguY6wYeZOBOBF5UURexJ4t+jOPqHInWgB+pT/o4izNmjUjLS2tyHx5AD4t+tNBklgeX/T2rrtRVaKionj88cct9/3HP/7B8ePHmThxogeUXZnMmjULPz8/+vfv71T7Pn368PzzzxMUVHa+ABkMBoPBGk4beWoPiR+JvTzHcWCkqo73lDC3oQVlotrFWc7Wrt26dWuRbUOjBuAjyqmNP3la1gXk5OQwatQorr/+est9O3bsSNeuXfnoo4/MdqEbUFVmzZrFtddeS1iY8/W2z5w5w5w5c8zfwGAwGC5TLDmqqb0u4ToPafEMZWy7tlmzZgAkJSXRrl0RuTNrtSXDvxqNjy3m+OkcKpYrudzUAQEBxUqq+/DDDzNs2DAWLlzokqFo+CtfffUVPj4+lvrMmDGDu+66i/Xr19OmTRsPKTMYDAaDt7ASXRsoIk86atbOEpEnRMR5ZyxvUca2axs3bozNZnPOL0+E7IbXc5UtkSVbUj0vzkF+fj5ff/21U1vKF2PQoEEMGTKEChUquFHZlYmIEBMTQ3R0tKV+/fr149dff6V58+YeUmYwGAwGb2LFJ+8LIBJ4H3tCwebAVE+IcitlbCUvICCAkSNH0qRJE6faV4rqSznJJjX+dw8r+5NFixZxxx138PPPP7s8RkBAAF9//TUxMTFuVHZlMmHCBBYvXmy5X+XKlenZsycBAQEeUGUwGAwGb2NluzZCVaPOef27iMS7W5Db0YIyUe3iXKzkL7M17E4evpRPXURBwd3YbJ4PeP7+++8JCgrixhuLn8x+165dpKamctVVVwGQlpJA6pIvCDySiGgBmRWbUb3LMBq07FTsuS5H8vLyGDduHMOHD+fqq6+23D8lJYVPP/2UZ5991qyqGgwGw2WGFSNvvYh0UtWVACISA5SO4qmXQgvKRCLk88nMzCQoKKjQGqR/ISCEY1Xa0/HQerYcSCeylmdv1KrKjz/+SK9evQgOLv7vdcSIERw6dIj49WuJn/wYbQ/MpAbKbp96FIgPzdKm4z/zC9b90oPw4R9QqXodN/wUlw++vr6kpqaSmelaGca0tDRef/11YmJiuPnmm92szmAwGAzexMp2bQwQKyK7RGQX9lpsV4tIoqM2X+mkjG3XAkydOpWQkBD27t3rVPvg5jfQzLaXtQmeL1WZnJzMzp073bKKB/Duu+8y69uv2flWLzocnMHqygM4+kACDccl0Pj59Zx5LInYuvfTMn0puR91Z+emOLfMezkRGBhIpUqVXOrbpUsXypcvz7x58/44l5qykTVv30rmC9XIfKEa697sx64ta9wl12AwGAwlhBUjrzfQALjacTQAbgT6Ac4l5/IGBWVvu7Z9+/a8+OKLTvtKhbTsA0BW0nxPygLgxx9/BKBv375uGa95RGNscx6iYU4yq6LfovPoKVSrVe+P6xUqVaPLPW+y55Y5CErlb29mx0Zj6J3lscce47333nO5v5+fHz169OC3334DYOPyuYROu44WJ5eQWPkGEqv2pcmpNdT8ujfr509zl2yDwWAwlABW8uTtvtThSZHFogyu5LVo0YJx48ZRvXp15zpUa066X1XqHoslMyfPo9p+/PFHWrZsSb169Ypu7ASJE+/nxI54bvytEVHX3XnRdo2jupJ3189kEUDozMEcTN3ulvnLMjk5OUycOJGUlJRijXP11Vezfft2Vvz+Iw3n38MxWxXS71lOzOipxDwymZyH17DLryGtlz/CxqWz3aTeYDAYDJ7GykpeGUXLpE/ekSNH2LFjh3ONRcisdw1dJZFV2w95TFN6ejpLly5121bt+p8mEn3sR1aE3MDCZWuYO3fuJdvXCo/gzJCZBGkWJz6/g+ws1/zQLhfWrVvHmTNn6N69e7HGORuwsfbD+8mUIMrdO5ca9f6M7q5cvQ61R//CXp861P51lDGwDQaDoYxwBRh5lLntWoAbb7yRBx54wOn2laJuJFQy2bFhkcc0LVq0iLy8PPr06VPssdKPHqT+qpdI8o3g4de+pFatWkybVvR2YP3m7Unu/DoRecls+HRUsXWUZZYuXQpAt27dijVOVFQUwYH+bNp1mAPXvk/VWuEXtAkJrYjPHdMJ1Gz2fTW6WPMZDAaDoWQoMrpWRDIALewSoKoa6nZV7qaMbdeCvbzZokWLnG7v36Qn+dgI2Pk7MNQjmrZs2UJwcDCdO3cu9lhbpz1GlJ7m6ID3CAgM5Pbbb+f999/n2LFjRQYRtOt9Fyt3rqDTwa9JXHITrboPLLaessiSJUuIiIhwflv/Ihzam0K3OsovqYF8dNWAi7ar2ySKFY0epPOO99jw69e06XV7seY1eIYqVapoeHi4t2UYDAYPsXbt2iOqWtWZtkUaeapavviSvEwZqnhxloiICKZNm8bp06cpV84J/UFhHK4QRavjq9l34gy1wtxv2D7zzDOMGjWq2Mlzt69fTPTxeSyveSddW9vz3w0bNoy3336bmTNncv/99xc5RpsRb7HnP0up+tv/capNd0JCKxZLU1lDVYmNjXVL2pPUOS9zVT0/fvktg0OHDlGtWrWLto2+/Tn2/nsmFZa/Sv7Vt+Lja6kyoqEECA8PZ80aEw1tMFyuiIjTcRClartWRHqLSLKIpIjI2EKuB4jIN47rcSIS7tTAZXAl72wN261btzrdx6fptbSUXaza6ERJNBcJCQkp9hhZP4/jGKG0uuOlP861bduWZs2aMX36dKfGCAwOIbPPe1TTI2ya6noN3bLKjh07OHbsWLErhuzfnUzbY/Oo0aYH4eHh7Nmz55Lt/fwDOBj9JA0KdrN+3mfFmttgMBgMnsWSkSciFUWko4h0P3u4S4iI+AAfAn2AFsAdItLivGb3AMdVtTHwDvC6U4P7l73Ai4iICADnatg6qBLVG5soxzcucLue2bNn06dPHw4ePFiscbYsm01k9gaSGt9HaIU/t2VFhKFDh7JkyRKn8wM263Atq6sMpP2h/7Fz8+pi6SprrF5t/3k7dOhQrHF2z3sXQen72Dvs3LnTqfq37XrfzU5bfSqvn4AWFBRrfoPBYDB4DqeNPBG5F1gC/AL80/H4ohu1dARSVHWHquYAXwPnO1sNBKY4ns8EekmRJSEok9u1jRs3RkQsGXlSqy2nfUKpfGApBQWFuVG6TlZWFkePHqVy5cquD6KKz+JXOUAV2g36vwsuDx1q9yX86quvnB6y6e2vkSlBnJ791BVlcKxatYrAwEAiIyNdHiM7K5OIA3NICOlKzXrO1UoGsPn4cKTlPTQo2MWm5ZeOiDYYzqWgQFm4+SBPfrOBPu8upedbixg6cSXjF25l77ErO1reYPAEVlbyHgM6ALtVtQfQFjjsRi21gXOXcFId5wpto6p5wEngAqtDRO4XkTUiskaxlcno2qCgIMLDw0lKSnK+k82H4zW6EqPxbEo76VY9Q4YMYdWqVfgWwwcrefUCmuYmsSPiPgKDLvybNGrUiE6dOjm9ZQtQsWpNNkc8QsvsDWxY+KXL2soaVatWZdCgQfj5+bk8RuKCqVQkA7+YewCYMmUKDRo0IDs7u8i+rfrcy1EqkB/7ocvzG64sElNP0v+DZdz7xRp+Tz5E9dAAmtcMJSMrj3d/3cbVb/7O379L5GRmrrelGgyXDVbu2FmqmiUiiEiAqiaJSIQbtRS2Inf+cpQzbVDVT4BPAKKjo5UaLYuvzgtERERYWskDCG3Zm9C0eSyKX0GrusVPdQKQl5eHzWbDZiueC2fW4vEcpzxR/R++aJunn36a1NRU8vPz8fHxcWrc6FufYte/v6LaipfIveY2/PyLFxhSFnj22WeLPUZw4lRSpQaRXe0RtbVq1aJr166cPHnyksEXAIFB5VhfdzCd905kz9YN1Gvapth6DJcvM9em8vf/JVKpnD/vDImiX+ta+Pn8+Xmy78QZPlmyg2krd7Mo6RAfDmtH23pXVjCVweAJrNy1U0UkDPgeWCAis4F9btSSCtQ953WdQsb/o42I+AIVgGNu1FCqiIiIYOvWrag6v/Ua2uI6AHK3LnSbjh9//JGKFSuyefNml8fYl5JIq1OxbK59G+VCLp5155ZbbmH06NFOG3gAvn7+nLzqH9TWg6yb/YHLGssKeXl5lt4ThbE3JZEWOYmkNhiMzfG7vu6665g2bVqRBt5ZmvR9jFz1Yd9vnxRLi+Hy5su4PTz1bTwdGlRk3mPduLltnb8YeAC1woJ4cUAksx7qgo+PcPsnK1mwuXj+vwaDwUkjz+H3NlpVT6jqi8DzwGfATW7UshpoIiINRMQfuB2Yc16bOcAIx/Nbgd+0uHe7Uszf/vY3pkyZQn5+vvOdKtTmcFBDGpyIc1uJs9jYWLKysmjYsKHLY6T9/Ba5+NKk7xNFtj158iTTp0+nwIKPXetrBpPk25zwTR+Sdea0yzrLAlOmTKFKlSqkpaW5PEbqUnvi6QY97/rLeVXl6NGjTo1RpUZdNoZ0psmBH8jNKXqL13Dl8VvSQZ77PpGezaox6a4OVCznf8n2UXXD+P7hrjSrGcpD09ay0Bh6BkOxcMrIcxhS35/zerGqznEESLgFh4/dI9gDOrYAM1R1k4i8JCJnM7R+BlQWkRTgSeCCNCuXE+3atWPQoEGW/eCy619DtCSxepvrRsC5xMbG0r59ewIDA13qn37sIK0O/8D6itdTrVbRNW/nzp3L8OHDWbVqldNziM1G3jV/pzpH2fDd2y7pLCs0bdqUIUOGULNmTZfHqLF3Hlv8Iqlep9Ffzt977720b9/e6XGk7XAqc5JNi2e5rMVwebLnaCajv9pAi1qhfDC0LQG+zq3OVw4JYOo9HYmsFcojX61jo5v9iw2GKwkr27UrRaR4+RqKQFV/UtWmqtpIVV9xnBunqnMcz7NU9TZVbayqHVXVyeKuZZOCggIWLVrEpk2bLPWr1qYPAZJL2obib9nm5OSwevXqYlW5SP5lIoGSS+UezpXDuummm4iLi7OcA67lVQPYGNCGJlsncjrjhCtSywTdunVjwoQJLvtI7tqyhgYFu0lv1P+Ca5GRkezevZv9+/c7NVbLqwdxhDB0/VSXtBguT/LyC3jsm/WIwH+HtyfY39oX1dBAPz4d0YFKwf7c98UaDmVkeUipwXB5Y+Uu0QO7obddRBJEJFFEEjwlzGDPHde/f3/++9//Wurn36gbOfgTuHtRsTWsX7+e7OxsunTp4toAqlRNmUGyTxOaRHVyqktISAgdO3bEmew45+N73Tgqc5KE/71huW9ZICcnh61bt1rayj6fA8u/JF+FRtcMu+DaWWM+Li7OqbF8/fzZVrMfrU6v5OjBVJc1GS4vPo/dxfo9J/jXTS2pU9G1PKVVywcwcUQ0JzJzeWDqWnLyrpwUSQaDu7Bi5PUBGgI9gf5AP8ejwUOICAsWLODvf/+7tY5+QRys2I5WWWs5mF68b8CxsbEALq/kbd+whPD83RxrOsRSv6NHj3L//fezcKG11chm0b2ID4qh+c4pl+Vq3oYNG4iIiGD27Nkuj1Fz3y8kBbSmSo0Lt87btm2Ln58fK1eudHq8Glf9DV8pIGXxlZPCxnBx9p88w9sLttKzWTUGRNUq1liRtSrw1uAo1u85wbu/Ol/9x2Aw2LFi5O0BugEjVHU39tQlxauMbiiSTp06ueR75RdxHU1saaxNSCzW/LGxsYSHh1Orlmsf1seWfcYZ9afF9Xdb6le+fHlmzZrFpEmTLM8Z2GssYZwicfZ4y31LO/Hx8QBERUW51D81ZSP1C1LJaNC70OuBgYG0bduWFStWOD1mePMO7LbVJSTl/Dgpw5XIe79uIy9f+eeASJdW48/nxlY1GRxdh48WbWft7ss2mYLB4BGsGHkTgM7AHY7XGdjLkBk8SHJyMm+++SaZmdaywVdrY8+Rd3LjLy7PrarExsa6vFWbdTqd5kfnk1ChBxUqWquU4e/vz+DBg5k9ezanTp2y1DciuicbA9rQOGXyZRdpGx8fT/ny5QkPD3epf+qq7wCo1+mWi7aJiYlhzZo15OU5F50tNhv76txI8+yNHN63yyVdBjeS4733/M4jp5mxJpWhMfWoW8l95SSf79eCWmFBPDkjntPZ7skaYDBcCVgx8mJUdRSQBaCqx4FLx8Mbik1CQgJjxoxh61ZrWxW26i044VuFKgeXuVzibM+ePezbt89lI2/zr1MJ4QzBMXe51H/o0KFkZma6tjXZ7f+owgk2zJ3g0tyllYSEBFq1auVy0EXI7oXsstWjVoNmF23TsWNHMjMzLeVFrH3VUGyibF80zSVdBjeS570ghXcWbMXfx8aoHo3dOm75QD/eui2KPccy+c98awniDYYrGSt3ilwR8cFRYUJEqgLGE9bDRETYi4pYrXyBCMdrXkWHggSS9rnmmxYcHMxbb73FDTfc4FL/oI1fsldqEtmp8K3BoujatSv16tWzVObsLJFd+pHs24x6Wz65bHK4qSoJCQkub9WmnzhKRFYi+6tfc8l2Z6OaraSwqde0Ddt9GlBxh6ll63UKLOTVdCNb9qczN2EfI7uGU7W8+6vOxDSszNCO9ZgSu4tN+0xaFYPBGawYee8B3wHVROQVYBnwb4+oMvxBkyZNEBHrRh5QsVVvwuQ0yesXuzR31apVefLJJ2nc2Pq38n0piTTP2cieeoOw+bi26mSz2Rg6dCjz58/n0KFDlvqKzUZWp8eppYfY8NOnLs1f2ti9ezcnT5502cjbFjsHP8knrE2/S7Zr3LgxYWFhlow8gEP1+hKRl8T+3WalxasUeGc784PfUwjx9+WB7o2KbuwiY25oRsVgf57/fqPLOxQGw5WE03dfVZ0OjMFu2O0HblLVGZ4SZrATFBREvXr1SEpKstw3rOUNFCAUbPvVpbkXLlxo2bg6S+rvn5CnNhpef59L/c8ybNgw8vPzmTHD+lutdc8h7LCFUy1hAgVWqoaUUhIS7BmLWrdu7VL/guSfOUEITdv3umQ7EeGDDz7g7rutBcvU6zYcgN1LTZStV9GSf6/vO3GGnzce4I6YelQI9vPYPBWC/Xj2xuas23OCGWv2emweg+FywWkjT0ReV9UkVf1QVT9Q1S0i8ronxRnsREREuGTkEVyJ/cHNCD8ZR1autQ/+06dP07t3b9577z3L0+bn5dIwbQ4JwR2pWTvccv9zadmyJa1ateLLL60bDmKzcbz9o9QvSCV+Ydn3FTsbWduqVSvLfbWggPonV7E9JBofJyqoDBs2jE6dnMtreJbaDZuT4tOIsN3zLesrLYjI3SIS4Hg+UEQeEBEXk0R6CS9s105buRtV5c5O9T0+16B2tekQXpE3f0kmIyvX4/MZDGUZK/to1xVyro+7hBguTosWLUhKSnIpAW5ueA+i2Mb6rXss9QsICGD58uWWV3MANi+eSRVOUBB1p+W+hTFs2DBWrFjBjh3WC5y0ueEu0qQ6Qas/oqyXOU5ISKBRo0aEhIRY7rtn6waqcYz8Btc41T4rK4tffvmFXbt2WZrncO1raZqzhSMHrL3fikNxEkMXwmOqmi0iL2IvndgAeEFEYkWkhjsn8hglvF2blZvPV6v2cF2L6m6NqL0YIsLz/Vpw9HQO/1283ePzGQxlmSKNPBF5SEQSgQhHpYuzx06geEnYDE7RsmVLzpw5w86dOy33rdHuRnylgH3r51nq5+vrS0xMDA0bNrQ8Z8G6LzhCBVr1uNVy38IYOnQoY8eOdal2ro+vL6nN7qZZ3haSVy1wix5v8fTTTzN+vGu5//Y7/v512t/oVPsTJ07Qu3dv/ve//1map0bMIHuU7dKS8+Q4ccKtSa/P1uO+EeipqmNV9QbgFexppEo/JbySN2fDPo5n5nJXlwYlNmfrOmHc1KYWny7dyb4TZ0psXoOhrOHMSt6X2CtbzHE8nj3aq+qFdZEMbqdly5YAlmvYAgQ26ESmBBO4+3dL/SZOnGi52gTAsQN7iDy1kuTq/QkIsG6UFUbdunX597//7XJC5lZ9H+IEIZxZ/I5b9HiLjh070q/fpYMmLkbgniWkSk1qhUc41b5GjRosXryYe++919I84c07kCo1CNpu7UtFcahUqZI7h9srIp8D1YCgsydV9Ufsq3qlnxL0yVNVJsfuolmN8nRq6Na/Q5E8dUMECialisFwCYo08lT1pKruUtU7gHTsVS7qAy1FpLunBRrs27UAW7Zssd7Zx4+DVbvSIWcVu49kONWloKCAZ555hq+//trydCkLJuIrBdTsUbyAi/PJy8tj3rx5pKSkWO4bHFKBpDpDiDq9gj1bN7hVV0mxd+9evv/+e8uJoQFyc7JpkrmBtEoxlvp1796d0NBQS33EZiO1ei+anVlP+omjlvq6wokTJ9izx61bw3cBi4GBwCwReUJErheRZ/hzlc8tiEhvEUkWkRQRGVvI9e4isk5E8kTE+WXxElzJ27QvnS370xnWqb5bqltYoU7FYO65qgH/W5fG5n3pJTq3wVBWsBJ4cS+wBPgF+Kfj8UXPyDKcS/ny5UlLS2PMmDGu9W8zkGpygvg451bztm7dyvHjxy0nQdaCAmrunMVm3xY0bNbGFakXJT09nYEDB/Lpp66lQ2nS/0ly8WX/z2+5VVdJMW/ePG6++WYOHz5suW/K+kWUkyz8m/a01G/79u288MILHD1qzVgLa3cz/pLP1mWzLPVzhUmTJtGggfsW2FQ1XVUnq2o8cBvgi93wqwdYK8B8CRw5Rz/E7tfcArhDRFqc12yPY25rUUcF+eBeP8WLMnNtKv4+Nvq3tl560R082L0R5QN9eWehqWtrMBSGlcCLx4AOwG5V7QG0BazfcQwuUatWLZe/KVdp2498bBRs+cGp9rGxsQCWjbxtaxZStyCNk81vt6yxKCpVqsSSJUv45z//6VL/ytXrsKFyH9ocnceRA2Uv9cLw4cNZuXIl9etbj148sXE++So07OCcP95Z0tLSeOmll4iLi7PUr2n7nhwhDFuSc++34vDFF1/Qvn17j4ztMPjeVNWhqjpKVXe5cfiOQIqq7lDVHOBr7KuH586/S1UTsJx0XiHb88mCc/MLmBO/j2tbVCMs2DvFjyoE+3F/t4Ys2HyQhFS3+maWWUaPHs3zzz//x+sxY8bw/vvvuxS4Zij7WDHyslQ1C0BEAlQ1CXDOwcdQbBYvXszIkSPJzXUhZUBQRVJD29EifRknM4vuHxsbS6VKlWjatKmladJXTOK0BhJ53d+sa3SCTp06ERDgeib9Wn2ewo88tv3wthtVlQzBwcHExMS4VM6s4v7lbPdrQoVKVS31a9euHTabzXJSZJuPD9srX01ERpxHawcnJiYSHx/PnXe6J4q7hKkNnPttI9Vxzj2cOe62oS7GouTDHDudw6B2dTw+16UYeVUDKgb78db8K281T1X5+eefeeaZZ/44d+zYsT/uEwUFBcyaNYvRo0fTqFEjevXqxbJly7wl1+AFrNwxUkUkDPgeWCAis4E0z8gynM+ePXuYP38++/fvd6m/NO9LU1saq9etLrJtbGwsnTt3tmRQZGYco8Wx30is2IvQ0IouaXSGjz/+mBEjRrjUt26TKOLLdaF56gwyT5WdskiqynPPPcfq1UX/7c7nVPpxGucmc7S69VRvISEhREZGWl7JAwhqNZBykkVSrOfKnE2bNg1fX19uv939K8clQGHL8i7n+BGR+0VkjYisAeCM51e1Zq1NpUqIP92bWvvy4G5CAnx58OpGLN56mDW7jnlVS0myc+dO+vbtS58+fZg6deofbhXTpk3jtddeA+xVg7Zv38727dt59dVX2bx5M926daNv374uZWswlD2sVLy4WVVPqOqLwPPAZ8AiD+kynMfw4cNJS0ujXr16LvWvHTMIgIz4OZdsd+zYMbZs2WJ5q3bz/MkESzYhne9xSZ+zHDlyhC+++MKlMm8AQdc8ThinSPyhbGTDAHs5s1deeYV169ZZ7rtz/e/4SgEhTa92ae6OHTuyatUqyzkGm3XuS4YGkbPx0u83V8nPz2f69On07t2bqlW9a2S4SCpQ95zXdYB9rg6mqp+oarSqRgOQ5Vkj7/jpHH5NOsjANrXxc7FsoTv5W+dwqoQEXDGreRMnTiQyMpKlS5cyfvx4du/eTeXKlS/avmHDhjz77LNs376d119/nWXLltGmTRuXkswbyhYu/Xeq6mJVnQM86mY9hotQ3Mg1n0r1SQtsQr3Dv5Odd/Hou5UrVwLW/fEqbvmS7bZwIjv0KJbOorjnnnvw9fXlv//9r0v9m3W8niTf5tRNnkxerluDJT3G2UoXrtSsPbV1sb28XDtrQRdn6dixI8eOHbPsz+MfEEhyaBcaH19Kfp77k/MuWrSItLQ0hg8f7vaxAcTOcBEZ53hdT0Q6unGK1UATEWkgIv7A7djTVLkHD2/X/rzpALn5ys1t3bfDXByC/H0Y1aMRK3YcJTbliLfleIzc3FxGjRrF/fffT7du3diyZQuPPfYYfn7OlZILDg5mzJgxxMfH07JlS4YNG8ajj5rb+OVMcb+ClWzM/BXO448/zhNPPOFy/5zGfWirycTFXzzfXmxsLD4+PnTo0MHpcXcmLKNRXgoHmtyBuOAzZoUaNWpwyy238Pnnn5OZmenSGJkdRlFLDxK/YLqb1XmGhIQEROSPfIlWCDu0iu1+TShXPsyluWNi7GlXrPrlAUjz/lQineTV7k9CPXXqVEJDQxkwYIDbx3YwAegM3OF4nYE9GtYtqGqvUclJAAAgAElEQVQe8Aj2LAVbgBmquklEXhKRAQAi0kFEUrFH+X4sIs4nyvTwdu1PifsJrxxMZC1rKXY8yR0d61GzQuBlG2mbmZnJjTfeyIQJE3j66af56aefqFPHNX/I8PBwFi9ezNNPP01kZKSblRpKE8W9I5ftOlFljD179jBvnutJZmt3G45NlKNxF89/t3fvXtq0aUO5cuWcHvfo4o85o/60uMFa4lxXGTVqFCdOnGDq1Kku9Y/qdQepUpPy6yagJZRqojjEx8e7VM7szOkMGuUkc7xKtMtzR0ZGEhQU5JKR16zbzWSrH+nrrVXNKIrMzExmzZrFrbfeSlBQUNEdXCNGVUcBWQCqehxwawipqv6kqk1VtZGqvuI4N86xS4KqrlbVOqpaTlUrq6rzd2MPruQdP51D7Paj9GlVs8Rz412KQD8fHry6Eat3HWflDs/naCxJTp8+Tb9+/fjtt9+YPHkyb7zxBj4+PsUa09fXlzfeeIMHH3wQgDVr1rj8xdlQenGmrFmGiKQXcmQArpUgMLhEy5Yt2bZtG1lZWS71968eQWpQBI0P/syZnMK3bKdMmcLy5cudHvNMxglaHPmFhLBeVKxUxSVdVunWrRvR0dG89dZbLtUt9fH1Ja35PTTN28qWuF88oNC9xMfH07p1a8v9tm9YhL/kE9TU9Zzlvr6+tG/f3qXgi3Llw9hSLprwQ7+71Zheu3Ytubm5HtuqdZDryGWnACJSFcupTLyE2Dxq5C3YfJD8AqVvK+/kxrsUQzrUpUpIAB/8Zj1pemlHRJg6dSp33XWX28c+ePAg11xzzV+idA2XB85UvCivqqGFHOVV1bckRBrstGzZkoKCAtcqXzjIa34LrWQ7cWsuvjJjJU3Jpvmf2QMuupTMKh7YP+yeeuoptm3bxty5rkVvtu77ICcIIXvp+25W515OnTrF9u3bXfLHy0haTL4KDdpdVywNHTt25PDhwy4Z1LlN+1GDw2zbsLRYGs6lW7duHD58mO7dPVpw5z3gO6C6iLwCLAf+7ckJ3YbN16NG3k8b91O3UlCp2qo9S6CfDw90b8iylCOs2+P5NDKepqCggKysLMqVK8eCBQsYOnSoR+apXr06U6ZM4YUXXvDI+Abv4f2wKIPTnL3Rb9jgemmuut2HU4CQsfqrC65NmTKFAQMGcPq0c7nNtKCAqps+J8XWgBYdXHPsd5VBgwZRv3593nzzTZf6B5X7f/bOOz7m+3/gz89lD0mISCIyCbFHIjax96YoHWaNWh2qVaNfrdIarb2qglJKzdoaIzGjCJHYJCFIQrasu/fvjws/JZGbGXrPx+PzuMvde7ySu3w+r89rliLSpQ+1005y/7b6PYELiytXriCE0EjJs3l0hjvGXtjY5Z91pwqzZ8/mxo0bGtXoq9y8H9nCiIRzW7WS4TnPs3xLlSqltbuqgH1+AyYBs1BmvXYTQmzR24a6RGYM6fpxVyalZxNyM55ONfTsqhUCbhyGPz+ChfVgVgX4wQt+aQ9B30PCrXynvtvAjdKWJm+FNW/y5Mm0atWK9PR0jf7/1KF3796ULVuWrKwsdu7cqde9DBQeBiWvBOHt7Y2VlRUXLlzQeA0juwpElapH7Sf7eJz0b2UuMzOT5ORkLC0tVVorImQX7ooo4msM03vCxasYGxszceJEQkJCNCotAlCp80RykBG9b4GOpdMdYWFhAGq7azMz0qmYGUG8vebxeM9RNXMvL2zLOBBhUQfXh4d04rLdtm0b/v7+xMTEaL1WXrwcngKcBWbnHudyXyv+yIz0puQdinhEtlzQSZ+u2gcXYWUA/NYbru+HclWh7iCo2hWEHI7NgUW+sHUoJL3+PbAyM2ZYMy/+jnzMlfslpx5mXjRo0IBGjRrpM/b0NRYuXEiPHj3YvHlzoe1pQH8UCyVPkqQykiQdkiTpRu5jntV0JUnaL0lSoiRJ+u+XVAyRyWTUrl1bKyUPwKLRUNykx5w99Me/Xh8xYgRHjx5V+Q5dfnIxcdhRp5N+a+Plx9ChQzl8+DB169bVaH7Z8h5csmtNrce7SHpaPMsuREdHY2tri4eHh1rzbl86gbmUjVkl3bg0R40axZdffqnR3GcVO1FBxHLnqvrFnF/FzMyM0qVL4+ysHyUjj/CUUi8dxc8/mRd6tOQdDH9IeVtzalWw1cv6hK6B1a0hJRa6L4XPbkD/36DjbOj6Mww7DJ9EQJPxELkHljSE8O2vLfNeI3dszI1LrDXveceK3r17M2/evEJNcBk3bhxNmzZl8ODBWl9rDBQ9xULJAyYDR4QQ3sCR3J/z4kegRPYw0hX16tXj4sWLGsVHPcfRvy+JstKUvroehULp/srJyVGr4G105HlqPjvHddd+mJsX3l3my1hbW9O6dWskSVK7WO9zSreagKWUScRfxTM2b+bMmcTGxqp9kk+MCALAs14bnciRk5ODXJ5/fcU3UbFZPxRC4tEZ7b2dXbt25cCBA3p11ZZ49KTkZebICbkZT0ufcvpROo7OgT0TwasljDkDdQeCcR4JzTbO0PYb5ZhyPvDHh7B3Esj/v2WjjbkJHzbxZH/4Q64/StG9rHrk6dOn1KxZU+PqAdpiamrKtm3bsLe3p2/fviQnlwwDtoG8KS5KXncgMPd5INAjr0FCiCMo61X9Z6lbty6pqancvKnFHaqxKY8rvUMj+XnO5Rba3bx5Mw4ODiq3uonb+y1pwpwqXSZoLoeOmDlzJv369dNobqXaTQg3rYXHzQ3FtjiyJq4aq4dnuSNzx66sk05kWLVqFT/88INGc8s6uRJpVgOn+9rVy4uJiSElpXD+/SVJ+iSPY6gkSXUKRQBtkBlDRtK/lB5dEHr3KWlZclpWKafTdQE4tQSOzoLa78K7m8FChdaIpT3gw73Q6GM4uwI2vwfZ/195YHBjD6xMjUqUNU8IweDBg7l9+zbe3t5FJke5cuX4/fffuXv3LiNHjtT4JtpA0VNclDxHIUQsQO6jHs4ibwf+/v60adOGZ8+eabWOe7vRCEki/m+lBSs4OJisrCyV2qZFRZylTlIQF8v3o6xj0VfRMTMzw8LC4oWLQ12y64/CiXguHVynY8m0Iyoqiq5du6rdszY7K5NKz67wuIyvzmXS1JqX7NkRT8U9om9c0njvL774gmrVqhXWBccPGAm45B4jgABglSRJkwpDAI2R5RY90HGG7dFrjzE1ktG4knaJPK9x/QAcmAJVu0H3xcqYQlUxNoX230HneXB9H/w+ALKUtd5KW5kyqJE7e8IecDsuVbcy64lVq1axc+dOfvjhBxo2bFiksjRp0oRvvvmGTZs28euvvxapLAY0p9CUPEmSDkuSdCWPo7se9nrRrDsuLk7XyxcpNWrU4NChQxplW76MWVkPbpVrT0DybsJv3iYkJIRGjRqp5AZL+Gsm6ZhTtddXWsmgKyZNmkRgYKDGCQK1WvUjRnLG+sLKYnXHGhcXx507d9SW6XZYCJZSJsZeuisxkp6ejpubG/PmzdNovkdTpaU15qRmwdwZGRns3r2bDh06FFZ8kj1QTwjxqRDiU5RKnwPQHPiwMATQmOdKno5dtkHX4mjgVQZLUx1WzkqKge0fgVNN6LlCPQXvZeoPU8bw3T4Kmwe+sGIOb+aFqbGMpUfzz8YtLly/fp2JEyfStm1bxo0bV9TiAP+f3Tt27Fhu3Sr4b5ielcOTlHTikp+Rlqn7doYG1KfQ6twJIfINDpIk6ZEkSc5CiFhJkpyBx1rutRJYCeDn51d8rto6JDs7W6usRwCXrlOw+mUfl7fM5MqVK/Tt27fAOdfPHaJu6nFCKgyliYNuXIG64p9//kEul6vVkg1AZmTEfZ8PaRDxPZGhR/Cpr5s4Nm3x9fXlypUras97elUZj+euo3g8UPa8NDU11ajzBYCTayWuG1embNQBlFVJ1OPQoUOkpKTQp08fjfbXADfgZf99NuAuhHgmSVJmYQmhEc8VpfQnOlsy+kk6Nx+n0r++q87WRAjYMUqpkPVdC6aqZfXnS92BgICdY2DXOOixlLLWZgzwd2PdqXuMb+2Naxkt99ATz4t7m5ubs3btWr2XS1EVIyMj1q5dS40aNRgyZAhBQUEvZMvMzub6uSMkhh/GPP4yzpm3sRUplJGeoRASqZhzV7IjxtSLTPvqmFdtS23/lliba3fdMqAexaWY8S7gA5SlCj4ADEV63sDXX3/NL7/8woMHD7Syali51uR6uY5YBG9DCEHTpk3fOF6ek4PR/kk8wp7a/YtX0Uy5XE7fvn2xsbEhNDRU7cD8Gp1GkhyxkPRjP0MxUfI0xSL2DFEyF9ycdHhBRhkqEBwcrPH8J+4daXjrZ2JuXqFCJfX68G7dupXSpUvTqlWh1WPcCJyWJGknyh7dXYFNkiRZAVcLSwiN0IMl7+h1pUekpY8OI2ku/wF3jkPn+WBfUTdr1h0ESfeV8X22LtDqaz5qXpHfTkex7NgtZvWsqZt9dMyiRYs4d+4cmzdvpnz5og+BeRlXV1cWLFjAnj17SEtLI+lxFA8O/ozno4PU5CkKIfHAuALxtjWJt3ZEmNlghAIyUzBKfUCl1Os4xYZA7EoeHSnNmTJtsW32Eb51fYtVW7y3leKi5M0GtkiSNBSIQtmQG0mS/ICRQohhuT+fAHwA69zG3UOFEMW/L5WOady4MUZGRmRlZanVnSIv3Pr9wOpNuzGSSdSqU++NY89t+JqG8tuc8/+J+qX0VEJBQ4yMjPjuu+8YMGCARq1/rErZcdq5F/UfbODB3WuU96iiH0HVoHHjxnTo0IFp06apPEeek4NXehhX7dtScHSlevj7+7Np0yZiY2M1KmHiEfA+ipsLiT6+jgqVVE/ieF6ctWfPnlpbr1VFCDFTkqS9wPM7n4+EEKG5zwcWihCaogcl79i1x7iVscSrrOo9rd/Is0RlHJ6LL/h+qJs1n9NiEiRFw/EfoVw1nGr0oq9fBf4IjWFsq0o42xZNNYD8iImJYfr06XTu3Fklb0pRMHjwYDo0rcPtlQOpmXyccsgIt27Mw5o98WzUkwq2ZajwhvnZKXFEn9lBxuVdNH+6DZNdWzi7rz5Zzb6gSbM2BmVPjxQLm7AQIkEI0VoI4Z37+CT39dDnCl7uz82EEA5CCIvcxt3/OQUPoFOnTnzzzTdaK3gA5vZuBCU4UM9JIuTPxfmOu3pqH353VnCuVGv8On6o9b76oF+/fjRo0IApU6Zo1Gjbs/NEFMiI2jdfD9KpR2pqKqdPn1b75Hcn/DSlpGcYeb7ZKqsJ/v7+AFq5bCPMalEherdahZGPHDlCUlJSYbpqkSTJDKgCWAG2QCdJklTXtosSHSt5GdlyQm4mEFDFQXcX46DvID1eacXTNA4vPyRJua5rA9j5MTy6ysgWFVEIwYpjt3W7lw6YNGkScrmcRYsWFUtlJyMpjgvLh1Hut9Yo7pykf7AHicNDqfv5Hmp2GIq1bZkC1zAp5YBXm+FUm7gbxfgwrniPpkp2JE3/7sOp2V0IDw8rhN/kv0mxUPIMqE9KSgo3btzQep20tDQu3rhP3SputI+az/4dr9dmun4xmAoHhvBA5kzlISuL5YkIlD1t582bx4MHDzRKEHCsUJFLNgHUeLiTlCTdxTNpwvN2Zup2uogPV8bjudbVvcu5bt26GBsba6zkAaT79MZVPODGxeMqz9m6dSu2tra0aVOobvSdKEs75QBpLx3FH0kCU2udxeSdvfOEZ9k6LJ0Sdw3OrVYmS5TXU0UaY1PoGwhm1vD7u7haZNGrngubzkbxODmj4PmFyLfffsu6devw9PQsalFe4+aJzaT/5Efth9s4XaYbxyt9xaF/7hGXpHl1B7PSFagx8HusJ10h3HsUdTJD8drSmv3LJ5GiYktNA6pjUPJKKN27d9dJs+rg4GBycnLoMf5HHpt70PLCBPYu+Yzrd+4R/eAhJ9b/D9ftPXmGBSYfbse2dFkdSK8/mjRpQp8+fZgzZw73799Xe75tq/FYS8+4smeJHqRTnUu59QvVzaI2iznFfckRxwo6inF6CQsLC2rVqqWVklel1SAyhQlPT6lW6DU7O5sdO3bQrVs3nViu1aCCEKKfEOIHIcS850dhCqAVlmV0Zsk7ei0OU2MZDb10VDrl2BwwtoAW+dW81xE2zvDOOqXrdvd4Pg6oRI5CsOxY8ci0lcvlCCHw8vIqVCu1Kigy04hY/h6VjowgATsudtpJ0/GBjBs/kVu3blG9enWt9zCytKP6wNmIMWe5V6YxHR6u4MHcJoSeP6OD38DAcwxKXgmlYcOGXLx4Uet6eVWqVGHWrFm0aNsJp7EHuWffnE5xq6gcWAvXlVVodmsedyyqYzLqKM7uRR+npgo//PADOTk5TJ6s/kXEu24LIkyq435zHfKcoisBcOnSJWxsbHB3d1d5jkIuxzP9EvdtdV8f7zn+/v6cO3dO444rNnb2hJdqjHfcQbKzCk5SDQ0N5cmTJ0VxETwpSVLxjNJXBUt7pTtUBxy99phGXvZYmOrArfo4Eq78CQ1GgJWO6+3lhVtDaPU1XN2B272t9KrrwsYzxcOaN2PGDLp27UpWVvEqwp5yP5L7c5tQJXY3+0oPwumzk/g2aAEovSVlypRBoVBw6JB2xc2fY+Xgjs/4ndxuvQIn4qm2qysH189BLte+17UBg5JXYmnUqBE5OTmcP39eq3U8PDz48ssvsbS0xMTansrjtpMwYB+XfcZzyWcC93rsoNoXQZRx1HUYv/7w9PTks88+Y8OGDZw6dUrt+Rl+H1FePObS4Q16kE41Ll26RO3atdVyjd+LDMWOVCSPJnqTy9/fn6SkJK5fv67xGrLa/SlDMleDX+85+iqNGjXi3r17tG/fXuP9NKQpcF6SpGuSJIVJknRZkqSSEzhkVQ5StapEBcC9hDRux6fRsoqDDoQCjv8AplbQaKxu1lOFxuPBKwD2fcGEOoIchWDF8aKPzXN0dMTNzQ1T0zxatxURjy/sRbaqJdZZcRzxXUKHcYspZfV6ss0vv/xCu3btOHBAd2HxXs36Y/rxKe5bV6fdrVmc+7EbcfFvV53bosCg5JVQGjRoAMDp06c1XiM5OZmdO3e+1irKvkpjavb/H7X7f4N7nZbKGJ8SxuTJk3F2dlYrM/U5tVoP5IHkiMX5FXqQrGAUCsULJU8dHl/+GwCXOm31IRYALVq0YNq0aVjlceJXlWrNexGPHYrQwIIHA25uboXtqgXoCHgD7VCWT+mS+1gysHaANO0vkEevKdcI0EU83nMrnn8hWfGeI5MpCy2bWuFyeAx9a5dlw+l7PE4pWmvexx9/zNKlS4tUhpeJ+ns1ZXYOIgYHbvfaS9tuA/O9yXz//fepXLkyY8aM0dqb9DIW9q54f3qEMJ+J+D0LIXlxSy5fvqCz9f+LGJS8Ekq5cuWoWLEiISEhGq8RFBREjx49uHjxog4lKx5YW1uzbds2Nm7cqPZcI2Njorzfp2r2Va7/c1T3whXA7du3SUtLo04d9YLSTWJO8hAHvZZ/8fLy4ptvvsHVVfMafKZm5txw6UGttFM8jM6/r+ixY8fo2rUr0dHRGu+lKUKIe0Ay4Ai4v3SUDKzKKZU8LTu4HL32GM+yVnjoonTKqcVgYqHsNVvYlHKCHsvg0RW+MtlEjkKwsogybbdv386GDRuKT3cdIYjZ8Q1uxz/loqw6JsMP4FvADaaZmRnLli3j1q1bfP/997qVRyajVv8ZxHbdSFkSqbC1CyGHtup2j/8QBiWvBNO8eXOOHz+ucXxUp06dCAoKemEVfNto1KgRDg4OKBQKMjPVa1JQo8sYUoQFyUEL9SRd/jxXutVR8oRCgXvqJaJt6+pLrBckJyer3U/3VdzbjkYC7hxclu+YuLg4bt26hb19IVp9cpEkaRhwHDgAfJP7OKPQBdEUKwdQ5GjVvzYjW87JWwm0qKwDV21agrL4ce3+hWvFe5nK7aDhaGzC1vBFxSg2nLlHXErhNi9JTExk5MiRLFy4UOPztk5RKIjdOIYKF+dzyDiACh//hZeLajUwW7VqxaBBg5g9ezaRkZE6F83VryMMO0KqSRkaBA/n7/Wzio9iXIIwKHklmFatWvHkyRPCwjQLFTIxMSEgIKBYxYTomszMTBo3bsyMGTPUmmdtU5pwp+7UTj7Ko5jCzcZzcHCgb9++amWwRV2/iD1JCDf9xeM957vvvqNJkyZqK84vU96jCpct61Mpelu+CRh9+vTh6tWrWFoWSSuq8UB94J4QoiVQFyg5AULWue5VLVy2p24nkJmj0E2Xi38CIScD/D/Sfi1taD0dylVjSMJcrHMSWXm8cP+3p0yZQnx8PMuXL1e7K4/OUSi4v2EEzjd+Y4tZb+qO34xTGRu1lpg7dy5WVlaMHj1aLwqYbYUqlJt4nOulGtDq1hwOLR5LVrZc5/u8zRiUvBJMy5YtAfj777/Vnnv16lU+++wzHjx4oGuxihVmZma0aNFC7XpzAG4dJiJDwe29P+tBsvxp0aIFW7ZswdzcXOU5D8OOAOBcu7W+xHrBBx98wO7du7Xvr1l/OA485eK+X157KykpCbm8SE/mGUKIDFAWRhZCRKIsjlwysMq1vmmRfHHsWhzmJjIaeBZc7PaNyHOUdfG8AqCcj3ZraYuJOfRahXFmEr/ab2D96buFZs07d+4cy5YtY8yYMdSr9+buQnpHoSB2/TBcbv/BZov+tBu3jLKlVD/fPMfR0ZHZs2cTFBSkUWiMKphZlabqxN2EO/WgXcJ6Tv30Luk6jAN82zEoeSUYFxcXKleurJGS9+effzJ//vyiv5ssBObMmcOAAQPUnlfe04cw6yZUfbCNZ2kpBU/QEfHx6pe+MI4+SRylqeClff2qgqhWrRrt27fXusVYrYA+3JF5UO7SUhSvKHRTpkzB29u7KBW9GEmS7IAdwKHcHrYl547ohSVPcyUv6NpjGlcsi7mJlueIyD2QfB8ajNRuHV3hVAPazKBmajC9xBGWBOUfF6or5HI5o0aNwtHRkZkzZ+p9vzeikBP323Cc72zjN/MBdBi7CDsrzRObhg0bRv369fn0009JSkrSoaD/j2RkQvWP1nKl0ihapO3n6oKuJCZqHorwX8Kg5JVwAgMDWbYs/7im/NizZw/+/v44OjrqQariR3Z2NvPnzycoKEiteWbNxmJHKmF7V+pJsn+TkJCAg4MDixYtUnmOUChwS7lAVKm6SNpa11Tk9OnT/PHHH1qtIclkJNQdjbsimktHNr14XaFQsG3bNurVq1dkNyFCiJ5CiEQhxAxgKvAL0KNIhNEEq1wlL1Uzd+2d+DTuJaTrpnTK2VVg5w7e7bRfS1c0GAWeLZhhsoHgM6eJfqJ+G0R1WLZsGefPn2fBggXY2hZh328hSNgyFodbW1lrOoAOH/+MraV24TpGRkYsXbqUx48fs2KFHisSSBI1Bs0mvN431M0MJXZhOx4+jNHffm8JBiWvhNOwYUO1Mx0fPXrE2bNn6dKli56kKn7I5XKWLFnC2LFjyVGjyHFV/3bcNKqIU8QaFIVQnFMmkzFv3jwCAgJUnhNzOxwHnpLj2lh/gr3CokWLGDdunNZxOHU6DOa+5Ij1mQUvrHkhISE8fPiw2HQBEEIcE0LsEkIUr6q1b8KiNEhGGlvygiKV87QunZJwC+4Fg+8Huu9Rqw0yGfRYhrGpOfOMl/Dzwat62yo2NpYpU6bQpk0b+vXrp7d9VOHJ7mnYR/7GeqNetB+zAHtr3ZQm8vPzIygoiE8//VQn672J6t0mcCNgGZ7yOzxb0Z67dzSv2VkcEUKQlJTE/fv3X5Q3E0JofK41KHlvAYGBgWrdQW3duhUhBN27d9ejVMULc3Nz5s2bR3h4uFp/K0km42mtYbgrYrhyouDivdpSunRpPvnkE2rWVL3ZQuylwwA41dJ/PN5zWrduzcOHD4mIiNBqHWMTUx7UHoe3/Cbn9yg/l61bt2JmZkbnzp1VWuNuRKhWMryVyGTKuDw1YvISEhI4fvw4u3btYt2mLdg8ukBUxAVu3ryp1o3Rv7i4ESQZ1FY/XELv2Log6/YztaVbuF1ZTOTDZL1s88knn5CRkcGSJUuKtO93StBPlPlnIdulNjQftQhnWwudrt+iRQuMjIx4+vSp3jOHfVoO4EHXjTiIJ5gFduR6+D963U+fJCQksHXrVkaPHk2DBg2wt7fHzs6OChUqvPCWhIaGUqpUKY2KTxuUvLeAP//8k02bNhU8MJeNGzdSs2ZNtRSJt4Hu3bvTunVrpk6dSkKC6n09a7X/kHjs4LT6bnF1uXjxIjEx6rkgpKiTJGCLW2U9NXvPg1atWgGaJf28im/XUdww9sb9wo+kJD1h27ZtdOzYkVKlShU4NyszA/nW4VrL8FZSQEHkiIgIRowYwZ07dwDleaRFixZ0796dY0snc3ntVJo2bYq3tzc2NjY0atSISZMmERsbq9r+Cjlc2gQVW4NNeV38Rrqneg+yavRnjPEO/tzxp86XT01NJTw8nMmTJ1O5cmWdr68qmaEbKHVsOgdEAyoNXoV7WWu97HPjxg0qVapEYKBqhc61wcuvHUn9tmNGNmW3dCPs7FG976krcnJy2LZtG927d8fJyYm+ffuyfv16rK2t6d+/Pz/++CMrVqygSRNltQQHBweGDRtGpUqV1N/suRnwbT18fX3F205ycrJQKBQqjb19+7YAxPfff69nqYonly9fFjKZTHz22WdqzTu5ZpIQ023E3YjzepJMSe3atUXHjh3VmhM7vaI4/2MXPUmUP56enqJHjx46WSvi7CEhptuIpZ/2EoDYsGGDSvNOrhgrxHQbAYSKYnC+KQ7Hi3Peuh5CrAh48bdKSEgQy5cvFydOnBBCCBEeHi7s7OzE4cOHhRBCREdHi0OHDomlfxwQzoMXiV92HBEHDhwQv/76q5gwYYJo2rSpMDMzEw8fPhRCKP+XYmJi8v9wbig/U3Fle2sd6d8AACAASURBVP5jigPPkkTi91XFvakVRei1uzpfPisrS2RkZOh8XVXJubpH5Ey3E8FTG4mgK1F63Usul4vx48eLy5cv63Wfl3l8J1zEzqgkUqY5inNBOwttX214+vSpsLGxEc7OzuLzzz8XJ0+eFFlZWSrPV+d8V+QnJH0f/wUl7zmqKHrTpk0TkiSJu3d1fzIrKbz//vvCwsJCxMbGqjwn/mG0yJhmL04vfF9vcmVmZgoTExMxefJklefcvx0hxHQbcXrTLL3JlR9Dhw4VdnZ2IicnRyfrnflpgBjhayoszM1ESkpKgeMvHN4k5NNsxZkF/XWu5AGf5HEMBerocI8OwDXgJjA5j/fNgM25758BPFRZ98U578+PhGJuVXHgwAHRs2dPYWpqKgAxYcIEIYTyfJHXZ/f19sui6tR9IiP79fdSU1NfPA8ICBCenp75f/5bPhRitrsQ2UWn4KhKxs1gkTPdThz5vpfKN8wFsW/fPpGYmKiTtTRFceeEyJpRVlyYWkf8HhxepLLok6exd8W9/9UQGdPsxcnda4tanDzZt2+feOedd4RcLhdCCBEREaHxuVOd853BXfuWsHr1aipXrkx2dna+Y7KyslixYgWdOnXC3b3kdGjSNVOnTiUrK4s5c+aoPMfesQKXyrSjVvxekhIe6UWuiIgIsrOz1ep0EXN+LwBOddrrRaY30apVKxITE7lwQTe9JSsNXMDm8Bw6eRuTknD/jWOvhf6N9/Hx3DKuSM3hesl89gNGAi65xwggAFglSdIkbReXJMkIWIKyR241YIAkSdVeGTYUeCqEqAQsAFT+wqalpbH8aAzV51yjffv2hISEMGbMGP755x/mz5//XIbXspeFEC9Kp5gZv54o8XLP4tWrV7N+/XqMjIzIzMxk1qxZpKfnZqk+S1SWTqnVD4wLve+w2phVbMI17+G0yjjM2b1rtF7v6dOn9O7dm0mTtP6qaE7sJbLW9+OevCzH6y+jX5NXv176Iz4+noEDB2rdGUdV7Jzcsf/4CNFmFfE/N57jW34qlH3VIT4+nitXrrwId/Dx8Smc6gGqaoMl9fivWPL++usvAYiNGzfmO+bp06di3LhxL9wz/2WGDBkizMzMxP3791Wec/vKaSGm24iTgVP0ItPatWsFICIjI1WeE/pjN/FouodQ5N4dFiYPHz4UgJg5c6ZO1tu7d68AxNZ3S4uoGT4i5tbVPMeFHdshkqc5ipgZ3uJRzG0hhHp3tqocKNuYWb/0szWwH7AArupg/UbAgZd+/hL4Mg8ZGuU+NwbiAamgtR0dHYWdnZ0AhK+zTKxbuVhld+GNRynC/Ys9YsPpuyqNf86+ffsEIDw9PcWBAweEuPCb0lUbfU6tdYoSeVamuDbTTyROLy9S4+5pvV5oaKha5xedEn9TPJvlKWKmeYqvA/cLuVw31klVSUxMFE5OTsLPz09nln5VyEhLFOGzWwox3UYE/TpVZ1ZZTVAoFOLXX38Vq1atevGzOi7ZN6HO+c5gyXtL6NChAz4+PsyZM+f5BeI17Ozs+Pnnn2nduvCyMIsrX3/9NXK5nA0bNqg8x7N6A66Y1cHr9sZ8W3Fpw8WLF7G0tFQ5uFYhl+OVep57tvULrT7eyzg6OuLv78/u3bt1sl6HDh04e/Yslcf8RimRjNW6NpzZPIeUpCcA3L8dwZnFQ6h+5AMSjMpiPHQf5Vw8dbJ3HrgBL5dMyQbchRDPAF18+C5A9Es/x+S+lucYIUQOkAQU2Pj10aNHtGvXjuD1szk33Ir3ujTFzEw1a9rRa5qVTunQoQNBQUGYmJjQvn17xn4xnWcWLuDiq9Y6RYnMxJSc7ssxEdk8XjcENMwQfV4Q2NfXl/LliyDhJPkBmWu6kp6RxQ/lZvP1u22QyQo3q9fW1pZ58+YRGhrKqlWrCm1fM0tbqkzcS5hNSwLu/szxZWMLpfTVq6SmpjJw4EAGDx7Mtm3bEEIgSZLWBeQ1waDkvSXIZDImTZrEpUuX2LVr12vv79mzh2PHjhWBZMUTT09PLly4wOeff67WvBz/UTiSwKVD63QuU2hoKHXr1lXZhH/7ymlKk4JUsaXOZVGVrl278vDhw/9302mBJEnUr1+fmo07kjroAA9MPWkQMQvL+V6kTXfEZV1D6sXt4FzZ7pT7JATHChV18Bvky0bgtCRJ0yVJmgGcBDZJkmQF6KKoWl5X3VfvzlQZoxwoSSMkSQqVJCnUxcWFzZs30ySgtbJkR8pDlYU6ei2Oyo7WuNipX14jICCAS5cuMeHjUSw+eBP/5fFEXrum9jpFSbWavuwtPxbP5HMkHFHf5ZecnEyNGjX4/vvv9SCdCqQ/IfPX7uSkJfCl1Qy+GdwjT7d7YTBgwAACAgL46quviIsrvLbPRqbm1By/lX/K9aTF4/WcWvg+WVn5hzHpmsjISPz9/dm8eTPffvstf/31V5GWzilyd6q+j/+Ku1YIZeB+tWrVhIuLy78CfhUKhfD39xeNGzcuUvN1cSUzM1PlsfKcHBE9o4qInFlfpzJkZ2cLS0tLMX78eJXnnFo3VYjpNiLuvnquNV2SkZGhk+/Uxo0bxfDhw0VaWtqL1xRyubh6er84ufoTcWrJcHFq43fi/p28XdnoIbsW8AXG5x5+Ol5bb+7aF+e8xGily/TcGpU+g9SMbFHpq7/Ed3/l7SZXmYu/i73vWggH+9LC1tZW7N27V7v1CpnHSc/E39Naiazp9kI8uKTW3LFjxwpJksTp06f1JN0byEgWWcsCRMZ0ezHim/kiKiGt4Dl6Jjw8XBgbG4shQ4YU/uYKhQj9ZYIQ023E2dmdRFKy/pNgDh8+LGxtbYWDg4New6LUOd8ZLHlvEaampqxdu5bY2FgGDBhAYmIiCoUCSZI4dOgQmzdvLto7imLI77//jqurq8p182RGRtyv8gFVcq4RGXpEZ3JERkaSnp6On5+fynMs7wdzV+ZG2fJFl0RjZmaGJEn5hgioSlRUFBcuXMDC4v8tSJJMRtUG7Wk0dB4NR6+k4YCvKO9RRVuRVUKSJDOgCmAF2AKdJEmapsMtzgHekiR5SpJkCvQHXjXB7wI+yH3eB/hbqPOHtnYEJEhRra5dyM14suWCAG1bmV3dQUdfD86dO4+HhwddunRh3rx5Wn9HCgsHG3PuN59DvChF+oYBkP5EpXlnzpxh8eLFjBkzhgYNGuhZylfIzkC+cQCyhxeZKJ/AmCGDcS1jWbgy5EG1atWYOHEia9asITg4uHA3lyR8hyzgQrVJ+KaH8HBBKx5E3dLbdmvWrKFDhw5UqFCBc+fOFZuwKIOS95ZRv359li9fzoEDByhXrhxNmjRBoVBgY2NDhQoVilq8YkfNmjVp164dGRkZKs+p0XkUyViSdkz1/rIFERqq7NqgqpKX8SwN72eXeVi2oc5k0JSNGzfi7e3Ns2fPNF7jiy++4MyZM8XpJmQn0B3IAdJeOnSCUMbYfYzSWhcBbBFChEuS9D9JkrrlDvsFsJck6SbKEi6T1drEyASsy6ms5B29Hoe1mTF+7mXU2uZfZCTDzSNQrTvunp6EhITQs2dPTpw4UWKUPIB+AfWYYzMF47SHZG8ZrCzs/Aays7MZMWIE5cuX57vvviskKXOR56DYOgSjeyeYlDOS3u8Op1YFu8KV4Q1MmzYNd3d3hg0bptZ5VlfUfWcK11quxEURg/GaNoSH6jZsSQjBV199xdChQ2nZsiUhISHFqnqFcVELYED3DB8+nPr16/P7779jbm5OamoqNjY2RS1WsaR69eqsX79erTlWpew47dQTv9hNPIy+iZOrBlXIX6FHjx44OTmpXBX/5j9/U0PKwsKn6O8WXVxcqFevHk+fPv2XJU5VYmJiqFChArIiSB55AxWEEB30uYEQYi+w95XXpr30PAPoq9UmpZwguWAlTwjB0cjHNKlkj6mxFp/D9QMgz4RqypaJVlZWbNmyhczMTGQyGY8ePcLa2vpfpViKIyZGMob178u0ZdeYfXclHPkftP0m3/Hz588nLCyM7du3F+65VqFA7B6L7NpfTM/+gPrdR9G6qmPh7a8C1tbWrFy5kvbt2/O///2PWbNmFboMVQPeIcbJC+PNA/Da3Yfge9Np0muMTm4q7969y5IlSxg+fDhLliwpkuSKN1GszqoGdEedOnWYPXs2M2bMMCh4KnDx4kUOHjyo8nj3juORENzZq5t6THZ2dnTo0EFlRSflyn6yhREV/Qq/Pt6rtGjRgi1btmiUSZiamkq1atWYOnWqHiTTipOSJJX8vn+lyqtkyYt8mMKDpAxa+aiXVfsaV3dAKWeo4P/iJZlMhoWFBXK5nE6dOtG9e/cSYdWr4WJLmaZD+S2nNYT8BFfybnt269YtZsyYQc+ePenRo0fhCahQwO5xSBc3siC7N6Vbfkx/f7fC218N2rVrx7Bhw4pUhgo+fliOPsY9cx+aXp7CqQUDSE3VvF/x8/68np6eXLp0iRUrVhQ7BQ8MSp4BAwCMHj2ajz76SOVG7M7uVbhUqhnVYv8kPTVJq72zsrKYNWsWkZGRKs9xfnSca+Y1sbYprdXeuuTatWsvykeoyi+//EJKSgqdO3fWk1Qa0xQ4L0nSNUmSwiRJuixJUlhRC6U2pZxUUvKORCgLfLfURsnLTIEbh6BqN8jjZsXIyIhp06bxxRdfFCe3/BsZ19qbDaVHEyZVQewYBVFn/vW+QqFgxIgRmJiYsGiR7sI3CkQhh10fw4X1/JzTk8f1xjO+tXfh7a8BK1euLBIr3svYOJSn8ud/E+o2lIZJ+3k8vyk3wkPVXicrK4t33nmHH3/8EQAPD49C+04/uBOh1vhioeRJklRGkqRDkiTdyH187colSVIdSZJOSZIUnnvS7VcUshp4O5k0aRJ3797lzz9Vb1Ju2WwstqRxee8Krfa+fv06U6ZM4dKlSyqNj713DQ9FFKlurbTaV5dcu3YNHx8ffv31V5XnZGVlMXfuXJo3b07DhkUfW/gKHQFvoB3QFeiS+1iysCkP6QmQ/eZYqMMRj6ntake5Uuaa7/XcVVs9f2tW9+7dadu2LQCBgYHcuHFD8/0KAXMTI+YOqM/wrE95jD1iUz+Iu/7i/dWrV/P3338zb948XFxeLXOoJxRy2DEaLv7GgpzehFUaw8weNYu94vxcvqCgoEKtnfcqMmMT/IbM53qbXymteIrrlg6cWPs1WVlZBU/OxcjICBMTE4yNCyfiLSszg3/2ruHK9wGUD1TvXFkslDyUAcVHhBDewBHyDjBOB94XQlRH2fPxJ0mSik90qYESTdeuXfHy8lLrbrxK/TbcMPbGOXItCvmbA7PfRI0aNXj69CldunRRaXzU6R0AlK/fXeM9dU2VKlVo3LgxS5YseeHGKIiNGzcSExPD5Mnq5RMUBkKIe3kdRS2X2tjmJlsl598mLi4lk0sxibTW2lW7U5nR61pwZmlSUhKTJk2iWbNmXL58Wbt99Uz18raM7ORPn7TPyJBLsKE3JCrrWPfr148FCxYUnisyJxO2DYWw31mQ05dTFYaz6N26GBsVl0t5waxYsYKFCxe+sQVnYeDTrCeMCuFaqYY0u7uIe7Mbcu1iyBvnpKWlERsbi5GRERs3bmTixIl6lTHq+kVOLx9N6vfe1Ds7EfvMGE66faTWGsXlm9EdCMx9Hgi8disohLguhLiR+/wB8BjQMtffgAElRkZGjBkzhuDgYJV7sUoyGUm1huGmuM/l46pbAPPCzs5O5WB087uHiZGccK1US6s9dc3HH3/MzZs3OXDgQIFjc3JymDNnDrVr16ZDB73mN6iFJEnBuY8pkiQlv3SkSJKkeQBPUWGXG6OVmL9+GhT5GCGgdVUtlLystJdctQUX37W1teXo0aMYGRkREBBQaD1ONeXDxh5U8alJ/7TPyEl/Ss4vnch8dANbW1smTJhQOFa0Z4mwvheEb2d2zrucKD+YNYPrY2lasvInly9fzqlTp4pF/FppRzdqf7aHsMaLKKNIoOL2LpxcNJi4RzGvjU1KSqJ9+/a0bduW7OxsvX3m6alJnNu+kMjvGuG2sQW+sb9z17I2lwLWUO7rSBoP+UGt9YqLkucohIgFyH1849lGkiR/wBTIs+jNy9XfC7PStoGSzZAhQ7C0tFTLmler/Yc8pgzSmWUa7zt+/Hh+++03lcY+S0uhSvoFYso2K5JWZm+id+/euLq6MmPGjAID61evXk1kZCTTpk0rVm4mIURTSSlQdSGEzUtHKSFEyctgsnVVPiZG5zvkSOQjnG3Nqeasxa93/QDkPHujq/ZVqlatyokTJ7C1taVVq1bFuiOPJEnM61uH5NLV+VA+he/3R1G/bk2S7xWSFTLhFqzpgCL6DJ/kfMwp50GsHeKPtVnJUvBAeUNrbW1Nenq6yuc9fVOr3fuYjD/HRYfu+MfvwHypHyGrP+dJnLJbzJMnT2jTpg1nzpxhxowZOldQFXI5kWcPcnbheyh+rEL9S1OxyEnmVMUJJI66SL1Je6gd0BsjDdzDhXaVkCTpsCRJV/I41PI5SZLkDKwHBgsh8vQLCSFWCiH8hBB+Dg4GY58B1bCzs+ODDz5g48aNKrfhMTUz57bHAGplnOduhPoBvKmpqSxZskTlpItrJ3djLmVjVbPYJSpgamrKjBkzOHv2LDt37sx3XEJCAlOnTqVFixb07NmzECVUjdyCw9uLWg6dYOMCkhEk5a3kZWTLOXEjnlY+5bRTtq/uBKty4NZIrWleXl6cOHECV1dXOnTowP79+zWXQc/YWpqw+gM/whRenHH7kJaepths6QnRerZCXj8AK1uSlfiAD7Imcd2xA+sG+2NjXvSWMG1YtmwZgwYNYuvWrUUtCgA2pcvh9/FaHg0M4pa1L01iVmK2uBZ7Zg2kccMGXL58me3bt9OnTx+d7KeQy7l+PojTy0YSN9Mbn719qZFwgKulW3K14x+4Tb1Mo/e+wcFJu4zpQlPyhBBthBA18jh2Ao9ylbfnStzjvNaQJMkG+Av4WghxurBkN/Df4eOPPyYzM1OtwOAqnceSIUx4dOhntfc7e/YscrmcJk2aqDQ++8p2krDCp2EntfcqDN5//32qVq3KuHHjSExMfO19IQTDhw8nKSmJn3/+uVhZ8V7htCRJ9YtaCK0xMlYmXyRG5fn2qVsJpGfJaaNNbbWsdLhxEKp2VclV+youLi4cO3aMqlWr0q1bt2Jz0c8LT3tLlgysR4Rje6R3V6MwtYa1neGyHmTOzoADU2BjPxJMnWmV+j8yXZuxcXhDbC1LtoIHMG7cOOrXr8/w4cO5d6/4hLu6VK5Dnc//IqrfEY6K+nwy/3ei795k2QB3yjw5y+2roQgV445fRigUPLgTwbkdizg/rydJM92pvLsH9R5uIdaiMufqzUH+SST+EzZRrUE7nXlqiout93n7ntm5j6+ZAXJb/2wH1gkh/ihc8Qz8V6hWrRpt27Zl2bJlfP755yqZ5Us7OHPWvgO1E/bxNC6W0g7OKu8XEhKCJEkqZZdmZqRTJSmYSLsA/E3NVN6jMDE2NiYwMJBGjRoxdOhQNm/e/FoGWvPmzWnRogW1a9cuIilVoiUwUpKkuyg7XUgojXzFKxBSFezc8nXX7gmLpZS5MY0r2Wu+/s1DkJ3+ogCyJjg4OPD333/TuXNn+vXrR2BgIIMGDdJcJj2gUCjo2bMnTZs25ad+Axn/+wWeuc9hWfn5yLYNhZuHoeMcMLfVfrPoc7BzDMRf41zZHgyK6UGLaq4sHFAXcxP1FeniiImJCZs2baJevXr07NmT4OBgLC2LvhXbc7JM7BgdeIXELCt++vIdWhifx+v2z3D7Z+KxI9rCh4wy1ZGVcceirCumlrYYm1kgFAqyn6WQmZJA1pNoxNN7WCdG4JZ5g/KkUR6Ix46bto2RKrXGu0lv6pQpq7ffo7goebOBLZIkDQWiyK3yLkmSHzBSCDEMeAdojrLNz4e58z4UQlwsAnkNvMV89tlnHD58mIyMDJVjL8q1nYD55t1c/GsRDT9UvRZUSEgI1atXx86u4ETxyJN7qE06ZrWKn4vzZerXr8+PP/7IJ598wooVKxgzZgxxcXGEhYXRunVrJkyYUNQiqkLHohZAZ9i6wt3X+4Zm5sg5ePUh7ao5YWasheIQvgMsy4K7atbo/LCzs+PgwYMMGjQIT09PrdbSB7NmzWLXrl20a9eOrrXLky1X8Okflxjk/jVrGh/F/NQC5d+5zQyo3ivPWoEFkhgFh7+BK1uRWzszp8y3rIzxYngzT77o4FOismhVoWLFimzcuJGuXbsyYsQI1q9fXyys+1euXHmRYBF09Cj16tUDIO7+Le6e2o4UfRaHlKu4xpxBdv/N8ceZwoQoEw8iyrQB59qU9WmKZ7X6lC2kz7JYKHlCiATgtf5MQohQYFju8w3AhkIWzcB/kHbt2tGuXTu15nhU9eOyWT0q3t1IVuY0TM0Krjcml8s5deoUAwYMUGmPzLDtJGOJT2PVSq0UJRMnTqRUqVK88847AHz//fesWbOG6OhoSpUqVcTSqUQUMBDwEkL8T5IkN8AJKD5+JVWxc4WUByDPVvazzeX49XhSMnLoUlt1y/NrZD9TxozV6qt0DWuJlZUV27f/fzjk6dOni0UNxT/++IOpU6cycOBARo8eDUCvehUwkkl8/kcYHVObsaFXc1yCpyhLnAT/BI3HQtUuYFpA1rwQEH0GzqyAiF0gM+F21dF8eL0RDxNNWNCvJj3rvr19xzt37sy3337LlClTqFWrFpMmTSpqkfjll1+QyWQcP36catWqvXjdwaUiDn0+e/FzdlYGjx7cJfHhHbKfpSLPegaSDBOLUphZl6a0syf25VzwLsIkuWKh5BkwUNxQKBQcPHgQDw8PfHx8VJojGo7B4dhQzu5Zjn/vgq1V4eHhJCcnqxSPl5mRTpXEY1y3aYKfefFxabyJl2uH9e/fn+HDh5cUBQ9gKaAAWgH/A1KAbUDJi9OzcwOhUNbKK+3x4uU9YQ+wszShaSUtXEU3D0N2GlTTfTuvw4cP07ZtW37//Xf69Su62vfnzp3j/fffp3HjxqxevfpflqbudVxwsbPgo/Xn6bBVwVedNtDf4izS0e9h+wj4yxoqtYHydcGpBpjZgJEppMVD4l24/4/yb5j6CMxsSa87jHkprfnlQjZVnW3Y1a82Pk4lL6lbXb788kvCwsL44osvKF++fJG56jMyMjA3N2fu3Ll89tlnBRa4NjE1x9nDB2cP1a4RRYFByTNgIA+Sk5Pp3bs3Q4YMUbmkSs0WvbgeMocKV5aQ3XUUJgXEzYWEKAtvqqLkhQdtph5pmNZTzepX3PD39y94UPGigRCiniRJFwCEEE9z44JLHnbuysend18oeRnZcg5ffUTX2uUx0cZtdHUnWJQBj2Zai/kqrVq1YtmyZfTq1Uvna6tKZGQkXbp0wcnJie3bt2Nu/rqF3s+jDDvGNGHS1jC+3B7O7opufNnzb2rKr8KlTXDnmLKnb15YlAavlqS5tyQwsQ6Lgx+QLc9hVEBFJrTx1s6NXoKQJInAwEDi4uIYPHgwHh4eNG3atFBlCAwMZObMmYSEhODo6Fh4HUz0jEHJM2AgD+zs7Dh69KhayQGSTMazxp9T+fhwzu5eVqA1Lzg4GCcnJ5Xij4wu/cYj7KnetPh0uXjLyZYkyQgQAJIkOaC07JU87CsqHxNugVcAoCyAnJYlp0ut8pqvm50B1/ZDjZ46cdW+ikwmY+TIkQA8fvyYL7/8krlz51K6dOH0a7516xatWyujiPbt20e5cvmXb3UtY8lvwxrw+7lovt8XQdclJ2lc0Z7+/l/RtM18ykipEBepTFDJyQRLe9ItXTgbb8Jflx+xa9cDMnOi6VDdickdffAoq1ph9LcJMzMztm/fznfffYevr2+h71+jRg18fX1VLkpfUjAoeQYM5EP9+up75moF9OH6yR9VsubZ2trSq1evAgONH9+/Q41noZyt8AGOhdQr0QALUWbzO0qS9B3QB/i6aEXSkFLlwdhCqeTlsuPifcpam9LQq4zm6946AlkpenHVvsrp06dZt24dJ06cYOfOnVStWlWv+928eZM2bdqQkZHB0aNHVQrZkMkk3m3gRpfazmw6E8WakDuM23QBSYJKDtY42phja1mGlIwcHidncCvuMtlygYWJEb19K/BeQ3eqalOQ+i3AxsaGOXPmAMp6mmfPnqVjR/3lQMXFxbF161ZGjRqFr68vmzdv1tteRYXhimHAwBvYsmULixYtIigoSKVm1C9b887sXEyDvp/mO3bp0qUqyXDr8GrKSYIKLQupP6YBhBC/SZJ0HmVCmAT0EEJEFLFYmiGTKa15T5RK3uOUDI5EPGZIU0/tsjWv7lS6Gz2b60jQ/OnWrRtBQUH07t2bBg0asHbtWr26cbOysjAxMeHgwYPUrFlTrbk25iZ81KIiQ5t6EnY/iRPX47l8P4mEtEzuJz7DxtwYFzsLAqqUo0kle/zcy2Bh+t9wy6rD119/zYYNG4iKitKL9TY4OJj+/fsTHx9P+/bt8fLy0vkexQGDkmfAwBswMTEhODiYXbt2qXxRqRXQh8iT86kYvpC0DkOxKvV6eZTnAb4FIc/Jwf3uH1w1rUm1SupdbAxoTm5rMz/A/nl2rSRJ/kKIs0Utm0aU8YLHVwHYej6GHIWgf31XzdfLyYRr+6Bat39l7OqTpk2bEhoaSq9evejduzeDBw/mp59+wsZGd9avo0eP0qJFC6pVq0ZERIRKN3b5YWwko55baeq5FY57+W3jxx9/pG/fvi8UvKysLExNtQ+LTUtLY8qUKSxcuBAvLy9OnTr11ip4UHx61xowUCzp2rUrbm5uavWzlWQyaP8tZUkkbMvMPMf07t2bTp0K7loRduQ3yotHZPoOV3l/AzphKdAIeJ7pkgIsKTpxtMS+Ejy9S052Fr+djqKBZxm8HKw1X+9WEGQmF4qrzSMLLQAAFqRJREFU9mVcXV0JCQlhypQpBAYGUqNGDbZu3Vpgr2RV2L9/Py1btuSPP5S19rVR8Axoj7W1Na1atQJg7dq1VK1alX379mm8nkKhYPPmzdSsWZOff/6Z0aNHc+HCBerWrasrkYslBiXPgIE3YGxszOjRozl69CiXL6vejNynfhvOWwdQJ2odj2JuvfZ+t27d6N79zUkUQqHAInQ5DyRHarUeqLbsBrSigRBiDJAByuxaoERk16Zl5rz+on0lUORw9Ewo9xOfMayZlpaLqzuUnR08W2i3jgaYmpry7bffEhwcTOnSpenbty9du3bVSNGTy+WEhYUByvqYv/76a5Fm8xrIGy8vL0xNTenUqRPdunXj5MmTKn/emZmZbN68GT8/P/r374+1tTXHjx9n8eLFJamkk8YYlDwDBgpg2LBhmJubs3jxYrXmOfdWBhA/2Pjxa70OP/roIz766KM3zr8SvBOf7KtEVxmMkcGqUNiU2OzahLSs11/MzbA9fvo0FR2saO2Tf6ZogeRkQeReqNIZjItO723UqBHnz59n6dKl+Pr6IkkSQggCAwN5/DjP9ucvePjwIT/99BM1a9akWbNmPHnyBJlMxocffmiw4BVDmjdvzqVLl/juu+84ceIETZo0wd/fn1mzZnHmzBnS09NfjFUoFNy/f5+kpCQAVq1aRf/+/UlKSmLDhg1cuHCBZs10X/Kn2CKEeKsPX19fYcCAtgwZMkRYWlqKJ0+eqDXv1PppQky3Eef3rnnxWmhoqIiLi3vjPIVcLiJn1hex071ExrM0jWT+LwGECh2eN1B2u9gFxADfAdeAd3S5h74OM6dK4mHSs3//gdIShJhuI779aqT4859ozf/QQghx7YAQ022EuLZfu3X0wD///CMAsXbtWiGEEFevXhXTp08XM2fOFNOnTxcffPCBqFOnjpDJZAIQvr6+Ytu2bUKhUBSx5AZUJTU1VSxdulTUrVtXoLwJE4CYPHmyEEKIqKgoAYhVq1YJIYRISEgQ+/fvF3K5vCjF1inqnO8k5fi3Fz8/PxEaGlrUYhgo4Vy8eJG6desyd+5cPv00/4zZV8nJzuLu7IbYyRNg5AnKOrnh4+ODm5sbBw8ezHde6K5l+P0zmbM1v1Gpe8Z/HUmSzgsh/HS8pg//n117RJSQ7FozZ28x/ZddfNXp/8uMJKZnkfVDFa6Y1SXgi23IZFr0B90+UmnJ+/wGGL+54HdhI4QgLCwMDw8PbG1t2bBhA++9996L952cnKhTpw7+/v70799f76VYDOiXuLg4jh49yu3bt/H396dly5ZkZWWxcuVK2rdvj7e3d1GLqBfUOd8ZlDwDBlSkWbNm3L9/nxs3bmBkpHrJgztXz+G0uRN3zKog77KUWrVrs3jxYsaMGZPn+KSER+Qsqk+8sRPeX55CpsZe/1V0reRJkjRHCPFFQa8VR5wqVheW7/zA9tFNqO1qR45cweC15xh273P8HbKxGHtK88WzM+DHSlC9O3QvGXko2dnZABgZGSErwh6iBgzoCnXOd4ZvvAEDKjJx4kTu3LmjdoaXZ7X6hPvOpFrWZZZ9PQQg36QLhVzO3dXvUUqkYtztJ4OCV3S0zeM1/VVl1SHOtuY42pgz6JczzDt4jT7LT3HiRjzlKtXDIvEmyLM1X/zmIWUB5Bq9dSewnjExMcHExMSg4Bn4T2L41hswoCI9evTgyJEjdO7cWe25ft1Gcqpcf46fvkANL0fKOzu/NkYoFJxd/hG1n53hQrVJVKzVWBdiG1ADSZJGSZJ0GagiSVJY7nFZkqQ7QFhRy6cKRjKJ30c0pIpjKRb9fZPoJ+ksHFCXqnUagzwLEm5qvvjlrWDlAB76L4BswIAB7TGkERkwoCIymexF3SYhRIHtyF7F1H844XErWe6fxOW5HbDvvYAKlWoAkPAohrvrx9Aw9Siny/WjQd9JOpffgEpsBPYB3wOTX3o9RQjxpGhEUh93eyv+GNmIzBwFZsYy5Xf1UXXlm4/CoZwGsWiZKXD9ANQdpJdetQYMGNA9hv9UAwbUZO7cuRw8eJADBw6opeitW78eU1NTvHpOonLUUkzXN+WmsRdyyQSv7BvUBE55jaHhe98qCyobKAoqA9FCiAEAkiS9D/QG7kmSNKMkKXqSJGFu8pK7394bZCYQewlq9lF/wWv7IedZiXLVGjDwX8dwJTFgQE3KlClD2bJl/1WbqSBSU1MJDAykZ8+etB36DWkfhXLWfTjPjG3IkZlx3rk/se/+TaMPZhkUvKJlBZAFIElSc2A2sA5IAlYWoVzaY2wKzrUgRsNEtMv/1969B0tR3mkc/z4iKgiKCBoFFlBZwUJRJIrXCmISYoxo1t2EUsMGLctSVzG6ioVJjFu7ZTSaGLPEsN6IS7km4JWNroiXeFkliCggXoiiICiuCuIVkd/+0e+R8XDmcODM6W6G51PVNd09PfM+854+77zT1z/CDj2g18G1zWVmbcZb8sw20pgxYxgzZsxGveamm25i5cqVjB2bXQ6l2+696TbmyraIZ63TrmJr3feAiRExFZgqaU6BuWqj18Ew68bs5IuNuefs+0uzky4OOxf8I8Rss+H/VrNNNHfuXO66664WLbt48WIOPfRQhg4d2saprJXaSWr48TsceLDiuZr8KJbUVdJ0SS+nxybvYC/pPkkrJE2rRbkA9DoI1nwCb27kOSRzJkOshcE/qFkUM2t77uSZbaJx48Zx0kknsWjRog0ue8UVVzB9+vS2D2WtdSvwiKS7gI+BRwEk7UW2y7YWxpFdXLkfMIMvn+BR6UrglCrPbZqeB2WPi2e2/DVr18LsW6DvkdC1lfe8NbNcuZNntokmTJiAJE455RRWr27ifqHA0qVLmTkz+0Lt2LFjnvFsE0TEvwLnAzcDh8e6q8VvBfxTjYoZCUxK45OA46tkmQGsqlGZmR17wI694PWNuCDyqw/Ditdg8OiaRjGztudOntkm6t27NxMnTuSxxx7jzDPPZO3a9e9ff+GFFzJixAhWrartd7W1nYh4MiLuiIgPK+a9FBGza1TErhGxLL3vMmCX1r6hpNMlzZI06+23325+4b5HwisPw+drWvbms38PHXaC/se2NqaZ5cydPLNWGDVqFJdccgk33HADo0aNYvny5QANN7nn2muvZerUqXTu3LnImJYzSQ9ImtfE0PStTlopIiZGxJCIGNK9e/fmF/7bb8InK2FJC3bZfrAcFkyDQaOg/Xa1CWtmufHZtWatdNlll9GpUyfGjx/PtGnTGDhwIO3bt+eRRx5hp512YtiwYUVHtJxFxNHVnpP0lqTdImKZpN2A5TlGgz2GZdfLe+k+6L2Bu6o8dR2sXQNDTs0nm5nVlLfkmbWSJC666CLmzZvH6NGj6dKlC/vuuy8rVqwoOpqV091AwwFuo4GWnaJdK9vtkHXuXrwXvjjksAmfroKZ18OA70C3vfLLZ2Y1U4oteZK6ArcBfYBFwD9ExHuNlukN3A60A9oD10bEdfkmNauuf//+TJgwoegYVn6XA3+QdCrwOvD3AJKGAGdExGlp+lGgP9BJ0hLg1Ij4n5okGPh3cM85sPgp+Jsql/V54jfw6Uo4/LyaFGlm+SvLlryWXFJgGXBoROwPHAyMk7R7jhnNzFotIt6JiOER0S89vpvmz2ro4KXpIyKie0R0iIieNevgQXZbs213yC6M3JRVb8ITv4Z9RkKPwTUr1szyVZZO3gYvKRARqyPi0zS5LeXJbma2edlmexj0fZh/B7y36MvPRcCf/jm7K8bwnxYSz8xqoywdpRZdUkBSL0nPAYuBn0fE0hwzmpnVj8PGQrttsg5d5bF5s26EBXfD8B/DznsWl8/MWi23Tl4tLikQEYsjYj9gL2C0pF2rlNXya0aZmW2JduwBw8bDy/fDf5+fbdF79OpsfK+j4ZCzi05oZq2U24kXtbykQEQslTQfOAKY0sTzE4GJAEOGDGnm9DEzsy3YwWfAB2/C49fArBuyeXt/G068AbZqV2w2M2u1Upxdy7pLClxOlUsKSOoJvBMRH6cbeh8GXJ1rSjOzerLVVvD1y2DASFg2B76yL/T8KkhFJzOzGihLJ68llxQYAFwlKQABv4iIuUUFNjOrGz0PzAYzqyul6ORFxDvA8CbmzwJOS+PTgf1yjmZmZma2WSrL2bVmZmZmVkPu5JmZmZnVIUVz9y6sA5JWAS8WnaMFugH/V3SIFtgccm4OGcE5a2nviOhcdIgyKEmbV4Z1pgwZoBw5nGGdMuRobYbeEdG9JQuW4pi8NvZiRAwpOsSGSJrlnLWxOWQE56wlSbOKzlAihbd5ZVhnypChLDmcoVw58szg3bVmZmZmdcidPDMzM7M6tCV08iYWHaCFnLN2NoeM4Jy1tDlkzEsZ6sIZ1ilDDmdYpww5cstQ9ydemJmZmW2JtoQteWZmZmZbHHfyzMzMzOpQXXfyJI2Q9KKkhZLGFZ2nGkmLJM2VNKdMl4KQdKOk5ZLmVczrKmm6pJfT404lzHippDdSfc6RdEzBGXtJekjSAknzJZ2b5petLqvlLFt9bidppqRnU86fpfl9JT2V6vM2SdsUmTNvRbd31dafokhqJ+kZSdMKKr+LpCmSXkh1ckgBGc5Lf4t5km6VtF1O5Rb+3VElw5Xp7/GcpDskdWnLDNVyVDx3gaSQ1K2tyq/bTp6kdsC/A98C9gFGSdqn2FTNGhYR+xd9/Z5GbgZGNJo3DpgREf2AGWm6SDezfkaAX6b63D8i/pRzpsbWAOdHxABgKHBWWhfLVpfVckK56vNT4KiIGATsD4yQNBT4OVnOfsB7wKkFZsxVSdq75tafIpwLLCiw/GuA+yKiPzAo7yySegDnAEMiYiDQDvh+TsXfTPHfHU1lmA4MjIj9gJeAi9s4Q7UcSOoFfB14vS0Lr9tOHnAQsDAiXomI1cB/ASMLzrRZiYg/A+82mj0SmJTGJwHH5xqqkSoZSyUilkXE7DS+iqyx70H56rJazlKJzAdpsn0aAjgKmJLmF16fOSu8vSvT+iOpJ/Bt4PqCyt8BOBK4ASAiVkfEigKibA10kLQ10BFYmkehZfjuaCpDRNwfEWvS5JNAz7bMUC1H8kvgQrK2q83UcyevB7C4YnoJJfzCSgK4X9LTkk4vOswG7BoRyyBr1IFdCs5Tzdlpk/yNRe8GrSSpD3AA8BQlrstGOaFk9Zl2xc0BlpP9Ov8rsKKiAS/z/3tbKFV718T6k7dfkX2Bri2o/D2At4Gb0i7j6yVtn2eAiHgD+AXZlqJlwMqIuD/PDI2Urb0bA9xbRMGSjgPeiIhn27qseu7kqYl5Zb1ezGERMZhsV8tZko4sOtBm7rfAnmS78pYBVxUbJyOpEzAVGBsR7xedp5omcpauPiPi84jYn+yX+EHAgKYWyzdVoUrT3hW9nks6FlgeEU/nXXaFrYHBwG8j4gDgQ3I+HCP9GBsJ9AV2B7aXdHKeGcpK0niywwsmF1B2R2A88JM8yqvnTt4SoFfFdE9y2lS9sSJiaXpcDtxB9qVVVm9J2g0gPS4vOM96IuKt1AlYC/wHJahPSe3JvvgmR8TtaXbp6rKpnGWszwZpF9jDZMeAdUm7paDE/+9tpBTtXZX1PG+HAcdJWkS22/ooSf+Zc4YlwJKIaNiSOYWs05eno4FXI+LtiPgMuB04NOcMlUrR3kkaDRwLnBTFXCh4T7KO97NpHe0JzJb0lbYorJ47eX8B+qUz7rYhO+D07oIzrUfS9pI6N4wD3wDWOwunRO4GRqfx0cBdBWZpUkNDkpxAwfUpSWTH5iyIiKsrnipVXVbLWcL67N5wVpykDmRfZguAh4AT02KF12fOCm/vmlnPcxURF0dEz4joQ1YPD0ZErluwIuJNYLGkvdOs4cDzeWYg2007VFLH9LcZTrEnohTe3kkaAVwEHBcRH+VdPkBEzI2IXSKiT1pHlwCD0zrTJgXW7QAcQ3YGzV+B8UXnqZJxD+DZNMwvU07gVrLdc5+lFfFUYGeyM6NeTo9dS5jxFmAu8BxZw7JbwRkPJ9t19hwwJw3HlLAuq+UsW33uBzyT8swDfpLm7wHMBBYCfwS2LTJnAfVSaHtXbf0puE6+BkwrqOz9gVmpPu4Ediogw8+AF9L/yS15/U+U4bujSoaFZMeuNqyf1xVRF42eXwR0a6vyfVszMzMzszpUz7trzczMzLZY7uSZmZmZ1SF38szMzMzqkDt5ZmZmZnXInTwzMzOzOuROnpmZmVkdcifPzMzMrA65k7eFkxSSrqqYvkDSpTln+KBi/IkavN+lki5oYn4XSWfWsqzWktRT0vcazfudpMMkfU3SLUVlM6s3bu+K5fYuf+7k2afAdyV129gXKlPTdSgi2vLeil2ALxq9Ni6rpYaz/j0tDwaeJLti/jO5JzKrX27viuX2Lmfu5NkaYCJwXuMnJP1I0rw0jE3z+khaIGkCMBs4QtILkq5Py02WdLSkxyW9LOmgive7U9LTkuZLOr2pMA2/ciWdIWlOGl6V9FCaf7KkmWn+7yS1S/PHS3pR0gPA3k29N3A5sGd67ZUVZfXZiM/QZPkVzx/YkDVND5T0v1U+6+HA1cCJ6f36ShoAvBQRnwODgB6SnpL0iqSvVflcZtYybu/c3m1Z8riPnYfyDsAHwA5k98/bEbgAuBQ4kOx+pdsDncjuq3sA0AdYCwxNr+9D1nDuS/aj4WngRkDASODOirK6pscOZPdS3LkhQ2WeRvnaA48C3wEGAPcA7dNzE4AfVGTtmD7LQuCCJj5rH2Be47Ja+hmqld+ojI7AGxXTtwNHN1P/9wEDK6Z/BIxJ488Al6bxbwCPFr2+ePCwOQ9u79zebWnD1tgWLyLel/R74Bzg4zT7cOCOiPgQQNLtwBFkN6h/LSKerHiLVyNiblpuPjAjIkLSXLIGpcE5kk5I472AfsA7G4h3DfBgRNwj6WyyBu4vkiBrPJcDXVPWj1KGuze2Dlr4GYZXKf8LEfGRpE8kdQH2ILsp+QOStidrJFcDD0fE5PSSvYEXK97im8APJW1NdkPvf0vz5wAbvYvJzL7M7V2LP4PbuzrgTp41+BXZ7oib0rSaWfbDRtOfVoyvrZheS1rH0qb3o4FDUsPwMLBdc4Ek/SPQGzi7ItOkiLi40XJjgWjuvVpgg5+hWvlNeB7oD/wYuCTN+y4wJTXetwGTJe0MrIyIz9Ln6Ah0iYilkvYDFkbE6vT6wcCzm/7xzKyC27t13N7VMR+TZwBExLvAH4BT06w/A8dL6ph+lZ1AththU+0IvJcavP7A0OYWlnQg2a6UkyNibZo9g+x4jl3SMl0l9U5ZT5DUQVJnsl0dTVkFdG7FZ6hWfmPzgR8CiojH07yewOI0/nl67AssrXjdMKDh+JZBQF9J20rqBPyU7IvJzFrJ7V2LuL2rA96SZ5WuIv2KjIjZkm4GZqbnro+IZyT12cT3vg84Q9JzZJvrn9zA8meT7ZZ4KO0qmBURp0m6BLhf2VlunwFnRcST6dfiHOA1qjTOEfFOOrh4HnDvxn6AiHi+qfJTmZXmA5OAr1bMW0LW8M1h3Y+rF4BuKc/pwLeAKem5QcBk4Amy3ST/0miXkZm1jtu7Zri9qw+KaO1WXzPbkLR14DfAJ8BjFceoVC4zGzi4YXeGmdnmyO1debiTZ2ZmZlaHfEyemZmZWR1yJ8/MzMysDrmTZ2ZmZlaH3MkzMzMzq0Pu5JmZmZnVIXfyzMzMzOqQO3lmZmZmdcidPDMzM7M69P+E4qWMeJkn7AAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Compute the feedback gains\n", + "# K, kf, clsys = normalized_place(1, 0.707) # Gains from MATLAB\n", + "# K, kf, clsys = normalized_place(0.07, 0.707) # Original gains\n", + "K, kf, clsys = normalized_place(0.7, 0.707) # Final gains\n", + "\n", + "# Print out the gains\n", + "print(\"K = \", K)\n", + "print(\"kf = \", kf)\n", + "\n", + "# Construct an output-based controller for the system\n", + "clsys = ct.StateSpace(\n", + " np.block([[A, -B@K], [L@C, A - B@K - L@C]]),\n", + " np.block([[B], [B]]) * kf, \n", + " np.block([[C, np.zeros(C.shape)], [np.zeros(C.shape), C]]), \n", + " np.zeros((2,1)))\n", + "\n", + "# Simulate the system\n", + "t, y, x = ct.forced_response(clsys, tau, y_ref, [0.4, 0, 0.0, 0])\n", + "\n", + "# Calcaluate the input used to generate the control response\n", + "u_sfb = kf * y_ref - K @ x[0:2]\n", + "u_ofb = kf * y_ref - K @ x[2:4]\n", + "\n", + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure(figsize=[9, 4.5])\n", + "\n", + "# Plot the actual and estimated states\n", + "ax = plt.subplot(1, 2, 1)\n", + "plt.plot(t, x[0])\n", + "plt.plot(t, x[2])\n", + "plt.plot(t, y_ref, 'k-.')\n", + "ax.set(xlim=[0, 30])\n", + "plt.legend(['state feedback', 'output feedback', 'reference'])\n", + "plt.xlabel('Normalized time $v_0 t / b$')\n", + "plt.ylabel('Lateral position $y/b$')\n", + "\n", + "ax = plt.subplot(2, 2, 2)\n", + "plt.plot(t, x[1])\n", + "plt.plot(t, x[3])\n", + "plt.plot(t, theta_ref, 'k-.')\n", + "ax.set(xlim=[0, 15])\n", + "plt.ylabel('Vehicle angle $\\\\theta$')\n", + "\n", + "ax = plt.subplot(2, 2, 4)\n", + "plt.plot(t, u_sfb[0])\n", + "plt.plot(t, u_ofb[0])\n", + "plt.plot(t, delta_curvy, 'k-.')\n", + "ax.set(xlim=[0, 15])\n", + "plt.xlabel('Normalized time $v_0 t / b$')\n", + "plt.ylabel('Steering angle $\\\\delta$')\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Trajectory Generation (Example 8.8)\n", + "\n", + "To illustrate how we can use a two degree-of-freedom design to improve the performance of the system, consider the problem of steering a car to change lanes on a road. We use the non-normalized form of the dynamics, which were derived in Example 3.11." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "KJA comment, 1 Jul 2019:\n", + "1. I think the reference trajectory is too much curved in the end compare with Example 3.11\n", + "\n", + "In summary I think it is OK to change the reference trajectories but we should make sure that the curvature is less than $\\rho=600 m$ not to have too high acceleratarion.\n", + "\n", + "RMM response, 16 Jul 2019:\n", + "* Not sure if the comment about the trajectory being too curved is referring to this example. The steering angles (and hence radius of curvature/acceleration) are quite low. ??\n", + "\n", + "KJA response, 20 Jul 2019: You are right the curvature is not too small. We could add the sentence \"The small deviations can be eliminated by adding feedback.\"\n", + "\n", + "RMM response, 23 Jul 2019: I think the small deviation you are referring to is in the velocity trace. This occurs because I gave a fixed endpoint in time and so the velocity had to be adjusted to hit that exact point at that time. This doesn't show up in the book, so it won't be a problem ($\\implies$ no additional explanation required)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import control.flatsys as fs\n", + "\n", + "# Function to take states, inputs and return the flat flag\n", + "def vehicle_flat_forward(x, u, params={}):\n", + " # Get the parameter values\n", + " b = params.get('wheelbase', 3.)\n", + " \n", + " # Create a list of arrays to store the flat output and its derivatives\n", + " zflag = [np.zeros(3), np.zeros(3)]\n", + " \n", + " # Flat output is the x, y position of the rear wheels\n", + " zflag[0][0] = x[0]\n", + " zflag[1][0] = x[1]\n", + " \n", + " # First derivatives of the flat output\n", + " zflag[0][1] = u[0] * np.cos(x[2]) # dx/dt\n", + " zflag[1][1] = u[0] * np.sin(x[2]) # dy/dt\n", + " \n", + " # First derivative of the angle\n", + " thdot = (u[0]/b) * np.tan(u[1])\n", + "\n", + " # Second derivatives of the flat output (setting vdot = 0)\n", + " zflag[0][2] = -u[0] * thdot * np.sin(x[2])\n", + " zflag[1][2] = u[0] * thdot * np.cos(x[2])\n", + " \n", + " return zflag\n", + "\n", + "# Function to take the flat flag and return states, inputs\n", + "def vehicle_flat_reverse(zflag, params={}):\n", + " # Get the parameter values\n", + " b = params.get('wheelbase', 3.) \n", + "\n", + " # Create a vector to store the state and inputs\n", + " x = np.zeros(3)\n", + " u = np.zeros(2)\n", + " \n", + " # Given the flat variables, solve for the state\n", + " x[0] = zflag[0][0] # x position\n", + " x[1] = zflag[1][0] # y position\n", + " x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # tan(theta) = ydot/xdot\n", + " \n", + " # And next solve for the inputs\n", + " u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2])\n", + " thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2])\n", + " u[1] = np.arctan2(thdot_v, u[0]**2 / b)\n", + " \n", + " return x, u\n", + "\n", + "vehicle_flat = fs.FlatSystem(vehicle_flat_forward, vehicle_flat_reverse, inputs=2, states=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To find a trajectory from an initial state $x_0$ to a final state $x_\\text{f}$ in time $T_\\text{f}$ we solve a point-to-point trajectory generation problem. We also set the initial and final inputs, which sets the vehicle velocity $v$ and steering wheel angle $\\delta$ at the endpoints." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Define the endpoints of the trajectory \n", + "x0 = [0., 2., 0.]; u0 = [15, 0.]\n", + "xf = [75, -2., 0.]; uf = [15, 0.]\n", + "Tf = xf[0] / uf[0]\n", + "\n", + "# Define a set of basis functions to use for the trajectories\n", + "poly = fs.PolyFamily(6)\n", + "\n", + "# Find a trajectory between the initial condition and the final condition\n", + "traj = fs.point_to_point(vehicle_flat, x0, u0, xf, uf, Tf, basis=poly)\n", + "\n", + "# Create the trajectory\n", + "t = np.linspace(0, Tf, 100)\n", + "x, u = traj.eval(t)\n", + "\n", + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure(figsize=[9, 4.5])\n", + "\n", + "# Plot the trajectory in xy coordinate\n", + "plt.subplot(1, 4, 2)\n", + "plt.plot(x[1], x[0])\n", + "plt.xlabel('y [m]')\n", + "plt.ylabel('x [m]')\n", + "\n", + "# Add lane lines and scale the axis\n", + "plt.plot([-4, -4], [0, x[0, -1]], 'k-', linewidth=1)\n", + "plt.plot([0, 0], [0, x[0, -1]], 'k--', linewidth=1)\n", + "plt.plot([4, 4], [0, x[0, -1]], 'k-', linewidth=1)\n", + "plt.axis([-10, 10, -5, x[0, -1] + 5])\n", + "\n", + "# Time traces of the state and input\n", + "plt.subplot(2, 4, 3)\n", + "plt.plot(t, x[1])\n", + "plt.ylabel('y [m]')\n", + "\n", + "plt.subplot(2, 4, 4)\n", + "plt.plot(t, x[2])\n", + "plt.ylabel('theta [rad]')\n", + "\n", + "plt.subplot(2, 4, 7)\n", + "plt.plot(t, u[0])\n", + "plt.xlabel('Time t [sec]')\n", + "plt.ylabel('v [m/s]')\n", + "plt.axis([0, Tf, u0[0] - 1, uf[0] +1])\n", + "\n", + "plt.subplot(2, 4, 8)\n", + "plt.plot(t, u[1]);\n", + "plt.xlabel('Time t [sec]')\n", + "plt.ylabel('$\\delta$ [rad]')\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vehicle transfer functions for forward and reverse driving (Example 10.11)\n", + "\n", + "The vehicle steering model has different properties depending on whether we are driving forward or in reverse. The figures below show step responses from steering angle to lateral translation for a the linearized model when driving forward (dashed) and reverse (solid). In this simulation we have added an extra pole with the time constant $T=0.1$ to approximately account for the dynamics in the steering system.\n", + "\n", + "With rear-wheel steering the center of mass first moves in the wrong direction and the overall response with rear-wheel steering is significantly delayed compared with that for front-wheel steering. (b) Frequency response for driving forward (dashed) and reverse (solid). Notice that the gain curves are identical, but the phase curve for driving in reverse has non-minimum phase." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM note, 27 Jun 2019:\n", + "* I cannot recreate the figures in Example 10.11. Since we are looking at the lateral *velocity*, there is a differentiator in the output and this takes the step function and creates an offset at $t = 0$ (intead of a smooth curve).\n", + "* The transfer functions are also different, and I don't quite understand why. Need to spend a bit more time on this one.\n", + "\n", + "KJA comment, 1 Jul 2019: The reason why you cannot recreate figures i Example 10.11 is because the caption in figure is wrong, sorry my fault, the y-axis should be lateral position not lateral velocity. The approximate expression for the transfer functions\n", + "\n", + "$$\n", + "G_{y\\delta}=\\frac{av_0s+v_0^2}{bs} = \\frac{1.5 s + 1}{3s^2}=\\frac{0.5s + 0.33}{s}\n", + "$$\n", + "\n", + "are quite close to the values that you get numerically\n", + "\n", + "In this case I think it is useful to have v=1 m/s because we do not drive to fast backwards.\n", + "\n", + "RMM response, 17 Jul 2019\n", + "* Updated figures below use the same parameters as the running example (the current text uses different parameters)\n", + "* Following the material in the text, a pole is added at s = -1 to approximate the dynamics of the steering system. This is not strictly needed, so we could decide to take it out (and update the text)\n", + "\n", + "KJA comment, 20 Jul 2019: I have been oscillating a bit about this example. Of course it does not make sense to drive in reverse in 30 m/s but it seems a bit silly to change parameters just in this case (if we do we have to motivate it). On the other hand what we are doing is essentially based on transfer functions and a RHP zero. My current view which has changed a few times is to keep the standard parameters. In any case we should eliminate the extra time constant. A small detail, I could not see the time response in the file you sent, do not resend it!, I will look at the final version.\n", + "\n", + "RMM comment, 23 Jul 2019: I think it is OK to have the speed be different and just talk about this in the text. I have removed the extra time constant in the current version." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Forward TF = \n", + " s + 1.333\n", + "-----------------------------\n", + "s^2 + 7.828e-16 s - 1.848e-16\n", + "\n", + "Reverse TF = \n", + " -s + 1.333\n", + "-----------------------------\n", + "s^2 - 7.828e-16 s - 1.848e-16\n", + "\n" + ] + } + ], + "source": [ + "# Magnitude of the steering input (half maximum)\n", + "Msteer = vehicle_params['maxsteer'] / 2\n", + "\n", + "# Create a linearized model of the system going forward at 2 m/s\n", + "forward_lateral = ct.linearize(lateral, [0, 0], [0], params={'velocity': 2})\n", + "forward_tf = ct.ss2tf(forward_lateral)[0, 0]\n", + "print(\"Forward TF = \", forward_tf)\n", + "\n", + "# Create a linearized model of the system going in reverise at 1 m/s\n", + "reverse_lateral = ct.linearize(lateral, [0, 0], [0], params={'velocity': -2})\n", + "reverse_tf = ct.ss2tf(reverse_lateral)[0, 0]\n", + "print(\"Reverse TF = \", reverse_tf)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZEAAAEOCAYAAABIESrBAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJzt3XecVOXVwPHfWRakL9I7iwpIW+lFxFjQYBQsQUXFHgmvxkSNRg1RsSQaY/LmtYuK2IJdEQ2KDakqRZQmReoKwlKlSFvO+8e54wzr1tlpu3u+n8/9zM69d+59dr3O4WnnEVXFOeeci0ZasgvgnHOu7PIg4pxzLmoeRJxzzkXNg4hzzrmoeRBxzjkXNQ8izjnnouZBxDnnXNQ8iDjnnIuaBxHnnHNR8yDinHMuaunJLkC81a9fXzMzM5NdDOecK1PmzJmzSVUbFHVeuQ8imZmZzJ49O9nFcM65MkVEVhfnPG/Ocs45FzUPIgW4+mo4/nj473+TXRLnnEtdZSqIiMhZIvKkiIwXkVPjdR9VWLYMpk6F00+HWrVg2DBYsyZed3TOubIp6UFERMaIyEYRWZBn/0ARWSIiy0XkFgBVfUtVrwIuA86PX5nggw/gq6/gjDNg/3548UVo1QoGDrRjubnxurtzzpUdSQ8iwFhgYOQOEakEPAKcBnQALhCRDhGn/CU4HldZWTBhAuzeDWPHwjHHwOefw6mnQv36cNxx8NFH8S6Fc86lrqQHEVWdAmzJs7sXsFxVV6jqPuAl4EwxfwcmqurcRJUxLQ0uvRTmzYP16+Hll6FmTZg+HQYMgIwMuOwyyM5OVImccy41JD2IFKAZsDbifXaw71pgADBEREYU9GERGS4is0Vkdk5OTkwLVrUqnHcerF0Ls2ZZ89aePfDss5CZCZdcAp984s1dzrmKIVWDiOSzT1X1QVXtrqojVPXxgj6sqqOBO4G5VapUiVshe/SAiRPhxx/hiSfg7LNh/Hg46SQ47DD4xS9g8uS43d4555IuVYNINtAi4n1zYF2SylKktDQYPhxefRW+/x7+9jdr4poyBU48EerUgd/8BmJcKXLOuaRL1SAyC2gjIq1FpAowFHi7JBdQ1QmqOjwjIyMuBSxItWpw662weTN89hmccop1zD/9NLRuDZdfDm+9BQcOJLRYzjkXF0kPIiIyDpgJtBORbBG5UlUPAL8D3gcWA6+o6sISXneQiIzevn177AtdTL17w6RJ1mfyn//A0KHw+uvW7FWtmjV7TZuWtOI551ypiaomuwxx1aNHD02l3Fk7d8JNN9kIr61bbV+ouWvUKKhRI6nFc845AERkjqr2KOq8pNdE4iUVaiL5qVkTHnsMtmyxWshJJ1lgeeABaNzYZsY//DAcPJjskjrnXNG8JpICDh6ETz+FF16wmfF790J6uo3uuvtu6Ns32SV0zlU0XhNJ0ZpIftLSbBTX00/D6tVw1VVWY/noIzj2WKhbF555BnbtSnZJnXPuUOU2iCRrdFZpNWoEo0dbf8nUqXDCCZa764orrLnr2GO9ucs5lzrKbRApD447zma/b99uzV1nnw0zZ8K119pkxpNPhhkzkl1K51xFVm6DSFlqzipKWpqtbfLcc5a7K9Tc9fHH0K8ftG8Pzz/vzV3OucQrt0GkrDZnFaVx40Obu0480YLHJZdAgwbQti089JA3dznnEqPcBpGK4LjjrDayapU1d3XpYotp/f731tzlkxmdc/HmQaQcCDV3zZgBGzbAb39rqzF+8gn072+1lbFjrW/FOediqdwGkfLUJ1ISDRvC44/bZMYZM2wmfHa25ew6/HA48kj41788d5dzLjZ8smEFoGop6//4R/jmG9tXqZINF/7rX6224pxzkeI22TBYXXCYiNwevG8pIr2iKaRLDBH41a9g8WLLLnz11Zava+pUawY77jirnaxZk+ySOufKmmiasx4F+gIXBO93kID1zl1s1K0LjzwCmzbBF1/Y2iebN1stpVUrW53xr3+FffuSXVLnXFkQTRDprarXAHsAVHUrEL/lA13c9Oxpa58sWmQd7506WW3kL3+xVPXHH2/HnHOuINEEkf0iUglQABFpAKTcrISK2rEeDRG49FKYPx+2bYMbboD69W14cMeOFmyGDbPhw845FymaIPIg8CbQUET+CkwD/hbTUsVAeZ1sGG+1a8M//2lDhdets76SH36w7MJt20LTplZ72bkz2SV1zqWCqEZnicjRwMmAAB+p6uJYFyxWfHRWbLzyCtx/P3z5pc2GF4HzzoMbb4Tu3e29c678KO7orGIHERGpW9hxVd1SzLIllAeR2NqzB+67D5591vJ47d1rtZOsLLjrLmv6cs6VffEY4jsHmB285gBLgWXBz3OiKWRJ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Configure matplotlib plots to be a bit bigger and optimize layout\n", + "plt.figure()\n", + "\n", + "# Forward motion\n", + "t, y = ct.step_response(forward_tf * Msteer, np.linspace(0, 4, 500))\n", + "plt.plot(t, y, 'b--')\n", + "\n", + "# Reverse motion\n", + "t, y = ct.step_response(reverse_tf * Msteer, np.linspace(0, 4, 500))\n", + "plt.plot(t, y, 'b-')\n", + "\n", + "# Add labels and reference lines\n", + "plt.axis([0, 4, -0.5, 2.5])\n", + "plt.legend(['forward', 'reverse'], loc='upper left')\n", + "plt.xlabel('Time $t$ [s]')\n", + "plt.ylabel('Lateral position [m]')\n", + "plt.plot([0, 4], [0, 0], 'k-', linewidth=1)\n", + "\n", + "# Plot the Bode plots\n", + "plt.figure()\n", + "plt.subplot(1, 2, 2)\n", + "ct.bode_plot(forward_tf[0, 0], np.logspace(-1, 1, 100), color='b', linestyle='--', \n", + " initial_phase=-180)\n", + "ct.bode_plot(reverse_tf[0, 0], np.logspace(-1, 1, 100), color='b', linestyle='-',\n", + " initial_phase=-180);\n", + "plt.legend(('forward', 'reverse'));\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feedforward Compensation (Example 12.6)\n", + "\n", + "For a lane transfer system we would like to have a nice response without overshoot, and we therefore consider the use of feedforward compensation to provide a reference trajectory for the closed loop system. We choose the desired response as $F_\\text{m}(s) = a^22/(s + a)^2$, where the response speed or aggressiveness of the steering is governed by the parameter $a$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RMM note, 27 Jun 2019:\n", + "* $a$ was used in the original description of the dynamics as the reference offset. Perhaps choose a different symbol here?\n", + "* In current version of Ch 12, the $y$ axis is labeled in absolute units, but it should actually be in normalized units, I think.\n", + "* The steering angle input for this example is quite high. Compare to Example 8.8, above. Also, we should probably make the size of the \"lane change\" from this example match whatever we use in Example 8.8\n", + "\n", + "KJA comments, 1 Jul 2019: Chosen parameters look good to me\n", + "\n", + "RMM response, 17 Jul 2019\n", + "* I changed the time constant for the feedforward model to give something that is more reasonable in terms of turning angle at the speed of $v_0 = 30$ m/s. Note that this takes about 30 body lengths to change lanes (= 9 seconds at 105 kph).\n", + "* The time to change lanes is about 2X what it is using the differentially flat trajectory above. This is mainly because the feedback controller applies a large pulse at the beginning of the trajectory (based on the input error), whereas the differentially flat trajectory spreads the turn over a longer interval. Since are living the steering angle, we have to limit the size of the pulse => slow down the time constant for the reference model.\n", + "\n", + "KJA response, 20 Jul 2019: I think the time for lane change is too long, which may depend on the small steering angles used. The largest steering angle is about 0.03 rad, but we have admitted larger values in previous examples. I suggest that we change the design so that the largest sterring angel is closer to 0.05, see the remark from Bjorn O a lane change could take about 5 s at 30m/s. \n", + "\n", + "RMM response, 23 Jul 2019: I reset the time constant to 0.2, which gives something closer to what we had for trajectory generation. It is still slower, but this is to be expected since it is a linear controller. We now finish the trajectory in 20 body lengths, which is about 6 seconds." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Define the desired response of the system\n", + "a = 0.2\n", + "P = ct.ss2tf(lateral_normalized)\n", + "Fm = ct.TransferFunction([a**2], [1, 2*a, a**2])\n", + "Fr = Fm / P\n", + "\n", + "# Compute the step response of the feedforward components\n", + "t, y_ffwd = ct.step_response(Fm, np.linspace(0, 25, 100))\n", + "t, delta_ffwd = ct.step_response(Fr, np.linspace(0, 25, 100))\n", + "\n", + "# Scale and shift to correspond to lane change (-2 to +2)\n", + "y_ffwd = 0.5 - 1 * y_ffwd\n", + "delta_ffwd *= 1\n", + "\n", + "# Overhead view\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(y_ffwd, t)\n", + "plt.plot(-1*np.ones(t.shape), t, 'k-', linewidth=1)\n", + "plt.plot(0*np.ones(t.shape), t, 'k--', linewidth=1)\n", + "plt.plot(1*np.ones(t.shape), t, 'k-', linewidth=1)\n", + "plt.axis([-5, 5, -2, 27])\n", + "\n", + "# Plot the response\n", + "plt.subplot(2, 2, 2)\n", + "plt.plot(t, y_ffwd)\n", + "# plt.axis([0, 10, -5, 5])\n", + "plt.ylabel('Normalized position y/b')\n", + "\n", + "plt.subplot(2, 2, 4)\n", + "plt.plot(t, delta_ffwd)\n", + "# plt.axis([0, 10, -1, 1])\n", + "plt.ylabel('$\\\\delta$ [rad]')\n", + "plt.xlabel('Normalized time $v_0 t / b$');\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fundamental Limits (Example 14.13)\n", + "\n", + "Consider a controller based on state feedback combined with an observer where we want a faster closed loop system and choose $\\omega_\\text{c} = 10$, $\\zeta_\\text{c} = 0.707$, $\\omega_\\text{o} = 20$, and $\\zeta_\\text{o} = 0.707$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "KJA comment, 20 Jul 2019: This is a really troublesome case. If we keep it as a vehicle steering problem we must have an order of magnitude lower valuer for $\\omega_c$ and $\\omega_o$ and then the zero will not be slow. My recommendation is to keep it as a general system with the transfer function. $P(s)=(s+1)/s^2$. The text then has to be reworded.\n", + "\n", + "RMM response, 23 Jul 2019: I think the way we have it is OK. Our current value for the controller and observer is $\\omega_\\text{c} = 0.7$ and $\\omega_\\text{o} = 1$. Here we way we want something faster and so we got to $\\omega_\\text{c} = 7$ (10X) and $\\omega_\\text{o} = 10$ (10X)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "K = [100. -35.86]\n", + "L = [ 28.28 400. ]\n", + "C(s) = \n", + "-1.152e+04 s + 4e+04\n", + "--------------------\n", + "s^2 + 42.42 s + 6658\n", + "\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Compute the feedback gain using eigenvalue placement\n", + "wc = 10\n", + "zc = 0.707\n", + "eigs = np.roots([1, 2*zc*wc, wc**2])\n", + "K = ct.place(A, B, eigs)\n", + "kr = np.real(1/clsys.evalfr(0))\n", + "print(\"K = \", np.squeeze(K))\n", + "\n", + "# Compute the estimator gain using eigenvalue placement\n", + "wo = 20\n", + "zo = 0.707\n", + "eigs = np.roots([1, 2*zo*wo, wo**2])\n", + "L = np.transpose(\n", + " ct.place(np.transpose(A), np.transpose(C), eigs))\n", + "print(\"L = \", np.squeeze(L))\n", + "\n", + "# Construct an output-based controller for the system\n", + "C1 = ct.ss2tf(ct.StateSpace(A - B@K - L@C, L, K, 0))\n", + "print(\"C(s) = \", C1)\n", + "\n", + "# Compute the loop transfer function and plot Nyquist, Bode\n", + "L1 = P * C1\n", + "plt.figure(); ct.nyquist_plot(L1, np.logspace(0.5, 3, 500))\n", + "plt.figure(); ct.bode_plot(L1, np.logspace(-1, 3, 500));" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "K = [100. 2.]\n", + "C(s) = \n", + " 3628 s + 4e+04\n", + "---------------------\n", + "s^2 + 80.28 s + 156.6\n", + "\n" + ] + } + ], + "source": [ + "# Modified control law\n", + "wc = 10\n", + "zc = 2.6\n", + "eigs = np.roots([1, 2*zc*wc, wc**2])\n", + "K = ct.place(A, B, eigs)\n", + "kr = np.real(1/clsys.evalfr(0))\n", + "print(\"K = \", np.squeeze(K))\n", + "\n", + "# Construct an output-based controller for the system\n", + "C2 = ct.ss2tf(ct.StateSpace(A - B@K - L@C, L, K, 0))\n", + "print(\"C(s) = \", C2)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the gang of four for the two designs\n", + "ct.gangof4(P, C1, np.logspace(-1, 3, 100))\n", + "ct.gangof4(P, C2, np.logspace(-1, 3, 100))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/test-response.py b/examples/test-response.py index 745a14fb6..0ccc70b6c 100644 --- a/examples/test-response.py +++ b/examples/test-response.py @@ -1,7 +1,8 @@ # test-response.py - Unit tests for system response functions # RMM, 11 Sep 2010 -from matplotlib.pyplot import * # Grab MATLAB plotting functions +import os +import matplotlib.pyplot as plt # MATLAB plotting functions from control.matlab import * # Load the controls systems library from scipy import arange # function to create range of numbers @@ -11,8 +12,11 @@ # Generate step responses (y1a, T1a) = step(sys1) -(y1b, T1b) = step(sys1, T = arange(0, 10, 0.1)) -(y1c, T1c) = step(sys1, X0 = [1, 0]) -(y2a, T2a) = step(sys2, T = arange(0, 10, 0.1)) +(y1b, T1b) = step(sys1, T=arange(0, 10, 0.1)) +(y1c, T1c) = step(sys1, X0=[1, 0]) +(y2a, T2a) = step(sys2, T=arange(0, 10, 0.1)) -plot(T1a, y1a, T1b, y1b, T1c, y1c, T2a, y2a) +plt.plot(T1a, y1a, T1b, y1b, T1c, y1c, T2a, y2a) + +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show() \ No newline at end of file diff --git a/examples/tfvis.py b/examples/tfvis.py index 056fd62eb..60b837d99 100644 --- a/examples/tfvis.py +++ b/examples/tfvis.py @@ -56,6 +56,7 @@ from control.matlab import logspace from numpy import conj + def make_poly(facts): """ Create polynomial from factors """ poly = [1] @@ -63,7 +64,8 @@ def make_poly(facts): poly = polymul(poly, [1, -factor]) return real(poly) - + + def coeff_string_check(text): """ Check so textfield entry is valid string of coeffs. """ try: @@ -73,6 +75,7 @@ def coeff_string_check(text): return Pmw.OK + class TFInput: """ Class for handling input of transfer function coeffs.""" def __init__(self, parent): @@ -150,6 +153,7 @@ def set_zeros(self, zeros): self.numerator_widget.setentry( ' '.join([format(i,'.3g') for i in self.numerator])) + class Analysis: """ Main class for GUI visualising transfer functions """ def __init__(self, parent): @@ -179,7 +183,7 @@ def __init__(self, parent): self.sys = self.tfi.get_tf() tkinter.Button(self.entries, text='Apply', command=self.apply, - width=9).grid(row=0, column=1, rowspan=3, padx=10, pady=5) + width=9).grid(row=0, column=1, rowspan=3, padx=10, pady=5) self.f_bode = plt.figure(figsize=(4, 4)) self.f_nyquist = plt.figure(figsize=(4, 4)) @@ -187,35 +191,35 @@ def __init__(self, parent): self.f_step = plt.figure(figsize=(4, 4)) self.canvas_pzmap = FigureCanvasTkAgg(self.f_pzmap, - master=self.figure) + master=self.figure) self.canvas_pzmap.draw() self.canvas_pzmap.get_tk_widget().grid(row=0, column=0, - padx=0, pady=0) + padx=0, pady=0) self.canvas_bode = FigureCanvasTkAgg(self.f_bode, - master=self.figure) + master=self.figure) self.canvas_bode.draw() self.canvas_bode.get_tk_widget().grid(row=0, column=1, - padx=0, pady=0) + padx=0, pady=0) self.canvas_step = FigureCanvasTkAgg(self.f_step, - master=self.figure) + master=self.figure) self.canvas_step.draw() self.canvas_step.get_tk_widget().grid(row=1, column=0, - padx=0, pady=0) + padx=0, pady=0) self.canvas_nyquist = FigureCanvasTkAgg(self.f_nyquist, master=self.figure) self.canvas_nyquist.draw() self.canvas_nyquist.get_tk_widget().grid(row=1, column=1, - padx=0, pady=0) + padx=0, pady=0) self.canvas_pzmap.mpl_connect('button_press_event', - self.button_press) + self.button_press) self.canvas_pzmap.mpl_connect('button_release_event', - self.button_release) + self.button_release) self.canvas_pzmap.mpl_connect('motion_notify_event', - self.mouse_move) + self.mouse_move) self.apply() @@ -223,7 +227,7 @@ def button_press(self, event): """ Handle button presses, detect if we are going to move any poles/zeros""" # find closest pole/zero - if (event.xdata != None and event.ydata != None): + if event.xdata != None and event.ydata != None: new = event.xdata + 1.0j*event.ydata @@ -361,6 +365,7 @@ def redraw(self): self.canvas_step.draw() self.canvas_nyquist.draw() + def create_analysis(): """ Create main object """ def handler(): @@ -376,6 +381,7 @@ def handler(): Analysis(root) root.mainloop() + if __name__ == '__main__': import os if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: diff --git a/examples/type2_type3.py b/examples/type2_type3.py index 951e5df54..250aa266c 100644 --- a/examples/type2_type3.py +++ b/examples/type2_type3.py @@ -2,10 +2,11 @@ # tracking and disturbance rejection for two proposed controllers # Gunnar Ristroph, 15 January 2010 -from matplotlib.pyplot import * # Grab MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import os +import matplotlib.pyplot as plt # Grab MATLAB plotting functions +from control.matlab import * # MATLAB-like functions from scipy import pi -integrator = tf( [0, 1], [1, 0] ) # 1/s +integrator = tf([0, 1], [1, 0]) # 1/s # Parameters defining the system J = 1.0 @@ -16,8 +17,8 @@ # Plant transfer function from torque to rate inertia = integrator*1/J -friction = b # transfer function from rate to torque -P = inertia # friction is modelled as a separate block +friction = b # transfer function from rate to torque +P = inertia # friction is modelled as a separate block # Gyro transfer function from rate to rate gyro = 1. # for now, our gyro is perfect @@ -28,16 +29,20 @@ # System Transfer Functions # tricky because the disturbance (base motion) is coupled in by friction -closed_loop_type2 = feedback(C_type2*feedback(P,friction),gyro) -disturbance_rejection_type2 = P*friction/(1.+P*friction+P*C_type2) -closed_loop_type3 = feedback(C_type3*feedback(P,friction),gyro) -disturbance_rejection_type3 = P*friction/(1.+P*friction+P*C_type3) +closed_loop_type2 = feedback(C_type2*feedback(P, friction), gyro) +disturbance_rejection_type2 = P*friction/(1. + P*friction+P*C_type2) +closed_loop_type3 = feedback(C_type3*feedback(P, friction), gyro) +disturbance_rejection_type3 = P*friction/(1. + P*friction + P*C_type3) # Bode plot for the system -figure(1) -bode(closed_loop_type2, logspace(0,2)*2*pi, dB=True, Hz=True) # blue -bode(closed_loop_type3, logspace(0,2)*2*pi, dB=True, Hz=True) # green +plt.figure(1) +bode(closed_loop_type2, logspace(0, 2)*2*pi, dB=True, Hz=True) # blue +bode(closed_loop_type3, logspace(0, 2)*2*pi, dB=True, Hz=True) # green +plt.show(block=False) -figure(2) -bode(disturbance_rejection_type2, logspace(0,2)*2*pi, Hz=True) # blue -bode(disturbance_rejection_type3, logspace(0,2)*2*pi, Hz=True) # green +plt.figure(2) +bode(disturbance_rejection_type2, logspace(0, 2)*2*pi, Hz=True) # blue +bode(disturbance_rejection_type3, logspace(0, 2)*2*pi, Hz=True) # green + +if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + plt.show()